diff options
author | Joe <rbo@gmx.us> | 2024-04-22 22:45:24 +0200 |
---|---|---|
committer | Joe <rbo@gmx.us> | 2024-04-22 22:45:24 +0200 |
commit | 7c8ed4b1bf9d289a5b126e784bbcf645bb578d14 (patch) | |
tree | 8163d9b317195375303e76d90b47a52c4a820791 | |
parent | up (diff) | |
download | dotfiles-bsd-7c8ed4b1bf9d289a5b126e784bbcf645bb578d14.tar.gz dotfiles-bsd-7c8ed4b1bf9d289a5b126e784bbcf645bb578d14.tar.bz2 dotfiles-bsd-7c8ed4b1bf9d289a5b126e784bbcf645bb578d14.tar.xz dotfiles-bsd-7c8ed4b1bf9d289a5b126e784bbcf645bb578d14.tar.zst dotfiles-bsd-7c8ed4b1bf9d289a5b126e784bbcf645bb578d14.zip |
up
28 files changed, 14 insertions, 8570 deletions
diff --git a/.config/mimeapps.list b/.config/mimeapps.list index 7a6587c..2dec259 100644 --- a/.config/mimeapps.list +++ b/.config/mimeapps.list @@ -1,16 +1,16 @@ [Default Applications] -x-scheme-handler/http=userapp-Firefox-SWW0Y1.desktop -x-scheme-handler/https=userapp-Firefox-SWW0Y1.desktop +x-scheme-handler/http=firefox.desktop +x-scheme-handler/https=firefox.desktop x-scheme-handler/ftp=librewolf.desktop image/png=sxiv.desktop -x-scheme-handler/chrome=userapp-Firefox-SWW0Y1.desktop -text/html=userapp-Firefox-SWW0Y1.desktop -application/x-extension-htm=userapp-Firefox-SWW0Y1.desktop -application/x-extension-html=userapp-Firefox-SWW0Y1.desktop -application/x-extension-shtml=userapp-Firefox-SWW0Y1.desktop -application/xhtml+xml=userapp-Firefox-SWW0Y1.desktop -application/x-extension-xhtml=userapp-Firefox-SWW0Y1.desktop -application/x-extension-xht=userapp-Firefox-SWW0Y1.desktop +x-scheme-handler/chrome=firefox.desktop +text/html=firefox.desktop +application/x-extension-htm=firefox.desktop +application/x-extension-html=firefox.desktop +application/x-extension-shtml=firefox.desktop +application/xhtml+xml=firefox.desktop +application/x-extension-xhtml=firefox.desktop +application/x-extension-xht=firefox.desktop application/pdf=org.pwmt.zathura-pdf-poppler.desktop image/jpeg=sxiv.desktop image/gif=nsxiv.desktop diff --git a/.config/mpv/fonts/uosc_icons.otf b/.config/mpv/fonts/uosc_icons.otf Binary files differdeleted file mode 100644 index 4c4e0dc..0000000 --- a/.config/mpv/fonts/uosc_icons.otf +++ /dev/null diff --git a/.config/mpv/fonts/uosc_textures.ttf b/.config/mpv/fonts/uosc_textures.ttf Binary files differdeleted file mode 100644 index e89f1d8..0000000 --- a/.config/mpv/fonts/uosc_textures.ttf +++ /dev/null diff --git a/.config/mpv/script-opts/uosc.conf b/.config/mpv/script-opts/uosc.conf deleted file mode 100644 index a9d28e1..0000000 --- a/.config/mpv/script-opts/uosc.conf +++ /dev/null @@ -1,203 +0,0 @@ -# Display style of current position. available: line, bar -timeline_style=line -# Line display style config -timeline_line_width=2 -timeline_line_width_fullscreen=2 -# Scale the width of the line when minimized (timeline_size_min) -timeline_line_width_minimized_scale=2 -# Timeline size when minimized, 0 will hide it completely -timeline_size_min=0 -# 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=50 -# Same thing as calling toggle-progress command once on startup -timeline_start_hidden=no -# Comma separated states when timeline should always be visible. available: paused, audio, image, video, idle -timeline_persistency= -# Timeline opacity -timeline_opacity=1 -# Top border of background color to help visually separate timeline from video -timeline_border=2 -# When scrolling above timeline, wheel will seek by this amount of seconds -timeline_step=5 -# Opacity of chapter indicators in timeline, 0 to disable -timeline_chapters_opacity=0.8 -# Render cache indicators for streaming content -timeline_cache=yes - -# A comma delimited list of items to construct the controls bar above the timeline. Set to `never` to disable. -# Parameter spec: enclosed in `{}` means value, enclosed in `[]` means optional -# Full item syntax: `[<[!]{disposition1}[,[!]{dispositionN}]>]{element}[:{paramN}][#{badge}[>{limit}]][?{tooltip}]` -# Common properties: -# `{icon}` - parameter used to specify an icon name (example: `face`) -# - you can pick one here: https://fonts.google.com/icons?selected=Material+Icons&icon.style=Rounded -# `{element}`s and their parameters: -# `{usoc_command}` - preconfigured shorthands for uosc commands that make sense to have as buttons: -# - `menu`, `subtitles`, `audio`, `video`, `playlist`, `chapters`, `editions`, `stream-quality`, -# `open-file`, `items`, `next`, `prev`, `first`, `last`, `audio-device` -# `fullscreen` - toggle fullscreen -# `loop-playlist` - button to toggle playlist looping -# `loop-file` - button to toggle current file looping -# `shuffle` - toggle for uosc's shuffle mode -# `speed[:{scale}]` - display speed slider, [{scale}] - factor of controls_size, default: 1.3 -# `command:{icon}:{command}` - button that executes a {command} when pressed -# `toggle:{icon}:{prop}[@{owner}]` - button that toggles mpv property -# `cycle:{default_icon}:{prop}[@{owner}]:{value1}[={icon1}][!]/{valueN}[={iconN}][!]` -# - button that cycles mpv property between values, each optionally having different icon and active flag -# - presence of `!` at the end will style the button as active -# - `{owner}` is the name of a scrip that manages this property if any -# `gap[:{scale}]` - display an empty gap, {scale} - factor of controls_size, default: 0.3 -# `space` - fills all available space between previous and next item, useful to align items to the right -# - multiple spaces divide the available space among themselves, which can be used for centering -# Item visibility control: -# `<[!]{disposition1}[,[!]{dispositionN}]>` - optional prefix to control element's visibility -# - `{disposition}` can be one of: -# - `idle` - true if mpv is in idle mode (no file loaded) -# - `image` - true if current file is a single image -# - `audio` - true for audio only files -# - `video` - true for files with a video track -# - `has_many_video` - true for files with more than one video track -# - `has_audio` - true for files with an audio track -# - `has_many_audio` - true for files with more than one audio track -# - `has_sub` - true for files with an subtitle track -# - `has_many_sub` - true for files with more than one subtitle track -# - `has_many_edition` - true for files with more than one edition -# - `has_chapter` - true for files with chapter list -# - `stream` - true if current file is read from a stream -# - `has_playlist` - true if current playlist has 2 or more items in it -# - prefix with `!` to negate the required disposition -# Examples: -# - `<stream>stream-quality` - show stream quality button only for streams -# - `<has_audio,!audio>audio` - show audio tracks button for all files that have -# an audio track, but are not exclusively audio only files -# Place `#{badge}[>{limit}]` after the element params to give it a badge. Available badges: -# `sub`, `audio`, `video` - track type counters -# `{mpv_prop}` - any mpv prop that makes sense to you: https://mpv.io/manual/master/#property-list -# - if prop value is an array it'll display its size -# `>{limit}` will display the badge only if it's numerical value is above this threshold. -# Example: `#audio>1` -# Place `?{tooltip}` after the element config to give it a tooltip. -# Example: `<stream>stream-quality?Stream quality` -# Example implementations of some of the available shorthands: -# menu = command:menu:script-binding uosc/menu-blurred?Menu -# subtitles = command:subtitles:script-binding uosc/subtitles#sub?Subtitles -# fullscreen = cycle:crop_free:fullscreen:no/yes=fullscreen_exit!?Fullscreen -# loop-playlist = cycle:repeat:loop-playlist:no/inf!?Loop playlist -# toggle:{icon}:{prop} = cycle:{icon}:{prop}:no/yes! -controls=menu,gap,subtitles,<has_many_audio>audio,<has_many_video>video,<has_many_edition>editions,<stream>stream-quality,gap,space,speed,space,shuffle,loop-playlist,loop-file,gap,prev,items,next,gap,fullscreen -controls_size=32 -controls_size_fullscreen=40 -controls_margin=8 -controls_spacing=2 -controls_persistency= - -# Where to display volume controls: none, left, right -volume=right -volume_size=40 -volume_size_fullscreen=52 -volume_opacity=0.9 -volume_border=1 -volume_step=1 -volume_persistency= - -# Playback speed widget: mouse drag or wheel to change, click to reset -speed_opacity=0.6 -speed_step=0.1 -speed_step_is_factor=no -speed_persistency= - -# Controls all menus, such as context menu, subtitle loader/selector, etc -menu_item_height=36 -menu_item_height_fullscreen=50 -menu_min_width=260 -menu_min_width_fullscreen=360 -menu_opacity=1 -menu_parent_opacity=0.4 - -# Top bar with window controls and media title -# Can be: never, no-border, always -top_bar=no-border -top_bar_size=40 -top_bar_size_fullscreen=46 -top_bar_controls=yes -top_bar_title=yes -top_bar_title_opacity=0.8 -top_bar_persistency= - -# Window border drawn in no-border mode -window_border_size=1 -window_border_opacity=0.8 - -# If there's no playlist and file ends, load next file in the directory -# Requires `keep-open=yes` in `mpv.conf`. -autoload=no -# Enable uosc's playlist/directory shuffle mode -# This simply makes the next selected playlist or directory item be random, just -# like any other player in the world. It also has an easily togglable control button. -shuffle=no - -# Scale the interface by this factor -ui_scale=1 -# Adjust the text scaling to fit your font -font_scale=1 -# Border of text and icons when drawn directly on top of video -text_border=1.2 -# Use a faster estimation method instead of accurate measurement -# setting this to `no` might have a noticable impact on performance, especially in large menus. -text_width_estimation=yes -# Execute command for background clicks shorter than this number of milliseconds, 0 to disable -# Execution always waits for `input-doubleclick-time` to filter out double-clicks -click_threshold=0 -click_command=cycle pause; script-binding uosc/flash-pause-indicator -# Flash duration in milliseconds used by `flash-{element}` commands -flash_duration=1000 -# Distances in pixels below which elements are fully faded in/out -proximity_in=60 -proximity_out=10 -# RGB HEX color codes -foreground=ebdbb2 -foreground_text=1d2021 -background=1d2021 -background_text=ebdbb2 -# Use only bold font weight throughout the whole UI -font_bold=no -# Show total time instead of time remaining -total_time=yes -# Display sub second fraction in timestamps up to this precision -time_precision=0 -# Display stream's buffered time in timeline if it's lower than this amount of seconds, 0 to disable -buffered_time_threshold=60 -# Hide UI when mpv autohides the cursor -autohide=yes -# Can be: none, flash, static, manual (controlled by flash-pause-indicator and decide-pause-indicator commands) -pause_indicator=none -# Screen dim when stuff like menu is open, 0 to disable -curtain_opacity=0.4 -# Sizes to list in stream quality menu -stream_quality_options=4320,2160,1440,1080,720,480,360,240,144 -# File types to look for when navigating media files -media_types=3g2,3gp,aac,aiff,ape,apng,asf,au,avi,avif,bmp,dsf,f4v,flac,flv,gif,h264,h265,j2k,jp2,jfif,jpeg,jpg,jxl,m2ts,m4a,m4v,mid,midi,mj2,mka,mkv,mov,mp3,mp4,mp4a,mp4v,mpeg,mpg,oga,ogg,ogm,ogv,opus,png,rm,rmvb,spx,svg,tak,tga,tta,tif,tiff,ts,vob,wav,weba,webm,webp,wma,wmv,wv,y4m -# File types to look for when loading external subtitles -subtitle_types=aqt,ass,gsub,idx,jss,lrc,mks,pgs,pjs,psb,rt,slt,smi,sub,sup,srt,ssa,ssf,ttxt,txt,usf,vt,vtt -# Default open-file menu directory -default_directory=~/ - -# Convers some common chapter types into chapter range indicators. -# Instead of displaying the start of the chapter as a diamond icon on top of the -# timeline, the portion of the timeline owned by that chapter is colored based -# on the config below. -# -# The syntax is a comma delimited list of `{type}:{color}` pairs, where. -# `{type}` - range type. Currently suported ones are: -# - `openings`, `endings` - anime openings/endings -# - `intros`, `outros` - video intros/outros -# - `ads` - sponsor segments created by script: https://github.com/po5/mpv_sponsorblock -# `{color}` - an RGB(A) HEX color code (`rrggbb`, or `rrggbbaa`) -# -# To not convert any of the range types, simply removed it from the list. -chapter_ranges=openings:30abf964,endings:30abf964,ads:c54e4e80 -# Add alternative lua patterns to identify begining's of simple chapter ranges (all but `ads`) -# Syntax: `{type}:{pattern}[,{patternN}][;{type}:{pattern}[,{patternN}]]` -chapter_range_patterns=openings:オープニング;endings:エンディング diff --git a/.config/mpv/scripts-osc/quality-menu-osc.lua b/.config/mpv/scripts-osc/quality-menu-osc.lua deleted file mode 100644 index 0a2f6c2..0000000 --- a/.config/mpv/scripts-osc/quality-menu-osc.lua +++ /dev/null @@ -1,2911 +0,0 @@ -local assdraw = require 'mp.assdraw' -local msg = require 'mp.msg' -local opt = require 'mp.options' -local utils = require 'mp.utils' - --- --- Parameters --- --- default user option values --- do not touch, change them in osc.conf -local user_opts = { - showwindowed = true, -- show OSC when windowed? - showfullscreen = true, -- show OSC when fullscreen? - idlescreen = true, -- show mpv logo on idle - 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 = true, -- scale the controller with the video? - valign = 0.8, -- vertical alignment, -1 (top) to 1 (bottom) - halign = 0, -- horizontal alignment, -1 (left) to 1 (right) - barmargin = 0, -- vertical margin of top/bottombar - boxalpha = 80, -- alpha of the background box, - -- 0 (opaque) to 255 (fully transparent) - 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 - deadzonesize = 0.5, -- size of deadzone - 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) - layout = "bottombar", - seekbarstyle = "bar", -- bar, diamond or knob - seekbarhandlesize = 0.6, -- size ratio of the diamond and knob handle - seekrangestyle = "inverted",-- bar, line, slider, inverted or none - seekrangeseparate = true, -- whether the seekranges overlay on the bar-style seekbar - seekrangealpha = 200, -- 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 - tooltipborder = 1, -- border of tooltip in bottom/topbar - timetotal = false, -- display total time instead of remaining time? - timems = false, -- display timecodes with milliseconds? - tcspace = 100, -- timecode spacing (compensate font size estimation) - visibility = "auto", -- only used at init to set visibility_mode(...) - boxmaxchars = 80, -- title crop threshold for box layout - boxvideo = false, -- apply osc_param.video_margins to video - windowcontrols = "auto", -- whether to show window controls - windowcontrols_alignment = "right", -- which side to show window controls on - greenandgrumpy = false, -- disable santa hat - livemarkers = true, -- update seekbar chapter markers on duration change - chapters_osd = true, -- whether to show chapters OSD on next/prev - playlist_osd = true, -- whether to show playlist OSD on next/prev - chapter_fmt = "Chapter: %s", -- chapter print format for seekbar-hover. "no" to disable - unicodeminus = false, -- whether to use the Unicode minus sign character -} - --- read options from config and command-line -opt.read_options(user_opts, "osc", function(list) update_options(list) end) - -local osc_param = { -- calculated by osc_init() - playresy = 0, -- canvas size Y - playresx = 0, -- canvas size X - display_aspect = 1, - unscaled_y = 0, - areas = {}, - video_margins = { - l = 0, r = 0, t = 0, b = 0, -- left/right/top/bottom - }, -} - -local osc_styles = { - bigButtons = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs50\\fnmpv-osd-symbols}", - smallButtonsL = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs19\\fnmpv-osd-symbols}", - smallButtonsLlabel = "{\\fscx105\\fscy105\\fn" .. mp.get_property("options/osd-font") .. "}", - smallButtonsR = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs30\\fnmpv-osd-symbols}", - topButtons = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs12\\fnmpv-osd-symbols}", - - elementDown = "{\\1c&H999999}", - timecodes = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs20}", - vidtitle = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs12\\q2}", - box = "{\\rDefault\\blur0\\bord1\\1c&H000000\\3c&HFFFFFF}", - - topButtonsBar = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs18\\fnmpv-osd-symbols}", - smallButtonsBar = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs28\\fnmpv-osd-symbols}", - timecodesBar = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs27}", - timePosBar = "{\\blur0\\bord".. user_opts.tooltipborder .."\\1c&HFFFFFF\\3c&H000000\\fs30}", - vidtitleBar = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs18\\q2}", - - wcButtons = "{\\1c&HFFFFFF\\fs24\\fnmpv-osd-symbols}", - wcTitle = "{\\1c&HFFFFFF\\fs24\\q2}", - 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? - marginsREQ = false, -- is a margins update 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, - using_video_margins = false, - border = true, - maximized = false, - osd = mp.create_osd_overlay("ass-events"), - chapter_list = {}, -- sorted by time -} - -local window_control_box_width = 80 -local tick_delay = 0.03 - -local is_december = os.date("*t").month == 12 - --- --- Helperfunctions --- - -function kill_animation() - state.anistart = nil - state.animation = nil - state.anitype = nil -end - -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 - -local margins_opts = { - {"l", "video-margin-ratio-left"}, - {"r", "video-margin-ratio-right"}, - {"t", "video-margin-ratio-top"}, - {"b", "video-margin-ratio-bottom"}, -} - --- 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 - --- align: -1 .. +1 --- frame: size of the containing area --- obj: size of the object that should be positioned inside the area --- margin: min. distance from object to frame (as long as -1 <= align <= +1) -function get_align(align, frame, obj, margin) - return (frame / 2) + (((frame / 2) - margin - (obj / 2)) * align) -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_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 = "Video", audio = "Audio", sub = "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 = "Available " .. nicetypes[type] .. " Tracks: " - if not tracks_osc or #tracks_osc[type] == 0 then - msg = msg .. "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 - else - return val ~= "no" - end -end - -function window_controls_alignment() - return user_opts.windowcontrols_alignment -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 r1 = 0 - local r2 = 0 - local slider_lo = element.layout.slider - -- offset between element outline and drag-area - local foV = slider_lo.border + slider_lo.gap - - -- calculate positions of min and max points - if (slider_lo.stype ~= "bar") then - r1 = elem_geo.h / 2 - element.slider.min.ele_pos = elem_geo.h / 2 - element.slider.max.ele_pos = elem_geo.w - (elem_geo.h / 2) - if (slider_lo.stype == "diamond") then - r2 = (elem_geo.h - 2 * slider_lo.border) / 2 - elseif (slider_lo.stype == "knob") then - r2 = r1 - end - else - element.slider.min.ele_pos = - slider_lo.border + slider_lo.gap - element.slider.max.ele_pos = - elem_geo.w - (slider_lo.border + slider_lo.gap) - end - - 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() - - -- the box - ass_draw_rr_h_cw(static_ass, 0, 0, elem_geo.w, elem_geo.h, r1, slider_lo.stype == "diamond") - - -- the "hole" - ass_draw_rr_h_ccw(static_ass, slider_lo.border, slider_lo.border, - elem_geo.w - slider_lo.border, elem_geo.h - slider_lo.border, - r2, slider_lo.stype == "diamond") - - -- 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 > 1) then -- draw triangles - - local a = slider_lo.gap / 0.5 --0.866 - - --top - if (slider_lo.nibbles_top) then - static_ass:move_to(s - (a/2), slider_lo.border) - static_ass:line_to(s + (a/2), slider_lo.border) - static_ass:line_to(s, foV) - end - - --bottom - if (slider_lo.nibbles_bottom) then - static_ass:move_to(s - (a/2), - elem_geo.h - slider_lo.border) - static_ass:line_to(s, - elem_geo.h - foV) - static_ass:line_to(s + (a/2), - elem_geo.h - slider_lo.border) - end - - else -- draw 2x1px nibbles - - --top - if (slider_lo.nibbles_top) then - static_ass:rect_cw(s - 1, slider_lo.border, - s + 1, slider_lo.border + slider_lo.gap); - end - - --bottom - if (slider_lo.nibbles_bottom) then - static_ass:rect_cw(s - 1, - elem_geo.h -slider_lo.border -slider_lo.gap, - s + 1, elem_geo.h - slider_lo.border); - 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 --- - --- returns nil or a chapter element from the native property chapter-list -function get_chapter(possec) - local cl = state.chapter_list -- sorted, get latest before possec, if any - - for n=#cl,1,-1 do - if possec >= cl[n].time then - return cl[n] - end - end -end - -function render_elements(master_ass) - - -- when the slider is dragged or hovered and we have a target chapter name - -- then we use it instead of the normal title. we calculate it before the - -- render iterations because the title may be rendered before the slider. - state.forced_title = nil - local se, ae = state.slider_element, elements[state.active_element] - if user_opts.chapter_fmt ~= "no" and se and (ae == se or (not ae and mouse_hit(se))) then - local dur = mp.get_property_number("duration", 0) - if dur > 0 then - local possec = get_slider_value(se) * dur / 100 -- of mouse pos - local ch = get_chapter(possec) - if ch and ch.title and ch.title ~= "" then - state.forced_title = string.format(user_opts.chapter_fmt, ch.title) - end - end - end - - 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 foH, xp - local pos = element.slider.posF() - local foV = slider_lo.border + slider_lo.gap - local innerH = elem_geo.h - (2 * foV) - local seekRanges = element.slider.seekRangesF() - local seekRangeLineHeight = innerH / 5 - - if slider_lo.stype ~= "bar" then - foH = elem_geo.h / 2 - else - foH = slider_lo.border + slider_lo.gap - end - - if pos then - xp = get_slider_ele_pos_for(element, pos) - - if slider_lo.stype ~= "bar" then - local r = (user_opts.seekbarhandlesize * innerH) / 2 - ass_draw_rr_h_cw(elem_ass, xp - r, foH - r, - xp + r, foH + r, - r, slider_lo.stype == "diamond") - else - local h = 0 - if seekRanges and user_opts.seekrangeseparate and slider_lo.rtype ~= "inverted" then - h = seekRangeLineHeight - end - elem_ass:rect_cw(foH, foV, xp, elem_geo.h - foV - h) - - if seekRanges and not user_opts.seekrangeseparate and slider_lo.rtype ~= "inverted" then - -- Punch holes for the seekRanges to be drawn later - for _,range in pairs(seekRanges) do - if range["start"] < pos then - local pstart = get_slider_ele_pos_for(element, range["start"]) - local pend = xp - - if pos > range["end"] then - pend = get_slider_ele_pos_for(element, range["end"]) - end - elem_ass:rect_ccw(pstart, elem_geo.h - foV - seekRangeLineHeight, pend, elem_geo.h - foV) - end - end - end - end - - if slider_lo.rtype == "slider" then - ass_draw_rr_h_cw(elem_ass, foH - innerH / 6, foH - innerH / 6, - xp, foH + innerH / 6, - innerH / 6, slider_lo.stype == "diamond", 0) - ass_draw_rr_h_cw(elem_ass, xp, foH - innerH / 15, - elem_geo.w - foH + innerH / 15, foH + innerH / 15, - 0, slider_lo.stype == "diamond", innerH / 15) - for _,range in pairs(seekRanges or {}) do - local pstart = get_slider_ele_pos_for(element, range["start"]) - local pend = get_slider_ele_pos_for(element, range["end"]) - ass_draw_rr_h_ccw(elem_ass, pstart, foH - innerH / 21, - pend, foH + innerH / 21, - innerH / 21, slider_lo.stype == "diamond") - end - end - end - - if seekRanges then - if slider_lo.rtype ~= "inverted" 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) - end - - 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"]) - - if slider_lo.rtype == "slider" then - ass_draw_rr_h_cw(elem_ass, pstart, foH - innerH / 21, - pend, foH + innerH / 21, - innerH / 21, slider_lo.stype == "diamond") - elseif slider_lo.rtype == "line" then - if slider_lo.stype == "bar" then - elem_ass:rect_cw(pstart, elem_geo.h - foV - seekRangeLineHeight, pend, elem_geo.h - foV) - else - ass_draw_rr_h_cw(elem_ass, pstart - innerH / 8, foH - innerH / 8, - pend + innerH / 8, foH + innerH / 8, - innerH / 8, slider_lo.stype == "diamond") - end - elseif slider_lo.rtype == "bar" then - if slider_lo.stype ~= "bar" then - ass_draw_rr_h_cw(elem_ass, pstart - innerH / 2, foV, - pend + innerH / 2, foV + innerH, - innerH / 2, slider_lo.stype == "diamond") - elseif range["end"] >= (pos or 0) then - elem_ass:rect_cw(pstart, foV, pend, elem_geo.h - foV) - else - elem_ass:rect_cw(pstart, elem_geo.h - foV - seekRangeLineHeight, pend, elem_geo.h - foV) - end - elseif slider_lo.rtype == "inverted" then - if slider_lo.stype ~= "bar" then - ass_draw_rr_h_ccw(elem_ass, pstart, (elem_geo.h / 2) - 1, pend, - (elem_geo.h / 2) + 1, - 1, slider_lo.stype == "diamond") - else - elem_ass:rect_ccw(pstart, (elem_geo.h / 2) - 1, pend, (elem_geo.h / 2) + 1) - end - end - 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 - slider_lo.border - 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 - - local maxchars = element.layout.button.maxchars - if not (maxchars == nil) and (#buttontext > maxchars) then - local max_ratio = 1.25 -- up to 25% more chars while shrinking - local limit = math.max(0, math.floor(maxchars * max_ratio) - 3) - if (#buttontext > limit) then - while (#buttontext > limit) do - buttontext = buttontext:gsub(".[\128-\191]*$", "") - end - buttontext = buttontext .. "..." - end - local _, nchars2 = buttontext:gsub(".[\128-\191]*", "") - local stretch = (maxchars/#buttontext)*100 - buttontext = string.format("{\\fscx%f}", - (maxchars/#buttontext)*100) .. buttontext - end - - elem_ass:append(buttontext) - 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 'Empty playlist.' - end - - local message = string.format('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 'No chapters.' - end - - local message = string.format('Chapters [%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('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, - stype = "slider", - 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(topbar) - local wc_geo = { - x = 0, - y = 30 + user_opts.barmargin, - an = 1, - w = osc_param.playresx, - h = 30, - } - - local alignment = window_controls_alignment() - 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 - - if alignment == "left" then - controlbox_left = wc_geo.x - titlebox_left = wc_geo.x + controlbox_w - titlebox_right = wc_geo.w - end - - add_area("window-controls", - get_hitbox_coords(controlbox_left, wc_geo.y, wc_geo.an, - controlbox_w, wc_geo.h)) - - local lo - - -- Background Bar - new_element("wcbar", "box") - lo = add_layout("wcbar") - lo.geometry = wc_geo - lo.layer = 10 - lo.style = osc_styles.wcBar - lo.alpha[1] = user_opts.boxalpha - - local button_y = wc_geo.y - (wc_geo.h / 2) - local first_geo = - {x = controlbox_left + 5, y = button_y, an = 4, w = 25, h = 25} - local second_geo = - {x = controlbox_left + 30, y = button_y, an = 4, w = 25, h = 25} - local third_geo = - {x = controlbox_left + 55, y = button_y, an = 4, w = 25, h = 25} - - -- 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 = alignment == "left" and first_geo or third_geo - lo.style = osc_styles.wcButtons - - -- Minimize: 🗕 - ne = new_element("minimize", "button") - ne.content = "\238\132\146" - ne.eventresponder["mbtn_left_up"] = - function () mp.commandv("cycle", "window-minimized") end - lo = add_layout("minimize") - lo.geometry = alignment == "left" and second_geo or first_geo - lo.style = osc_styles.wcButtons - - -- 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 = alignment == "left" and third_geo or second_geo - lo.style = osc_styles.wcButtons - - -- deadzone below window controls - local sh_area_y0, sh_area_y1 - sh_area_y0 = user_opts.barmargin - sh_area_y1 = (wc_geo.y + (wc_geo.h / 2)) + - get_align(1 - (2 * user_opts.deadzonesize), - osc_param.playresy - (wc_geo.y + (wc_geo.h / 2)), 0, 0) - add_area("showhide_wc", wc_geo.x, sh_area_y0, wc_geo.w, sh_area_y1) - - if topbar then - -- The title is already there as part of the top bar - return - else - -- Apply boxvideo margins to the control bar - osc_param.video_margins.t = wc_geo.h / osc_param.playresy - end - - -- Window Title - ne = new_element("wctitle", "button") - ne.content = function () - local title = mp.command_native({"expand-text", user_opts.title}) - -- escape ASS, and strip newlines and trailing slashes - title = title:gsub("\\n", " "):gsub("\\$", ""):gsub("{","\\{") - return not (title == "") and title or "mpv" - end - local left_pad = 5 - local right_pad = 10 - lo = add_layout("wctitle") - lo.geometry = - { x = titlebox_left + left_pad, y = wc_geo.y - 3, an = 1, - w = titlebox_w, h = wc_geo.h } - lo.style = string.format("%s{\\clip(%f,%f,%f,%f)}", - osc_styles.wcTitle, - titlebox_left + left_pad, wc_geo.y - wc_geo.h, - titlebox_right - right_pad , wc_geo.y + wc_geo.h) - - add_area("window-controls-title", - titlebox_left, 0, titlebox_right, wc_geo.h) -end - --- --- Layouts --- - -local layouts = {} - --- Classic box layout -layouts["box"] = function () - - local osc_geo = { - w = 550, -- width - h = 138, -- height - r = 10, -- corner-radius - p = 15, -- padding - } - - -- make sure the OSC actually fits into the video - if (osc_param.playresx < (osc_geo.w + (2 * osc_geo.p))) then - osc_param.playresy = (osc_geo.w+(2*osc_geo.p))/osc_param.display_aspect - osc_param.playresx = osc_param.playresy * osc_param.display_aspect - end - - -- position of the controller according to video aspect and valignment - local posX = math.floor(get_align(user_opts.halign, osc_param.playresx, - osc_geo.w, 0)) - local posY = math.floor(get_align(user_opts.valign, osc_param.playresy, - osc_geo.h, 0)) - - -- position offset for contents aligned at the borders of the box - local pos_offsetX = (osc_geo.w - (2*osc_geo.p)) / 2 - local pos_offsetY = (osc_geo.h - (2*osc_geo.p)) / 2 - - osc_param.areas = {} -- delete areas - - -- area for active mouse input - add_area("input", get_hitbox_coords(posX, posY, 5, osc_geo.w, osc_geo.h)) - - -- area for show/hide - local sh_area_y0, sh_area_y1 - if user_opts.valign > 0 then - -- deadzone above OSC - sh_area_y0 = get_align(-1 + (2*user_opts.deadzonesize), - posY - (osc_geo.h / 2), 0, 0) - sh_area_y1 = osc_param.playresy - else - -- deadzone below OSC - sh_area_y0 = 0 - sh_area_y1 = (posY + (osc_geo.h / 2)) + - get_align(1 - (2*user_opts.deadzonesize), - osc_param.playresy - (posY + (osc_geo.h / 2)), 0, 0) - end - add_area("showhide", 0, sh_area_y0, osc_param.playresx, sh_area_y1) - - -- fetch values - local osc_w, osc_h, osc_r, osc_p = - osc_geo.w, osc_geo.h, osc_geo.r, osc_geo.p - - local lo - - -- - -- Background box - -- - - new_element("bgbox", "box") - lo = add_layout("bgbox") - - lo.geometry = {x = posX, y = posY, an = 5, w = osc_w, h = osc_h} - lo.layer = 10 - lo.style = osc_styles.box - lo.alpha[1] = user_opts.boxalpha - lo.alpha[3] = user_opts.boxalpha - lo.box.radius = osc_r - - -- - -- Title row - -- - - local titlerowY = posY - pos_offsetY - 10 - - lo = add_layout("title") - lo.geometry = {x = posX, y = titlerowY, an = 8, w = 496, h = 12} - lo.style = osc_styles.vidtitle - lo.button.maxchars = user_opts.boxmaxchars - - lo = add_layout("pl_prev") - lo.geometry = - {x = (posX - pos_offsetX), y = titlerowY, an = 7, w = 12, h = 12} - lo.style = osc_styles.topButtons - - lo = add_layout("pl_next") - lo.geometry = - {x = (posX + pos_offsetX), y = titlerowY, an = 9, w = 12, h = 12} - lo.style = osc_styles.topButtons - - -- - -- Big buttons - -- - - local bigbtnrowY = posY - pos_offsetY + 35 - local bigbtndist = 60 - - lo = add_layout("playpause") - lo.geometry = - {x = posX, y = bigbtnrowY, an = 5, w = 40, h = 40} - lo.style = osc_styles.bigButtons - - lo = add_layout("skipback") - lo.geometry = - {x = posX - bigbtndist, y = bigbtnrowY, an = 5, w = 40, h = 40} - lo.style = osc_styles.bigButtons - - lo = add_layout("skipfrwd") - lo.geometry = - {x = posX + bigbtndist, y = bigbtnrowY, an = 5, w = 40, h = 40} - lo.style = osc_styles.bigButtons - - lo = add_layout("ch_prev") - lo.geometry = - {x = posX - (bigbtndist * 2), y = bigbtnrowY, an = 5, w = 40, h = 40} - lo.style = osc_styles.bigButtons - - lo = add_layout("ch_next") - lo.geometry = - {x = posX + (bigbtndist * 2), y = bigbtnrowY, an = 5, w = 40, h = 40} - lo.style = osc_styles.bigButtons - - lo = add_layout("cy_audio") - lo.geometry = - {x = posX - pos_offsetX, y = bigbtnrowY, an = 1, w = 70, h = 18} - lo.style = osc_styles.smallButtonsL - - lo = add_layout("cy_sub") - lo.geometry = - {x = posX - pos_offsetX, y = bigbtnrowY, an = 7, w = 70, h = 18} - lo.style = osc_styles.smallButtonsL - - lo = add_layout("tog_fs") - lo.geometry = - {x = posX+pos_offsetX - 25, y = bigbtnrowY, an = 4, w = 25, h = 25} - lo.style = osc_styles.smallButtonsR - - lo = add_layout("volume") - lo.geometry = - {x = posX+pos_offsetX - (25 * 2) - osc_geo.p, - y = bigbtnrowY, an = 4, w = 25, h = 25} - lo.style = osc_styles.smallButtonsR - - -- - -- Seekbar - -- - - lo = add_layout("seekbar") - lo.geometry = - {x = posX, y = posY+pos_offsetY-22, an = 2, w = pos_offsetX*2, h = 15} - lo.style = osc_styles.timecodes - lo.slider.tooltip_style = osc_styles.vidtitle - lo.slider.stype = user_opts["seekbarstyle"] - lo.slider.rtype = user_opts["seekrangestyle"] - - -- - -- Timecodes + Cache - -- - - local bottomrowY = posY + pos_offsetY - 5 - - lo = add_layout("tc_left") - lo.geometry = - {x = posX - pos_offsetX, y = bottomrowY, an = 4, w = 110, h = 18} - lo.style = osc_styles.timecodes - - lo = add_layout("tc_right") - lo.geometry = - {x = posX + pos_offsetX, y = bottomrowY, an = 6, w = 110, h = 18} - lo.style = osc_styles.timecodes - - lo = add_layout("cache") - lo.geometry = - {x = posX, y = bottomrowY, an = 5, w = 110, h = 18} - lo.style = osc_styles.timecodes - -end - --- slim box layout -layouts["slimbox"] = function () - - local osc_geo = { - w = 660, -- width - h = 70, -- height - r = 10, -- corner-radius - } - - -- make sure the OSC actually fits into the video - if (osc_param.playresx < (osc_geo.w)) then - osc_param.playresy = (osc_geo.w)/osc_param.display_aspect - osc_param.playresx = osc_param.playresy * osc_param.display_aspect - end - - -- position of the controller according to video aspect and valignment - local posX = math.floor(get_align(user_opts.halign, osc_param.playresx, - osc_geo.w, 0)) - local posY = math.floor(get_align(user_opts.valign, osc_param.playresy, - osc_geo.h, 0)) - - osc_param.areas = {} -- delete areas - - -- area for active mouse input - add_area("input", get_hitbox_coords(posX, posY, 5, osc_geo.w, osc_geo.h)) - - -- area for show/hide - local sh_area_y0, sh_area_y1 - if user_opts.valign > 0 then - -- deadzone above OSC - sh_area_y0 = get_align(-1 + (2*user_opts.deadzonesize), - posY - (osc_geo.h / 2), 0, 0) - sh_area_y1 = osc_param.playresy - else - -- deadzone below OSC - sh_area_y0 = 0 - sh_area_y1 = (posY + (osc_geo.h / 2)) + - get_align(1 - (2*user_opts.deadzonesize), - osc_param.playresy - (posY + (osc_geo.h / 2)), 0, 0) - end - add_area("showhide", 0, sh_area_y0, osc_param.playresx, sh_area_y1) - - local lo - - local tc_w, ele_h, inner_w = 100, 20, osc_geo.w - 100 - - -- styles - local styles = { - box = "{\\rDefault\\blur0\\bord1\\1c&H000000\\3c&HFFFFFF}", - timecodes = "{\\1c&HFFFFFF\\3c&H000000\\fs20\\bord2\\blur1}", - tooltip = "{\\1c&HFFFFFF\\3c&H000000\\fs12\\bord1\\blur0.5}", - } - - - new_element("bgbox", "box") - lo = add_layout("bgbox") - - lo.geometry = {x = posX, y = posY - 1, an = 2, w = inner_w, h = ele_h} - lo.layer = 10 - lo.style = osc_styles.box - lo.alpha[1] = user_opts.boxalpha - lo.alpha[3] = 0 - if not (user_opts["seekbarstyle"] == "bar") then - lo.box.radius = osc_geo.r - lo.box.hexagon = user_opts["seekbarstyle"] == "diamond" - end - - - lo = add_layout("seekbar") - lo.geometry = - {x = posX, y = posY - 1, an = 2, w = inner_w, h = ele_h} - lo.style = osc_styles.timecodes - lo.slider.border = 0 - lo.slider.gap = 1.5 - lo.slider.tooltip_style = styles.tooltip - lo.slider.stype = user_opts["seekbarstyle"] - lo.slider.rtype = user_opts["seekrangestyle"] - lo.slider.adjust_tooltip = false - - -- - -- Timecodes - -- - - lo = add_layout("tc_left") - lo.geometry = - {x = posX - (inner_w/2) + osc_geo.r, y = posY + 1, - an = 7, w = tc_w, h = ele_h} - lo.style = styles.timecodes - lo.alpha[3] = user_opts.boxalpha - - lo = add_layout("tc_right") - lo.geometry = - {x = posX + (inner_w/2) - osc_geo.r, y = posY + 1, - an = 9, w = tc_w, h = ele_h} - lo.style = styles.timecodes - lo.alpha[3] = user_opts.boxalpha - - -- Cache - - lo = add_layout("cache") - lo.geometry = - {x = posX, y = posY + 1, - an = 8, w = tc_w, h = ele_h} - lo.style = styles.timecodes - lo.alpha[3] = user_opts.boxalpha - - -end - -function bar_layout(direction) - local osc_geo = { - x = -2, - y, - an = (direction < 0) and 7 or 1, - w, - h = 56, - } - - local padX = 9 - local padY = 3 - local buttonW = 27 - local tcW = (state.tc_ms) and 170 or 110 - if user_opts.tcspace >= 50 and user_opts.tcspace <= 200 then - -- adjust our hardcoded font size estimation - tcW = tcW * user_opts.tcspace / 100 - end - - local tsW = 90 - local minW = (buttonW + padX)*5 + (tcW + padX)*4 + (tsW + padX)*2 - - -- Special topbar handling when window controls are present - local padwc_l - local padwc_r - if direction < 0 or not window_controls_enabled() then - padwc_l = 0 - padwc_r = 0 - elseif window_controls_alignment() == "left" then - padwc_l = window_control_box_width - padwc_r = 0 - else - padwc_l = 0 - padwc_r = window_control_box_width - end - - if ((osc_param.display_aspect > 0) and (osc_param.playresx < minW)) then - osc_param.playresy = minW / osc_param.display_aspect - osc_param.playresx = osc_param.playresy * osc_param.display_aspect - end - - osc_geo.y = direction * (54 + user_opts.barmargin) - osc_geo.w = osc_param.playresx + 4 - if direction < 0 then - osc_geo.y = osc_geo.y + osc_param.playresy - end - - local line1 = osc_geo.y - direction * (9 + padY) - local line2 = osc_geo.y - direction * (36 + padY) - - osc_param.areas = {} - - add_area("input", get_hitbox_coords(osc_geo.x, osc_geo.y, osc_geo.an, - osc_geo.w, osc_geo.h)) - - local sh_area_y0, sh_area_y1 - if direction > 0 then - -- deadzone below OSC - sh_area_y0 = user_opts.barmargin - sh_area_y1 = (osc_geo.y + (osc_geo.h / 2)) + - get_align(1 - (2*user_opts.deadzonesize), - osc_param.playresy - (osc_geo.y + (osc_geo.h / 2)), 0, 0) - else - -- deadzone above OSC - sh_area_y0 = get_align(-1 + (2*user_opts.deadzonesize), - osc_geo.y - (osc_geo.h / 2), 0, 0) - sh_area_y1 = osc_param.playresy - user_opts.barmargin - end - add_area("showhide", 0, sh_area_y0, osc_param.playresx, sh_area_y1) - - local lo, geo - - -- Background bar - new_element("bgbox", "box") - lo = add_layout("bgbox") - - lo.geometry = osc_geo - lo.layer = 10 - lo.style = osc_styles.box - lo.alpha[1] = user_opts.boxalpha - - - -- Playlist prev/next - geo = { x = osc_geo.x + padX, y = line1, - an = 4, w = 18, h = 18 - padY } - lo = add_layout("pl_prev") - lo.geometry = geo - lo.style = osc_styles.topButtonsBar - - geo = { x = geo.x + geo.w + padX, y = geo.y, an = geo.an, w = geo.w, h = geo.h } - lo = add_layout("pl_next") - lo.geometry = geo - lo.style = osc_styles.topButtonsBar - - local t_l = geo.x + geo.w + padX - - -- Cache - geo = { x = osc_geo.x + osc_geo.w - padX, y = geo.y, - an = 6, w = 150, h = geo.h } - lo = add_layout("cache") - lo.geometry = geo - lo.style = osc_styles.vidtitleBar - - local t_r = geo.x - geo.w - padX*2 - - -- Title - geo = { x = t_l, y = geo.y, an = 4, - w = t_r - t_l, h = geo.h } - lo = add_layout("title") - lo.geometry = geo - lo.style = string.format("%s{\\clip(%f,%f,%f,%f)}", - osc_styles.vidtitleBar, - geo.x, geo.y-geo.h, geo.w, geo.y+geo.h) - - - -- Playback control buttons - geo = { x = osc_geo.x + padX + padwc_l, y = line2, an = 4, - w = buttonW, h = 36 - padY*2} - lo = add_layout("playpause") - lo.geometry = geo - lo.style = osc_styles.smallButtonsBar - - geo = { x = geo.x + geo.w + padX, y = geo.y, an = geo.an, w = geo.w, h = geo.h } - lo = add_layout("ch_prev") - lo.geometry = geo - lo.style = osc_styles.smallButtonsBar - - geo = { x = geo.x + geo.w + padX, y = geo.y, an = geo.an, w = geo.w, h = geo.h } - lo = add_layout("ch_next") - lo.geometry = geo - lo.style = osc_styles.smallButtonsBar - - -- Left timecode - geo = { x = geo.x + geo.w + padX + tcW, y = geo.y, an = 6, - w = tcW, h = geo.h } - lo = add_layout("tc_left") - lo.geometry = geo - lo.style = osc_styles.timecodesBar - - local sb_l = geo.x + padX - - -- Fullscreen button - geo = { x = osc_geo.x + osc_geo.w - buttonW - padX - padwc_r, y = geo.y, an = 4, - w = buttonW, h = geo.h } - lo = add_layout("tog_fs") - lo.geometry = geo - lo.style = osc_styles.smallButtonsBar - - -- START quality-menu - geo = { x = geo.x - geo.w - padX, y = geo.y, an = geo.an, w = geo.w, h = geo.h } - lo = add_layout("quality-menu") - lo.geometry = geo - lo.style = osc_styles.smallButtonsBar - -- END quality-menu - - -- Volume - geo = { x = geo.x - geo.w - padX, y = geo.y, an = geo.an, w = geo.w, h = geo.h } - lo = add_layout("volume") - lo.geometry = geo - lo.style = osc_styles.smallButtonsBar - - -- Track selection buttons - geo = { x = geo.x - tsW - padX, y = geo.y, an = geo.an, w = tsW, h = geo.h } - lo = add_layout("cy_sub") - lo.geometry = geo - lo.style = osc_styles.smallButtonsBar - - geo = { x = geo.x - geo.w - padX, y = geo.y, an = geo.an, w = geo.w, h = geo.h } - lo = add_layout("cy_audio") - lo.geometry = geo - lo.style = osc_styles.smallButtonsBar - - - -- Right timecode - geo = { x = geo.x - padX - tcW - 10, y = geo.y, an = geo.an, - w = tcW, h = geo.h } - lo = add_layout("tc_right") - lo.geometry = geo - lo.style = osc_styles.timecodesBar - - local sb_r = geo.x - padX - - - -- Seekbar - geo = { x = sb_l, y = geo.y, an = geo.an, - w = math.max(0, sb_r - sb_l), h = geo.h } - new_element("bgbar1", "box") - lo = add_layout("bgbar1") - - lo.geometry = geo - lo.layer = 15 - lo.style = osc_styles.timecodesBar - lo.alpha[1] = - math.min(255, user_opts.boxalpha + (255 - user_opts.boxalpha)*0.8) - if not (user_opts["seekbarstyle"] == "bar") then - lo.box.radius = geo.h / 2 - lo.box.hexagon = user_opts["seekbarstyle"] == "diamond" - end - - lo = add_layout("seekbar") - lo.geometry = geo - lo.style = osc_styles.timecodesBar - lo.slider.border = 0 - lo.slider.gap = 2 - lo.slider.tooltip_style = osc_styles.timePosBar - lo.slider.tooltip_an = 5 - lo.slider.stype = user_opts["seekbarstyle"] - lo.slider.rtype = user_opts["seekrangestyle"] - - if direction < 0 then - osc_param.video_margins.b = osc_geo.h / osc_param.playresy - else - osc_param.video_margins.t = osc_geo.h / osc_param.playresy - end -end - -layouts["bottombar"] = function() - bar_layout(-1) -end - -layouts["topbar"] = function() - bar_layout(1) -end - --- Validate string type user options -function validate_user_opts() - if layouts[user_opts.layout] == nil then - msg.warn("Invalid setting \""..user_opts.layout.."\" for layout") - user_opts.layout = "bottombar" - end - - if user_opts.seekbarstyle ~= "bar" and - user_opts.seekbarstyle ~= "diamond" and - user_opts.seekbarstyle ~= "knob" then - msg.warn("Invalid setting \"" .. user_opts.seekbarstyle - .. "\" for seekbarstyle") - user_opts.seekbarstyle = "bar" - end - - if user_opts.seekrangestyle ~= "bar" and - user_opts.seekrangestyle ~= "line" and - user_opts.seekrangestyle ~= "slider" and - user_opts.seekrangestyle ~= "inverted" and - user_opts.seekrangestyle ~= "none" then - msg.warn("Invalid setting \"" .. user_opts.seekrangestyle - .. "\" for seekrangestyle") - user_opts.seekrangestyle = "inverted" - end - - if user_opts.seekrangestyle == "slider" and - user_opts.seekbarstyle == "bar" then - msg.warn("Using \"slider\" seekrangestyle together with \"bar\" seekbarstyle is not supported") - user_opts.seekrangestyle = "inverted" - end - - 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 - if user_opts.windowcontrols_alignment ~= "right" and - user_opts.windowcontrols_alignment ~= "left" then - msg.warn("windowcontrols_alignment cannot be \"" .. - user_opts.windowcontrols_alignment .. "\". Ignoring.") - user_opts.windowcontrols_alignment = "right" - end -end - -function update_options(list) - validate_user_opts() - request_tick() - visibility_mode(user_opts.visibility, true) - update_duration_watch() - request_init() -end - -local UNICODE_MINUS = string.char(0xe2, 0x88, 0x92) -- UTF-8 for U+2212 MINUS SIGN - --- 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 - - osc_param.video_margins = {l = 0, r = 0, t = 0, b = 0} - - 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 - - -- title - ne = new_element("title", "button") - - ne.content = function () - local title = state.forced_title or - mp.command_native({"expand-text", user_opts.title}) - -- escape ASS, and strip newlines and trailing slashes - title = title:gsub("\\n", " "):gsub("\\$", ""):gsub("{","\\{") - return not (title == "") and title or "mpv" - end - - ne.eventresponder["mbtn_left_up"] = function () - local title = mp.get_property_osd("media-title") - if (have_pl) then - title = string.format("[%d/%d] %s", countone(pl_pos - 1), - pl_count, title) - end - show_message(title) - end - - ne.eventresponder["mbtn_right_up"] = - function () show_message(mp.get_property_osd("filename")) end - - -- playlist buttons - - -- prev - ne = new_element("pl_prev", "button") - - ne.content = "\238\132\144" - ne.enabled = (pl_pos > 1) or (loop ~= "no") - ne.eventresponder["mbtn_left_up"] = - function () - mp.commandv("playlist-prev", "weak") - if user_opts.playlist_osd then - show_message(get_playlist(), 3) - end - end - ne.eventresponder["shift+mbtn_left_up"] = - function () show_message(get_playlist(), 3) end - ne.eventresponder["mbtn_right_up"] = - function () show_message(get_playlist(), 3) end - - --next - ne = new_element("pl_next", "button") - - ne.content = "\238\132\129" - ne.enabled = (have_pl and (pl_pos < pl_count)) or (loop ~= "no") - ne.eventresponder["mbtn_left_up"] = - function () - mp.commandv("playlist-next", "weak") - if user_opts.playlist_osd then - show_message(get_playlist(), 3) - end - end - ne.eventresponder["shift+mbtn_left_up"] = - function () show_message(get_playlist(), 3) end - ne.eventresponder["mbtn_right_up"] = - function () show_message(get_playlist(), 3) end - - - -- big buttons - - --playpause - ne = new_element("playpause", "button") - - ne.content = function () - if mp.get_property("pause") == "yes" then - return ("\238\132\129") - else - return ("\238\128\130") - end - end - ne.eventresponder["mbtn_left_up"] = - function () mp.commandv("cycle", "pause") end - - --skipback - ne = new_element("skipback", "button") - - ne.softrepeat = true - ne.content = "\238\128\132" - ne.eventresponder["mbtn_left_down"] = - 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.commandv("seek", -30, "relative", "keyframes") end - - --skipfrwd - ne = new_element("skipfrwd", "button") - - ne.softrepeat = true - ne.content = "\238\128\133" - ne.eventresponder["mbtn_left_down"] = - function () mp.commandv("seek", 10, "relative", "keyframes") end - ne.eventresponder["shift+mbtn_left_down"] = - function () mp.commandv("frame-step") end - ne.eventresponder["mbtn_right_down"] = - function () mp.commandv("seek", 60, "relative", "keyframes") end - - --ch_prev - ne = new_element("ch_prev", "button") - - ne.enabled = have_ch - ne.content = "\238\132\132" - ne.eventresponder["mbtn_left_up"] = - function () - mp.commandv("add", "chapter", -1) - if user_opts.chapters_osd then - show_message(get_chapterlist(), 3) - end - end - ne.eventresponder["shift+mbtn_left_up"] = - function () show_message(get_chapterlist(), 3) end - ne.eventresponder["mbtn_right_up"] = - function () show_message(get_chapterlist(), 3) end - - --ch_next - ne = new_element("ch_next", "button") - - ne.enabled = have_ch - ne.content = "\238\132\133" - ne.eventresponder["mbtn_left_up"] = - function () - mp.commandv("add", "chapter", 1) - if user_opts.chapters_osd then - show_message(get_chapterlist(), 3) - end - end - ne.eventresponder["shift+mbtn_left_up"] = - function () show_message(get_chapterlist(), 3) end - ne.eventresponder["mbtn_right_up"] = - function () show_message(get_chapterlist(), 3) end - - -- - update_tracklist() - - --cy_audio - ne = new_element("cy_audio", "button") - - ne.enabled = (#tracks_osc.audio > 0) - ne.content = function () - local aid = "–" - if not (get_track("audio") == 0) then - aid = get_track("audio") - end - return ("\238\132\134" .. osc_styles.smallButtonsLlabel - .. " " .. aid .. "/" .. #tracks_osc.audio) - 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"), 2) end - - --cy_sub - ne = new_element("cy_sub", "button") - - ne.enabled = (#tracks_osc.sub > 0) - ne.content = function () - local sid = "–" - if not (get_track("sub") == 0) then - sid = get_track("sub") - end - return ("\238\132\135" .. osc_styles.smallButtonsLlabel - .. " " .. sid .. "/" .. #tracks_osc.sub) - 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"), 2) end - - --tog_fs - ne = new_element("tog_fs", "button") - ne.content = function () - if (state.fullscreen) then - return ("\238\132\137") - else - return ("\238\132\136") - end - end - ne.eventresponder["mbtn_left_up"] = - function () mp.commandv("cycle", "fullscreen") end - - --seekbar - ne = new_element("seekbar", "slider") - - ne.enabled = not (mp.get_property("percent-pos") == nil) - state.slider_element = ne.enabled and ne or nil -- used for forced_title - 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 user_opts.seekrangestyle == "none" 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) - -- 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") end - ne.eventresponder["reset"] = - function (element) element.state.lastseek = nil 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.visible = (mp.get_property_number("duration", 0) > 0) - ne.content = function () - if (state.rightTC_trem) then - local minus = user_opts.unicodeminus and UNICODE_MINUS or "-" - if state.tc_ms then - return (minus..mp.get_property_osd("playtime-remaining/full")) - else - return (minus..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 - - -- cache - ne = new_element("cache", "button") - - ne.content = function () - local cache_state = state.cache_state - if not (cache_state and cache_state["seekable-ranges"] and - #cache_state["seekable-ranges"] > 0) then - -- probably not a network stream - return "" - end - local dmx_cache = cache_state and cache_state["cache-duration"] - local thresh = math.min(state.dmx_cache * 0.05, 5) -- 5% or 5s - if dmx_cache and math.abs(dmx_cache - state.dmx_cache) >= thresh then - state.dmx_cache = dmx_cache - else - dmx_cache = state.dmx_cache - end - local min = math.floor(dmx_cache / 60) - local sec = math.floor(dmx_cache % 60) -- don't round e.g. 59.9 to 60 - return "Cache: " .. (min > 0 and - string.format("%sm%02.0fs", min, sec) or - string.format("%3.0fs", sec)) - end - - -- START quality-menu - ne = new_element("quality-menu", "button") - ne.content = function() - return ("≚") - end - ne.eventresponder["mbtn_left_up"] = - function () mp.commandv("script-message", "video_formats_toggle") end - ne.eventresponder["mbtn_right_up"] = - function () mp.commandv("script-message", "audio_formats_toggle") end - -- END quality-menu - - -- volume - ne = new_element("volume", "button") - - ne.content = function() - local volume = mp.get_property_number("volume", 0) - local mute = mp.get_property_native("mute") - local volicon = {"\238\132\139", "\238\132\140", - "\238\132\141", "\238\132\142"} - if volume == 0 or mute then - return "\238\132\138" - else - return volicon[math.min(4,math.ceil(volume / (100/3)))] - end - end - ne.eventresponder["mbtn_left_up"] = - function () mp.commandv("cycle", "mute") end - - ne.eventresponder["wheel_up_press"] = - function () mp.commandv("osd-auto", "add", "volume", 5) end - ne.eventresponder["wheel_down_press"] = - function () mp.commandv("osd-auto", "add", "volume", -5) end - - - -- load layout - layouts[user_opts.layout]() - - -- load window controls - if window_controls_enabled() then - window_controls(user_opts.layout == "topbar") - end - - --do something with the elements - prepare_elements() - - update_margins() -end - -function reset_margins() - if state.using_video_margins then - for _, opt in ipairs(margins_opts) do - mp.set_property_number(opt[2], 0.0) - end - state.using_video_margins = false - end -end - -function update_margins() - local margins = osc_param.video_margins - - -- Don't use margins if it's visible only temporarily. - if (not state.osc_visible) or (get_hidetimeout() >= 0) or - (state.fullscreen and not user_opts.showfullscreen) or - (not state.fullscreen and not user_opts.showwindowed) - then - margins = {l = 0, r = 0, t = 0, b = 0} - end - - if user_opts.boxvideo then - -- check whether any margin option has a non-default value - local margins_used = false - - if not state.using_video_margins then - for _, opt in ipairs(margins_opts) do - if mp.get_property_number(opt[2], 0.0) ~= 0.0 then - margins_used = true - end - end - end - - if not margins_used then - for _, opt in ipairs(margins_opts) do - local v = margins[opt[1]] - if (v ~= 0) or state.using_video_margins then - mp.set_property_number(opt[2], v) - state.using_video_margins = true - end - end - end - else - reset_margins() - end - - utils.shared_script_property_set("osc-margins", - string.format("%f,%f,%f,%f", margins.l, margins.r, margins.t, margins.b)) -end - -function shutdown() - reset_margins() - utils.shared_script_property_set("osc-margins", nil) -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 - update_margins() - end - request_tick() -end - -function pause_state(name, enabled) - state.paused = enabled - 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.data = "" -- allows set_osd to immediately update on enable - 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.active_element then - -- mouse is held down on some element - keep ticking and igore initReq - -- till it's released, or else the mouse-up (click) will misbehave or - -- get ignored. that's because osc_init() recreates the osc elements, - -- but mouse handling depends on the elements staying unmodified - -- between mouse-down and mouse-up (using the index active_element). - request_tick() - elseif state.initREQ then - osc_init() - state.initREQ = false - - -- store initial mouse position - if (state.last_mouseX == nil or state.last_mouseY == nil) - and not (mouseX == nil or mouseY == nil) then - - state.last_mouseX, state.last_mouseY = mouseX, mouseY - end - end - - - -- fade animation - if 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 - kill_animation() - end - else - kill_animation() - 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 - end - - -- ensure rendering after any (mouse) event - icons could change etc - request_tick() -end - - -local logo_lines = { - -- White border - "{\\c&HE5E5E5&\\p6}m 895 10 b 401 10 0 410 0 905 0 1399 401 1800 895 1800 1390 1800 1790 1399 1790 905 1790 410 1390 10 895 10 {\\p0}", - -- Purple fill - "{\\c&H682167&\\p6}m 925 42 b 463 42 87 418 87 880 87 1343 463 1718 925 1718 1388 1718 1763 1343 1763 880 1763 418 1388 42 925 42{\\p0}", - -- Darker fill - "{\\c&H430142&\\p6}m 1605 828 b 1605 1175 1324 1456 977 1456 631 1456 349 1175 349 828 349 482 631 200 977 200 1324 200 1605 482 1605 828{\\p0}", - -- White fill - "{\\c&HDDDBDD&\\p6}m 1296 910 b 1296 1131 1117 1310 897 1310 676 1310 497 1131 497 910 497 689 676 511 897 511 1117 511 1296 689 1296 910{\\p0}", - -- Triangle - "{\\c&H691F69&\\p6}m 762 1113 l 762 708 b 881 776 1000 843 1119 911 1000 978 881 1046 762 1113{\\p0}", -} - -local santa_hat_lines = { - -- Pompoms - "{\\c&HC0C0C0&\\p6}m 500 -323 b 491 -322 481 -318 475 -311 465 -312 456 -319 446 -318 434 -314 427 -304 417 -297 410 -290 404 -282 395 -278 390 -274 387 -267 381 -265 377 -261 379 -254 384 -253 397 -244 409 -232 425 -228 437 -228 446 -218 457 -217 462 -216 466 -213 468 -209 471 -205 477 -203 482 -206 491 -211 499 -217 508 -222 532 -235 556 -249 576 -267 584 -272 584 -284 578 -290 569 -305 550 -312 533 -309 523 -310 515 -316 507 -321 505 -323 503 -323 500 -323{\\p0}", - "{\\c&HE0E0E0&\\p6}m 315 -260 b 286 -258 259 -240 246 -215 235 -210 222 -215 211 -211 204 -188 177 -176 172 -151 170 -139 163 -128 154 -121 143 -103 141 -81 143 -60 139 -46 125 -34 129 -17 132 -1 134 16 142 30 145 56 161 80 181 96 196 114 210 133 231 144 266 153 303 138 328 115 373 79 401 28 423 -24 446 -73 465 -123 483 -174 487 -199 467 -225 442 -227 421 -232 402 -242 384 -254 364 -259 342 -250 322 -260 320 -260 317 -261 315 -260{\\p0}", - -- Main cap - "{\\c&H0000F0&\\p6}m 1151 -523 b 1016 -516 891 -458 769 -406 693 -369 624 -319 561 -262 526 -252 465 -235 479 -187 502 -147 551 -135 588 -111 1115 165 1379 232 1909 761 1926 800 1952 834 1987 858 2020 883 2053 912 2065 952 2088 1000 2146 962 2139 919 2162 836 2156 747 2143 662 2131 615 2116 567 2122 517 2120 410 2090 306 2089 199 2092 147 2071 99 2034 64 1987 5 1928 -41 1869 -86 1777 -157 1712 -256 1629 -337 1578 -389 1521 -436 1461 -476 1407 -509 1343 -507 1284 -515 1240 -519 1195 -521 1151 -523{\\p0}", - -- Cap shadow - "{\\c&H0000AA&\\p6}m 1657 248 b 1658 254 1659 261 1660 267 1669 276 1680 284 1689 293 1695 302 1700 311 1707 320 1716 325 1726 330 1735 335 1744 347 1752 360 1761 371 1753 352 1754 331 1753 311 1751 237 1751 163 1751 90 1752 64 1752 37 1767 14 1778 -3 1785 -24 1786 -45 1786 -60 1786 -77 1774 -87 1760 -96 1750 -78 1751 -65 1748 -37 1750 -8 1750 20 1734 78 1715 134 1699 192 1694 211 1689 231 1676 246 1671 251 1661 255 1657 248 m 1909 541 b 1914 542 1922 549 1917 539 1919 520 1921 502 1919 483 1918 458 1917 433 1915 407 1930 373 1942 338 1947 301 1952 270 1954 238 1951 207 1946 214 1947 229 1945 239 1939 278 1936 318 1924 356 1923 362 1913 382 1912 364 1906 301 1904 237 1891 175 1887 150 1892 126 1892 101 1892 68 1893 35 1888 2 1884 -9 1871 -20 1859 -14 1851 -6 1854 9 1854 20 1855 58 1864 95 1873 132 1883 179 1894 225 1899 273 1908 362 1910 451 1909 541{\\p0}", - -- Brim and tip pompom - "{\\c&HF8F8F8&\\p6}m 626 -191 b 565 -155 486 -196 428 -151 387 -115 327 -101 304 -47 273 2 267 59 249 113 219 157 217 213 215 265 217 309 260 302 285 283 373 264 465 264 555 257 608 252 655 292 709 287 759 294 816 276 863 298 903 340 972 324 1012 367 1061 394 1125 382 1167 424 1213 462 1268 482 1322 506 1385 546 1427 610 1479 662 1510 690 1534 725 1566 752 1611 796 1664 830 1703 880 1740 918 1747 986 1805 1005 1863 991 1897 932 1916 880 1914 823 1945 777 1961 725 1979 673 1957 622 1938 575 1912 534 1862 515 1836 473 1790 417 1755 351 1697 305 1658 266 1633 216 1593 176 1574 138 1539 116 1497 110 1448 101 1402 77 1371 37 1346 -16 1295 15 1254 6 1211 -27 1170 -62 1121 -86 1072 -104 1027 -128 976 -133 914 -130 851 -137 794 -162 740 -181 679 -168 626 -191 m 2051 917 b 1971 932 1929 1017 1919 1091 1912 1149 1923 1214 1970 1254 2000 1279 2027 1314 2066 1325 2139 1338 2212 1295 2254 1238 2281 1203 2287 1158 2282 1116 2292 1061 2273 1006 2229 970 2206 941 2167 938 2138 918{\\p0}", -} - --- called by mpv on every frame -function tick() - if state.marginsREQ == true then - update_margins() - state.marginsREQ = false - end - - if (not state.enabled) then return end - - if (state.idle) then - - -- render idle message - msg.trace("idle message") - local icon_x, icon_y = 320 - 26, 140 - local line_prefix = ("{\\rDefault\\an7\\1a&H00&\\bord0\\shad0\\pos(%f,%f)}"):format(icon_x, icon_y) - - local ass = assdraw.ass_new() - -- mpv logo - if user_opts.idlescreen then - for i, line in ipairs(logo_lines) do - ass:new_event() - ass:append(line_prefix .. line) - end - end - - -- Santa hat - if is_december and user_opts.idlescreen and not user_opts.greenandgrumpy then - for i, line in ipairs(santa_hat_lines) do - ass:new_event() - ass:append(line_prefix .. line) - end - end - - if user_opts.idlescreen then - ass:new_event() - ass:pos(320, icon_y+65) - ass:an(8) - ass:append("Drop files or URLs to play here.") - end - set_osd(640, 360, ass.text) - - if state.showhide_enabled then - mp.disable_key_bindings("showhide") - mp.disable_key_bindings("showhide_wc") - state.showhide_enabled = false - end - - - elseif (state.fullscreen and user_opts.showfullscreen) - or (not state.fullscreen and user_opts.showwindowed) then - - -- render the OSC - render() - else - -- Flush OSD - render_wipe() - end - - state.tick_last_time = mp.get_time() - - if state.anitype ~= nil then - -- state.anistart can be nil - animation should now start, or it can - -- be a timestamp when it started. state.idle has no animation. - if not state.idle and - (not state.anistart or - mp.get_time() < 1 + state.anistart + user_opts.fadeduration/1000) - then - -- animating or starting, or still within 1s past the deadline - request_tick() - else - kill_animation() - end - end -end - -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 - --- duration is observed for the sole purpose of updating chapter markers --- positions. live streams with chapters are very rare, and the update is also --- expensive (with request_init), so it's only observed when we have chapters --- and the user didn't disable the livemarkers option (update_duration_watch). -function on_duration() request_init() end - -local duration_watched = false -function update_duration_watch() - local want_watch = user_opts.livemarkers and - (mp.get_property_number("chapters", 0) or 0) > 0 and - true or false -- ensure it's a boolean - - if (want_watch ~= duration_watched) then - if want_watch then - mp.observe_property("duration", nil, on_duration) - else - mp.unobserve_property(on_duration) - end - duration_watched = want_watch - end -end - -validate_user_opts() -update_duration_watch() - -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.observe_property("chapter-list", "native", function(_, list) - list = list or {} -- safety, shouldn't return nil - table.sort(list, function(a, b) return a.time < b.time end) - state.chapter_list = list - update_duration_watch() - request_init() -end) - -mp.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 - state.marginsREQ = true - 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}, - {"shift+mbtn_left", function(e) process_event("shift+mbtn_left", "up") end, - function(e) process_event("shift+mbtn_left", "down") end}, - {"mbtn_right", function(e) process_event("mbtn_right", "up") end, - function(e) process_event("mbtn_right", "down") end}, - -- alias to shift_mbtn_left for single-handed mouse use - {"mbtn_mid", function(e) process_event("shift+mbtn_left", "up") end, - function(e) process_event("shift+mbtn_left", "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"}, - {"shift+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 - -- recalculated on the next render cycle, except in 'never' mode where it - -- will just stay disabled. - mp.disable_key_bindings("input") - mp.disable_key_bindings("window-controls") - state.input_enabled = false - - update_margins() - request_tick() -end - -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) - -set_virt_mouse_area(0, 0, 0, 0, "input") -set_virt_mouse_area(0, 0, 0, 0, "window-controls") diff --git a/.config/mpv/scripts/uosc.lua b/.config/mpv/scripts/uosc.lua deleted file mode 100644 index 4c9abdb..0000000 --- a/.config/mpv/scripts/uosc.lua +++ /dev/null @@ -1,1081 +0,0 @@ ---[[ uosc 4.5.0 - 2022-Dec-07 | https://github.com/tomasklaen/uosc ]] -local uosc_version = '4.5.0' - -assdraw = require('mp.assdraw') -opt = require('mp.options') -utils = require('mp.utils') -msg = require('mp.msg') -osd = mp.create_osd_overlay('ass-events') -infinity = 1e309 -quarter_pi_sin = math.sin(math.pi / 4) - --- Enables relative requires from `scripts` directory -package.path = package.path .. ';' .. mp.find_config_file('scripts') .. '/?.lua' - -require('uosc_shared/lib/std') - ---[[ OPTIONS ]] - -defaults = { - timeline_style = 'line', - timeline_line_width = 2, - timeline_line_width_fullscreen = 3, - timeline_line_width_minimized_scale = 10, - timeline_size_min = 2, - timeline_size_max = 40, - timeline_size_min_fullscreen = 0, - timeline_size_max_fullscreen = 60, - timeline_start_hidden = false, - timeline_persistency = 'paused', - timeline_opacity = 0.9, - timeline_border = 1, - timeline_step = 5, - timeline_chapters_opacity = 0.8, - timeline_cache = true, - - controls = 'menu,gap,subtitles,<has_many_audio>audio,<has_many_video>video,<has_many_edition>editions,<stream>stream-quality,gap,space,speed,space,shuffle,loop-playlist,loop-file,gap,prev,items,next,gap,fullscreen', - controls_size = 32, - controls_size_fullscreen = 40, - controls_margin = 8, - controls_spacing = 2, - controls_persistency = '', - - volume = 'right', - volume_size = 40, - volume_size_fullscreen = 52, - volume_persistency = '', - volume_opacity = 0.9, - volume_border = 1, - volume_step = 1, - - speed_persistency = '', - speed_opacity = 0.6, - speed_step = 0.1, - speed_step_is_factor = false, - - menu_item_height = 36, - menu_item_height_fullscreen = 50, - menu_min_width = 260, - menu_min_width_fullscreen = 360, - menu_opacity = 1, - menu_parent_opacity = 0.4, - - top_bar = 'no-border', - top_bar_size = 40, - top_bar_size_fullscreen = 46, - top_bar_persistency = '', - top_bar_controls = true, - top_bar_title = true, - top_bar_title_opacity = 0.8, - - window_border_size = 1, - window_border_opacity = 0.8, - - autoload = false, - shuffle = false, - - ui_scale = 1, - font_scale = 1, - text_border = 1.2, - text_width_estimation = true, - pause_on_click_shorter_than = 0, -- deprecated by below - click_threshold = 0, - click_command = 'cycle pause; script-binding uosc/flash-pause-indicator', - flash_duration = 1000, - proximity_in = 40, - proximity_out = 120, - foreground = 'ffffff', - foreground_text = '000000', - background = '000000', - background_text = 'ffffff', - total_time = false, - time_precision = 0, - font_bold = false, - autohide = false, - buffered_time_threshold = 60, - pause_indicator = 'flash', - curtain_opacity = 0.5, - stream_quality_options = '4320,2160,1440,1080,720,480,360,240,144', - media_types = '3g2,3gp,aac,aiff,ape,apng,asf,au,avi,avif,bmp,dsf,f4v,flac,flv,gif,h264,h265,j2k,jp2,jfif,jpeg,jpg,jxl,m2ts,m4a,m4v,mid,midi,mj2,mka,mkv,mov,mp3,mp4,mp4a,mp4v,mpeg,mpg,oga,ogg,ogm,ogv,opus,png,rm,rmvb,spx,svg,tak,tga,tta,tif,tiff,ts,vob,wav,weba,webm,webp,wma,wmv,wv,y4m', - subtitle_types = 'aqt,ass,gsub,idx,jss,lrc,mks,pgs,pjs,psb,rt,slt,smi,sub,sup,srt,ssa,ssf,ttxt,txt,usf,vt,vtt', - default_directory = '~/', - chapter_ranges = 'openings:30abf964,endings:30abf964,ads:c54e4e80', - chapter_range_patterns = 'openings:オープニング;endings:エンディング', -} -options = table_shallow_copy(defaults) -opt.read_options(options, 'uosc') --- Normalize values -options.proximity_out = math.max(options.proximity_out, options.proximity_in + 1) -if options.chapter_ranges:sub(1, 4) == '^op|' then options.chapter_ranges = defaults.chapter_ranges end -if options.pause_on_click_shorter_than > 0 and options.click_threshold == 0 then - msg.warn('`pause_on_click_shorter_than` is deprecated. Use `click_threshold` and `click_command` instead.') - options.click_threshold = options.pause_on_click_shorter_than -end --- Ensure required environment configuration -if options.autoload then mp.commandv('set', 'keep-open-pause', 'no') end --- Color shorthands -fg, bg = serialize_rgba(options.foreground).color, serialize_rgba(options.background).color -fgt, bgt = serialize_rgba(options.foreground_text).color, serialize_rgba(options.background_text).color - ---[[ CONFIG ]] - -function create_default_menu() - return { - {title = 'Subtitles', value = 'script-binding uosc/subtitles'}, - {title = 'Audio tracks', value = 'script-binding uosc/audio'}, - {title = 'Stream quality', value = 'script-binding uosc/stream-quality'}, - {title = 'Playlist', value = 'script-binding uosc/items'}, - {title = 'Chapters', value = 'script-binding uosc/chapters'}, - {title = 'Navigation', items = { - {title = 'Next', hint = 'playlist or file', value = 'script-binding uosc/next'}, - {title = 'Prev', hint = 'playlist or file', value = 'script-binding uosc/prev'}, - {title = 'Delete file & Next', value = 'script-binding uosc/delete-file-next'}, - {title = 'Delete file & Prev', value = 'script-binding uosc/delete-file-prev'}, - {title = 'Delete file & Quit', value = 'script-binding uosc/delete-file-quit'}, - {title = 'Open file', value = 'script-binding uosc/open-file'}, - },}, - {title = 'Utils', items = { - {title = 'Aspect ratio', items = { - {title = 'Default', value = 'set video-aspect-override "-1"'}, - {title = '16:9', value = 'set video-aspect-override "16:9"'}, - {title = '4:3', value = 'set video-aspect-override "4:3"'}, - {title = '2.35:1', value = 'set video-aspect-override "2.35:1"'}, - },}, - {title = 'Audio devices', value = 'script-binding uosc/audio-device'}, - {title = 'Editions', value = 'script-binding uosc/editions'}, - {title = 'Screenshot', value = 'async screenshot'}, - {title = 'Show in directory', value = 'script-binding uosc/show-in-directory'}, - {title = 'Open config folder', value = 'script-binding uosc/open-config-directory'}, - },}, - {title = 'Quit', value = 'quit'}, - } -end - -config = { - version = uosc_version, - -- sets max rendering frequency in case the - -- native rendering frequency could not be detected - render_delay = 1 / 60, - font = mp.get_property('options/osd-font'), - media_types = split(options.media_types, ' *, *'), - subtitle_types = split(options.subtitle_types, ' *, *'), - stream_quality_options = split(options.stream_quality_options, ' *, *'), - menu_items = (function() - local input_conf_property = mp.get_property_native('input-conf') - local input_conf_path = mp.command_native({ - 'expand-path', input_conf_property == '' and '~~/input.conf' or input_conf_property, - }) - 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 create_default_menu() end - - local main_menu = {items = {}, items_by_command = {}} - local by_id = {} - - for line in io.lines(input_conf_path) do - local key, command, comment = string.match(line, '%s*([%S]+)%s+(.-)%s+#%s*(.-)%s*$') - local title = '' - if comment then - local comments = split(comment, '#') - local titles = itable_filter(comments, function(v, i) return v:match('^!') or v:match('^menu:') end) - if titles and #titles > 0 then - title = titles[1]:match('^!%s*(.*)%s*') or titles[1]:match('^menu:%s*(.*)%s*') - end - end - if title ~= '' then - local is_dummy = key:sub(1, 1) == '#' - local submenu_id = '' - local target_menu = main_menu - 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 by_id[submenu_id] then - local items = {} - by_id[submenu_id] = {items = items, items_by_command = {}} - target_menu.items[#target_menu.items + 1] = {title = title_part, items = items} - end - - target_menu = by_id[submenu_id] - else - if command == 'ignore' then break end - -- If command is already in menu, just append the key to it - if target_menu.items_by_command[command] then - local hint = target_menu.items_by_command[command].hint - target_menu.items_by_command[command].hint = hint and hint .. ', ' .. key or key - else - local item = { - title = title_part, - hint = not is_dummy and key or nil, - value = command, - } - target_menu.items_by_command[command] = item - target_menu.items[#target_menu.items + 1] = item - end - end - end - end - end - - if #main_menu.items > 0 then - return main_menu.items - else - -- Default context menu - return create_default_menu() - end - end)(), - chapter_ranges = (function() - ---@type table<string, string[]> Alternative patterns. - local alt_patterns = {} - if options.chapter_range_patterns and options.chapter_range_patterns ~= '' then - for _, definition in ipairs(split(options.chapter_range_patterns, ';+ *')) do - local name_patterns = split(definition, ' *:') - local name, patterns = name_patterns[1], name_patterns[2] - if name and patterns then alt_patterns[name] = split(patterns, ',') end - end - end - - ---@type table<string, {color: string; opacity: number; patterns?: string[]}> - local ranges = {} - if options.chapter_ranges and options.chapter_ranges ~= '' then - for _, definition in ipairs(split(options.chapter_ranges, ' *,+ *')) do - local name_color = split(definition, ' *:+ *') - local name, color = name_color[1], name_color[2] - if name and color - and name:match('^[a-zA-Z0-9_]+$') and color:match('^[a-fA-F0-9]+$') - and (#color == 6 or #color == 8) then - local range = serialize_rgba(name_color[2]) - range.patterns = alt_patterns[name] - ranges[name_color[1]] = range - end - end - end - return ranges - end)(), -} --- Adds `{element}_persistency` property with table of flags when the element should be visible (`{paused = true}`) -for _, name in ipairs({'timeline', 'controls', 'volume', 'top_bar', 'speed'}) do - local option_name = name .. '_persistency' - local value, flags = options[option_name], {} - if type(value) == 'string' then - for _, state in ipairs(split(value, ' *, *')) do flags[state] = true end - end - config[option_name] = flags -end - ---[[ STATE ]] - -display = {width = 1280, height = 720, scale_x = 1, scale_y = 1, initialized = false} -cursor = {hidden = true, x = 0, y = 0} -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'), - path = nil, -- current file path or URL - title = nil, - time = nil, -- current media playback time - speed = 1, - duration = nil, -- current media duration - time_human = nil, -- current playback time in human format - duration_or_remaining_time_human = nil, -- depends on options.total_time - pause = mp.get_property_native('pause'), - chapters = {}, - current_chapter = nil, - chapter_ranges = {}, - border = mp.get_property_native('border'), - fullscreen = mp.get_property_native('fullscreen'), - maximized = mp.get_property_native('window-maximized'), - fullormaxed = mp.get_property_native('fullscreen') or mp.get_property_native('window-maximized'), - render_timer = nil, - render_last_time = 0, - volume = nil, - volume_max = nil, - mute = nil, - is_idle = false, - is_video = false, - is_audio = false, -- true if file is audio only (mp3, etc) - is_image = false, - is_stream = false, - has_audio = false, - has_sub = false, - has_chapter = false, - has_playlist = false, - shuffle = options.shuffle, - 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, - uncached_ranges = nil, - cache = nil, - cache_buffering = 100, - cache_underrun = false, - core_idle = false, - eof_reached = false, - render_delay = config.render_delay, - first_real_mouse_move_received = false, - playlist_count = 0, - playlist_pos = 0, - margin_top = 0, - margin_bottom = 0, - hidpi_scale = 1, -} -thumbnail = {width = 0, height = 0, disabled = false} -external = {} -- Properties set by external scripts -Elements = require('uosc_shared/elements/Elements') -Menu = require('uosc_shared/elements/Menu') - --- State dependent utilities -require('uosc_shared/lib/utils') -require('uosc_shared/lib/text') -require('uosc_shared/lib/ass') -require('uosc_shared/lib/menus') - ---[[ STATE UPDATERS ]] - -function update_display_dimensions() - local scale = (state.hidpi_scale or 1) * options.ui_scale - local real_width, real_height = mp.get_osd_size() - if real_width <= 0 then return end - local scaled_width, scaled_height = round(real_width / scale), round(real_height / scale) - display.width, display.height = scaled_width, scaled_height - display.scale_x, display.scale_y = real_width / scaled_width, real_height / scaled_height - display.initialized = true - - -- Tell elements about this - Elements:trigger('display') - - -- Some elements probably changed their rectangles as a reaction to `display` - Elements:update_proximities() - request_render() -end - -function update_fullormaxed() - state.fullormaxed = state.fullscreen or state.maximized - update_display_dimensions() - Elements:trigger('prop_fullormaxed', state.fullormaxed) -end - -function update_human_times() - if state.time then - state.time_human = format_time(state.time) - if state.duration then - local speed = state.speed or 1 - state.duration_or_remaining_time_human = format_time( - options.total_time and state.duration or ((state.time - state.duration) / speed) - ) - else - state.duration_or_remaining_time_human = nil - end - else - state.time_human = nil - end -end - --- Notifies other scripts such as console about where the unoccupied parts of the screen are. -function update_margins() - -- margins are normalized to window size - local timeline, top_bar, controls = Elements.timeline, Elements.top_bar, Elements.controls - local bottom_y = controls and controls.enabled and controls.ay or timeline.ay - local top, bottom = 0, (display.height - bottom_y) / display.height - - if top_bar.enabled and top_bar:get_visibility() > 0 then - top = (top_bar.size or 0) / display.height - end - - if top == state.margin_top and bottom == state.margin_bottom then return end - - state.margin_top = top - state.margin_bottom = bottom - - utils.shared_script_property_set('osc-margins', string.format('%f,%f,%f,%f', 0, 0, top, bottom)) -end -function create_state_setter(name, callback) - return function(_, value) - set_state(name, value) - if callback then callback() end - request_render() - end -end - -function set_state(name, value) - state[name] = value - Elements:trigger('prop_' .. name, value) -end - -function update_cursor_position(x, y) - -- mpv reports initial mouse position on linux as (0, 0), which always - -- displays the top bar, so we hardcode cursor position as infinity until - -- we receive a first real mouse move event with coordinates other than 0,0. - if not state.first_real_mouse_move_received then - if x > 0 and y > 0 then state.first_real_mouse_move_received = true - else x, y = infinity, infinity end - end - - -- add 0.5 to be in the middle of the pixel - cursor.x, cursor.y = (x + 0.5) / display.scale_x, (y + 0.5) / display.scale_y - - Elements:update_proximities() - request_render() -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_visibility', element:get_visibility(), 0, function() - element.forced_visibility = nil - end) - end - end - - cursor.hidden = true - Elements:update_proximities() - Elements:trigger('global_mouse_leave') -end - -function handle_mouse_enter(x, y) - cursor.hidden = false - update_cursor_position(x, y) - Elements:trigger('global_mouse_enter') -end - -function handle_mouse_move(x, y) - update_cursor_position(x, y) - Elements:proximity_trigger('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 handle_file_end() - local resume = false - if not state.loop_file then - if state.has_playlist then resume = state.shuffle and navigate_playlist(1) - else resume = options.autoload and navigate_directory(1) end - end - -- Resume only when navigation happened - if resume then mp.command('set pause no') end -end -local file_end_timer = mp.add_timeout(1, handle_file_end) -file_end_timer:kill() - -function load_file_index_in_current_directory(index) - if not state.path or is_protocol(state.path) then return end - - local serialized = serialize_path(state.path) - if serialized and serialized.dirname then - local files = read_directory(serialized.dirname, config.media_types) - - if not files then return end - sort_filenames(files) - if index < 0 then index = #files + index + 1 end - - if files[index] then - mp.commandv('loadfile', join_path(serialized.dirname, files[index])) - end - end -end - -function update_render_delay(name, fps) - if fps then state.render_delay = 1 / fps end -end - -function observe_display_fps(name, fps) - if fps then - mp.unobserve_property(update_render_delay) - mp.unobserve_property(observe_display_fps) - mp.observe_property('display-fps', 'native', update_render_delay) - end -end - -function select_current_chapter() - local current_chapter - if state.time and state.chapters then - _, current_chapter = itable_find(state.chapters, function(c) return state.time >= c.time end, true) - end - set_state('current_chapter', current_chapter) -end - ---[[ STATE HOOKS ]] - --- Click detection -if options.click_threshold > 0 then - -- Executes custom command for clicks shorter than `options.click_threshold` - -- while filtering out double clicks. - local click_time = options.click_threshold / 1000 - local doubleclick_time = mp.get_property_native('input-doubleclick-time') / 1000 - local last_down, last_up = 0, 0 - local click_timer = mp.add_timeout(math.max(click_time, doubleclick_time), function() - local delta = last_up - last_down - if delta > 0 and delta < click_time and delta > 0.02 then mp.command(options.click_command) end - end) - click_timer:kill() - mp.set_key_bindings({{'mbtn_left', - function() last_up = mp.get_time() end, - function() - last_down = mp.get_time() - if click_timer:is_enabled() then click_timer:kill() else click_timer:resume() end - end, - },}, 'mouse_movement', 'force') - mp.enable_key_bindings('mouse_movement', 'allow-vo-dragging+allow-hide-cursor') -end - -function update_mouse_pos(_, mouse, ignore_hover) - if ignore_hover or mouse.hover then - if cursor.hidden then handle_mouse_enter(mouse.x, mouse.y) end - handle_mouse_move(mouse.x, mouse.y) - else handle_mouse_leave() end -end -mp.observe_property('mouse-pos', 'native', update_mouse_pos) -mp.observe_property('osc', 'bool', function(name, value) if value == true then mp.set_property('osc', 'no') end end) -mp.register_event('file-loaded', function() - set_state('path', normalize_path(mp.get_property_native('path'))) -end) -mp.register_event('end-file', function(event) - if event.reason == 'eof' then - file_end_timer:kill() - handle_file_end() - end -end) -do - local template = nil - function update_title() - if template:sub(-6) == ' - mpv' then template = template:sub(1, -7) end - -- escape ASS, and strip newlines and trailing slashes and trim whitespace - local t = mp.command_native({'expand-text', template}):gsub('\\n', ' '):gsub('[\\%s]+$', ''):gsub('^%s+', '') - set_state('title', ass_escape(t)) - end - mp.observe_property('title', 'string', function(_, title) - mp.unobserve_property(update_title) - template = title - local props = get_expansion_props(title) - for prop, _ in pairs(props) do - mp.observe_property(prop, 'native', update_title) - end - if not next(props) then update_title() end - end) -end -mp.observe_property('playback-time', 'number', create_state_setter('time', function() - -- Create a file-end event that triggers right before file ends - file_end_timer:kill() - if state.duration and state.time and not state.pause then - local remaining = (state.duration - state.time) / state.speed - if remaining < 5 then - local timeout = remaining - 0.02 - if timeout > 0 then - file_end_timer.timeout = timeout - file_end_timer:resume() - else handle_file_end() end - end - end - - update_human_times() - select_current_chapter() -end)) -mp.observe_property('duration', 'number', create_state_setter('duration', update_human_times)) -mp.observe_property('speed', 'number', create_state_setter('speed', update_human_times)) -mp.observe_property('track-list', 'native', function(name, value) - -- checks the file dispositions - local is_image = false - local types = {sub = 0, audio = 0, video = 0} - for _, track in ipairs(value) do - if track.type == 'video' then - is_image = track.image - if not is_image and not track.albumart then types.video = types.video + 1 end - elseif types[track.type] then types[track.type] = types[track.type] + 1 end - end - set_state('is_audio', types.video == 0 and types.audio > 0) - set_state('is_image', is_image) - set_state('has_audio', types.audio > 0) - set_state('has_many_audio', types.audio > 1) - set_state('has_sub', types.sub > 0) - set_state('has_many_sub', types.sub > 1) - set_state('is_video', types.video > 0) - set_state('has_many_video', types.video > 1) - Elements:trigger('dispositions') -end) -mp.observe_property('editions', 'number', function(_, editions) - if editions then set_state('has_many_edition', editions > 1) end - Elements:trigger('dispositions') -end) -mp.observe_property('chapter-list', 'native', function(_, chapters) - local chapters, chapter_ranges = serialize_chapters(chapters), {} - if chapters then chapters, chapter_ranges = serialize_chapter_ranges(chapters) end - set_state('chapters', chapters) - set_state('chapter_ranges', chapter_ranges) - set_state('has_chapter', #chapters > 0) - select_current_chapter() - Elements:trigger('dispositions') -end) -mp.observe_property('border', 'bool', create_state_setter('border')) -mp.observe_property('loop-file', 'native', create_state_setter('loop_file')) -mp.observe_property('ab-loop-a', 'number', create_state_setter('ab_loop_a')) -mp.observe_property('ab-loop-b', 'number', create_state_setter('ab_loop_b')) -mp.observe_property('playlist-pos-1', 'number', create_state_setter('playlist_pos')) -mp.observe_property('playlist-count', 'number', function(_, value) - set_state('playlist_count', value) - set_state('has_playlist', value > 1) - Elements:trigger('dispositions') -end) -mp.observe_property('fullscreen', 'bool', create_state_setter('fullscreen', update_fullormaxed)) -mp.observe_property('window-maximized', 'bool', create_state_setter('maximized', update_fullormaxed)) -mp.observe_property('idle-active', 'bool', function(_, idle) - set_state('is_idle', idle) - Elements:trigger('dispositions') -end) -mp.observe_property('pause', 'bool', create_state_setter('pause', function() file_end_timer:kill() end)) -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('osd-dimensions', 'native', function(name, val) - update_display_dimensions() - request_render() -end) -mp.observe_property('display-hidpi-scale', 'native', create_state_setter('hidpi_scale', update_display_dimensions)) -mp.observe_property('cache', 'string', create_state_setter('cache')) -mp.observe_property('cache-buffering-state', 'number', create_state_setter('cache_buffering')) -mp.observe_property('demuxer-via-network', 'native', create_state_setter('is_stream', function() - Elements:trigger('dispositions') -end)) -mp.observe_property('demuxer-cache-state', 'native', function(prop, cache_state) - local cached_ranges, bof, eof, uncached_ranges = nil, nil, nil, nil - if cache_state then - cached_ranges, bof, eof = cache_state['seekable-ranges'], cache_state['bof-cached'], cache_state['eof-cached'] - set_state('cache_underrun', cache_state['underrun']) - else cached_ranges = {} end - - if not (state.duration and (#cached_ranges > 0 or state.cache == 'yes' or - (state.cache == 'auto' and state.is_stream))) then - if state.uncached_ranges then set_state('uncached_ranges', nil) end - return - end - - -- Normalize - local ranges = {} - for _, range in ipairs(cached_ranges) do - ranges[#ranges + 1] = { - math.max(range['start'] or 0, 0), - math.min(range['end'] or state.duration, state.duration), - } - end - table.sort(ranges, function(a, b) return a[1] < b[1] end) - if bof then ranges[1][1] = 0 end - if eof then ranges[#ranges][2] = state.duration end - -- Invert cached ranges into uncached ranges, as that's what we're rendering - local inverted_ranges = {{0, state.duration}} - for _, cached in pairs(ranges) do - inverted_ranges[#inverted_ranges][2] = cached[1] - inverted_ranges[#inverted_ranges + 1] = {cached[2], state.duration} - end - uncached_ranges = {} - local last_range = nil - for _, range in ipairs(inverted_ranges) do - if last_range and last_range[2] + 0.5 > range[1] then -- fuse ranges - last_range[2] = range[2] - else - if range[2] - range[1] > 0.5 then -- skip short ranges - uncached_ranges[#uncached_ranges + 1] = range - last_range = range - end - end - end - - set_state('uncached_ranges', uncached_ranges) -end) -mp.observe_property('display-fps', 'native', observe_display_fps) -mp.observe_property('estimated-display-fps', 'native', update_render_delay) -mp.observe_property('eof-reached', 'native', create_state_setter('eof_reached')) -mp.observe_property('core-idle', 'native', create_state_setter('core_idle')) - ---[[ KEY BINDS ]] - -mp.add_key_binding(nil, 'toggle-ui', function() Elements:toggle({'timeline', 'controls', 'volume', 'top_bar'}) end) -mp.add_key_binding(nil, 'flash-ui', function() Elements:flash({'timeline', 'controls', 'volume', 'top_bar'}) end) -mp.add_key_binding(nil, 'flash-timeline', function() Elements:flash({'timeline'}) end) -mp.add_key_binding(nil, 'flash-top-bar', function() Elements:flash({'top_bar'}) end) -mp.add_key_binding(nil, 'flash-volume', function() Elements:flash({'volume'}) end) -mp.add_key_binding(nil, 'flash-speed', function() Elements:flash({'speed'}) end) -mp.add_key_binding(nil, 'flash-pause-indicator', function() Elements:flash({'pause_indicator'}) 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, 'decide-pause-indicator', function() Elements.pause_indicator:decide() end) -mp.add_key_binding(nil, 'menu', function() toggle_menu_with_items() end) -mp.add_key_binding(nil, 'menu-blurred', function() toggle_menu_with_items({mouse_nav = true}) end) -local track_loaders = { - {name = 'subtitles', prop = 'sub', allowed_types = config.subtitle_types}, - {name = 'audio', prop = 'audio', allowed_types = config.media_types}, - {name = 'video', prop = 'video', allowed_types = config.media_types}, -} -for _, loader in ipairs(track_loaders) do - local menu_type = 'load-' .. loader.name - mp.add_key_binding(nil, menu_type, function() - if Menu:is_open(menu_type) then Menu:close() return end - - local path = state.path - if path then - if is_protocol(path) then - path = false - else - local serialized_path = serialize_path(path) - path = serialized_path ~= nil and serialized_path.dirname or false - end - end - if not path then - path = get_default_directory() - end - open_file_navigation_menu( - path, - function(path) mp.commandv(loader.prop .. '-add', path) end, - {type = menu_type, title = 'Load ' .. loader.name, allowed_types = loader.allowed_types} - ) - end) -end -mp.add_key_binding(nil, 'subtitles', create_select_tracklist_type_menu_opener( - 'Subtitles', 'sub', 'sid', 'script-binding uosc/load-subtitles' -)) -mp.add_key_binding(nil, 'audio', create_select_tracklist_type_menu_opener( - 'Audio', 'audio', 'aid', 'script-binding uosc/load-audio' -)) -mp.add_key_binding(nil, 'video', create_select_tracklist_type_menu_opener( - 'Video', 'video', 'vid', 'script-binding uosc/load-video' -)) -mp.add_key_binding(nil, 'playlist', create_self_updating_menu_opener({ - title = 'Playlist', - type = 'playlist', - list_prop = 'playlist', - serializer = function(playlist) - local items = {} - for index, item in ipairs(playlist) do - local is_url = item.filename:find('://') - local item_title = type(item.title) == 'string' and #item.title > 0 and item.title or false - items[index] = { - title = item_title or (is_url and item.filename or serialize_path(item.filename).basename), - hint = tostring(index), - active = item.current, - value = index, - } - end - return items - end, - on_select = function(index) mp.commandv('set', 'playlist-pos-1', tostring(index)) end, -})) -mp.add_key_binding(nil, 'chapters', create_self_updating_menu_opener({ - title = 'Chapters', - type = 'chapters', - list_prop = 'chapter-list', - active_prop = 'chapter', - serializer = function(chapters, current_chapter) - local items = {} - chapters = normalize_chapters(chapters) - for index, chapter in ipairs(chapters) do - items[index] = { - title = chapter.title or '', - hint = mp.format_time(chapter.time), - value = index, - active = index - 1 == current_chapter, - } - end - return items - end, - on_select = function(index) mp.commandv('set', 'chapter', tostring(index - 1)) end, -})) -mp.add_key_binding(nil, 'editions', create_self_updating_menu_opener({ - title = 'Editions', - type = 'editions', - list_prop = 'edition-list', - active_prop = 'current-edition', - serializer = function(editions, current_id) - local items = {} - for _, edition in ipairs(editions or {}) do - items[#items + 1] = { - title = edition.title or 'Edition', - hint = tostring(edition.id + 1), - value = edition.id, - active = edition.id == current_id, - } - end - return items - end, - on_select = function(id) mp.commandv('set', 'edition', id) end, -})) -mp.add_key_binding(nil, 'show-in-directory', function() - -- Ignore URLs - if not state.path or is_protocol(state.path) then return end - - if state.os == 'windows' then - utils.subprocess_detached({args = {'explorer', '/select,', state.path}, cancellable = false}) - elseif state.os == 'macos' then - utils.subprocess_detached({args = {'open', '-R', state.path}, cancellable = false}) - elseif state.os == 'linux' then - local result = utils.subprocess({args = {'nautilus', state.path}, cancellable = false}) - - -- Fallback opens the folder with xdg-open instead - if result.status ~= 0 then - utils.subprocess({args = {'xdg-open', serialize_path(state.path).dirname}, cancellable = false}) - end - end -end) -mp.add_key_binding(nil, 'stream-quality', function() - if Menu:is_open('stream-quality') then Menu:close() return end - - local ytdl_format = mp.get_property_native('ytdl-format') - local items = {} - - for _, height in ipairs(config.stream_quality_options) do - local format = 'bestvideo[height<=?' .. height .. ']+bestaudio/best[height<=?' .. height .. ']' - items[#items + 1] = {title = height .. 'p', value = format, active = format == ytdl_format} - end - - Menu:open({type = 'stream-quality', title = 'Stream quality', items = items}, function(format) - mp.set_property('ytdl-format', format) - - -- Reload the video to apply new format - -- This is taken from https://github.com/jgreco/mpv-youtube-quality - -- which is in turn taken from https://github.com/4e6/mpv-reload/ - -- Dunno if playlist_pos shenanigans below are necessary. - local playlist_pos = mp.get_property_number('playlist-pos') - local duration = mp.get_property_native('duration') - local time_pos = mp.get_property('time-pos') - - mp.set_property_number('playlist-pos', playlist_pos) - - -- Tries to determine live stream vs. pre-recorded VOD. VOD has non-zero - -- duration property. When reloading VOD, to keep the current time position - -- we should provide offset from the start. Stream doesn't have fixed start. - -- Decent choice would be to reload stream from it's current 'live' position. - -- That's the reason we don't pass the offset when reloading streams. - if duration and duration > 0 then - local function seeker() - mp.commandv('seek', time_pos, 'absolute') - mp.unregister_event(seeker) - end - mp.register_event('file-loaded', seeker) - end - end) -end) -mp.add_key_binding(nil, 'open-file', function() - if Menu:is_open('open-file') then Menu:close() return end - - local directory - local active_file - - if state.path == nil or is_protocol(state.path) then - local serialized = serialize_path(get_default_directory()) - if serialized then - directory = serialized.path - active_file = nil - end - else - local serialized = serialize_path(state.path) - if serialized then - directory = serialized.dirname - active_file = serialized.path - end - end - - if not directory then - msg.error('Couldn\'t serialize path "' .. state.path .. '".') - return - end - - -- Update active file in directory navigation menu - local function handle_file_loaded() - if Menu:is_open('open-file') then - Elements.menu:activate_one_value(normalize_path(mp.get_property_native('path'))) - end - end - - open_file_navigation_menu( - directory, - function(path) mp.commandv('loadfile', path) end, - { - type = 'open-file', - allowed_types = config.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, 'shuffle', function() set_state('shuffle', not state.shuffle) end) -mp.add_key_binding(nil, 'items', function() - if state.has_playlist then - mp.command('script-binding uosc/playlist') - else - mp.command('script-binding uosc/open-file') - end -end) -mp.add_key_binding(nil, 'next', function() navigate_item(1) end) -mp.add_key_binding(nil, 'prev', function() navigate_item(-1) end) -mp.add_key_binding(nil, 'next-file', function() navigate_directory(1) end) -mp.add_key_binding(nil, 'prev-file', function() navigate_directory(-1) end) -mp.add_key_binding(nil, 'first', function() - if state.has_playlist then - mp.commandv('set', 'playlist-pos-1', '1') - else - load_file_index_in_current_directory(1) - end -end) -mp.add_key_binding(nil, 'last', function() - if state.has_playlist then - mp.commandv('set', 'playlist-pos-1', tostring(state.playlist_count)) - else - load_file_index_in_current_directory(-1) - end -end) -mp.add_key_binding(nil, 'first-file', function() load_file_index_in_current_directory(1) end) -mp.add_key_binding(nil, 'last-file', function() load_file_index_in_current_directory(-1) end) -mp.add_key_binding(nil, 'delete-file-next', function() - local next_file = nil - local is_local_file = state.path and not is_protocol(state.path) - - if is_local_file then - if Menu:is_open('open-file') then Elements.menu:delete_value(state.path) end - end - - if state.has_playlist then - mp.commandv('playlist-remove', 'current') - else - if is_local_file then - local paths, current_index = get_adjacent_files(state.path, config.media_types) - if paths and current_index then - local index, path = decide_navigation_in_list(paths, current_index, 1) - if path then next_file = path end - end - end - - if next_file then mp.commandv('loadfile', next_file) - else mp.commandv('stop') end - end - - if is_local_file then delete_file(state.path) end -end) -mp.add_key_binding(nil, 'delete-file-quit', function() - mp.command('stop') - if state.path and not is_protocol(state.path) then delete_file(state.path) end - mp.command('quit') -end) -mp.add_key_binding(nil, 'audio-device', create_self_updating_menu_opener({ - title = 'Audio devices', - type = 'audio-device-list', - list_prop = 'audio-device-list', - active_prop = 'audio-device', - serializer = function(audio_device_list, current_device) - current_device = current_device or 'auto' - local ao = mp.get_property('current-ao') or '' - local items = {} - for _, device in ipairs(audio_device_list) do - if device.name == 'auto' or string.match(device.name, '^' .. ao) then - local hint = string.match(device.name, ao .. '/(.+)') - if not hint then hint = device.name end - items[#items + 1] = { - title = device.description, - hint = hint, - active = device.name == current_device, - value = device.name, - } - end - end - return items - end, - on_select = function(name) mp.commandv('set', 'audio-device', name) end, -})) -mp.add_key_binding(nil, 'open-config-directory', function() - local config_path = mp.command_native({'expand-path', '~~/mpv.conf'}) - local config = serialize_path(normalize_path(config_path)) - - if config then - 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}) - else - msg.error('Couldn\'t serialize config path "' .. config_path .. '".') - end -end) - ---[[ MESSAGE HANDLERS ]] - -mp.register_script_message('show-submenu', function(id) toggle_menu_with_items({submenu = id}) end) -mp.register_script_message('get-version', function(script) - mp.commandv('script-message-to', script, 'uosc-version', config.version) -end) -mp.register_script_message('open-menu', function(json, submenu_id) - local data = utils.parse_json(json) - if type(data) ~= 'table' or type(data.items) ~= 'table' then - msg.error('open-menu: received json didn\'t produce a table with menu configuration') - else - if data.type and Menu:is_open(data.type) then Menu:close() - else open_command_menu(data, {submenu_id = submenu_id}) end - end -end) -mp.register_script_message('update-menu', function(json) - local data = utils.parse_json(json) - if type(data) ~= 'table' or type(data.items) ~= 'table' then - msg.error('update-menu: received json didn\'t produce a table with menu configuration') - else - local menu = data.type and Menu:is_open(data.type) - if menu then menu:update(data) - else open_command_menu(data) end - end -end) -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 - thumbnail.disabled = true - msg.error('thumbfast-info: received json didn\'t produce a table with thumbnail information') - else - thumbnail = data - request_render() - end -end) -mp.register_script_message('set', function(name, value) - external[name] = value - Elements:trigger('external_prop_' .. name, value) -end) -mp.register_script_message('toggle-elements', function(elements) Elements:toggle(split(elements, ' *, *')) end) -mp.register_script_message('set-min-visibility', function(visibility, elements) - local fraction = tonumber(visibility) - local ids = split(elements and elements ~= '' and elements or 'timeline,controls,volume,top_bar', ' *, *') - if fraction then Elements:set_min_visibility(clamp(0, fraction, 1), ids) end -end) -mp.register_script_message('flash-elements', function(elements) Elements:flash(split(elements, ' *, *')) end) - ---[[ ELEMENTS ]] - -require('uosc_shared/elements/WindowBorder'):new() -require('uosc_shared/elements/BufferingIndicator'):new() -require('uosc_shared/elements/PauseIndicator'):new() -require('uosc_shared/elements/TopBar'):new() -require('uosc_shared/elements/Timeline'):new() -if options.controls and options.controls ~= 'never' then require('uosc_shared/elements/Controls'):new() end -if itable_index_of({'left', 'right'}, options.volume) then require('uosc_shared/elements/Volume'):new() end -require('uosc_shared/elements/Curtain'):new() diff --git a/.config/mpv/scripts/uosc_shared/elements/BufferingIndicator.lua b/.config/mpv/scripts/uosc_shared/elements/BufferingIndicator.lua deleted file mode 100644 index e2aa071..0000000 --- a/.config/mpv/scripts/uosc_shared/elements/BufferingIndicator.lua +++ /dev/null @@ -1,37 +0,0 @@ -local Element = require('uosc_shared/elements/Element') - ----@class BufferingIndicator : Element -local BufferingIndicator = class(Element) - -function BufferingIndicator:new() return Class.new(self) --[[@as BufferingIndicator]] end -function BufferingIndicator:init() - Element.init(self, 'buffer_indicator') - self.ignores_menu = true - self.enabled = false -end - -function BufferingIndicator:decide_enabled() - local cache = state.cache_underrun or state.cache_buffering and state.cache_buffering < 100 - local player = state.core_idle and not state.eof_reached - if self.enabled then - if not player or (state.pause and not cache) then self.enabled = false end - elseif player and cache and state.uncached_ranges then self.enabled = true end -end - -function BufferingIndicator:on_prop_pause() self:decide_enabled() end -function BufferingIndicator:on_prop_core_idle() self:decide_enabled() end -function BufferingIndicator:on_prop_eof_reached() self:decide_enabled() end -function BufferingIndicator:on_prop_uncached_ranges() self:decide_enabled() end -function BufferingIndicator:on_prop_cache_buffering() self:decide_enabled() end -function BufferingIndicator:on_prop_cache_underrun() self:decide_enabled() end - -function BufferingIndicator:render() - local ass = assdraw.ass_new() - ass:rect(0, 0, display.width, display.height, {color = bg, opacity = 0.3}) - local size = round(30 + math.min(display.width, display.height) / 10) - local opacity = (Elements.menu and not Elements.menu.is_closing) and 0.3 or 0.8 - ass:spinner(display.width / 2, display.height / 2, size, {color = fg, opacity = opacity}) - return ass -end - -return BufferingIndicator diff --git a/.config/mpv/scripts/uosc_shared/elements/Button.lua b/.config/mpv/scripts/uosc_shared/elements/Button.lua deleted file mode 100644 index 8d0f725..0000000 --- a/.config/mpv/scripts/uosc_shared/elements/Button.lua +++ /dev/null @@ -1,90 +0,0 @@ -local Element = require('uosc_shared/elements/Element') - ----@alias ButtonProps {icon: string; on_click: function; anchor_id?: string; active?: boolean; badge?: string|number; foreground?: string; background?: string; tooltip?: string} - ----@class Button : Element -local Button = class(Element) - ----@param id string ----@param props ButtonProps -function Button:new(id, props) return Class.new(self, id, props) --[[@as Button]] end ----@param id string ----@param props ButtonProps -function Button:init(id, props) - self.icon = props.icon - self.active = props.active - self.tooltip = props.tooltip - self.badge = props.badge - self.foreground = props.foreground or fg - self.background = props.background or bg - ---@type fun() - self.on_click = props.on_click - Element.init(self, id, props) -end - -function Button:on_coordinates() self.font_size = round((self.by - self.ay) * 0.7) end -function Button:on_mbtn_left_down() - -- Don't accept clicks while hidden. - if self:get_visibility() <= 0 then return end - -- We delay the callback to next tick, otherwise we are risking race - -- conditions as we are in the middle of event dispatching. - -- For example, handler might add a menu to the end of the element stack, and that - -- than picks up this click even we are in right now, and instantly closes itself. - mp.add_timeout(0.01, self.on_click) -end - -function Button:render() - local visibility = self:get_visibility() - if visibility <= 0 then return end - - local ass = assdraw.ass_new() - local is_hover = self.proximity_raw == 0 - local is_hover_or_active = is_hover or self.active - local foreground = self.active and self.background or self.foreground - local background = self.active and self.foreground or self.background - - -- Background - if is_hover_or_active then - ass:rect(self.ax, self.ay, self.bx, self.by, { - color = self.active and background or foreground, radius = 2, - opacity = visibility * (self.active and 1 or 0.3), - }) - end - - -- Tooltip on hover - if is_hover and self.tooltip then ass:tooltip(self, self.tooltip) end - - - -- Badge - local icon_clip - if self.badge then - local badge_font_size = self.font_size * 0.6 - local badge_opts = {size = badge_font_size, color = background, opacity = visibility} - local badge_width = text_width(self.badge, badge_opts) - local width, height = math.ceil(badge_width + (badge_font_size / 7) * 2), math.ceil(badge_font_size * 0.93) - local bx, by = self.bx - 1, self.by - 1 - ass:rect(bx - width, by - height, bx, by, { - color = foreground, radius = 2, opacity = visibility, - border = self.active and 0 or 1, border_color = background, - }) - ass:txt(bx - width / 2, by - height / 2, 5, self.badge, badge_opts) - - local clip_border = math.max(self.font_size / 20, 1) - local clip_path = assdraw.ass_new() - clip_path:round_rect_cw( - math.floor((bx - width) - clip_border), math.floor((by - height) - clip_border), bx, by, 3 - ) - icon_clip = '\\iclip(' .. clip_path.scale .. ', ' .. clip_path.text .. ')' - end - - -- Icon - local x, y = round(self.ax + (self.bx - self.ax) / 2), round(self.ay + (self.by - self.ay) / 2) - ass:icon(x, y, self.font_size, self.icon, { - color = foreground, border = self.active and 0 or options.text_border, border_color = background, - opacity = visibility, clip = icon_clip, - }) - - return ass -end - -return Button diff --git a/.config/mpv/scripts/uosc_shared/elements/Controls.lua b/.config/mpv/scripts/uosc_shared/elements/Controls.lua deleted file mode 100644 index 9a6be72..0000000 --- a/.config/mpv/scripts/uosc_shared/elements/Controls.lua +++ /dev/null @@ -1,329 +0,0 @@ -local Element = require('uosc_shared/elements/Element') -local Button = require('uosc_shared/elements/Button') -local CycleButton = require('uosc_shared/elements/CycleButton') -local Speed = require('uosc_shared/elements/Speed') - --- `scale` - `options.controls_size` scale factor. --- `ratio` - Width/height ratio of a static or dynamic element. --- `ratio_min` Min ratio for 'dynamic' sized element. ----@alias ControlItem {element?: Element; kind: string; sizing: 'space' | 'static' | 'dynamic'; scale: number; ratio?: number; ratio_min?: number; hide: boolean; dispositions?: table<string, boolean>} - ----@class Controls : Element -local Controls = class(Element) - -function Controls:new() return Class.new(self) --[[@as Controls]] end -function Controls:init() - Element.init(self, 'controls') - ---@type ControlItem[] All control elements serialized from `options.controls`. - self.controls = {} - ---@type ControlItem[] Only controls that match current dispositions. - self.layout = {} - - -- Serialize control elements - local shorthands = { - menu = 'command:menu:script-binding uosc/menu-blurred?Menu', - subtitles = 'command:subtitles:script-binding uosc/subtitles#sub>0?Subtitles', - audio = 'command:graphic_eq:script-binding uosc/audio#audio>1?Audio', - ['audio-device'] = 'command:speaker:script-binding uosc/audio-device?Audio device', - video = 'command:theaters:script-binding uosc/video#video>1?Video', - playlist = 'command:list_alt:script-binding uosc/playlist?Playlist', - chapters = 'command:bookmark:script-binding uosc/chapters#chapters>0?Chapters', - ['editions'] = 'command:bookmarks:script-binding uosc/editions#editions>1?Editions', - ['stream-quality'] = 'command:high_quality:script-binding uosc/stream-quality?Stream quality', - ['open-file'] = 'command:file_open:script-binding uosc/open-file?Open file', - ['items'] = 'command:list_alt:script-binding uosc/items?Playlist/Files', - prev = 'command:arrow_back_ios:script-binding uosc/prev?Previous', - next = 'command:arrow_forward_ios:script-binding uosc/next?Next', - first = 'command:first_page:script-binding uosc/first?First', - last = 'command:last_page:script-binding uosc/last?Last', - ['loop-playlist'] = 'cycle:repeat:loop-playlist:no/inf!?Loop playlist', - ['loop-file'] = 'cycle:repeat_one:loop-file:no/inf!?Loop file', - shuffle = 'toggle:shuffle:shuffle?Shuffle', - fullscreen = 'cycle:crop_free:fullscreen:no/yes=fullscreen_exit!?Fullscreen', - } - - -- Parse out disposition/config pairs - local items = {} - local in_disposition = false - local current_item = nil - for c in options.controls:gmatch('.') do - if not current_item then current_item = {disposition = '', config = ''} end - if c == '<' and #current_item.config == 0 then in_disposition = true - elseif c == '>' and #current_item.config == 0 then in_disposition = false - elseif c == ',' and not in_disposition then - items[#items + 1] = current_item - current_item = nil - else - local prop = in_disposition and 'disposition' or 'config' - current_item[prop] = current_item[prop] .. c - end - end - items[#items + 1] = current_item - - -- Create controls - self.controls = {} - for i, item in ipairs(items) do - local config = shorthands[item.config] and shorthands[item.config] or item.config - local config_tooltip = split(config, ' *%? *') - local tooltip = config_tooltip[2] - config = shorthands[config_tooltip[1]] - and split(shorthands[config_tooltip[1]], ' *%? *')[1] or config_tooltip[1] - local config_badge = split(config, ' *# *') - config = config_badge[1] - local badge = config_badge[2] - local parts = split(config, ' *: *') - local kind, params = parts[1], itable_slice(parts, 2) - - -- Serialize dispositions - local dispositions = {} - for _, definition in ipairs(split(item.disposition, ' *, *')) do - if #definition > 0 then - local value = definition:sub(1, 1) ~= '!' - local name = not value and definition:sub(2) or definition - local prop = name:sub(1, 4) == 'has_' and name or 'is_' .. name - dispositions[prop] = value - end - end - - -- Convert toggles into cycles - if kind == 'toggle' then - kind = 'cycle' - params[#params + 1] = 'no/yes!' - end - - -- Create a control element - local control = {dispositions = dispositions, kind = kind} - - if kind == 'space' then - control.sizing = 'space' - elseif kind == 'gap' then - table_assign(control, {sizing = 'dynamic', scale = 1, ratio = params[1] or 0.3, ratio_min = 0}) - elseif kind == 'command' then - if #params ~= 2 then - mp.error(string.format( - 'command button needs 2 parameters, %d received: %s', #params, table.concat(params, '/') - )) - else - local element = Button:new('control_' .. i, { - icon = params[1], - anchor_id = 'controls', - on_click = function() mp.command(params[2]) end, - tooltip = tooltip, - count_prop = 'sub', - }) - table_assign(control, {element = element, sizing = 'static', scale = 1, ratio = 1}) - if badge then self:register_badge_updater(badge, element) end - end - elseif kind == 'cycle' then - if #params ~= 3 then - mp.error(string.format( - 'cycle button needs 3 parameters, %d received: %s', - #params, table.concat(params, '/') - )) - else - local state_configs = split(params[3], ' */ *') - local states = {} - - for _, state_config in ipairs(state_configs) do - local active = false - if state_config:sub(-1) == '!' then - active = true - state_config = state_config:sub(1, -2) - end - local state_params = split(state_config, ' *= *') - local value, icon = state_params[1], state_params[2] or params[1] - states[#states + 1] = {value = value, icon = icon, active = active} - end - - local element = CycleButton:new('control_' .. i, { - prop = params[2], anchor_id = 'controls', states = states, tooltip = tooltip, - }) - table_assign(control, {element = element, sizing = 'static', scale = 1, ratio = 1}) - if badge then self:register_badge_updater(badge, element) end - end - elseif kind == 'speed' then - if not Elements.speed then - local element = Speed:new({anchor_id = 'controls'}) - table_assign(control, { - element = element, sizing = 'dynamic', scale = params[1] or 1.3, ratio = 3.5, ratio_min = 2, - }) - else - msg.error('there can only be 1 speed slider') - end - else - msg.error('unknown element kind "' .. kind .. '"') - break - end - - self.controls[#self.controls + 1] = control - end - - self:reflow() -end - -function Controls:reflow() - -- Populate the layout only with items that match current disposition - self.layout = {} - for _, control in ipairs(self.controls) do - local matches = true - for prop, value in pairs(control.dispositions) do - if state[prop] ~= value then - matches = false - break - end - end - if control.element then control.element.enabled = matches end - if matches then self.layout[#self.layout + 1] = control end - end - - self:update_dimensions() - Elements:trigger('controls_reflow') -end - ----@param badge string ----@param element Element An element that supports `badge` property. -function Controls:register_badge_updater(badge, element) - local prop_and_limit = split(badge, ' *> *') - local prop, limit = prop_and_limit[1], tonumber(prop_and_limit[2] or -1) - local observable_name, serializer, is_external_prop = prop, nil, false - - if itable_index_of({'sub', 'audio', 'video'}, prop) then - observable_name = 'track-list' - serializer = function(value) - local count = 0 - for _, track in ipairs(value) do if track.type == prop then count = count + 1 end end - return count - end - else - local parts = split(prop, '@') - -- Support both new `prop@owner` and old `@prop` syntaxes - if #parts > 1 then prop, is_external_prop = parts[1] ~= '' and parts[1] or parts[2], true end - serializer = function(value) return value and (type(value) == 'table' and #value or tostring(value)) or nil end - end - - local function handler(_, value) - local new_value = serializer(value) --[[@as nil|string|integer]] - local value_number = tonumber(new_value) - if value_number then new_value = value_number > limit and value_number or nil end - element.badge = new_value - request_render() - end - - if is_external_prop then element['on_external_prop_' .. prop] = function(_, value) handler(prop, value) end - else mp.observe_property(observable_name, 'native', handler) end -end - -function Controls:get_visibility() - return (Elements.speed and Elements.speed.dragging) and 1 or Elements.timeline:get_is_hovered() - and -1 or Element.get_visibility(self) -end - -function Controls:update_dimensions() - local window_border = Elements.window_border.size - local size = state.fullormaxed and options.controls_size_fullscreen or options.controls_size - local spacing = options.controls_spacing - local margin = options.controls_margin - - -- Disable when not enough space - local available_space = display.height - Elements.window_border.size * 2 - if Elements.top_bar.enabled then available_space = available_space - Elements.top_bar.size end - if Elements.timeline.enabled then available_space = available_space - Elements.timeline.size_max end - self.enabled = available_space > size + 10 - - -- Reset hide/enabled flags - for c, control in ipairs(self.layout) do - control.hide = false - if control.element then control.element.enabled = self.enabled end - end - - if not self.enabled then return end - - -- Container - self.bx = display.width - window_border - margin - self.by = (Elements.timeline.enabled and Elements.timeline.ay or display.height - window_border) - margin - self.ax, self.ay = window_border + margin, self.by - size - - -- Controls - local available_width = self.bx - self.ax - local statics_width = (#self.layout - 1) * spacing - local min_content_width = statics_width - local max_dynamics_width, dynamic_units, spaces = 0, 0, 0 - - -- Calculate statics_width, min_content_width, and count spaces - for c, control in ipairs(self.layout) do - if control.sizing == 'space' then - spaces = spaces + 1 - elseif control.sizing == 'static' then - local width = size * control.scale * control.ratio - statics_width = statics_width + width - min_content_width = min_content_width + width - elseif control.sizing == 'dynamic' then - min_content_width = min_content_width + size * control.scale * control.ratio_min - max_dynamics_width = max_dynamics_width + size * control.scale * control.ratio - dynamic_units = dynamic_units + control.scale * control.ratio - end - end - - -- Hide & disable elements in the middle until we fit into available width - if min_content_width > available_width then - local i = math.ceil(#self.layout / 2 + 0.1) - for a = 0, #self.layout - 1, 1 do - i = i + (a * (a % 2 == 0 and 1 or -1)) - local control = self.layout[i] - - if control.kind ~= 'gap' and control.kind ~= 'space' then - control.hide = true - if control.element then control.element.enabled = false end - if control.sizing == 'static' then - local width = size * control.scale * control.ratio - min_content_width = min_content_width - width - spacing - statics_width = statics_width - width - spacing - elseif control.sizing == 'dynamic' then - min_content_width = min_content_width - size * control.scale * control.ratio_min - spacing - max_dynamics_width = max_dynamics_width - size * control.scale * control.ratio - dynamic_units = dynamic_units - control.scale * control.ratio - end - - if min_content_width < available_width then break end - end - end - end - - -- Lay out the elements - local current_x = self.ax - local width_for_dynamics = available_width - statics_width - local space_width = (width_for_dynamics - max_dynamics_width) / spaces - - for c, control in ipairs(self.layout) do - if not control.hide then - local sizing, element, scale, ratio = control.sizing, control.element, control.scale, control.ratio - local width, height = 0, 0 - - if sizing == 'space' then - if space_width > 0 then width = space_width end - elseif sizing == 'static' then - height = size * scale - width = height * ratio - elseif sizing == 'dynamic' then - height = size * scale - width = max_dynamics_width < width_for_dynamics - and height * ratio or width_for_dynamics * ((scale * ratio) / dynamic_units) - end - - local bx = current_x + width - if element then element:set_coordinates(round(current_x), round(self.by - height), bx, self.by) end - current_x = bx + spacing - end - end - - Elements:update_proximities() - request_render() -end - -function Controls:on_dispositions() self:reflow() end -function Controls:on_display() self:update_dimensions() end -function Controls:on_prop_border() self:update_dimensions() end -function Controls:on_prop_fullormaxed() self:update_dimensions() end -function Controls:on_timeline_enabled() self:update_dimensions() end - -return Controls diff --git a/.config/mpv/scripts/uosc_shared/elements/Curtain.lua b/.config/mpv/scripts/uosc_shared/elements/Curtain.lua deleted file mode 100644 index 99b9f14..0000000 --- a/.config/mpv/scripts/uosc_shared/elements/Curtain.lua +++ /dev/null @@ -1,35 +0,0 @@ -local Element = require('uosc_shared/elements/Element') - ----@class Curtain : Element -local Curtain = class(Element) - -function Curtain:new() return Class.new(self) --[[@as Curtain]] end -function Curtain:init() - Element.init(self, 'curtain', {ignores_menu = true}) - self.opacity = 0 - ---@type string[] - self.dependents = {} -end - ----@param id string -function Curtain:register(id) - self.dependents[#self.dependents + 1] = id - if #self.dependents == 1 then self:tween_property('opacity', self.opacity, 1) end -end - ----@param id string -function Curtain:unregister(id) - self.dependents = itable_filter(self.dependents, function(item) return item ~= id end) - if #self.dependents == 0 then self:tween_property('opacity', self.opacity, 0) end -end - -function Curtain:render() - if self.opacity == 0 or options.curtain_opacity == 0 then return end - local ass = assdraw.ass_new() - ass:rect(0, 0, display.width, display.height, { - color = '000000', opacity = options.curtain_opacity * self.opacity, - }) - return ass -end - -return Curtain diff --git a/.config/mpv/scripts/uosc_shared/elements/CycleButton.lua b/.config/mpv/scripts/uosc_shared/elements/CycleButton.lua deleted file mode 100644 index 7f1c02f..0000000 --- a/.config/mpv/scripts/uosc_shared/elements/CycleButton.lua +++ /dev/null @@ -1,64 +0,0 @@ -local Button = require('uosc_shared/elements/Button') - ----@alias CycleState {value: any; icon: string; active?: boolean} ----@alias CycleButtonProps {prop: string; states: CycleState[]; anchor_id?: string; tooltip?: string} - ----@class CycleButton : Button -local CycleButton = class(Button) - ----@param id string ----@param props CycleButtonProps -function CycleButton:new(id, props) return Class.new(self, id, props) --[[@as CycleButton]] end ----@param id string ----@param props CycleButtonProps -function CycleButton:init(id, props) - local is_state_prop = itable_index_of({'shuffle'}, props.prop) - self.prop = props.prop - self.states = props.states - - Button.init(self, id, props) - - self.icon = self.states[1].icon - self.active = self.states[1].active - self.current_state_index = 1 - self.on_click = function() - local new_state = self.states[self.current_state_index + 1] or self.states[1] - local new_value = new_state.value - if self.owner then - mp.commandv('script-message-to', self.owner, 'set', self.prop, new_value) - elseif is_state_prop then - if itable_index_of({'yes', 'no'}, new_value) then new_value = new_value == 'yes' end - set_state(self.prop, new_value) - else - mp.set_property(self.prop, new_value) - end - end - - self.handle_change = function(name, value) - if is_state_prop and type(value) == 'boolean' then value = value and 'yes' or 'no' end - local index = itable_find(self.states, function(state) return state.value == value end) - self.current_state_index = index or 1 - self.icon = self.states[self.current_state_index].icon - self.active = self.states[self.current_state_index].active - request_render() - end - - local prop_parts = split(self.prop, '@') - if #prop_parts == 2 then -- External prop with a script owner - self.prop, self.owner = prop_parts[1], prop_parts[2] - self['on_external_prop_' .. self.prop] = function(_, value) self.handle_change(self.prop, value) end - self.handle_change(self.prop, external[self.prop]) - elseif is_state_prop then -- uosc's state props - self['on_prop_' .. self.prop] = function(self, value) self.handle_change(self.prop, value) end - self.handle_change(self.prop, state[self.prop]) - else - mp.observe_property(self.prop, 'string', self.handle_change) - end -end - -function CycleButton:destroy() - Button.destroy(self) - mp.unobserve_property(self.handle_change) -end - -return CycleButton diff --git a/.config/mpv/scripts/uosc_shared/elements/Element.lua b/.config/mpv/scripts/uosc_shared/elements/Element.lua deleted file mode 100644 index 718782d..0000000 --- a/.config/mpv/scripts/uosc_shared/elements/Element.lua +++ /dev/null @@ -1,149 +0,0 @@ ----@alias ElementProps {enabled?: boolean; ax?: number; ay?: number; bx?: number; by?: number; ignores_menu?: boolean; anchor_id?: string;} - --- Base class all elements inherit from. ----@class Element : Class -local Element = class() - ----@param id string ----@param props? ElementProps -function Element:init(id, props) - self.id = id - -- `false` means element won't be rendered, or receive events - self.enabled = true - -- Element coordinates - self.ax, self.ay, self.bx, self.by = 0, 0, 0, 0 - -- Relative proximity from `0` - mouse outside `proximity_max` range, to `1` - mouse within `proximity_min` range. - self.proximity = 0 - -- Raw proximity in pixels. - self.proximity_raw = infinity - ---@type number `0-1` factor to force min visibility. Used for toggling element's permanent visibility. - self.min_visibility = 0 - ---@type number `0-1` factor to force a visibility value. Used for flashing, fading out, and other animations - self.forced_visibility = nil - ---@type boolean Render this element even when menu is open. - self.ignores_menu = false - ---@type nil|string ID of an element from which this one should inherit visibility. - self.anchor_id = nil - - if props then table_assign(self, props) end - - -- Flash timer - self._flash_out_timer = mp.add_timeout(options.flash_duration / 1000, function() - local getTo = function() return self.proximity end - self:tween_property('forced_visibility', 1, getTo, function() - self.forced_visibility = nil - end) - end) - self._flash_out_timer:kill() - - Elements:add(self) -end - -function Element:destroy() - self.destroyed = true - Elements:remove(self) -end - ----@param ax number ----@param ay number ----@param bx number ----@param by number -function Element:set_coordinates(ax, ay, bx, by) - self.ax, self.ay, self.bx, self.by = ax, ay, bx, by - Elements:update_proximities() - self:maybe('on_coordinates') -end - -function Element:update_proximity() - if cursor.hidden then - self.proximity_raw = infinity - self.proximity = 0 - else - local range = options.proximity_out - options.proximity_in - self.proximity_raw = get_point_to_rectangle_proximity(cursor, self) - self.proximity = 1 - (clamp(0, self.proximity_raw - options.proximity_in, range) / range) - end -end - --- Decide elements visibility based on proximity and various other factors -function Element:get_visibility() - -- Hide when menu is open, unless this is a menu - ---@diagnostic disable-next-line: undefined-global - if not self.ignores_menu and Menu and Menu:is_open() then return 0 end - - -- Persistency - local persist = config[self.id .. '_persistency'] - if persist and ( - (persist.audio and state.is_audio) - or (persist.paused and state.pause) - or (persist.video and state.is_video) - or (persist.image and state.is_image) - or (persist.idle and state.is_idle) - ) then return 1 end - - -- Forced visibility - if self.forced_visibility then return math.max(self.forced_visibility, self.min_visibility) end - - -- Anchor inheritance - -- If anchor returns -1, it means all attached elements should force hide. - local anchor = self.anchor_id and Elements[self.anchor_id] - local anchor_visibility = anchor and anchor:get_visibility() or 0 - - return anchor_visibility == -1 and 0 or math.max(self.proximity, anchor_visibility, self.min_visibility) -end - --- Call method if it exists -function Element:maybe(name, ...) - if self[name] then return self[name](self, ...) end -end - --- Attach a tweening animation to this element ----@param from number ----@param to number|fun():number ----@param setter fun(value: number) ----@param factor_or_callback? number|fun() ----@param callback? fun() Called either on animation end, or when animation is killed. -function Element:tween(from, to, setter, factor_or_callback, callback) - self:tween_stop() - self._kill_tween = self.enabled and tween( - from, to, setter, factor_or_callback, - function() - self._kill_tween = nil - if callback then callback() end - end - ) -end - -function Element:is_tweening() return self and self._kill_tween end -function Element:tween_stop() self:maybe('_kill_tween') end - --- Animate an element property between 2 values. ----@param prop string ----@param from number ----@param to number|fun():number ----@param factor_or_callback? number|fun() ----@param callback? fun() Called either on animation end, or when animation is killed. -function Element:tween_property(prop, from, to, factor_or_callback, callback) - self:tween(from, to, function(value) self[prop] = value end, factor_or_callback, callback) -end - ----@param name string -function Element:trigger(name, ...) - local result = self:maybe('on_' .. name, ...) - request_render() - return result -end - --- Briefly flashes the element for `options.flash_duration` milliseconds. --- Useful to visualize changes of volume and timeline when changed via hotkeys. -function Element:flash() - if options.flash_duration > 0 and (self.proximity < 1 or self._flash_out_timer:is_enabled()) then - self:tween_stop() - self.forced_visibility = 1 - request_render() - self._flash_out_timer:kill() - self._flash_out_timer:resume() - end -end - -return Element diff --git a/.config/mpv/scripts/uosc_shared/elements/Elements.lua b/.config/mpv/scripts/uosc_shared/elements/Elements.lua deleted file mode 100644 index fe2cd6b..0000000 --- a/.config/mpv/scripts/uosc_shared/elements/Elements.lua +++ /dev/null @@ -1,158 +0,0 @@ -local Elements = {itable = {}} - ----@param element Element -function Elements:add(element) - if not element.id then - msg.error('attempt to add element without "id" property') - return - end - - if self:has(element.id) then Elements:remove(element.id) end - - self.itable[#self.itable + 1] = element - self[element.id] = element - - request_render() -end - -function Elements:remove(idOrElement) - if not idOrElement then return end - local id = type(idOrElement) == 'table' and idOrElement.id or idOrElement - local element = Elements[id] - if element then - if not element.destroyed then element:destroy() end - element.enabled = false - self.itable = itable_remove(self.itable, self[id]) - self[id] = nil - request_render() - end -end - -function Elements:update_proximities() - local capture_mbtn_left = false - local capture_wheel = false - local menu_only = Elements.menu ~= nil - local mouse_leave_elements = {} - local mouse_enter_elements = {} - - -- Calculates proximities and opacities for defined elements - for _, element in self:ipairs() do - if element.enabled then - local previous_proximity_raw = element.proximity_raw - - -- If menu is open, all other elements have to be disabled - if menu_only then - if element.ignores_menu then - capture_mbtn_left = true - capture_wheel = true - element:update_proximity() - else - element.proximity_raw = infinity - element.proximity = 0 - end - else - element:update_proximity() - end - - -- Element has global forced key listeners - if element.on_global_mbtn_left_down then capture_mbtn_left = true end - if element.on_global_wheel_up or element.on_global_wheel_down then capture_wheel = true end - - if element.proximity_raw == 0 then - -- Element has local forced key listeners - if element.on_mbtn_left_down then capture_mbtn_left = true end - if element.on_wheel_up or element.on_wheel_up then capture_wheel = true end - - -- Mouse entered element area - if previous_proximity_raw ~= 0 then - mouse_enter_elements[#mouse_enter_elements + 1] = element - end - else - -- Mouse left element area - if previous_proximity_raw == 0 then - mouse_leave_elements[#mouse_leave_elements + 1] = element - end - end - end - end - - -- Enable key group captures requested by elements - mp[capture_mbtn_left and 'enable_key_bindings' or 'disable_key_bindings']('mbtn_left') - mp[capture_wheel and 'enable_key_bindings' or 'disable_key_bindings']('wheel') - - -- Trigger `mouse_leave` and `mouse_enter` events - for _, element in ipairs(mouse_leave_elements) do element:trigger('mouse_leave') end - for _, element in ipairs(mouse_enter_elements) do element:trigger('mouse_enter') end -end - --- Toggles passed elements' min visibilities between 0 and 1. ----@param ids string[] IDs of elements to peek. -function Elements:toggle(ids) - local has_invisible = itable_find(ids, function(id) return Elements[id] and Elements[id].min_visibility ~= 1 end) - self:set_min_visibility(has_invisible and 1 or 0, ids) -end - --- Set (animate) elements' min visibilities to passed value. ----@param visibility number 0-1 floating point. ----@param ids string[] IDs of elements to peek. -function Elements:set_min_visibility(visibility, ids) - for _, id in ipairs(ids) do - local element = Elements[id] - if element then element:tween_property('min_visibility', element.min_visibility, visibility) end - end -end - --- Flash passed elements. ----@param ids string[] IDs of elements to peek. -function Elements:flash(ids) - local elements = itable_filter(self.itable, function(element) return itable_index_of(ids, element.id) ~= nil end) - for _, element in ipairs(elements) do element:flash() end -end - ----@param name string Event name. -function Elements:trigger(name, ...) - for _, element in self:ipairs() do element:trigger(name, ...) end -end - --- Trigger two events, `name` and `global_name`, depending on element-cursor proximity. --- Disabled elements don't receive these events. ----@param name string Event name. -function Elements:proximity_trigger(name, ...) - local stop_normal, stop_global = false, false - for i = #self.itable, 1, -1 do - local element = self.itable[i] - if element.enabled then - if element.proximity_raw == 0 then - if element:trigger(name, ...) == 'stop_propagation' then break end - end - if element:trigger('global_' .. name, ...) == 'stop_propagation' then break end - end - end -end - -function Elements:has(id) return self[id] ~= nil end -function Elements:ipairs() return ipairs(self.itable) end - ----@param name string Event name. -function Elements:create_proximity_dispatcher(name) - return function(...) self:proximity_trigger(name, ...) end -end - -mp.set_key_bindings({ - { - 'mbtn_left', - Elements:create_proximity_dispatcher('mbtn_left_up'), - function(...) - update_mouse_pos(nil, mp.get_property_native('mouse-pos'), true) - Elements:proximity_trigger('mbtn_left_down', ...) - end, - }, - {'mbtn_left_dbl', 'ignore'}, -}, 'mbtn_left', 'force') - -mp.set_key_bindings({ - {'wheel_up', Elements:create_proximity_dispatcher('wheel_up')}, - {'wheel_down', Elements:create_proximity_dispatcher('wheel_down')}, -}, 'wheel', 'force') - -return Elements diff --git a/.config/mpv/scripts/uosc_shared/elements/Menu.lua b/.config/mpv/scripts/uosc_shared/elements/Menu.lua deleted file mode 100644 index aad5016..0000000 --- a/.config/mpv/scripts/uosc_shared/elements/Menu.lua +++ /dev/null @@ -1,783 +0,0 @@ -local Element = require('uosc_shared/elements/Element') - --- Menu data structure accepted by `Menu:open(menu)`. ----@alias MenuData {type?: string; title?: string; hint?: string; keep_open?: boolean; separator?: boolean; items?: MenuDataItem[]; selected_index?: integer;} ----@alias MenuDataItem MenuDataValue|MenuData ----@alias MenuDataValue {title?: string; hint?: string; icon?: string; value: any; bold?: boolean; italic?: boolean; muted?: boolean; active?: boolean; keep_open?: boolean; separator?: boolean;} ----@alias MenuOptions {mouse_nav?: boolean; on_open?: fun(); on_close?: fun(); on_back?: fun()} - --- Internal data structure created from `Menu`. ----@alias MenuStack {id?: string; type?: string; title?: string; hint?: string; selected_index?: number; keep_open?: boolean; separator?: boolean; items: MenuStackItem[]; parent_menu?: MenuStack; active?: boolean; width: number; height: number; top: number; scroll_y: number; scroll_height: number; title_width: number; hint_width: number; max_width: number; is_root?: boolean; fling?: Fling} ----@alias MenuStackItem MenuStackValue|MenuStack ----@alias MenuStackValue {title?: string; hint?: string; icon?: string; value: any; active?: boolean; bold?: boolean; italic?: boolean; muted?: boolean; keep_open?: boolean; separator?: boolean; title_width: number; hint_width: number} ----@alias Fling {y: number, distance: number, time: number, easing: fun(x: number), duration: number, update_cursor?: boolean} - ----@class Menu : Element -local Menu = class(Element) - ----@param data MenuData ----@param callback fun(value: any) ----@param opts? MenuOptions -function Menu:open(data, callback, opts) - local open_menu = self:is_open() - if open_menu then - open_menu.is_being_replaced = true - open_menu:close(true) - end - return Menu:new(data, callback, opts) -end - ----@param menu_type? string ----@return Menu|nil -function Menu:is_open(menu_type) - return Elements.menu and (not menu_type or Elements.menu.type == menu_type) and Elements.menu or nil -end - ----@param immediate? boolean Close immediately without fadeout animation. ----@param callback? fun() Called after the animation (if any) ends and element is removed and destroyed. ----@overload fun(callback: fun()) -function Menu:close(immediate, callback) - if type(immediate) ~= 'boolean' then callback = immediate end - - local menu = self == Menu and Elements.menu or self - - if menu and not menu.destroyed then - if menu.is_closing then - menu:tween_stop() - return - end - - local function close() - Elements:remove('menu') - menu.is_closing, menu.stack, menu.current, menu.all, menu.by_id = false, nil, nil, {}, {} - menu:disable_key_bindings() - Elements:update_proximities() - if callback then callback() end - request_render() - end - - menu.is_closing = true - - if immediate then close() - else menu:fadeout(close) end - end -end - ----@param data MenuData ----@param callback fun(value: any) ----@param opts? MenuOptions ----@return Menu -function Menu:new(data, callback, opts) return Class.new(self, data, callback, opts) --[[@as Menu]] end ----@param data MenuData ----@param callback fun(value: any) ----@param opts? MenuOptions -function Menu:init(data, callback, opts) - Element.init(self, 'menu', {ignores_menu = true}) - - -----@type fun() - self.callback = callback - self.opts = opts or {} - self.offset_x = 0 -- Used for submenu transition animation. - self.mouse_nav = self.opts.mouse_nav -- Stops pre-selecting items - self.item_height = nil - self.item_spacing = 1 - self.item_padding = nil - self.font_size = nil - self.font_size_hint = nil - self.scroll_step = nil -- Item height + item spacing. - self.scroll_height = nil -- Items + spacings - container height. - self.opacity = 0 -- Used to fade in/out. - self.type = data.type - ---@type MenuStack Root MenuStack. - self.root = nil - ---@type MenuStack Current MenuStack. - self.current = nil - ---@type MenuStack[] All menus in a flat array. - self.all = nil - ---@type table<string, MenuStack> Map of submenus by their ids, such as `'Tools > Aspect ratio'`. - self.by_id = {} - self.key_bindings = {} - self.is_being_replaced = false - self.is_closing, self.is_closed = false, false - ---@type {y: integer, time: number}[] - self.drag_data = nil - self.is_dragging = false - - self:update(data) - - if self.mouse_nav then - if self.current then self.current.selected_index = nil end - else - for _, menu in ipairs(self.all) do self:scroll_to_index(menu.selected_index, menu) end - end - - self:tween_property('opacity', 0, 1) - self:enable_key_bindings() - Elements.curtain:register('menu') - if self.opts.on_open then self.opts.on_open() end -end - -function Menu:destroy() - Element.destroy(self) - self:disable_key_bindings() - self.is_closed = true - if not self.is_being_replaced then Elements.curtain:unregister('menu') end - if self.opts.on_close then self.opts.on_close() end -end - ----@param data MenuData -function Menu:update(data) - self.type = data.type - - local new_root = {is_root = true} - local new_all = {} - local new_by_id = {} - local menus_to_serialize = {{new_root, data}} - local old_current_id = self.current and self.current.id - - table_assign(new_root, data, {'title', 'hint', 'keep_open'}) - - local i = 0 - while i < #menus_to_serialize do - i = i + 1 - local menu, menu_data = menus_to_serialize[i][1], menus_to_serialize[i][2] - local parent_id = menu.parent_menu and not menu.parent_menu.is_root and menu.parent_menu.id - if not menu.is_root then - menu.id = (parent_id and parent_id .. ' > ' or '') .. (menu_data.title or i) - end - menu.icon = 'chevron_right' - - -- Update items - local first_active_index = nil - menu.items = {} - - for i, item_data in ipairs(menu_data.items or {}) do - if item_data.active and not first_active_index then first_active_index = i end - - local item = {} - table_assign(item, item_data, { - 'title', 'icon', 'hint', 'active', 'bold', 'italic', 'muted', 'value', 'keep_open', 'separator', - }) - if item.keep_open == nil then item.keep_open = menu.keep_open end - - -- Submenu - if item_data.items then - item.parent_menu = menu - menus_to_serialize[#menus_to_serialize + 1] = {item, item_data} - end - - menu.items[i] = item - end - - if menu.is_root then menu.selected_index = menu_data.selected_index or first_active_index end - - -- Retain old state - local old_menu = self.by_id[menu.is_root and '__root__' or menu.id] - if old_menu then table_assign(menu, old_menu, {'selected_index', 'scroll_y', 'fling'}) end - - new_all[#new_all + 1] = menu - new_by_id[menu.is_root and '__root__' or menu.id] = menu - end - - self.root, self.all, self.by_id = new_root, new_all, new_by_id - self.current = self.by_id[old_current_id] or self.root - - self:update_content_dimensions() - self:reset_navigation() -end - ----@param items MenuDataItem[] -function Menu:update_items(items) - local data = table_shallow_copy(self.root) - data.items = items - self:update(data) -end - -function Menu:update_content_dimensions() - self.item_height = state.fullormaxed and options.menu_item_height_fullscreen or options.menu_item_height - self.font_size = round(self.item_height * 0.48 * options.font_scale) - self.font_size_hint = self.font_size - 1 - self.item_padding = round((self.item_height - self.font_size) * 0.6) - self.scroll_step = self.item_height + self.item_spacing - - local title_opts = {size = self.font_size, italic = false, bold = false} - local hint_opts = {size = self.font_size_hint} - - for _, menu in ipairs(self.all) do - -- Estimate width of a widest item - local max_width = 0 - for _, item in ipairs(menu.items) do - local icon_width = item.icon and self.font_size or 0 - item.title_width = text_width(item.title, title_opts) - item.hint_width = text_width(item.hint, hint_opts) - local spacings_in_item = 1 + (item.title_width > 0 and 1 or 0) - + (item.hint_width > 0 and 1 or 0) + (icon_width > 0 and 1 or 0) - local estimated_width = item.title_width + item.hint_width + icon_width - + (self.item_padding * spacings_in_item) - if estimated_width > max_width then max_width = estimated_width end - end - - -- Also check menu title - title_opts.bold, title_opts.italic = true, false - local menu_title_width = text_width(menu.title, title_opts) - if menu_title_width > max_width then max_width = menu_title_width end - - menu.max_width = max_width - end - - self:update_dimensions() -end - -function Menu:update_dimensions() - -- Coordinates and sizes are of the scrollable area to make - -- consuming values in rendering and collisions easier. Title drawn above this, so - -- we need to account for that in max_height and ay position. - local min_width = state.fullormaxed and options.menu_min_width_fullscreen or options.menu_min_width - - for _, menu in ipairs(self.all) do - menu.width = round(clamp(min_width, menu.max_width, display.width * 0.9)) - local title_height = (menu.is_root and menu.title) and self.scroll_step or 0 - local max_height = round((display.height - title_height) * 0.9) - local content_height = self.scroll_step * #menu.items - menu.height = math.min(content_height - self.item_spacing, max_height) - menu.top = round(math.max((display.height - menu.height) / 2, title_height * 1.5)) - menu.scroll_height = math.max(content_height - menu.height - self.item_spacing, 0) - menu.scroll_y = menu.scroll_y or 0 - self:scroll_to(menu.scroll_y, menu) -- clamps scroll_y to scroll limits - end - - local ax = round((display.width - self.current.width) / 2) + self.offset_x - self:set_coordinates(ax, self.current.top, ax + self.current.width, self.current.top + self.current.height) -end - -function Menu:reset_navigation() - local menu = self.current - - -- Reset indexes and scroll - self:scroll_to(menu.scroll_y) -- clamps scroll_y to scroll limits - if self.mouse_nav then - self:select_item_below_cursor() - else - self:select_index((menu.items and #menu.items > 0) and clamp(1, menu.selected_index or 1, #menu.items) or nil) - end - - -- Walk up the parent menu chain and activate items that lead to current menu - local parent = menu.parent_menu - while parent do - parent.selected_index = itable_index_of(parent.items, menu) - menu, parent = parent, parent.parent_menu - end - - request_render() -end - -function Menu:set_offset_x(offset) - local delta = offset - self.offset_x - self.offset_x = offset - self:set_coordinates(self.ax + delta, self.ay, self.bx + delta, self.by) -end - -function Menu:fadeout(callback) self:tween_property('opacity', 1, 0, callback) end - -function Menu:get_item_index_below_cursor() - local menu = self.current - if #menu.items < 1 or self.proximity_raw > 0 then return nil end - return math.max(1, math.min(math.ceil((cursor.y - self.ay + menu.scroll_y) / self.scroll_step), #menu.items)) -end - -function Menu:get_first_active_index(menu) - menu = menu or self.current - for index, item in ipairs(self.current.items) do - if item.active then return index end - end -end - ----@param pos? number ----@param menu? MenuStack -function Menu:set_scroll_to(pos, menu) - menu = menu or self.current - menu.scroll_y = clamp(0, pos or 0, menu.scroll_height) - request_render() -end - ----@param delta? number ----@param menu? MenuStack -function Menu:set_scroll_by(delta, menu) - menu = menu or self.current - self:set_scroll_to(menu.scroll_y + delta, menu) -end - ----@param pos? number ----@param menu? MenuStack ----@param fling_options? table -function Menu:scroll_to(pos, menu, fling_options) - menu = menu or self.current - menu.fling = { - y = menu.scroll_y, distance = clamp(-menu.scroll_y, pos - menu.scroll_y, menu.scroll_height - menu.scroll_y), - time = mp.get_time(), duration = 0.1, easing = ease_out_sext, - } - if fling_options then table_assign(menu.fling, fling_options) end - request_render() -end - ----@param delta? number ----@param menu? MenuStack ----@param fling_options? Fling -function Menu:scroll_by(delta, menu, fling_options) - menu = menu or self.current - self:scroll_to((menu.fling and (menu.fling.y + menu.fling.distance) or menu.scroll_y) + delta, menu, fling_options) -end - ----@param index? integer ----@param menu? MenuStack ----@param immediate? boolean -function Menu:scroll_to_index(index, menu, immediate) - menu = menu or self.current - if (index and index >= 1 and index <= #menu.items) then - local position = round((self.scroll_step * (index - 1)) - ((menu.height - self.scroll_step) / 2)) - if immediate then self:set_scroll_to(position, menu) - else self:scroll_to(position, menu) end - end -end - ----@param index? integer ----@param menu? MenuStack -function Menu:select_index(index, menu) - menu = menu or self.current - menu.selected_index = (index and index >= 1 and index <= #menu.items) and index or nil - request_render() -end - ----@param value? any ----@param menu? MenuStack -function Menu:select_value(value, menu) - menu = menu or self.current - local index = itable_find(menu.items, function(item) return item.value == value end) - self:select_index(index, 5) -end - ----@param menu? MenuStack -function Menu:deactivate_items(menu) - menu = menu or self.current - for _, item in ipairs(menu.items) do item.active = false end - request_render() -end - ----@param index? integer ----@param menu? MenuStack -function Menu:activate_index(index, menu) - menu = menu or self.current - if index and index >= 1 and index <= #menu.items then menu.items[index].active = true end - request_render() -end - ----@param index? integer ----@param menu? MenuStack -function Menu:activate_one_index(index, menu) - self:deactivate_items(menu) - self:activate_index(index, menu) -end - ----@param value? any ----@param menu? MenuStack -function Menu:activate_value(value, menu) - menu = menu or self.current - local index = itable_find(menu.items, function(item) return item.value == value end) - self:activate_index(index, menu) -end - ----@param value? any ----@param menu? MenuStack -function Menu:activate_one_value(value, menu) - menu = menu or self.current - local index = itable_find(menu.items, function(item) return item.value == value end) - self:activate_one_index(index, menu) -end - ----@param id string -function Menu:activate_submenu(id) - local submenu = self.by_id[id] - if submenu then - self.current = submenu - request_render() - else - msg.error(string.format('Requested submenu id "%s" doesn\'t exist', id)) - end - self:reset_navigation() -end - ----@param index? integer ----@param menu? MenuStack -function Menu:delete_index(index, menu) - menu = menu or self.current - if (index and index >= 1 and index <= #menu.items) then - table.remove(menu.items, index) - self:update_content_dimensions() - self:scroll_to_index(menu.selected_index, menu) - end -end - ----@param value? any ----@param menu? MenuStack -function Menu:delete_value(value, menu) - menu = menu or self.current - local index = itable_find(menu.items, function(item) return item.value == value end) - self:delete_index(index) -end - ----@param menu? MenuStack -function Menu:prev(menu) - menu = menu or self.current - menu.selected_index = math.max(menu.selected_index and menu.selected_index - 1 or #menu.items, 1) - self:scroll_to_index(menu.selected_index, menu, true) -end - ----@param menu? MenuStack -function Menu:next(menu) - menu = menu or self.current - menu.selected_index = math.min(menu.selected_index and menu.selected_index + 1 or 1, #menu.items) - self:scroll_to_index(menu.selected_index, menu, true) -end - -function Menu:back() - if self.opts.on_back then - self.opts.on_back() - if self.is_closed then return end - end - - local menu = self.current - local parent = menu.parent_menu - - if parent then - menu.selected_index = nil - self.current = parent - self:update_dimensions() - self:tween(self.offset_x - menu.width / 2, 0, function(offset) self:set_offset_x(offset) end) - self.opacity = 1 -- in case tween above canceled fade in animation - else - self:close() - end -end - ----@param opts? {keep_open?: boolean, preselect_submenu_item?: boolean} -function Menu:open_selected_item(opts) - opts = opts or {} - local menu = self.current - if menu.selected_index then - local item = menu.items[menu.selected_index] - -- Is submenu - if item.items then - self.current = item - if opts.preselect_submenu_item then - item.selected_index = #item.items > 0 and 1 or nil - end - self:update_dimensions() - self:tween(self.offset_x + menu.width / 2, 0, function(offset) self:set_offset_x(offset) end) - self.opacity = 1 -- in case tween above canceled fade in animation - else - self.callback(item.value) - if not item.keep_open and not opts.keep_open then self:close() end - end - end -end - -function Menu:open_selected_item_soft() self:open_selected_item({keep_open = true}) end -function Menu:open_selected_item_preselect() self:open_selected_item({preselect_submenu_item = true}) end -function Menu:select_item_below_cursor() self.current.selected_index = self:get_item_index_below_cursor() end - -function Menu:on_display() self:update_dimensions() end -function Menu:on_prop_fullormaxed() self:update_content_dimensions() end - -function Menu:on_global_mbtn_left_down() - if self.proximity_raw == 0 then - self.drag_data = {{y = cursor.y, time = mp.get_time()}} - self.current.fling = nil - else - if cursor.x < self.ax then self:back() - else self:close() end - end -end - -function Menu:fling_distance() - local first, last = self.drag_data[1], self.drag_data[#self.drag_data] - if mp.get_time() - last.time > 0.05 then return 0 end - for i = #self.drag_data - 1, 1, -1 do - local drag = self.drag_data[i] - if last.time - drag.time > 0.03 then return ((drag.y - last.y) / ((last.time - drag.time) / 0.03)) * 10 end - end - return #self.drag_data < 2 and 0 or ((first.y - last.y) / ((first.time - last.time) / 0.03)) * 10 -end - -function Menu:on_global_mbtn_left_up() - if self.proximity_raw == 0 and self.drag_data and not self.is_dragging then - self:select_item_below_cursor() - self:open_selected_item({preselect_submenu_item = false}) - end - if self.is_dragging then - local distance = self:fling_distance() - if math.abs(distance) > 50 then - self.current.fling = { - y = self.current.scroll_y, distance = distance, time = self.drag_data[#self.drag_data].time, - easing = ease_out_quart, duration = 0.5, update_cursor = true, - } - end - end - self.is_dragging = false - self.drag_data = nil -end - - -function Menu:on_global_mouse_move() - self.mouse_nav = true - if self.drag_data then - self.is_dragging = self.is_dragging or math.abs(cursor.y - self.drag_data[1].y) >= 10 - local distance = self.drag_data[#self.drag_data].y - cursor.y - if distance ~= 0 then self:set_scroll_by(distance) end - self.drag_data[#self.drag_data + 1] = {y = cursor.y, time = mp.get_time()} - end - if self.proximity_raw == 0 or self.is_dragging then self:select_item_below_cursor() - else self.current.selected_index = nil end - request_render() -end - -function Menu:on_wheel_up() self:scroll_by(self.scroll_step * -3, nil, {update_cursor = true}) end -function Menu:on_wheel_down() self:scroll_by(self.scroll_step * 3, nil, {update_cursor = true}) end - -function Menu:on_pgup() - local menu = self.current - local items_per_page = round((menu.height / self.scroll_step) * 0.4) - local paged_index = (menu.selected_index and menu.selected_index or #menu.items) - items_per_page - menu.selected_index = clamp(1, paged_index, #menu.items) - if menu.selected_index > 0 then self:scroll_to_index(menu.selected_index) end -end - -function Menu:on_pgdwn() - local menu = self.current - local items_per_page = round((menu.height / self.scroll_step) * 0.4) - local paged_index = (menu.selected_index and menu.selected_index or 1) + items_per_page - menu.selected_index = clamp(1, paged_index, #menu.items) - if menu.selected_index > 0 then self:scroll_to_index(menu.selected_index) end -end - -function Menu:on_home() - self.current.selected_index = math.min(1, #self.current.items) - if self.current.selected_index > 0 then self:scroll_to_index(self.current.selected_index) end -end - -function Menu:on_end() - self.current.selected_index = #self.current.items - if self.current.selected_index > 0 then self:scroll_to_index(self.current.selected_index) end -end - -function Menu:add_key_binding(key, name, fn, flags) - self.key_bindings[#self.key_bindings + 1] = name - mp.add_forced_key_binding(key, name, fn, flags) -end - -function Menu:enable_key_bindings() - -- The `mp.set_key_bindings()` method would be easier here, but that - -- doesn't support 'repeatable' flag, so we are stuck with this monster. - self:add_key_binding('up', 'menu-prev1', self:create_key_action('prev'), 'repeatable') - self:add_key_binding('down', 'menu-next1', self:create_key_action('next'), 'repeatable') - self:add_key_binding('left', 'menu-back1', self:create_key_action('back')) - self:add_key_binding('right', 'menu-select1', self:create_key_action('open_selected_item_preselect')) - self:add_key_binding('shift+right', 'menu-select-soft1', self:create_key_action('open_selected_item_soft')) - self:add_key_binding('shift+mbtn_left', 'menu-select-soft', self:create_key_action('open_selected_item_soft')) - self:add_key_binding('mbtn_back', 'menu-back-alt3', self:create_key_action('back')) - self:add_key_binding('bs', 'menu-back-alt4', self:create_key_action('back')) - self:add_key_binding('enter', 'menu-select-alt3', self:create_key_action('open_selected_item_preselect')) - self:add_key_binding('kp_enter', 'menu-select-alt4', self:create_key_action('open_selected_item_preselect')) - self:add_key_binding('shift+enter', 'menu-select-alt5', self:create_key_action('open_selected_item_soft')) - self:add_key_binding('shift+kp_enter', 'menu-select-alt6', self:create_key_action('open_selected_item_soft')) - self:add_key_binding('esc', 'menu-close', self:create_key_action('close')) - self:add_key_binding('pgup', 'menu-page-up', self:create_key_action('on_pgup'), 'repeatable') - self:add_key_binding('pgdwn', 'menu-page-down', self:create_key_action('on_pgdwn'), 'repeatable') - self:add_key_binding('home', 'menu-home', self:create_key_action('on_home')) - self:add_key_binding('end', 'menu-end', self:create_key_action('on_end')) -end - -function Menu:disable_key_bindings() - for _, name in ipairs(self.key_bindings) do mp.remove_key_binding(name) end - self.key_bindings = {} -end - -function Menu:create_key_action(name) - return function(...) - self.mouse_nav = false - self:maybe(name, ...) - end -end - -function Menu:render() - local update_cursor = false - for _, menu in ipairs(self.all) do - if menu.fling then - update_cursor = update_cursor or menu.fling.update_cursor or false - local time_delta = state.render_last_time - menu.fling.time - local progress = menu.fling.easing(math.min(time_delta / menu.fling.duration, 1)) - self:set_scroll_to(round(menu.fling.y + menu.fling.distance * progress), menu) - if progress < 1 then request_render() else menu.fling = nil end - end - end - if update_cursor then self:select_item_below_cursor() end - - local ass = assdraw.ass_new() - local opacity = options.menu_opacity * self.opacity - local spacing = self.item_padding - local icon_size = self.font_size - - function draw_menu(menu, x, y, opacity) - local ax, ay, bx, by = x, y, x + menu.width, y + menu.height - local draw_title = menu.is_root and menu.title - local scroll_clip = '\\clip(0,' .. ay .. ',' .. display.width .. ',' .. by .. ')' - local start_index = math.floor(menu.scroll_y / self.scroll_step) + 1 - local end_index = math.ceil((menu.scroll_y + menu.height) / self.scroll_step) - local selected_index = menu.selected_index or -1 - -- remove menu_opacity to start off with full opacity, but still decay for parent menus - local text_opacity = opacity / options.menu_opacity - - -- Background - ass:rect(ax, ay - (draw_title and self.item_height or 0) - 2, bx, by + 2, { - color = bg, opacity = opacity, radius = 4, - }) - - for index = start_index, end_index, 1 do - local item = menu.items[index] - local next_item = menu.items[index + 1] - local is_highlighted = selected_index == index or item.active - local next_is_active = next_item and next_item.active - local next_is_highlighted = selected_index == index + 1 or next_is_active - - if not item then break end - - local item_ay = ay - menu.scroll_y + self.scroll_step * (index - 1) - local item_by = item_ay + self.item_height - local item_center_y = item_ay + (self.item_height / 2) - local item_clip = (item_ay < ay or item_by > by) and scroll_clip or nil - local content_ax, content_bx = ax + spacing, bx - spacing - local font_color = item.active and fgt or bgt - local shadow_color = item.active and fg or bg - - -- Separator - local separator_ay = item.separator and item_by - 1 or item_by - local separator_by = item_by + (item.separator and 2 or 1) - if is_highlighted then separator_ay = item_by + 1 end - if next_is_highlighted then separator_by = item_by end - if separator_by - separator_ay > 0 and item_by < by then - ass:rect(ax + spacing / 2, separator_ay, bx - spacing / 2, separator_by, { - color = fg, opacity = opacity * (item.separator and 0.08 or 0.06), - }) - end - - -- Highlight - local highlight_opacity = 0 + (item.active and 0.8 or 0) + (selected_index == index and 0.15 or 0) - if highlight_opacity > 0 then - ass:rect(ax + 2, item_ay, bx - 2, item_by, { - radius = 2, color = fg, opacity = highlight_opacity * text_opacity, - clip = item_clip, - }) - end - - -- Icon - if item.icon then - local x, y = content_bx - (icon_size / 2), item_center_y - if item.icon == 'spinner' then - ass:spinner(x, y, icon_size * 1.5, {color = font_color, opacity = text_opacity * 0.8}) - else - ass:icon(x, y, icon_size * 1.5, item.icon, { - color = font_color, opacity = text_opacity, clip = item_clip, - shadow = 1, shadow_color = shadow_color, - }) - end - content_bx = content_bx - icon_size - spacing - end - - local title_cut_x = content_bx - if item.hint_width > 0 then - -- controls title & hint clipping proportional to the ratio of their widths - local title_content_ratio = item.title_width / (item.title_width + item.hint_width) - title_cut_x = round(content_ax + (content_bx - content_ax - spacing) * title_content_ratio - + (item.title_width > 0 and spacing / 2 or 0)) - end - - -- Hint - if item.hint then - item.ass_safe_hint = item.ass_safe_hint or ass_escape(item.hint) - local clip = '\\clip(' .. title_cut_x .. ',' .. - math.max(item_ay, ay) .. ',' .. bx .. ',' .. math.min(item_by, by) .. ')' - ass:txt(content_bx, item_center_y, 6, item.ass_safe_hint, { - size = self.font_size_hint, color = font_color, wrap = 2, opacity = 0.5 * opacity, clip = clip, - shadow = 1, shadow_color = shadow_color, - }) - end - - -- Title - if item.title then - item.ass_safe_title = item.ass_safe_title or ass_escape(item.title) - local clip = '\\clip(' .. ax .. ',' .. math.max(item_ay, ay) .. ',' - .. title_cut_x .. ',' .. math.min(item_by, by) .. ')' - ass:txt(content_ax, item_center_y, 4, item.ass_safe_title, { - size = self.font_size, color = font_color, italic = item.italic, bold = item.bold, wrap = 2, - opacity = text_opacity * (item.muted and 0.5 or 1), clip = clip, - shadow = 1, shadow_color = shadow_color, - }) - end - end - - -- Menu title - if draw_title then - local title_ay = ay - self.item_height - local title_height = self.item_height - 3 - menu.ass_safe_title = menu.ass_safe_title or ass_escape(menu.title) - - -- Background - ass:rect(ax + 2, title_ay, bx - 2, title_ay + title_height, { - color = fg, opacity = opacity * 0.8, radius = 2, - }) - ass:texture(ax + 2, title_ay, bx - 2, title_ay + title_height, 'n', { - size = 80, color = bg, opacity = opacity * 0.1, - }) - - -- Title - ass:txt(ax + menu.width / 2, title_ay + (title_height / 2), 5, menu.ass_safe_title, { - size = self.font_size, bold = true, color = bg, wrap = 2, opacity = opacity, - clip = '\\clip(' .. ax .. ',' .. title_ay .. ',' .. bx .. ',' .. ay .. ')', - }) - end - - -- Scrollbar - if menu.scroll_height > 0 then - local groove_height = menu.height - 2 - local thumb_height = math.max((menu.height / (menu.scroll_height + menu.height)) * groove_height, 40) - local thumb_y = ay + 1 + ((menu.scroll_y / menu.scroll_height) * (groove_height - thumb_height)) - ass:rect(bx - 3, thumb_y, bx - 1, thumb_y + thumb_height, {color = fg, opacity = opacity * 0.8}) - end - end - - -- Main menu - draw_menu(self.current, self.ax, self.ay, opacity) - - -- Parent menus - local parent_menu = self.current.parent_menu - local parent_offset_x = self.ax - local parent_opacity_factor = options.menu_parent_opacity - local menu_gap = 2 - - while parent_menu do - parent_offset_x = parent_offset_x - parent_menu.width - menu_gap - draw_menu(parent_menu, parent_offset_x, parent_menu.top, parent_opacity_factor * opacity) - parent_opacity_factor = parent_opacity_factor * parent_opacity_factor - parent_menu = parent_menu.parent_menu - end - - -- Selected menu - local selected_menu = self.current.items[self.current.selected_index] - - if selected_menu and selected_menu.items then - draw_menu(selected_menu, self.bx + menu_gap, selected_menu.top, options.menu_parent_opacity * opacity) - end - - return ass -end - -return Menu diff --git a/.config/mpv/scripts/uosc_shared/elements/PauseIndicator.lua b/.config/mpv/scripts/uosc_shared/elements/PauseIndicator.lua deleted file mode 100644 index 82a7e43..0000000 --- a/.config/mpv/scripts/uosc_shared/elements/PauseIndicator.lua +++ /dev/null @@ -1,80 +0,0 @@ -local Element = require('uosc_shared/elements/Element') - ----@class PauseIndicator : Element -local PauseIndicator = class(Element) - -function PauseIndicator:new() return Class.new(self) --[[@as PauseIndicator]] end -function PauseIndicator:init() - Element.init(self, 'pause_indicator') - self.ignores_menu = true - self.base_icon_opacity = options.pause_indicator == 'flash' and 1 or 0.8 - self.paused = state.pause - self.type = options.pause_indicator - self.is_manual = options.pause_indicator == 'manual' - self.fadeout_requested = false - self.opacity = 0 - - mp.observe_property('pause', 'bool', function(_, paused) - if Elements.timeline.pressed then return end - if options.pause_indicator == 'flash' then - if self.paused == paused then return end - self:flash() - elseif options.pause_indicator == 'static' then - self:decide() - end - end) -end - -function PauseIndicator:flash() - if not self.is_manual and self.type ~= 'flash' then return end - -- can't wait for pause property event listener to set this, because when this is used inside a binding like: - -- cycle pause; script-binding uosc/flash-pause-indicator - -- the pause event is not fired fast enough, and indicator starts rendering with old icon - self.paused = mp.get_property_native('pause') - if self.is_manual then self.type = 'flash' end - self.opacity = 1 - self:tween_property('opacity', 1, 0, 0.15) -end - --- decides whether static indicator should be visible or not -function PauseIndicator:decide() - if not self.is_manual and self.type ~= 'static' then return end - self.paused = mp.get_property_native('pause') -- see flash() for why this line is necessary - if self.is_manual then self.type = 'static' end - self.opacity = self.paused and 1 or 0 - request_render() - - -- Workaround for an mpv race condition bug during pause on windows builds, which causes osd updates to be ignored. - -- .03 was still loosing renders, .04 was fine, but to be safe I added 10ms more - mp.add_timeout(.05, function() osd:update() end) -end - -function PauseIndicator:render() - if self.opacity == 0 then return end - - local ass = assdraw.ass_new() - local is_static = self.type == 'static' - - -- Background fadeout - if is_static then - ass:rect(0, 0, display.width, display.height, {color = bg, opacity = self.opacity * 0.3}) - end - - -- Icon - local size = round(math.min(display.width, display.height) * (is_static and 0.20 or 0.15)) - size = size + size * (1 - self.opacity) - - if self.paused then - ass:icon(display.width / 2, display.height / 2, size, 'pause', - {border = 1, opacity = self.base_icon_opacity * self.opacity} - ) - else - ass:icon(display.width / 2, display.height / 2, size * 1.2, 'play_arrow', - {border = 1, opacity = self.base_icon_opacity * self.opacity} - ) - end - - return ass -end - -return PauseIndicator diff --git a/.config/mpv/scripts/uosc_shared/elements/Speed.lua b/.config/mpv/scripts/uosc_shared/elements/Speed.lua deleted file mode 100644 index 9681ccf..0000000 --- a/.config/mpv/scripts/uosc_shared/elements/Speed.lua +++ /dev/null @@ -1,183 +0,0 @@ -local Element = require('uosc_shared/elements/Element') - ----@alias Dragging { start_time: number; start_x: number; distance: number; speed_distance: number; start_speed: number; } - ----@class Speed : Element -local Speed = class(Element) - ----@param props? ElementProps -function Speed:new(props) return Class.new(self, props) --[[@as Speed]] end -function Speed:init(props) - Element.init(self, 'speed', props) - - self.width = 0 - self.height = 0 - self.notches = 10 - self.notch_every = 0.1 - ---@type number - self.notch_spacing = nil - ---@type number - self.font_size = nil - ---@type Dragging|nil - self.dragging = nil -end - -function Speed:on_coordinates() - self.height, self.width = self.by - self.ay, self.bx - self.ax - self.notch_spacing = self.width / (self.notches + 1) - self.font_size = round(self.height * 0.48 * options.font_scale) -end - -function Speed:speed_step(speed, up) - if options.speed_step_is_factor then - if up then - return speed * options.speed_step - else - return speed * 1 / options.speed_step - end - else - if up then - return speed + options.speed_step - else - return speed - options.speed_step - end - end -end - -function Speed:on_mbtn_left_down() - -- Don't accept clicks while hidden. - if self:get_visibility() <= 0 then return end - self:tween_stop() -- Stop and cleanup possible ongoing animations - self.dragging = { - start_time = mp.get_time(), - start_x = cursor.x, - distance = 0, - speed_distance = 0, - start_speed = state.speed, - } -end - -function Speed:on_global_mouse_move() - if not self.dragging then return end - - self.dragging.distance = cursor.x - self.dragging.start_x - self.dragging.speed_distance = (-self.dragging.distance / self.notch_spacing * self.notch_every) - - local speed_current = state.speed - local speed_drag_current = self.dragging.start_speed + self.dragging.speed_distance - speed_drag_current = clamp(0.01, speed_drag_current, 100) - local drag_dir_up = speed_drag_current > speed_current - - local speed_step_next = speed_current - local speed_drag_diff = math.abs(speed_drag_current - speed_current) - while math.abs(speed_step_next - speed_current) < speed_drag_diff do - speed_step_next = self:speed_step(speed_step_next, drag_dir_up) - end - local speed_step_prev = self:speed_step(speed_step_next, not drag_dir_up) - - local speed_new = speed_step_prev - local speed_next_diff = math.abs(speed_drag_current - speed_step_next) - local speed_prev_diff = math.abs(speed_drag_current - speed_step_prev) - if speed_next_diff < speed_prev_diff then - speed_new = speed_step_next - end - - if speed_new ~= speed_current then - mp.set_property_native('speed', speed_new) - end -end - -function Speed:on_mbtn_left_up() - -- Reset speed on short clicks - if self.dragging and math.abs(self.dragging.distance) < 6 and mp.get_time() - self.dragging.start_time < 0.15 then - mp.set_property_native('speed', 1) - end -end - -function Speed:on_global_mbtn_left_up() - self.dragging = nil - request_render() -end - -function Speed:on_global_mouse_leave() - self.dragging = nil - request_render() -end - -function Speed:on_wheel_up() mp.set_property_native('speed', self:speed_step(state.speed, true)) end -function Speed:on_wheel_down() mp.set_property_native('speed', self:speed_step(state.speed, false)) end - -function Speed:render() - local visibility = self:get_visibility() - local opacity = self.dragging and 1 or visibility - - if opacity <= 0 then return end - - local ass = assdraw.ass_new() - - -- Background - ass:rect(self.ax, self.ay, self.bx, self.by, {color = bg, radius = 2, opacity = opacity * options.speed_opacity}) - - -- Coordinates - local ax, ay = self.ax, self.ay - local bx, by = self.bx, ay + self.height - local half_width = (self.width / 2) - local half_x = ax + half_width - - -- Notches - local speed_at_center = state.speed - if self.dragging then - speed_at_center = self.dragging.start_speed + self.dragging.speed_distance - speed_at_center = clamp(0.01, speed_at_center, 100) - end - local nearest_notch_speed = round(speed_at_center / self.notch_every) * self.notch_every - local nearest_notch_x = half_x + (((nearest_notch_speed - speed_at_center) / self.notch_every) * self.notch_spacing) - local guide_size = math.floor(self.height / 7.5) - local notch_by = by - guide_size - local notch_ay_big = ay + round(self.font_size * 1.1) - local notch_ay_medium = notch_ay_big + ((notch_by - notch_ay_big) * 0.2) - local notch_ay_small = notch_ay_big + ((notch_by - notch_ay_big) * 0.4) - local from_to_index = math.floor(self.notches / 2) - - for i = -from_to_index, from_to_index do - local notch_speed = nearest_notch_speed + (i * self.notch_every) - - if notch_speed >= 0 and notch_speed <= 100 then - local notch_x = nearest_notch_x + (i * self.notch_spacing) - local notch_thickness = 1 - local notch_ay = notch_ay_small - if (notch_speed % (self.notch_every * 10)) < 0.00000001 then - notch_ay = notch_ay_big - notch_thickness = 1.5 - elseif (notch_speed % (self.notch_every * 5)) < 0.00000001 then - notch_ay = notch_ay_medium - end - - ass:rect(notch_x - notch_thickness, notch_ay, notch_x + notch_thickness, notch_by, { - color = fg, border = 1, border_color = bg, - opacity = math.min(1.2 - (math.abs((notch_x - ax - half_width) / half_width)), 1) * opacity, - }) - end - end - - -- Center guide - ass:new_event() - ass:append('{\\rDefault\\an7\\blur0\\bord1\\shad0\\1c&H' .. fg .. '\\3c&H' .. bg .. '}') - ass:opacity(opacity) - ass:pos(0, 0) - ass:draw_start() - ass:move_to(half_x, by - 2 - guide_size) - ass:line_to(half_x + guide_size, by - 2) - ass:line_to(half_x - guide_size, by - 2) - ass:draw_stop() - - -- Speed value - local speed_text = (round(state.speed * 100) / 100) .. 'x' - ass:txt(half_x, ay + (notch_ay_big - ay) / 2, 5, speed_text, { - size = self.font_size, color = bgt, border = options.text_border, border_color = bg, opacity = opacity, - }) - - return ass -end - -return Speed diff --git a/.config/mpv/scripts/uosc_shared/elements/Timeline.lua b/.config/mpv/scripts/uosc_shared/elements/Timeline.lua deleted file mode 100644 index 5ef798f..0000000 --- a/.config/mpv/scripts/uosc_shared/elements/Timeline.lua +++ /dev/null @@ -1,394 +0,0 @@ -local Element = require('uosc_shared/elements/Element') - ----@class Timeline : Element -local Timeline = class(Element) - -function Timeline:new() return Class.new(self) --[[@as Timeline]] end -function Timeline:init() - Element.init(self, 'timeline') - self.pressed = false - self.obstructed = false - self.size_max = 0 - self.size_min = 0 - self.size_min_override = options.timeline_start_hidden and 0 or nil - self.font_size = 0 - self.top_border = options.timeline_border - self.hovered_chapter = nil - - -- Release any dragging when file gets unloaded - mp.register_event('end-file', function() self.pressed = false end) -end - -function Timeline:get_visibility() - return Elements.controls and math.max(Elements.controls.proximity, Element.get_visibility(self)) - or Element.get_visibility(self) -end - -function Timeline:decide_enabled() - local previous = self.enabled - self.enabled = not self.obstructed and state.duration ~= nil and state.duration > 0 and state.time ~= nil - if self.enabled ~= previous then Elements:trigger('timeline_enabled', self.enabled) end -end - -function Timeline:get_effective_size_min() - return self.size_min_override or self.size_min -end - -function Timeline:get_effective_size() - if Elements.speed and Elements.speed.dragging then return self.size_max end - local size_min = self:get_effective_size_min() - return size_min + math.ceil((self.size_max - size_min) * self:get_visibility()) -end - -function Timeline:get_effective_line_width() - return state.fullormaxed and options.timeline_line_width_fullscreen or options.timeline_line_width -end - -function Timeline:get_is_hovered() return self.enabled and (self.proximity_raw == 0 or self.hovered_chapter ~= nil) end - -function Timeline:update_dimensions() - if state.fullormaxed then - self.size_min = options.timeline_size_min_fullscreen - self.size_max = options.timeline_size_max_fullscreen - else - self.size_min = options.timeline_size_min - self.size_max = options.timeline_size_max - end - self.font_size = math.floor(math.min((self.size_max + 60) * 0.2, self.size_max * 0.96) * options.font_scale) - self.ax = Elements.window_border.size - self.ay = display.height - Elements.window_border.size - self.size_max - self.top_border - self.bx = display.width - Elements.window_border.size - self.by = display.height - Elements.window_border.size - self.width = self.bx - self.ax - self.chapter_size = math.max((self.by - self.ay) / 10, 3) - self.chapter_size_hover = self.chapter_size * 2 - - -- Disable if not enough space - local available_space = display.height - Elements.window_border.size * 2 - if Elements.top_bar.enabled then available_space = available_space - Elements.top_bar.size end - self.obstructed = available_space < self.size_max + 10 - self:decide_enabled() -end - -function Timeline:get_time_at_x(x) - local line_width = (options.timeline_style == 'line' and self:get_effective_line_width() - 1 or 0) - local time_width = self.width - line_width - 1 - local fax = (time_width) * state.time / state.duration - local fbx = fax + line_width - -- time starts 0.5 pixels in - x = x - self.ax - 0.5 - if x > fbx then x = x - line_width - elseif x > fax then x = fax end - local progress = clamp(0, x / time_width, 1) - return state.duration * progress -end - ----@param fast? boolean -function Timeline:set_from_cursor(fast) - if state.time and state.duration then - mp.commandv('seek', self:get_time_at_x(cursor.x), fast and 'absolute+keyframes' or 'absolute+exact') - end -end -function Timeline:clear_thumbnail() mp.commandv('script-message-to', 'thumbfast', 'clear') end - -function Timeline:determine_chapter_click_handler() - if self.hovered_chapter then - if not self.on_global_mbtn_left_down then - self.on_global_mbtn_left_down = function() - if self.hovered_chapter then mp.commandv('seek', self.hovered_chapter.time, 'absolute+exact') end - end - end - else - if self.on_global_mbtn_left_down then - self.on_global_mbtn_left_down = nil - if self.proximity_raw ~= 0 then self:clear_thumbnail() end - end - end -end - -function Timeline:on_mbtn_left_down() - -- `self.on_global_mbtn_left_down` has precedent - if self.on_global_mbtn_left_down then return end - - self.pressed = true - self.pressed_pause = state.pause - mp.set_property_native('pause', true) - self:set_from_cursor() -end -function Timeline:on_prop_duration() self:decide_enabled() end -function Timeline:on_prop_time() self:decide_enabled() end -function Timeline:on_prop_border() self:update_dimensions() end -function Timeline:on_prop_fullormaxed() self:update_dimensions() end -function Timeline:on_display() self:update_dimensions() end -function Timeline:on_mouse_leave() - if not self.hovered_chapter then self:clear_thumbnail() end -end -function Timeline:on_global_mbtn_left_up() - if self.pressed then - mp.set_property_native('pause', self.pressed_pause) - self.pressed = false - end - self:clear_thumbnail() -end -function Timeline:on_global_mouse_leave() - self.pressed = false - self:clear_thumbnail() -end - -Timeline.seek_timer = mp.add_timeout(0.05, function() Elements.timeline:set_from_cursor() end) -Timeline.seek_timer:kill() -function Timeline:on_global_mouse_move() - if self.pressed then - if self.width / state.duration < 10 then - self:set_from_cursor(true) - self.seek_timer:kill() - self.seek_timer:resume() - else self:set_from_cursor() end - end - self:determine_chapter_click_handler() -end -function Timeline:on_wheel_up() mp.commandv('seek', options.timeline_step) end -function Timeline:on_wheel_down() mp.commandv('seek', -options.timeline_step) end - -function Timeline:render() - if self.size_max == 0 then return end - - local size_min = self:get_effective_size_min() - local size = self:get_effective_size() - local visibility = self:get_visibility() - - 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(self.font_size * 0.8, size_min * 2) - local hide_text_ramp = hide_text_below / 2 - local text_opacity = clamp(0, size - hide_text_below, hide_text_ramp) / hide_text_ramp - - local spacing = math.max(math.floor((self.size_max - self.font_size) / 2.5), 4) - local progress = state.time / state.duration - local is_line = options.timeline_style == 'line' - - -- Foreground & Background bar coordinates - local bax, bay, bbx, bby = self.ax, self.by - size - self.top_border, self.bx, self.by - local fax, fay, fbx, fby = 0, bay + self.top_border, 0, bby - local fcy = fay + (size / 2) - - local line_width = 0 - - if is_line then - local minimized_fraction = 1 - math.min((size - size_min) / ((self.size_max - size_min) / 8), 1) - local line_width_max = self:get_effective_line_width() - local max_min_width_delta = size_min > 0 - and line_width_max - line_width_max * options.timeline_line_width_minimized_scale - or 0 - line_width = line_width_max - (max_min_width_delta * minimized_fraction) - fax = bax + (self.width - line_width) * progress - fbx = fax + line_width - line_width = line_width - 1 - else - fax, fbx = bax, bax + self.width * progress - end - - local foreground_size = fby - fay - local foreground_coordinates = round(fax) .. ',' .. fay .. ',' .. round(fbx) .. ',' .. fby -- for clipping - - -- time starts 0.5 pixels in - local time_ax = bax + 0.5 - local time_width = self.width - line_width - 1 - - -- time to x: calculates x coordinate so that it never lies inside of the line - local function t2x(time) - local x = time_ax + time_width * time / state.duration - return time <= state.time and x or x + line_width - end - - -- Background - ass:new_event() - ass:pos(0, 0) - ass:append('{\\rDefault\\an7\\blur0\\bord0\\1c&H' .. bg .. '}') - ass:opacity(options.timeline_opacity) - ass:draw_start() - ass:rect_cw(bax, bay, fax, bby) --left of progress - ass:rect_cw(fbx, bay, bbx, bby) --right of progress - ass:rect_cw(fax, bay, fbx, fay) --above progress - ass:draw_stop() - - -- Progress - ass:rect(fax, fay, fbx, fby, {opacity = options.timeline_opacity}) - - -- Uncached ranges - local buffered_time = nil - if state.uncached_ranges then - local opts = {size = 80, anchor_y = fby} - local texture_char = visibility > 0 and 'b' or 'a' - local offset = opts.size / (visibility > 0 and 24 or 28) - for _, range in ipairs(state.uncached_ranges) do - if not buffered_time and (range[1] > state.time or range[2] > state.time) then - buffered_time = range[1] - state.time - end - if options.timeline_cache then - local ax = range[1] < 0.5 and bax or math.floor(t2x(range[1])) - local bx = range[2] > state.duration - 0.5 and bbx or math.ceil(t2x(range[2])) - opts.color, opts.opacity, opts.anchor_x = 'ffffff', 0.4 - (0.2 * visibility), bax - ass:texture(ax, fay, bx, fby, texture_char, opts) - opts.color, opts.opacity, opts.anchor_x = '000000', 0.6 - (0.2 * visibility), bax + offset - ass:texture(ax, fay, bx, fby, texture_char, opts) - end - end - end - - -- Custom ranges - for _, chapter_range in ipairs(state.chapter_ranges) do - local rax = chapter_range.start < 0.1 and bax or t2x(chapter_range.start) - local rbx = chapter_range['end'] > state.duration - 0.1 and bbx - or t2x(math.min(chapter_range['end'], state.duration)) - ass:rect(rax, fay, rbx, fby, {color = chapter_range.color, opacity = chapter_range.opacity}) - end - - -- Chapters - self.hovered_chapter = nil - if (options.timeline_chapters_opacity > 0 - and (#state.chapters > 0 or state.ab_loop_a or state.ab_loop_b) - ) then - local diamond_radius = foreground_size < 3 and foreground_size or self.chapter_size - local diamond_radius_hovered = diamond_radius * 2 - local diamond_border = options.timeline_border and math.max(options.timeline_border, 1) or 1 - - if diamond_radius > 0 then - local function draw_chapter(time, radius) - local chapter_x, chapter_y = t2x(time), fay - 1 - ass:new_event() - ass:append(string.format( - '{\\pos(0,0)\\rDefault\\an7\\blur0\\yshad0.01\\bord%f\\1c&H%s\\3c&H%s\\4c&H%s\\1a&H%X&\\3a&H00&\\4a&H00&}', - diamond_border, fg, bg, bg, opacity_to_alpha(options.timeline_opacity * options.timeline_chapters_opacity) - )) - ass:draw_start() - ass:move_to(chapter_x - radius, chapter_y) - ass:line_to(chapter_x, chapter_y - radius) - ass:line_to(chapter_x + radius, chapter_y) - ass:line_to(chapter_x, chapter_y + radius) - ass:draw_stop() - end - - if #state.chapters > 0 then - -- Find hovered chapter indicator - local hovered_chapter, closest_delta = nil, infinity - - if self.proximity_raw < diamond_radius_hovered then - for i, chapter in ipairs(state.chapters) do - local chapter_x, chapter_y = t2x(chapter.time), fay - 1 - local cursor_chapter_delta = math.sqrt((cursor.x - chapter_x) ^ 2 + (cursor.y - chapter_y) ^ 2) - if cursor_chapter_delta <= diamond_radius_hovered and cursor_chapter_delta < closest_delta then - hovered_chapter, closest_delta = chapter, cursor_chapter_delta - end - end - end - - for i, chapter in ipairs(state.chapters) do - if chapter ~= hovered_chapter then draw_chapter(chapter.time, diamond_radius) end - end - - -- Render hovered chapter above others - if hovered_chapter then - draw_chapter(hovered_chapter.time, diamond_radius_hovered) - self.hovered_chapter = hovered_chapter - self:determine_chapter_click_handler() - end - end - - if state.ab_loop_a and state.ab_loop_a > 0 then draw_chapter(state.ab_loop_a) end - if state.ab_loop_b and state.ab_loop_b > 0 then draw_chapter(state.ab_loop_b) end - end - end - - local function draw_timeline_text(x, y, align, text, opts) - opts.color, opts.border_color = fgt, fg - opts.clip = '\\clip(' .. foreground_coordinates .. ')' - ass:txt(x, y, align, text, opts) - opts.color, opts.border_color = bgt, bg - opts.clip = '\\iclip(' .. foreground_coordinates .. ')' - ass:txt(x, y, align, text, opts) - end - - -- Time values - if text_opacity > 0 then - local time_opts = {size = self.font_size, opacity = text_opacity, border = 2} - -- Upcoming cache time - if buffered_time and options.buffered_time_threshold > 0 and buffered_time < options.buffered_time_threshold then - local x, align = fbx + 5, 4 - local cache_opts = {size = self.font_size * 0.8, opacity = text_opacity * 0.6, border = 1} - local human = round(math.max(buffered_time, 0)) .. 's' - local width = text_width(human, cache_opts) - local time_width = text_width('00:00:00', time_opts) - local min_x, max_x = bax + spacing + 5 + time_width, bbx - spacing - 5 - time_width - if x < min_x then x = min_x elseif x + width > max_x then x, align = max_x, 6 end - draw_timeline_text(x, fcy, align, human, cache_opts) - end - - -- Elapsed time - if state.time_human then - draw_timeline_text(bax + spacing, fcy, 4, state.time_human, time_opts) - end - - -- End time - if state.duration_or_remaining_time_human then - draw_timeline_text(bbx - spacing, fcy, 6, state.duration_or_remaining_time_human, time_opts) - end - end - - -- Hovered time and chapter - if (self.proximity_raw == 0 or self.pressed or self.hovered_chapter) and - not (Elements.speed and Elements.speed.dragging) then - local cursor_x = self.hovered_chapter and t2x(self.hovered_chapter.time) or cursor.x - local hovered_seconds = self.hovered_chapter and self.hovered_chapter.time or self:get_time_at_x(cursor.x) - - -- Cursor line - -- 0.5 to switch when the pixel is half filled in - local color = ((fax - 0.5) < cursor_x and cursor_x < (fbx + 0.5)) and bg or fg - local ax, ay, bx, by = cursor_x - 0.5, fay, cursor_x + 0.5, fby - ass:rect(ax, ay, bx, by, {color = color, opacity = 0.2}) - local tooltip_anchor = {ax = ax, ay = ay, bx = bx, by = by} - - -- Timestamp - local offset = #state.chapters > 0 and 10 or 4 - local opts = {size = self.font_size, offset = offset} - opts.width_overwrite = text_width('00:00:00', opts) - ass:tooltip(tooltip_anchor, format_time(hovered_seconds), opts) - tooltip_anchor.ay = tooltip_anchor.ay - self.font_size - offset - - -- Thumbnail - if not thumbnail.disabled and thumbnail.width ~= 0 and thumbnail.height ~= 0 then - local scale_x, scale_y = display.scale_x, display.scale_y - local border, margin_x, margin_y = math.ceil(2 * scale_x), round(10 * scale_x), round(5 * scale_y) - local thumb_x_margin, thumb_y_margin = border + margin_x, border + margin_y - local thumb_width, thumb_height = thumbnail.width, thumbnail.height - local thumb_x = round(clamp( - thumb_x_margin, cursor_x * scale_x - thumb_width / 2, - display.width * scale_x - thumb_width - thumb_x_margin - )) - local thumb_y = round(tooltip_anchor.ay * scale_y - thumb_y_margin - thumb_height) - local ax, ay = (thumb_x - border) / scale_x, (thumb_y - border) / scale_y - local bx, by = (thumb_x + thumb_width + border) / scale_x, (thumb_y + thumb_height + border) / scale_y - ass:rect(ax, ay, bx, by, {color = bg, border = 1, border_color = fg, border_opacity = 0.08, radius = 2}) - mp.commandv('script-message-to', 'thumbfast', 'thumb', hovered_seconds, thumb_x, thumb_y) - tooltip_anchor.ax, tooltip_anchor.bx, tooltip_anchor.ay = ax, bx, ay - end - - -- Chapter title - if #state.chapters > 0 then - local _, chapter = itable_find(state.chapters, function(c) return hovered_seconds >= c.time end, true) - if chapter and not chapter.is_end_only then - ass:tooltip(tooltip_anchor, chapter.title_wrapped, { - size = self.font_size, offset = 10, responsive = false, bold = true, - width_overwrite = chapter.title_wrapped_width * self.font_size, - }) - end - end - end - - return ass -end - -return Timeline diff --git a/.config/mpv/scripts/uosc_shared/elements/TopBar.lua b/.config/mpv/scripts/uosc_shared/elements/TopBar.lua deleted file mode 100644 index 977d5b8..0000000 --- a/.config/mpv/scripts/uosc_shared/elements/TopBar.lua +++ /dev/null @@ -1,182 +0,0 @@ -local Element = require('uosc_shared/elements/Element') - ----@alias TopBarButtonProps {icon: string; background: string; anchor_id?: string; command: string|fun()} - ----@class TopBarButton : Element -local TopBarButton = class(Element) - ----@param id string ----@param props TopBarButtonProps -function TopBarButton:new(id, props) return Class.new(self, id, props) --[[@as TopBarButton]] end -function TopBarButton:init(id, props) - Element.init(self, id, props) - self.anchor_id = 'top_bar' - self.icon = props.icon - self.background = props.background - self.command = props.command -end - -function TopBarButton:on_mbtn_left_down() - mp.command(type(self.command) == 'function' and self.command() or self.command) -end - -function TopBarButton:render() - local visibility = self:get_visibility() - if visibility <= 0 then return end - local ass = assdraw.ass_new() - - -- Background on hover - if self.proximity_raw == 0 then - ass:rect(self.ax, self.ay, self.bx, self.by, {color = self.background, opacity = visibility}) - end - - local width, height = self.bx - self.ax, self.by - self.ay - local icon_size = math.min(width, height) * 0.5 - ass:icon(self.ax + width / 2, self.ay + height / 2, icon_size, self.icon, { - opacity = visibility, border = options.text_border, - }) - - return ass -end - ---[[ TopBar ]] - ----@class TopBar : Element -local TopBar = class(Element) - -function TopBar:new() return Class.new(self) --[[@as TopBar]] end -function TopBar:init() - Element.init(self, 'top_bar') - self.pressed = false - self.size, self.size_max, self.size_min = 0, 0, 0 - self.icon_size, self.spacing, self.font_size, self.title_bx = 1, 1, 1, 1 - self.size_min_override = options.timeline_start_hidden and 0 or nil - self.top_border = options.timeline_border - - local function decide_maximized_command() - return state.border - and (state.fullscreen and 'set fullscreen no;cycle window-maximized' or 'cycle window-maximized') - or 'set window-maximized no;cycle fullscreen' - end - - -- Order aligns from right to left - self.buttons = { - TopBarButton:new('tb_close', {icon = 'close', background = '2311e8', command = 'quit'}), - TopBarButton:new('tb_max', {icon = 'crop_square', background = '222222', command = decide_maximized_command}), - TopBarButton:new('tb_min', {icon = 'minimize', background = '222222', command = 'cycle window-minimized'}), - } -end - -function TopBar:decide_enabled() - if options.top_bar == 'no-border' then - self.enabled = not state.border or state.fullscreen - else - self.enabled = options.top_bar == 'always' - end - self.enabled = self.enabled and (options.top_bar_controls or options.top_bar_title) - for _, element in ipairs(self.buttons) do - element.enabled = self.enabled and options.top_bar_controls - end -end - -function TopBar:update_dimensions() - self.size = state.fullormaxed and options.top_bar_size_fullscreen or options.top_bar_size - self.icon_size = round(self.size * 0.5) - self.spacing = math.ceil(self.size * 0.25) - self.font_size = math.floor((self.size - (self.spacing * 2)) * options.font_scale) - self.button_width = round(self.size * 1.15) - self.ay = Elements.window_border.size - self.bx = display.width - Elements.window_border.size - self.by = self.size + Elements.window_border.size - self.title_bx = self.bx - (options.top_bar_controls and (self.button_width * 3) or 0) - self.ax = options.top_bar_title and Elements.window_border.size or self.title_bx - - local button_bx = self.bx - for _, element in pairs(self.buttons) do - element.ax, element.bx = button_bx - self.button_width, button_bx - element.ay, element.by = self.ay, self.by - button_bx = button_bx - self.button_width - end -end - -function TopBar:on_prop_border() - self:decide_enabled() - self:update_dimensions() -end - -function TopBar:on_prop_fullscreen() - self:decide_enabled() - self:update_dimensions() -end - -function TopBar:on_prop_maximized() - self:decide_enabled() - self:update_dimensions() -end - -function TopBar:on_display() self:update_dimensions() end - -function TopBar:render() - local visibility = self:get_visibility() - if visibility <= 0 then return end - local ass = assdraw.ass_new() - - -- Window title - if options.top_bar_title and (state.title or state.has_playlist) then - local bg_margin = math.floor((self.size - self.font_size) / 4) - local padding = self.font_size / 2 - local title_ax = self.ax + bg_margin - local title_ay = self.ay + bg_margin - local max_bx = self.title_bx - self.spacing - - -- Playlist position - if state.has_playlist then - local text = state.playlist_pos .. '' .. state.playlist_count - local formatted_text = '{\\b1}' .. state.playlist_pos .. '{\\b0\\fs' .. self.font_size * 0.9 .. '}/' - .. state.playlist_count - local opts = {size = self.font_size, wrap = 2, color = fgt, opacity = visibility} - local bx = round(title_ax + text_width(text, opts) + padding * 2) - ass:rect(title_ax, title_ay, bx, self.by - bg_margin, {color = fg, opacity = visibility, radius = 2}) - ass:txt(title_ax + (bx - title_ax) / 2, self.ay + (self.size / 2), 5, formatted_text, opts) - title_ax = bx + bg_margin - end - - -- Title - local text = state.title - if max_bx - title_ax > self.font_size * 3 and text and text ~= '' then - local opts = { - size = self.font_size, wrap = 2, color = bgt, border = 1, border_color = bg, opacity = visibility, - clip = string.format('\\clip(%d, %d, %d, %d)', self.ax, self.ay, max_bx, self.by), - } - local bx = math.min(max_bx, title_ax + text_width(text, opts) + padding * 2) - local by = self.by - bg_margin - ass:rect(title_ax, title_ay, bx, by, { - color = bg, opacity = visibility * options.top_bar_title_opacity, radius = 2, - }) - ass:txt(title_ax + padding, self.ay + (self.size / 2), 4, text, opts) - title_ay = by + 1 - end - - -- Subtitle: current chapter - if state.current_chapter and max_bx - title_ax > self.font_size * 3 then - local font_size = self.font_size * 0.8 - local height = font_size * 1.5 - local text = '└ ' .. state.current_chapter.index .. ': ' .. state.current_chapter.title - local by = title_ay + height - local opts = { - size = font_size, italic = true, wrap = 2, color = bgt, - border = 1, border_color = bg, opacity = visibility * 0.8, - } - local bx = math.min(max_bx, title_ax + text_width(text, opts) + padding * 2) - opts.clip = string.format('\\clip(%d, %d, %d, %d)', title_ax, title_ay, bx, by) - ass:rect(title_ax, title_ay, bx, by, { - color = bg, opacity = visibility * options.top_bar_title_opacity, radius = 2, - }) - ass:txt(title_ax + padding, title_ay + height / 2, 4, text, opts) - end - end - - return ass -end - -return TopBar diff --git a/.config/mpv/scripts/uosc_shared/elements/Volume.lua b/.config/mpv/scripts/uosc_shared/elements/Volume.lua deleted file mode 100644 index 8533ebd..0000000 --- a/.config/mpv/scripts/uosc_shared/elements/Volume.lua +++ /dev/null @@ -1,240 +0,0 @@ -local Element = require('uosc_shared/elements/Element') - ---[[ MuteButton ]] - ----@class MuteButton : Element -local MuteButton = class(Element) ----@param props? ElementProps -function MuteButton:new(props) return Class.new(self, 'volume_mute', props) --[[@as MuteButton]] end -function MuteButton:on_mbtn_left_down() mp.commandv('cycle', 'mute') end -function MuteButton:render() - local visibility = self:get_visibility() - if visibility <= 0 then return end - local ass = assdraw.ass_new() - local icon_name = state.mute and 'volume_off' or 'volume_up' - local width = self.bx - self.ax - ass:icon(self.ax + (width / 2), self.by, width * 0.7, icon_name, - {border = options.text_border, opacity = options.volume_opacity * visibility, align = 2} - ) - return ass -end - ---[[ VolumeSlider ]] - ----@class VolumeSlider : Element -local VolumeSlider = class(Element) ----@param props? ElementProps -function VolumeSlider:new(props) return Class.new(self, props) --[[@as VolumeSlider]] end -function VolumeSlider:init(props) - Element.init(self, 'volume_slider', props) - self.pressed = false - self.nudge_y = 0 -- vertical position where volume overflows 100 - self.nudge_size = 0 - self.draw_nudge = false - self.spacing = 0 - self.radius = 1 -end - -function VolumeSlider:set_volume(volume) - volume = round(volume / options.volume_step) * options.volume_step - if state.volume == volume then return end - mp.commandv('set', 'volume', clamp(0, volume, state.volume_max)) -end - -function VolumeSlider:set_from_cursor() - local volume_fraction = (self.by - cursor.y - options.volume_border) / (self.by - self.ay - options.volume_border) - self:set_volume(volume_fraction * state.volume_max) -end - -function VolumeSlider:on_coordinates() - if type(state.volume_max) ~= 'number' or state.volume_max <= 0 then return end - local width = self.bx - self.ax - self.nudge_y = self.by - round((self.by - self.ay) * (100 / state.volume_max)) - self.nudge_size = round(width * 0.18) - self.draw_nudge = self.ay < self.nudge_y - self.spacing = round(width * 0.2) - self.radius = math.max(2, (self.bx - self.ax) / 10) -end -function VolumeSlider:on_mbtn_left_down() - self.pressed = true - self:set_from_cursor() -end -function VolumeSlider:on_global_mbtn_left_up() self.pressed = false end -function VolumeSlider:on_global_mouse_leave() self.pressed = false end -function VolumeSlider:on_global_mouse_move() - if self.pressed then self:set_from_cursor() end -end -function VolumeSlider:on_wheel_up() self:set_volume(state.volume + options.volume_step) end -function VolumeSlider:on_wheel_down() self:set_volume(state.volume - options.volume_step) end - -function VolumeSlider:render() - local visibility = self:get_visibility() - local ax, ay, bx, by = self.ax, self.ay, self.bx, self.by - local width, height = bx - ax, by - ay - - if width <= 0 or height <= 0 or visibility <= 0 then return end - - local ass = assdraw.ass_new() - local nudge_y, nudge_size = self.draw_nudge and self.nudge_y or -infinity, self.nudge_size - local volume_y = self.ay + options.volume_border + - ((height - (options.volume_border * 2)) * (1 - math.min(state.volume / state.volume_max, 1))) - - -- Draws a rectangle with nudge at requested position - ---@param p number Padding from slider edges. - ---@param cy? number A y coordinate where to clip the path from the bottom. - function create_nudged_path(p, cy) - cy = cy or ay + p - local ax, bx, by = ax + p, bx - p, by - p - local r = math.max(1, self.radius - p) - local d, rh = r * 2, r / 2 - local nudge_size = ((quarter_pi_sin * (nudge_size - p)) + p) / quarter_pi_sin - local path = assdraw.ass_new() - path:move_to(bx - r, by) - path:line_to(ax + r, by) - if cy > by - d then - local subtracted_radius = (d - (cy - (by - d))) / 2 - local xbd = (r - subtracted_radius * 1.35) -- x bezier delta - path:bezier_curve(ax + xbd, by, ax + xbd, cy, ax + r, cy) - path:line_to(bx - r, cy) - path:bezier_curve(bx - xbd, cy, bx - xbd, by, bx - r, by) - else - path:bezier_curve(ax + rh, by, ax, by - rh, ax, by - r) - local nudge_bottom_y = nudge_y + nudge_size - - if cy + rh <= nudge_bottom_y then - path:line_to(ax, nudge_bottom_y) - if cy <= nudge_y then - path:line_to((ax + nudge_size), nudge_y) - local nudge_top_y = nudge_y - nudge_size - if cy <= nudge_top_y then - local r, rh = r, rh - if cy > nudge_top_y - r then - r = nudge_top_y - cy - rh = r / 2 - end - path:line_to(ax, nudge_top_y) - path:line_to(ax, cy + r) - path:bezier_curve(ax, cy + rh, ax + rh, cy, ax + r, cy) - path:line_to(bx - r, cy) - path:bezier_curve(bx - rh, cy, bx, cy + rh, bx, cy + r) - path:line_to(bx, nudge_top_y) - else - local triangle_side = cy - nudge_top_y - path:line_to((ax + triangle_side), cy) - path:line_to((bx - triangle_side), cy) - end - path:line_to((bx - nudge_size), nudge_y) - else - local triangle_side = nudge_bottom_y - cy - path:line_to((ax + triangle_side), cy) - path:line_to((bx - triangle_side), cy) - end - path:line_to(bx, nudge_bottom_y) - else - path:line_to(ax, cy + r) - path:bezier_curve(ax, cy + rh, ax + rh, cy, ax + r, cy) - path:line_to(bx - r, cy) - path:bezier_curve(bx - rh, cy, bx, cy + rh, bx, cy + r) - end - path:line_to(bx, by - r) - path:bezier_curve(bx, by - rh, bx - rh, by, bx - r, by) - end - return path - end - - -- BG & FG paths - local bg_path = create_nudged_path(0) - local fg_path = create_nudged_path(options.volume_border, volume_y) - - -- Background - ass:new_event() - ass:append('{\\rDefault\\an7\\blur0\\bord0\\1c&H' .. bg .. - '\\iclip(' .. fg_path.scale .. ', ' .. fg_path.text .. ')}') - ass:opacity(options.volume_opacity, visibility) - ass:pos(0, 0) - ass:draw_start() - ass:append(bg_path.text) - ass:draw_stop() - - -- Foreground - ass:new_event() - ass:append('{\\rDefault\\an7\\blur0\\bord0\\1c&H' .. fg .. '}') - ass:opacity(options.volume_opacity, visibility) - ass:pos(0, 0) - ass:draw_start() - ass:append(fg_path.text) - ass:draw_stop() - - -- Current volume value - local volume_string = tostring(round(state.volume * 10) / 10) - local font_size = round(((width * 0.6) - (#volume_string * (width / 20))) * options.font_scale) - if volume_y < self.by - self.spacing then - ass:txt(self.ax + (width / 2), self.by - self.spacing, 2, volume_string, { - size = font_size, color = fgt, opacity = visibility, - clip = '\\clip(' .. fg_path.scale .. ', ' .. fg_path.text .. ')', - }) - end - if volume_y > self.by - self.spacing - font_size then - ass:txt(self.ax + (width / 2), self.by - self.spacing, 2, volume_string, { - size = font_size, color = bgt, opacity = visibility, - clip = '\\iclip(' .. fg_path.scale .. ', ' .. fg_path.text .. ')', - }) - end - - -- Disabled stripes for no audio - if not state.has_audio then - local fg_100_path = create_nudged_path(options.volume_border) - local texture_opts = { - size = 200, color = 'ffffff', opacity = visibility * 0.1, anchor_x = ax, - clip = '\\clip(' .. fg_100_path.scale .. ',' .. fg_100_path.text .. ')', - } - ass:texture(ax, ay, bx, by, 'a', texture_opts) - texture_opts.color = '000000' - texture_opts.anchor_x = ax + texture_opts.size / 28 - ass:texture(ax, ay, bx, by, 'a', texture_opts) - end - - return ass -end - ---[[ Volume ]] - ----@class Volume : Element -local Volume = class(Element) - -function Volume:new() return Class.new(self) --[[@as Volume]] end -function Volume:init() - Element.init(self, 'volume') - self.mute = MuteButton:new({anchor_id = 'volume'}) - self.slider = VolumeSlider:new({anchor_id = 'volume'}) -end - -function Volume:get_visibility() - return self.slider.pressed and 1 or Elements.timeline:get_is_hovered() and -1 or Element.get_visibility(self) -end - -function Volume:update_dimensions() - local width = state.fullormaxed and options.volume_size_fullscreen or options.volume_size - local controls, timeline, top_bar = Elements.controls, Elements.timeline, Elements.top_bar - local min_y = top_bar.enabled and top_bar.by or 0 - local max_y = (controls and controls.enabled and controls.ay) or (timeline.enabled and timeline.ay) - or display.height - top_bar.size - local available_height = max_y - min_y - local max_height = available_height * 0.8 - local height = round(math.min(width * 8, max_height)) - self.enabled = height > width * 2 -- don't render if too small - local margin = (width / 2) + Elements.window_border.size - self.ax = round(options.volume == 'left' and margin or display.width - margin - width) - self.ay = min_y + round((available_height - height) / 2) - self.bx = round(self.ax + width) - self.by = round(self.ay + height) - self.mute.enabled, self.slider.enabled = self.enabled, self.enabled - self.mute:set_coordinates(self.ax, self.by - round(width * 0.8), self.bx, self.by) - self.slider:set_coordinates(self.ax, self.ay, self.bx, self.mute.ay) -end - -function Volume:on_display() self:update_dimensions() end -function Volume:on_prop_border() self:update_dimensions() end -function Volume:on_controls_reflow() self:update_dimensions() end - -return Volume diff --git a/.config/mpv/scripts/uosc_shared/elements/WindowBorder.lua b/.config/mpv/scripts/uosc_shared/elements/WindowBorder.lua deleted file mode 100644 index c5544f5..0000000 --- a/.config/mpv/scripts/uosc_shared/elements/WindowBorder.lua +++ /dev/null @@ -1,33 +0,0 @@ -local Element = require('uosc_shared/elements/Element') - ----@class WindowBorder : Element -local WindowBorder = class(Element) - -function WindowBorder:new() return Class.new(self) --[[@as WindowBorder]] end -function WindowBorder:init() - Element.init(self, 'window_border') - self.ignores_menu = true - self.size = 0 -end - -function WindowBorder:decide_enabled() - self.enabled = options.window_border_size > 0 and not state.fullormaxed and not state.border - self.size = self.enabled and options.window_border_size or 0 -end - -function WindowBorder:on_prop_border() self:decide_enabled() end -function WindowBorder:on_prop_fullormaxed() self:decide_enabled() end - -function WindowBorder:render() - if self.size > 0 then - local ass = assdraw.ass_new() - local clip = '\\iclip(' .. self.size .. ',' .. self.size .. ',' .. - (display.width - self.size) .. ',' .. (display.height - self.size) .. ')' - ass:rect(0, 0, display.width + 1, display.height + 1, { - color = bg, clip = clip, opacity = options.window_border_opacity, - }) - return ass - end -end - -return WindowBorder diff --git a/.config/mpv/scripts/uosc_shared/lib/ass.lua b/.config/mpv/scripts/uosc_shared/lib/ass.lua deleted file mode 100644 index 108953f..0000000 --- a/.config/mpv/scripts/uosc_shared/lib/ass.lua +++ /dev/null @@ -1,170 +0,0 @@ ---[[ ASSDRAW EXTENSIONS ]] - -local ass_mt = getmetatable(assdraw.ass_new()) - --- Opacity. ----@param opacity number|number[] Opacity of all elements, or an array of [primary, secondary, border, shadow] opacities. ----@param fraction? number Optionally adjust the above opacity by this fraction. -function ass_mt:opacity(opacity, fraction) - fraction = fraction ~= nil and fraction or 1 - if type(opacity) == 'number' then - self.text = self.text .. string.format('{\\alpha&H%X&}', opacity_to_alpha(opacity * fraction)) - else - self.text = self.text .. string.format( - '{\\1a&H%X&\\2a&H%X&\\3a&H%X&\\4a&H%X&}', - opacity_to_alpha((opacity[1] or 0) * fraction), - opacity_to_alpha((opacity[2] or 0) * fraction), - opacity_to_alpha((opacity[3] or 0) * fraction), - opacity_to_alpha((opacity[4] or 0) * fraction) - ) - end -end - --- Icon. ----@param x number ----@param y number ----@param size number ----@param name string ----@param opts? {color?: string; border?: number; border_color?: string; opacity?: number; clip?: string; align?: number} -function ass_mt:icon(x, y, size, name, opts) - opts = opts or {} - opts.font, opts.size, opts.bold = 'MaterialIconsRound-Regular', size, false - self:txt(x, y, opts.align or 5, name, opts) -end - --- Text. --- Named `txt` because `ass.text` is a value. ----@param x number ----@param y number ----@param align number ----@param value string|number ----@param opts {size: number; font?: string; color?: string; bold?: boolean; italic?: boolean; border?: number; border_color?: string; shadow?: number; shadow_color?: string; rotate?: number; wrap?: number; opacity?: number; clip?: string} -function ass_mt:txt(x, y, align, value, opts) - local border_size = opts.border or 0 - local shadow_size = opts.shadow or 0 - local tags = '\\pos(' .. x .. ',' .. y .. ')\\rDefault\\an' .. align .. '\\blur0' - -- font - tags = tags .. '\\fn' .. (opts.font or config.font) - -- font size - tags = tags .. '\\fs' .. opts.size - -- bold - if opts.bold or (opts.bold == nil and options.font_bold) then tags = tags .. '\\b1' end - -- italic - if opts.italic then tags = tags .. '\\i1' end - -- rotate - if opts.rotate then tags = tags .. '\\frz' .. opts.rotate end - -- wrap - if opts.wrap then tags = tags .. '\\q' .. opts.wrap end - -- border - tags = tags .. '\\bord' .. border_size - -- shadow - tags = tags .. '\\shad' .. shadow_size - -- colors - tags = tags .. '\\1c&H' .. (opts.color or bgt) - if border_size > 0 then tags = tags .. '\\3c&H' .. (opts.border_color or bg) end - if shadow_size > 0 then tags = tags .. '\\4c&H' .. (opts.shadow_color or bg) end - -- opacity - if opts.opacity then tags = tags .. string.format('\\alpha&H%X&', opacity_to_alpha(opts.opacity)) end - -- clip - if opts.clip then tags = tags .. opts.clip end - -- render - self:new_event() - self.text = self.text .. '{' .. tags .. '}' .. value -end - --- Tooltip. ----@param element {ax: number; ay: number; bx: number; by: number} ----@param value string|number ----@param opts? {size?: number; offset?: number; bold?: boolean; italic?: boolean; width_overwrite?: number, responsive?: boolean} -function ass_mt:tooltip(element, value, opts) - opts = opts or {} - opts.size = opts.size or 16 - opts.border = options.text_border - opts.border_color = bg - local offset = opts.offset or opts.size / 2 - local align_top = opts.responsive == false or element.ay - offset > opts.size * 2 - local x = element.ax + (element.bx - element.ax) / 2 - local y = align_top and element.ay - offset or element.by + offset - local margin = (opts.width_overwrite or text_width(value, opts)) / 2 + 10 - self:txt(clamp(margin, x, display.width - margin), y, align_top and 2 or 8, value, opts) -end - --- Rectangle. ----@param ax number ----@param ay number ----@param bx number ----@param by number ----@param opts? {color?: string; border?: number; border_color?: string; opacity?: number; border_opacity?: number; clip?: string, radius?: number} -function ass_mt:rect(ax, ay, bx, by, opts) - opts = opts or {} - local border_size = opts.border or 0 - local tags = '\\pos(0,0)\\rDefault\\an7\\blur0' - -- border - tags = tags .. '\\bord' .. border_size - -- colors - tags = tags .. '\\1c&H' .. (opts.color or fg) - if border_size > 0 then tags = tags .. '\\3c&H' .. (opts.border_color or bg) end - -- opacity - if opts.opacity then tags = tags .. string.format('\\alpha&H%X&', opacity_to_alpha(opts.opacity)) end - if opts.border_opacity then tags = tags .. string.format('\\3a&H%X&', opacity_to_alpha(opts.border_opacity)) end - -- clip - if opts.clip then - tags = tags .. opts.clip - end - -- draw - self:new_event() - self.text = self.text .. '{' .. tags .. '}' - self:draw_start() - if opts.radius then - self:round_rect_cw(ax, ay, bx, by, opts.radius) - else - self:rect_cw(ax, ay, bx, by) - end - self:draw_stop() -end - --- Circle. ----@param x number ----@param y number ----@param radius number ----@param opts? {color?: string; border?: number; border_color?: string; opacity?: number; clip?: string} -function ass_mt:circle(x, y, radius, opts) - opts = opts or {} - opts.radius = radius - self:rect(x - radius, y - radius, x + radius, y + radius, opts) -end - --- Texture. ----@param ax number ----@param ay number ----@param bx number ----@param by number ----@param char string Texture font character. ----@param opts {size?: number; color: string; opacity?: number; clip?: string; anchor_x?: number, anchor_y?: number} -function ass_mt:texture(ax, ay, bx, by, char, opts) - opts = opts or {} - local anchor_x, anchor_y = opts.anchor_x or ax, opts.anchor_y or ay - local clip = opts.clip or ('\\clip(' .. ax .. ',' .. ay .. ',' .. bx .. ',' .. by .. ')') - local tile_size, opacity = opts.size or 100, opts.opacity or 0.2 - local x, y = ax - (ax - anchor_x) % tile_size, ay - (ay - anchor_y) % tile_size - local width, height = bx - x, by - y - local line = string.rep(char, math.ceil((width / tile_size))) - local lines = '' - for i = 1, math.ceil(height / tile_size), 1 do lines = lines .. (lines == '' and '' or '\\N') .. line end - self:txt( - x, y, 7, lines, - {font = 'uosc_textures', size = tile_size, color = opts.color, bold = false, opacity = opacity, clip = clip}) -end - --- Rotating spinner icon. ----@param x number ----@param y number ----@param size number ----@param opts? {color?: string; opacity?: number; clip?: string; border?: number; border_color?: string;} -function ass_mt:spinner(x, y, size, opts) - opts = opts or {} - opts.rotate = (state.render_last_time * 1.75 % 1) * -360 - opts.color = opts.color or fg - self:icon(x, y, size, 'autorenew', opts) - request_render() -end diff --git a/.config/mpv/scripts/uosc_shared/lib/menus.lua b/.config/mpv/scripts/uosc_shared/lib/menus.lua deleted file mode 100644 index e2a4ccc..0000000 --- a/.config/mpv/scripts/uosc_shared/lib/menus.lua +++ /dev/null @@ -1,282 +0,0 @@ ----@param data MenuData ----@param opts? {submenu?: string; mouse_nav?: boolean} -function open_command_menu(data, opts) - local menu = Menu:open(data, function(value) - if type(value) == 'string' then - mp.command(value) - else - ---@diagnostic disable-next-line: deprecated - mp.commandv(unpack(value)) - end - end, opts) - if opts and opts.submenu then menu:activate_submenu(opts.submenu) end - return menu -end - ----@param opts? {submenu?: string; mouse_nav?: boolean} -function toggle_menu_with_items(opts) - if Menu:is_open('menu') then Menu:close() - else open_command_menu({type = 'menu', items = config.menu_items}, opts) end -end - ----@param options {type: string; title: string; list_prop: string; active_prop?: string; serializer: fun(list: any, active: any): MenuDataItem[]; on_select: fun(value: any)} -function create_self_updating_menu_opener(options) - return function() - if Menu:is_open(options.type) then Menu:close() return end - local list = mp.get_property_native(options.list_prop) - local active = options.active_prop and mp.get_property_native(options.active_prop) or nil - local menu - - local function update() menu:update_items(options.serializer(list, active)) end - - local ignore_initial_list = true - local function handle_list_prop_change(name, value) - if ignore_initial_list then ignore_initial_list = false - else list = value update() end - end - - local ignore_initial_active = true - local function handle_active_prop_change(name, value) - if ignore_initial_active then ignore_initial_active = false - else active = value update() end - end - - local initial_items, selected_index = options.serializer(list, active) - - -- Items and active_index are set in the handle_prop_change callback, since adding - -- a property observer triggers its handler immediately, we just let that initialize the items. - menu = Menu:open( - {type = options.type, title = options.title, items = initial_items, selected_index = selected_index}, - options.on_select, { - on_open = function() - mp.observe_property(options.list_prop, 'native', handle_list_prop_change) - if options.active_prop then - mp.observe_property(options.active_prop, 'native', handle_active_prop_change) - end - end, - on_close = function() - mp.unobserve_property(handle_list_prop_change) - mp.unobserve_property(handle_active_prop_change) - end, - }) - end -end - -function create_select_tracklist_type_menu_opener(menu_title, track_type, track_prop, load_command) - local function serialize_tracklist(tracklist) - local items = {} - - if load_command then - items[#items + 1] = { - title = 'Load', bold = true, italic = true, hint = 'open file', value = '{load}', separator = true, - } - end - - local first_item_index = #items + 1 - local active_index = nil - local disabled_item = nil - - -- Add option to disable a subtitle track. This works for all tracks, - -- but why would anyone want to disable audio or video? Better to not - -- let people mistakenly select what is unwanted 99.999% of the time. - -- If I'm mistaken and there is an active need for this, feel free to - -- open an issue. - if track_type == 'sub' then - disabled_item = {title = 'Disabled', italic = true, muted = true, hint = '—', value = nil, active = true} - items[#items + 1] = disabled_item - end - - for _, track in ipairs(tracklist) do - if track.type == track_type then - local hint_values = {} - local function h(value) hint_values[#hint_values + 1] = value end - - if track.lang then h(track.lang:upper()) end - if track['demux-h'] then - h(track['demux-w'] and (track['demux-w'] .. 'x' .. track['demux-h']) or (track['demux-h'] .. 'p')) - end - if track['demux-fps'] then h(string.format('%.5gfps', track['demux-fps'])) end - h(track.codec) - if track['audio-channels'] then h(track['audio-channels'] .. ' channels') end - if track['demux-samplerate'] then h(string.format('%.3gkHz', track['demux-samplerate'] / 1000)) end - if track.forced then h('forced') end - if track.default then h('default') end - if track.external then h('external') end - - items[#items + 1] = { - title = (track.title and track.title or 'Track ' .. track.id), - hint = table.concat(hint_values, ', '), - value = track.id, - active = track.selected, - } - - if track.selected then - if disabled_item then disabled_item.active = false end - active_index = #items - end - end - end - - return items, active_index or first_item_index - end - - local function selection_handler(value) - if value == '{load}' then - mp.command(load_command) - else - mp.commandv('set', track_prop, value and value or 'no') - - -- If subtitle track was selected, assume user also wants to see it - if value and track_type == 'sub' then - mp.commandv('set', 'sub-visibility', 'yes') - end - end - end - - return create_self_updating_menu_opener({ - title = menu_title, - type = track_type, - list_prop = 'track-list', - serializer = serialize_tracklist, - on_select = selection_handler, - }) -end - ----@alias NavigationMenuOptions {type: string, title?: string, allowed_types?: string[], active_path?: string, selected_path?: string; on_open?: fun(); on_close?: fun()} - --- Opens a file navigation menu with items inside `directory_path`. ----@param directory_path string ----@param handle_select fun(path: string): nil ----@param opts NavigationMenuOptions -function open_file_navigation_menu(directory_path, handle_select, opts) - directory = serialize_path(normalize_path(directory_path)) - opts = opts or {} - - if not directory then - msg.error('Couldn\'t serialize path "' .. directory_path .. '.') - return - end - - local files, directories = read_directory(directory.path, opts.allowed_types) - local is_root = not directory.dirname - local path_separator = path_separator(directory.path) - - if not files or not directories then return end - - sort_filenames(directories) - sort_filenames(files) - - -- Pre-populate items with parent directory selector if not at root - -- Each item value is a serialized path table it points to. - local items = {} - - if is_root then - if state.os == 'windows' then - items[#items + 1] = {title = '..', hint = 'Drives', value = '{drives}', separator = true} - end - else - items[#items + 1] = {title = '..', hint = 'parent dir', value = directory.dirname, separator = true} - end - - local back_path = items[#items] and items[#items].value - local selected_index = #items + 1 - - for _, dir in ipairs(directories) do - items[#items + 1] = {title = dir, value = join_path(directory.path, dir), hint = path_separator} - end - - for _, file in ipairs(files) do - items[#items + 1] = {title = file, value = join_path(directory.path, file)} - end - - for index, item in ipairs(items) do - if not item.value.is_to_parent and opts.active_path == item.value then - item.active = true - if not opts.selected_path then selected_index = index end - end - - if opts.selected_path == item.value then selected_index = index end - end - - local function open_path(path) - local is_drives = path == '{drives}' - local is_to_parent = is_drives or #path < #directory_path - local inheritable_options = { - type = opts.type, title = opts.title, allowed_types = opts.allowed_types, active_path = opts.active_path, - } - - if is_drives then - open_drives_menu(function(drive_path) - open_file_navigation_menu(drive_path, handle_select, inheritable_options) - end, { - type = inheritable_options.type, title = inheritable_options.title, selected_path = directory.path, - on_open = opts.on_open, on_close = opts.on_close, - }) - return - end - - local info, error = utils.file_info(path) - - if not info then - msg.error('Can\'t retrieve path info for "' .. path .. '". Error: ' .. (error or '')) - return - end - - if info.is_dir then - -- Preselect directory we are coming from - if is_to_parent then - inheritable_options.selected_path = directory.path - end - - open_file_navigation_menu(path, handle_select, inheritable_options) - else - handle_select(path) - end - end - - local function handle_back() - if back_path then open_path(back_path) end - end - - local menu_data = { - type = opts.type, title = opts.title or directory.basename .. path_separator, items = items, - selected_index = selected_index, - } - local menu_options = {on_open = opts.on_open, on_close = opts.on_close, on_back = handle_back} - - return Menu:open(menu_data, open_path, menu_options) -end - --- Opens a file navigation menu with Windows drives as items. ----@param handle_select fun(path: string): nil ----@param opts? NavigationMenuOptions -function open_drives_menu(handle_select, opts) - opts = opts or {} - local process = mp.command_native({ - name = 'subprocess', - capture_stdout = true, - playback_only = false, - args = {'wmic', 'logicaldisk', 'get', 'name', '/value'}, - }) - local items, selected_index = {}, 1 - - if process.status == 0 then - for _, value in ipairs(split(process.stdout, '\n')) do - local drive = string.match(value, 'Name=([A-Z]:)') - if drive then - local drive_path = normalize_path(drive) - items[#items + 1] = { - title = drive, hint = 'drive', value = drive_path, active = opts.active_path == drive_path, - } - if opts.selected_path == drive_path then selected_index = #items end - end - end - else - msg.error(process.stderr) - end - - return Menu:open( - {type = opts.type, title = opts.title or 'Drives', items = items, selected_index = selected_index}, - handle_select - ) -end diff --git a/.config/mpv/scripts/uosc_shared/lib/std.lua b/.config/mpv/scripts/uosc_shared/lib/std.lua deleted file mode 100644 index 1261666..0000000 --- a/.config/mpv/scripts/uosc_shared/lib/std.lua +++ /dev/null @@ -1,181 +0,0 @@ ---[[ Stateless utilities missing in lua standard library ]] - ----@param number number -function round(number) return math.floor(number + 0.5) end - ----@param min number ----@param value number ----@param max number -function clamp(min, value, max) return math.max(min, math.min(value, max)) end - ----@param rgba string `rrggbb` or `rrggbbaa` hex string. -function serialize_rgba(rgba) - local a = rgba:sub(7, 8) - return { - color = rgba:sub(5, 6) .. rgba:sub(3, 4) .. rgba:sub(1, 2), - opacity = clamp(0, tonumber(#a == 2 and a or 'ff', 16) / 255, 1), - } -end - --- Trim any `char` from the end of the string. ----@param str string ----@param char string ----@return string -function trim_end(str, char) - local char, end_i = char:byte(), 0 - for i = #str, 1, -1 do - if str:byte(i) ~= char then - end_i = i - break - end - end - return str:sub(1, end_i) -end - ----@param str string ----@param pattern string ----@return string[] -function split(str, pattern) - local list = {} - local full_pattern = '(.-)' .. pattern - local last_end = 1 - local start_index, end_index, capture = str:find(full_pattern, 1) - while start_index do - list[#list + 1] = capture - last_end = end_index + 1 - start_index, end_index, capture = str:find(full_pattern, last_end) - end - if last_end <= (#str + 1) then - capture = str:sub(last_end) - list[#list + 1] = capture - end - return list -end - --- Get index of the last appearance of `sub` in `str`. ----@param str string ----@param sub string ----@return integer|nil -function string_last_index_of(str, sub) - local sub_length = #sub - for i = #str, 1, -1 do - for j = 1, sub_length do - if str:byte(i + j - 1) ~= sub:byte(j) then break end - if j == sub_length then return i end - end - end -end - ----@param itable table ----@param value any ----@return integer|nil -function itable_index_of(itable, value) - for index, item in ipairs(itable) do - if item == value then return index end - end -end - ----@param itable table ----@param compare fun(value: any, index: number) ----@param from_end? boolean Search from the end of the table. ----@return number|nil index ----@return any|nil value -function itable_find(itable, compare, from_end) - local from, to, step = from_end and #itable or 1, from_end and 1 or #itable, from_end and -1 or 1 - for index = from, to, step do - if compare(itable[index], index) then return index, itable[index] end - end -end - ----@param itable table ----@param decider fun(value: any, index: number) -function itable_filter(itable, decider) - local filtered = {} - for index, value in ipairs(itable) do - if decider(value, index) then filtered[#filtered + 1] = value end - end - return filtered -end - ----@param itable table ----@param value any -function itable_remove(itable, value) - return itable_filter(itable, function(item) return item ~= value end) -end - ----@param itable table ----@param start_pos? integer ----@param end_pos? integer -function itable_slice(itable, start_pos, end_pos) - start_pos = start_pos and start_pos or 1 - end_pos = end_pos and end_pos or #itable - - if end_pos < 0 then end_pos = #itable + end_pos + 1 end - if start_pos < 0 then start_pos = #itable + start_pos + 1 end - - local new_table = {} - for index, value in ipairs(itable) do - if index >= start_pos and index <= end_pos then - new_table[#new_table + 1] = value - end - end - return new_table -end - ----@generic T ----@param a T[]|nil ----@param b T[]|nil ----@return T[] -function itable_join(a, b) - local result = {} - if a then for _, value in ipairs(a) do result[#result + 1] = value end end - if b then for _, value in ipairs(b) do result[#result + 1] = value end end - return result -end - ----@param target any[] ----@param source any[] -function itable_append(target, source) - for _, value in ipairs(source) do target[#target + 1] = value end - return target -end - ----@param target any[] ----@param source any[] ----@param props? string[] -function table_assign(target, source, props) - if props then - for _, name in ipairs(props) do target[name] = source[name] end - else - for prop, value in pairs(source) do target[prop] = value end - end - return target -end - ----@generic T ----@param table T ----@return T -function table_shallow_copy(table) - local result = {} - for key, value in pairs(table) do result[key] = value end - return result -end - ---[[ EASING FUNCTIONS ]] - -function ease_out_quart(x) return 1 - ((1 - x) ^ 4) end -function ease_out_sext(x) return 1 - ((1 - x) ^ 6) end - ---[[ CLASSES ]] - ----@class Class -Class = {} -function Class:new(...) - local object = setmetatable({}, {__index = self}) - object:init(...) - return object -end -function Class:init() end -function Class:destroy() end - -function class(parent) return setmetatable({}, {__index = parent or Class}) end diff --git a/.config/mpv/scripts/uosc_shared/lib/text.lua b/.config/mpv/scripts/uosc_shared/lib/text.lua deleted file mode 100644 index 7bb456f..0000000 --- a/.config/mpv/scripts/uosc_shared/lib/text.lua +++ /dev/null @@ -1,421 +0,0 @@ --- https://en.wikipedia.org/wiki/Unicode_block ----@alias CodePointRange {[1]: integer; [2]: integer} - ----@type CodePointRange[] -local zero_width_blocks = { - {0x0000, 0x001F}, -- C0 - {0x007F, 0x009F}, -- Delete + C1 - {0x034F, 0x034F}, -- combining grapheme joiner - {0x061C, 0x061C}, -- Arabic Letter Strong - {0x200B, 0x200F}, -- {zero-width space, zero-width non-joiner, zero-width joiner, left-to-right mark, right-to-left mark} - {0x2028, 0x202E}, -- {line separator, paragraph separator, Left-to-Right Embedding, Right-to-Left Embedding, Pop Directional Format, Left-to-Right Override, Right-to-Left Override} - {0x2060, 0x2060}, -- word joiner - {0x2066, 0x2069}, -- {Left-to-Right Isolate, Right-to-Left Isolate, First Strong Isolate, Pop Directional Isolate} - {0xFEFF, 0xFEFF}, -- zero-width non-breaking space - -- Some other characters can also be combined https://en.wikipedia.org/wiki/Combining_character - {0x0300, 0x036F}, -- Combining Diacritical Marks 0 BMP Inherited - {0x1AB0, 0x1AFF}, -- Combining Diacritical Marks Extended 0 BMP Inherited - {0x1DC0, 0x1DFF}, -- Combining Diacritical Marks Supplement 0 BMP Inherited - {0x20D0, 0x20FF}, -- Combining Diacritical Marks for Symbols 0 BMP Inherited - {0xFE20, 0xFE2F}, -- Combining Half Marks 0 BMP Cyrillic (2 characters), Inherited (14 characters) - -- Egyptian Hieroglyph Format Controls and Shorthand format Controls - {0x13430, 0x1345F}, -- Egyptian Hieroglyph Format Controls 1 SMP Egyptian Hieroglyphs - {0x1BCA0, 0x1BCAF}, -- Shorthand Format Controls 1 SMP Common - -- not sure how to deal with those https://en.wikipedia.org/wiki/Spacing_Modifier_Letters - {0x02B0, 0x02FF}, -- Spacing Modifier Letters 0 BMP Bopomofo (2 characters), Latin (14 characters), Common (64 characters) -} - --- All characters have the same width as the first one ----@type CodePointRange[] -local same_width_blocks = { - {0x3400, 0x4DBF}, -- CJK Unified Ideographs Extension A 0 BMP Han - {0x4E00, 0x9FFF}, -- CJK Unified Ideographs 0 BMP Han - {0x20000, 0x2A6DF}, -- CJK Unified Ideographs Extension B 2 SIP Han - {0x2A700, 0x2B73F}, -- CJK Unified Ideographs Extension C 2 SIP Han - {0x2B740, 0x2B81F}, -- CJK Unified Ideographs Extension D 2 SIP Han - {0x2B820, 0x2CEAF}, -- CJK Unified Ideographs Extension E 2 SIP Han - {0x2CEB0, 0x2EBEF}, -- CJK Unified Ideographs Extension F 2 SIP Han - {0x2F800, 0x2FA1F}, -- CJK Compatibility Ideographs Supplement 2 SIP Han - {0x30000, 0x3134F}, -- CJK Unified Ideographs Extension G 3 TIP Han - {0x31350, 0x323AF}, -- CJK Unified Ideographs Extension H 3 TIP Han -} - ----Get byte count of utf-8 character at index i in str ----@param str string ----@param i integer? ----@return integer -local function utf8_char_bytes(str, i) - local char_byte = str:byte(i) - if char_byte < 0xC0 then return 1 - elseif char_byte < 0xE0 then return 2 - elseif char_byte < 0xF0 then return 3 - elseif char_byte < 0xF8 then return 4 - else return 1 end -end - ----Creates an iterator for an utf-8 encoded string ----Iterates over utf-8 characters instead of bytes ----@param str string ----@return fun(): integer?, string? -local function utf8_iter(str) - local byte_start = 1 - return function() - local start = byte_start - if #str < start then return nil end - local byte_count = utf8_char_bytes(str, start) - byte_start = start + byte_count - return start, str:sub(start, start + byte_count - 1) - end -end - ----Extract Unicode code point from utf-8 character at index i in str ----@param str string ----@param i integer ----@return integer -local function utf8_to_unicode(str, i) - local byte_count = utf8_char_bytes(str, i) - local char_byte = str:byte(i) - local unicode = char_byte - if byte_count ~= 1 then - local shift = 2 ^ (8 - byte_count) - char_byte = char_byte - math.floor(0xFF / shift) * shift - unicode = char_byte * (2 ^ 6) ^ (byte_count - 1) - end - for j = 2, byte_count do - char_byte = str:byte(i + j - 1) - 0x80 - unicode = unicode + char_byte * (2 ^ 6) ^ (byte_count - j) - end - return round(unicode) -end - ----Convert Unicode code point to utf-8 string ----@param unicode integer ----@return string? -local function unicode_to_utf8(unicode) - if unicode < 0x80 then return string.char(unicode) - else - local byte_count - if unicode < 0x800 then byte_count = 2 - elseif unicode < 0x10000 then byte_count = 3 - elseif unicode < 0x110000 then byte_count = 4 - else return end -- too big - - local res = {} - local shift = 2 ^ 6 - local after_shift = unicode - for _ = byte_count, 2, -1 do - local before_shift = after_shift - after_shift = math.floor(before_shift / shift) - table.insert(res, 1, before_shift - after_shift * shift + 0x80) - end - shift = 2 ^ (8 - byte_count) - table.insert(res, 1, after_shift + math.floor(0xFF / shift) * shift) - ---@diagnostic disable-next-line: deprecated - return string.char(unpack(res)) - end -end - ----Update osd resolution if valid ----@param width integer ----@param height integer -local function update_osd_resolution(width, height) - if width > 0 and height > 0 then osd_width, osd_height = width, height end -end - -local text_osd = mp.create_osd_overlay("ass-events") -text_osd.compute_bounds, text_osd.hidden = true, true ----@type integer, integer -local osd_width, osd_height = 100, 100 -mp.observe_property('osd-dimensions', 'native', function (_, dim) - if dim then update_osd_resolution(dim.w, dim.h) end -end) - ----@param ass_text string ----@return integer, integer, integer, integer -local function measure_bounds(ass_text) - update_osd_resolution(mp.get_osd_size()) - text_osd.res_x, text_osd.res_y = osd_width, osd_height - text_osd.data = ass_text - local res = text_osd:update() - return res.x0, res.y0, res.x1, res.y1 -end - ----@type {wrap: integer; bold: boolean; italic: boolean, rotate: number; size: number} -local bounds_opts = {wrap = 2, bold = false, italic = false, rotate = 0, size = 0} - ----Measure text width and normalize to a font size of 1 ----text has to be ass safe ----@param text string ----@param size number ----@param bold boolean ----@param italic boolean ----@param horizontal boolean ----@return number, integer -local function normalized_text_width(text, size, bold, italic, horizontal) - bounds_opts.bold, bounds_opts.italic, bounds_opts.rotate = bold, italic, horizontal and 0 or -90 - local x1, y1 = nil, nil - size = size / 0.8 - -- prevent endless loop - local repetitions_left = 5 - repeat - size = size * 0.8 - bounds_opts.size = size - local ass = assdraw.ass_new() - ass:txt(0, 0, horizontal and 7 or 1, text, bounds_opts) - _, _, x1, y1 = measure_bounds(ass.text) - repetitions_left = repetitions_left - 1 - -- make sure nothing got clipped - until (x1 and x1 < osd_width and y1 < osd_height) or repetitions_left == 0 - local width = (repetitions_left == 0 and not x1) and 0 or (horizontal and x1 or y1) - return width / size, horizontal and osd_width or osd_height -end - ----Estimates character length based on utf8 byte count ----1 character length is roughly the size of a latin character ----@param char string ----@return number -local function char_length(char) - return #char > 2 and 2 or 1 -end - ----Estimates string length based on utf8 byte count ----Note: Making a string in the iterator with the character is a waste here, ----but as this function is only used when measuring whole string widths it's fine ----@param text string ----@return number -local function text_length(text) - if not text or text == '' then return 0 end - local text_length = 0 - for _, char in utf8_iter(tostring(text)) do text_length = text_length + char_length(char) end - return text_length -end - -local width_length_ratio = 0.5 ----@type {[boolean]: {[string]: {[1]: number, [2]: integer}}} -local char_width_cache = {} - ----Finds the best orientation of text on screen and returns the estimated max size ----and if the text should be drawn horizontally ----@param text string ----@return number, boolean -local function fit_on_screen(text) - local estimated_width = text_length(text) * width_length_ratio - if osd_width >= osd_height then - -- Fill the screen as much as we can, bigger is more accurate. - return math.min(osd_width / estimated_width, osd_height), true - else - return math.min(osd_height / estimated_width, osd_width), false - end -end - ----Gets next stage from cache ----@param cache {[any]: table} ----@param value any -local function get_cache_stage(cache, value) - local stage = cache[value] - if not stage then - stage = {} - cache[value] = stage - end - return stage -end - ----Is measured resolution sufficient ----@param px integer ----@return boolean -local function no_remeasure_required(px) - return px >= 800 or (px * 1.1 >= osd_width and px * 1.1 >= osd_height) -end - ----Get measured width of character ----@param char string ----@param bold boolean ----@return number, integer -local function character_width(char, bold) - ---@type {[string]: {[1]: number, [2]: integer}} - local char_widths = get_cache_stage(char_width_cache, bold) - local width_px = char_widths[char] - if width_px and no_remeasure_required(width_px[2]) then return width_px[1], width_px[2] end - - local unicode = utf8_to_unicode(char, 1) - for _, block in ipairs(zero_width_blocks) do - if unicode >= block[1] and unicode <= block[2] then - char_widths[char] = {0, infinity} - return 0, infinity - end - end - - local measured_char = nil - for _, block in ipairs(same_width_blocks) do - if unicode >= block[1] and unicode <= block[2] then - measured_char = unicode_to_utf8(block[1]) - width_px = char_widths[measured_char] - if width_px and no_remeasure_required(width_px[2]) then - char_widths[char] = width_px - return width_px[1], width_px[2] - end - break - end - end - - if not measured_char then measured_char = char end - -- half as many repetitions for wide characters - local char_count = 10 / char_length(char) - local max_size, horizontal = fit_on_screen(measured_char:rep(char_count)) - local size = math.min(max_size * 0.9, 50) - char_count = math.min(math.floor(char_count * max_size / size * 0.8), 100) - local enclosing_char, enclosing_width, next_char_count = '|', 0, char_count - if measured_char == enclosing_char then enclosing_char = '' - else enclosing_width = 2 * character_width(enclosing_char, bold) end - local width_ratio, width, px = nil, nil, nil - repeat - char_count = next_char_count - local str = enclosing_char .. measured_char:rep(char_count) .. enclosing_char - width, px = normalized_text_width(str, size, bold, false, horizontal) - width = width - enclosing_width - width_ratio = width * size / (horizontal and osd_width or osd_height) - next_char_count = math.min(math.floor(char_count / width_ratio * 0.9), 100) - until width_ratio < 0.05 or width_ratio > 0.5 or char_count == next_char_count - width = width / char_count - - width_px = {width, px} - if char ~= measured_char then char_widths[measured_char] = width_px end - char_widths[char] = width_px - return width, px -end - ----Calculate text width from individual measured characters ----@param text string|number ----@param bold boolean ----@return number, integer -local function character_based_width(text, bold) - local max_width = 0 - local min_px = infinity - for line in tostring(text):gmatch("([^\n]*)\n?") do - local total_width = 0 - for _, char in utf8_iter(line) do - local width, px = character_width(char, bold) - total_width = total_width + width - if px < min_px then min_px = px end - end - if total_width > max_width then max_width = total_width end - end - return max_width, min_px -end - ----Measure width of whole text ----@param text string|number ----@param bold boolean ----@param italic boolean ----@return number, integer -local function whole_text_width(text, bold, italic) - text = tostring(text) - local size, horizontal = fit_on_screen(text) - return normalized_text_width(ass_escape(text), size * 0.9, bold, italic, horizontal) -end - ----Get scale factor calculated from font size, bold and italic ----@param opts {size: number; bold?: boolean; italic?: boolean} -local function opts_scale_factor(opts) - return (opts.italic and 1.01 or 1) * opts.size -end - ----@type {[boolean]: {[boolean]: {[string|number]: {[1]: number, [2]: integer}}}} | {[boolean]: {[string|number]: {[1]: number, [2]: integer}}} -local width_cache = {} - ----Calculate width of text with the given opts ----@param text string|number ----@return number ----@param opts {size: number; bold?: boolean; italic?: boolean} -function text_width(text, opts) - if not text or text == '' then return 0 end - - ---@type boolean, boolean - local bold, italic = opts.bold or options.font_bold, opts.italic or false - - if options.text_width_estimation then - ---@type {[string|number]: {[1]: number, [2]: integer}} - local text_width = get_cache_stage(width_cache, bold) - local width_px = text_width[text] - if width_px and no_remeasure_required(width_px[2]) then return width_px[1] * opts_scale_factor(opts) end - - local width, px = character_based_width(text, bold) - width_cache[bold][text] = {width, px} - return width * opts_scale_factor(opts) - else - ---@type {[string|number]: {[1]: number, [2]: integer}} - local text_width = get_cache_stage(get_cache_stage(width_cache, bold), italic) - local width_px = text_width[text] - if width_px and no_remeasure_required(width_px[2]) then return width_px[1] * opts.size end - - local width, px = whole_text_width(text, bold, italic) - width_cache[bold][italic][text] = {width, px} - return width * opts.size - end -end - ----Wrap the text at the closest opportunity to target_line_length ----@param text string ----@param opts {size: number; bold?: boolean; italic?: boolean} ----@param target_line_length number ----@return string -function wrap_text(text, opts, target_line_length) - local target_line_width = target_line_length * width_length_ratio * opts.size - local bold, scale_factor = opts.bold or false, opts_scale_factor(opts) - local wrap_at_chars = {' ', ' ', '-', '–'} - local remove_when_wrap = {' ', ' '} - local lines = {} - for text_line in text:gmatch("([^\n]*)\n?") do - local line_width = 0 - local line_start = 1 - local before_end = nil - local before_width = 0 - local before_line_start = 0 - local before_removed_width = 0 - for char_start, char in utf8_iter(text_line) do - local char_end = char_start + #char - 1 - local can_wrap = false - for _, c in ipairs(wrap_at_chars) do - if char == c then - can_wrap = true - break - end - end - local char_width = character_width(char, bold) * scale_factor - line_width = line_width + char_width - if can_wrap or (char_end == #text_line) then - local remove = false - for _, c in ipairs(remove_when_wrap) do - if char == c then - remove = true - break - end - end - local line_width_after_remove = line_width - (remove and char_width or 0) - if line_width_after_remove < target_line_width then - before_end = remove and char_start - 1 or char_end - before_width = line_width_after_remove - before_line_start = char_end + 1 - before_removed_width = remove and char_width or 0 - else - if (target_line_width - before_width) < - (line_width_after_remove - target_line_width) then - lines[#lines + 1] = text_line:sub(line_start, before_end) - line_start = before_line_start - line_width = line_width - before_width - before_removed_width - else - lines[#lines + 1] = text_line:sub(line_start, remove and char_start - 1 or char_end) - line_start = char_end + 1 - line_width = remove and line_width - char_width or line_width - line_width = 0 - end - before_end = line_start - before_width = 0 - end - end - end - if #text_line >= line_start then lines[#lines + 1] = text_line:sub(line_start) - elseif text_line == '' then lines[#lines + 1] = '' end - end - return table.concat(lines, '\n') -end diff --git a/.config/mpv/scripts/uosc_shared/lib/utils.lua b/.config/mpv/scripts/uosc_shared/lib/utils.lua deleted file mode 100644 index 3202ef1..0000000 --- a/.config/mpv/scripts/uosc_shared/lib/utils.lua +++ /dev/null @@ -1,545 +0,0 @@ ---[[ UI specific utilities that might or might not depend on its state or options ]] - --- Sorting comparator close to (but not exactly) how file explorers sort files. -sort_filenames = (function() - local symbol_order - local default_order - - if state.os == 'windows' then - symbol_order = { - ['!'] = 1, ['#'] = 2, ['$'] = 3, ['%'] = 4, ['&'] = 5, ['('] = 6, [')'] = 6, [','] = 7, - ['.'] = 8, ["'"] = 9, ['-'] = 10, [';'] = 11, ['@'] = 12, ['['] = 13, [']'] = 13, ['^'] = 14, - ['_'] = 15, ['`'] = 16, ['{'] = 17, ['}'] = 17, ['~'] = 18, ['+'] = 19, ['='] = 20, - } - default_order = 21 - else - symbol_order = { - ['`'] = 1, ['^'] = 2, ['~'] = 3, ['='] = 4, ['_'] = 5, ['-'] = 6, [','] = 7, [';'] = 8, - ['!'] = 9, ["'"] = 10, ['('] = 11, [')'] = 11, ['['] = 12, [']'] = 12, ['{'] = 13, ['}'] = 14, - ['@'] = 15, ['$'] = 16, ['*'] = 17, ['&'] = 18, ['%'] = 19, ['+'] = 20, ['.'] = 22, ['#'] = 23, - } - default_order = 21 - end - - -- Alphanumeric sorting for humans in Lua - -- http://notebook.kulchenko.com/algorithms/alphanumeric-natural-sorting-for-humans-in-lua - local function pad_number(d) - local dec, n = d:match('(%.?)0*(.+)') - return #dec > 0 and ('%.12f'):format(d) or ('%03d%s'):format(#n, n) - end - - --- In place sorting of filenames - ---@param filenames string[] - return function(filenames) - local tuples = {} - for i, filename in ipairs(filenames) do - local first_char = filename:sub(1, 1) - local order = symbol_order[first_char] or default_order - local formatted = filename:lower():gsub('%.?%d+', pad_number) - tuples[i] = {order, formatted, filename} - end - table.sort(tuples, function(a, b) - if a[1] ~= b[1] then return a[1] < b[1] end - return a[2] == b[2] and #b[3] < #a[3] or a[2] < b[2] - end) - for i, tuple in ipairs(tuples) do filenames[i] = tuple[3] end - end -end)() - --- Creates in-between frames to animate value from `from` to `to` numbers. ----@param from number ----@param to number|fun():number ----@param setter fun(value: number) ----@param factor_or_callback? number|fun() ----@param callback? fun() Called either on animation end, or when animation is killed. -function tween(from, to, setter, factor_or_callback, callback) - local factor = factor_or_callback - if type(factor_or_callback) == 'function' then callback = factor_or_callback end - if type(factor) ~= 'number' then factor = 0.3 end - - local current, done, timeout = from, false, nil - local get_to = type(to) == 'function' and to or function() return to --[[@as number]] end - local cutoff = math.abs(get_to() - from) * 0.01 - - local function finish() - if not done then - done = true - timeout:kill() - if callback then callback() end - end - end - - local function tick() - local to = get_to() - current = current + ((to - current) * factor) - local is_end = math.abs(to - current) <= cutoff - setter(is_end and to or current) - request_render() - if is_end then finish() - else timeout:resume() end - end - - timeout = mp.add_timeout(state.render_delay, tick) - tick() - - return finish -end - ----@param point {x: number; y: number} ----@param rect {ax: number; ay: number; bx: number; by: number} -function get_point_to_rectangle_proximity(point, rect) - local dx = math.max(rect.ax - point.x, 0, point.x - rect.bx) - local dy = math.max(rect.ay - point.y, 0, point.y - rect.by) - return math.sqrt(dx * dx + dy * dy) -end - --- Extracts the properties used by property expansion of that string. ----@param str string ----@param res { [string] : boolean } | nil ----@return { [string] : boolean } -function get_expansion_props(str, res) - res = res or {} - for str in str:gmatch('%$(%b{})') do - local name, str = str:match('^{[?!]?=?([^:]+):?(.*)}$') - if name then - local s = name:find('==') or nil - if s then name = name:sub(0, s - 1) end - res[name] = true - if str and str ~= '' then get_expansion_props(str, res) end - end - end - return res -end - --- Escape a string for verbatim display on the OSD. ----@param str string -function ass_escape(str) - -- There is no escape for '\' in ASS (I think?) but '\' is used verbatim if - -- it isn't followed by a recognized character, so add a zero-width - -- non-breaking space - str = str:gsub('\\', '\\\239\187\191') - str = str:gsub('{', '\\{') - str = str:gsub('}', '\\}') - -- Precede newlines with a ZWNBSP to prevent ASS's weird collapsing of - -- consecutive newlines - str = str:gsub('\n', '\239\187\191\\N') - -- Turn leading spaces into hard spaces to prevent ASS from stripping them - str = str:gsub('\\N ', '\\N\\h') - str = str:gsub('^ ', '\\h') - return str -end - ----@param seconds number ----@return string -function format_time(seconds) - local human = mp.format_time(seconds) - if options.time_precision > 0 then - local formatted = string.format('%.' .. options.time_precision .. 'f', math.abs(seconds) % 1) - human = human .. '.' .. string.sub(formatted, 3) - end - return human -end - ----@param opacity number 0-1 -function opacity_to_alpha(opacity) - return 255 - math.ceil(255 * opacity) -end - -path_separator = (function() - local os_separator = state.os == 'windows' and '\\' or '/' - - -- Get appropriate path separator for the given path. - ---@param path string - ---@return string - return function(path) - return path:sub(1, 2) == '\\\\' and '\\' or os_separator - end -end)() - --- Joins paths with the OS aware path separator or UNC separator. ----@param p1 string ----@param p2 string ----@return string -function join_path(p1, p2) - local p1, separator = trim_trailing_separator(p1) - -- Prevents joining drive letters with a redundant separator (`C:\\foo`), - -- as `trim_trailing_separator()` doesn't trim separators from drive letters. - return p1:sub(#p1) == separator and p1 .. p2 or p1 .. separator.. p2 -end - --- Check if path is absolute. ----@param path string ----@return boolean -function is_absolute(path) - if path:sub(1, 2) == '\\\\' then return true - elseif state.os == 'windows' then return path:find('^%a+:') ~= nil - else return path:sub(1, 1) == '/' end -end - --- Ensure path is absolute. ----@param path string ----@return string -function ensure_absolute(path) - if is_absolute(path) then return path end - return join_path(state.cwd, path) -end - --- Remove trailing slashes/backslashes. ----@param path string ----@return string path, string trimmed_separator_type -function trim_trailing_separator(path) - local separator = path_separator(path) - path = trim_end(path, separator) - if state.os == 'windows' then - -- Drive letters on windows need trailing backslash - if path:sub(#path) == ':' then path = path .. '\\' end - else - if path == '' then path = '/' end - end - return path, separator -end - --- Ensures path is absolute, remove trailing slashes/backslashes. --- Lightweight version of normalize_path for performance critical parts. ----@param path string ----@return string -function normalize_path_lite(path) - if not path or is_protocol(path) then return path end - path = trim_trailing_separator(ensure_absolute(path)) - return path -end - --- Ensures path is absolute, remove trailing slashes/backslashes, normalization of path separators and deduplication. ----@param path string ----@return string -function normalize_path(path) - if not path or is_protocol(path) then return path end - - path = ensure_absolute(path) - local is_unc = path:sub(1, 2) == '\\\\' - if state.os == 'windows' or is_unc then path = path:gsub('/', '\\') end - path = trim_trailing_separator(path) - - --Deduplication of path separators - if is_unc then path = path:gsub('(.\\)\\+', '%1') - elseif state.os == 'windows' then path = path:gsub('\\\\+', '\\') - else path = path:gsub('//+', '/') end - - return path -end - --- Check if path is a protocol, such as `http://...`. ----@param path string -function is_protocol(path) - return type(path) == 'string' and (path:find('^%a[%a%d-_]+://') ~= nil or path:find('^%a[%a%d-_]+:\\?') ~= nil) -end - ----@param path string ----@param extensions string[] Lowercase extensions without the dot. -function has_any_extension(path, extensions) - local path_last_dot_index = string_last_index_of(path, '.') - if not path_last_dot_index then return false end - local path_extension = path:sub(path_last_dot_index + 1):lower() - for _, extension in ipairs(extensions) do - if path_extension == extension then return true end - end - return false -end - ----@return string -function get_default_directory() - return mp.command_native({'expand-path', options.default_directory}) -end - --- Serializes path into its semantic parts. ----@param path string ----@return nil|{path: string; is_root: boolean; dirname?: string; basename: string; filename: string; extension?: string;} -function serialize_path(path) - if not path or is_protocol(path) then return end - - local normal_path = normalize_path_lite(path) - local dirname, basename = utils.split_path(normal_path) - if basename == '' then basename, dirname = dirname:sub(1, #dirname - 1), nil end - local dot_i = string_last_index_of(basename, '.') - - return { - path = normal_path, - is_root = dirname == nil, - dirname = dirname, - basename = basename, - filename = dot_i and basename:sub(1, dot_i - 1) or basename, - extension = dot_i and basename:sub(dot_i + 1) or nil, - } -end - --- Reads items in directory and splits it into directories and files tables. ----@param path string ----@param allowed_types? string[] Filter `files` table to contain only files with these extensions. ----@return string[]|nil files ----@return string[]|nil directories -function read_directory(path, allowed_types) - local items, error = utils.readdir(path, 'all') - - if not items then - msg.error('Reading files from "' .. path .. '" failed: ' .. error) - return nil, nil - end - - local files, directories = {}, {} - - for _, item in ipairs(items) do - if item ~= '.' and item ~= '..' then - local info = utils.file_info(join_path(path, item)) - if info then - if info.is_file then - if not allowed_types or has_any_extension(item, allowed_types) then - files[#files + 1] = item - end - else directories[#directories + 1] = item end - end - end - end - - return files, directories -end - --- Returns full absolute paths of files in the same directory as file_path, --- and index of the current file in the table. ----@param file_path string ----@param allowed_types? string[] -function get_adjacent_files(file_path, allowed_types) - local current_file = serialize_path(file_path) - if not current_file then return end - local files = read_directory(current_file.dirname, allowed_types) - if not files then return end - sort_filenames(files) - local current_file_index - local paths = {} - for index, file in ipairs(files) do - paths[#paths + 1] = join_path(current_file.dirname, file) - if current_file.basename == file then current_file_index = index end - end - if not current_file_index then return end - return paths, current_file_index -end - --- Navigates in a list, using delta or, when `state.shuffle` is enabled, --- randomness to determine the next item. Loops around if `loop-playlist` is enabled. ----@param list table ----@param current_index number ----@param delta number -function decide_navigation_in_list(list, current_index, delta) - if #list < 2 then return #list, list[#list] end - - if state.shuffle then - local new_index = current_index - math.randomseed(os.time()) - while current_index == new_index do new_index = math.random(#list) end - return new_index, list[new_index] - end - - local new_index = current_index + delta - if mp.get_property_native('loop-playlist') then - if new_index > #list then new_index = new_index % #list - elseif new_index < 1 then new_index = #list - new_index end - elseif new_index < 1 or new_index > #list then - return - end - - return new_index, list[new_index] -end - ----@param delta number -function navigate_directory(delta) - if not state.path or is_protocol(state.path) then return false end - local paths, current_index = get_adjacent_files(state.path, config.media_types) - if paths and current_index then - local _, path = decide_navigation_in_list(paths, current_index, delta) - if path then mp.commandv('loadfile', path) return true end - end - return false -end - ----@param delta number -function navigate_playlist(delta) - local playlist, pos = mp.get_property_native('playlist'), mp.get_property_native('playlist-pos-1') - if playlist and #playlist > 1 and pos then - local index = decide_navigation_in_list(playlist, pos, delta) - if index then mp.commandv('playlist-play-index', index - 1) return true end - end - return false -end - ----@param delta number -function navigate_item(delta) - if state.has_playlist then return navigate_playlist(delta) else return navigate_directory(delta) end -end - --- Can't use `os.remove()` as it fails on paths with unicode characters. --- Returns `result, error`, result is table of: --- `status:number(<0=error), stdout, stderr, error_string, killed_by_us:boolean` ----@param path string -function delete_file(path) - local args = state.os == 'windows' and {'cmd', '/C', 'del', path} or {'rm', path} - return mp.command_native({ - name = 'subprocess', - args = args, - playback_only = false, - capture_stdout = true, - capture_stderr = true, - }) -end - -function serialize_chapter_ranges(normalized_chapters) - local ranges = {} - local simple_ranges = { - {name = 'openings', patterns = {'^op ', '^op$', ' op$', 'opening$'}, requires_next_chapter = true}, - {name = 'intros', patterns = {'^intro$'}, requires_next_chapter = true}, - {name = 'endings', patterns = {'^ed ', '^ed$', ' ed$', 'ending$', 'closing$'}}, - {name = 'outros', patterns = {'^outro$'}}, - } - local sponsor_ranges = {} - - -- Extend with alt patterns - for _, meta in ipairs(simple_ranges) do - local alt_patterns = config.chapter_ranges[meta.name] and config.chapter_ranges[meta.name].patterns - if alt_patterns then meta.patterns = itable_join(meta.patterns, alt_patterns) end - end - - -- Clone chapters - local chapters = {} - for i, normalized in ipairs(normalized_chapters) do chapters[i] = table_shallow_copy(normalized) end - - for i, chapter in ipairs(chapters) do - -- Simple ranges - for _, meta in ipairs(simple_ranges) do - if config.chapter_ranges[meta.name] then - local match = itable_find(meta.patterns, function(p) return chapter.lowercase_title:find(p) end) - if match then - local next_chapter = chapters[i + 1] - if next_chapter or not meta.requires_next_chapter then - ranges[#ranges + 1] = table_assign({ - start = chapter.time, - ['end'] = next_chapter and next_chapter.time or infinity, - }, config.chapter_ranges[meta.name]) - end - end - end - end - - -- Sponsor blocks - if config.chapter_ranges.ads then - local id = chapter.lowercase_title:match('segment start *%(([%w]%w-)%)') - if id then -- ad range from sponsorblock - for j = i + 1, #chapters, 1 do - local end_chapter = chapters[j] - local end_match = end_chapter.lowercase_title:match('segment end *%(' .. id .. '%)') - if end_match then - local range = table_assign({ - start_chapter = chapter, end_chapter = end_chapter, - start = chapter.time, ['end'] = end_chapter.time, - }, config.chapter_ranges.ads) - ranges[#ranges + 1], sponsor_ranges[#sponsor_ranges + 1] = range, range - end_chapter.is_end_only = true - break - end - end -- single chapter for ad - elseif not chapter.is_end_only and - (chapter.lowercase_title:find('%[sponsorblock%]:') or chapter.lowercase_title:find('^sponsors?')) then - local next_chapter = chapters[i + 1] - ranges[#ranges + 1] = table_assign({ - start = chapter.time, - ['end'] = next_chapter and next_chapter.time or infinity, - }, config.chapter_ranges.ads) - end - end - end - - -- Fix overlapping sponsor block segments - for index, range in ipairs(sponsor_ranges) do - local next_range = sponsor_ranges[index + 1] - if next_range then - local delta = next_range.start - range['end'] - if delta < 0 then - local mid_point = range['end'] + delta / 2 - range['end'], range.end_chapter.time = mid_point - 0.01, mid_point - 0.01 - next_range.start, next_range.start_chapter.time = mid_point, mid_point - end - end - end - table.sort(chapters, function(a, b) return a.time < b.time end) - - return chapters, ranges -end - --- Ensures chapters are in chronological order -function normalize_chapters(chapters) - if not chapters then return {} end - -- Ensure chronological order - table.sort(chapters, function(a, b) return a.time < b.time end) - -- Ensure titles - for index, chapter in ipairs(chapters) do - chapter.title = chapter.title or ('Chapter ' .. index) - chapter.lowercase_title = chapter.title:lower() - end - return chapters -end - -function serialize_chapters(chapters) - chapters = normalize_chapters(chapters) - if not chapters then return end - --- timeline font size isn't accessible here, so normalize to size 1 and then scale during rendering - local opts = {size = 1, bold = true} - for index, chapter in ipairs(chapters) do - chapter.index = index - chapter.title_wrapped = wrap_text(chapter.title, opts, 25) - chapter.title_wrapped_width = text_width(chapter.title_wrapped, opts) - chapter.title_wrapped = ass_escape(chapter.title_wrapped) - end - return chapters -end - ---[[ RENDERING ]] - -function render() - if not display.initialized then return end - state.render_last_time = mp.get_time() - - -- Actual rendering - local ass = assdraw.ass_new() - - for _, element in Elements:ipairs() do - if element.enabled then - local result = element:maybe('render') - if result then - ass:new_event() - ass:merge(result) - end - end - end - - -- submit - if osd.res_x == display.width and osd.res_y == display.height and osd.data == ass.text then - return - end - - osd.res_x = display.width - osd.res_y = display.height - osd.data = ass.text - osd.z = 2000 - osd:update() - - update_margins() -end - --- Request that render() is called. --- The render is then either executed immediately, or rate-limited if it was --- called a small time ago. -state.render_timer = mp.add_timeout(0, render) -state.render_timer:kill() -function request_render() - if state.render_timer:is_enabled() then return end - local timeout = math.max(0, state.render_delay - (mp.get_time() - state.render_last_time)) - state.render_timer.timeout = timeout - state.render_timer:resume() -end diff --git a/.config/mpv/scripts/uosc_shared/main.lua b/.config/mpv/scripts/uosc_shared/main.lua deleted file mode 100644 index 323027f..0000000 --- a/.config/mpv/scripts/uosc_shared/main.lua +++ /dev/null @@ -1,5 +0,0 @@ ---[[ -File required for compatibility between mpv: -- 0.32 - doesn't support `dir/main.lua`, so we need `uosc.lua` in root -- 0.33 - requires `main.lua` in directories -]] diff --git a/.config/openbox/rc.xml b/.config/openbox/rc.xml index 6ba6fc9..442384b 100755 --- a/.config/openbox/rc.xml +++ b/.config/openbox/rc.xml @@ -410,12 +410,12 @@ </keybind> <keybind key="W-F3"> <action name="Execute"> - <command>librewolf</command> + <command>firefox</command> </action> </keybind> <keybind key="W-S-F3"> <action name="Execute"> - <command>torify iridium-browser --force-dark-mode</command> + <command>torify librewolf --force-dark-mode</command> </action> </keybind> <keybind key="W-F4"> @@ -978,7 +978,7 @@ # order that they appear in this list - # each rule element can be left out or set to 'default' to specify to not + # each rule element can be left out or set to 'default' to specify to not # change that attribute of the window <decor>yes</decor> diff --git a/.local/bin/kb b/.local/bin/kb index 6391df1..29ba370 100755 --- a/.local/bin/kb +++ b/.local/bin/kb @@ -47,7 +47,7 @@ xset r rate 200 150 setxkbmap -option setxkbmap -layout us -variant intl -if [ "$(hostname -s)" = "motherqwe" ]; then +if [ "$(hostname -s)" = "mother" ]; then setxkbmap -option ctrl:swapcaps setxkbmap -option caps:none xmodmap -e 'keycode 9 = Tab' |