├── Material-Design-Iconic-Font.ttf ├── README.md ├── modernx.lua └── preview.png /Material-Design-Iconic-Font.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyl0/ModernX/3f2ed6b993059c6986bf34be3998048c50547187/Material-Design-Iconic-Font.ttf -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ModernX 2 | An MPV OSC script based on [mpv-osc-modern](https://github.com/maoiscat/mpv-osc-modern/) that aims to mirror the functionality of MPV's stock OSC while with a more modern-looking interface. 3 | 4 | ![img](https://github.com/cyl0/ModernX/blob/main/preview.png) 5 | 6 | # How to install 7 | 8 | Locate your MPV folder. It is typically located at `\%APPDATA%\mpv\` on Windows and `~/.config/mpv/` on Linux/MacOS. See the [Files section](https://mpv.io/manual/master/#files) in mpv's manual for more info. 9 | 10 | Put mordenx.lua into your mpv "\~\~/scripts/" folder. Create the "\~\~/scripts/" folder if you don't already have one and remove any other OSC scripts, 11 | then put `Material-Design-Iconic-Font.ttf` in the "\~\~/fonts" folder. 12 | 13 | in mpv.conf: 14 | 15 | ``` 16 | osc = no 17 | border = no # Optional, but recommended 18 | ``` 19 | `Material-Design-Iconic-Font.ttf` can also be downloaded from [here](https://zavoloklom.github.io/material-design-iconic-font/). 20 | 21 | # How to config 22 | 23 | edit osc.conf in "\~\~/script-opts/" folder, however many options are changed, so refer to the user_opts variable in the script file for details. 24 | 25 | # Thumbnails 26 | 27 | To enable thumbnails in timeline, install [thumbfast](https://github.com/po5/thumbfast). No other step necessary. 28 | 29 | # Buttons 30 | 31 | like the built-in script, some buttons may accept multiple mouse actions, here is a list: 32 | 33 | ## Seekbar 34 | * Left mouse button: seek to chosen position. 35 | * Right mouse button: seek to the head of chosen chapter 36 | ## Playlist back/forward buttons 37 | * Left mouse button: play previous/next file. 38 | * Right mouse button: show playlist. 39 | ## Skip back/forward buttons 40 | * Left mouse button: go to previous/next chapter. 41 | * Right mouse button: show chapter list. 42 | ## Jump back/forward buttons 43 | * Left mouse button: Jumps forwards/backwards by 5 seconds, or by the amount set in `user_opts`. 44 | * Right mouse button: Jumps forwards/backwards by 1 minute. 45 | * Shift + Left mouse button: Skips to the previous/next frame. 46 | ## Cycle audio/subtitle buttons 47 | * Left mouse button/Right mouse button: cycle to next/previous track. 48 | * Middle mouse button: show track list. 49 | ## Playback time 50 | * Left mouse button: display time in milliseconds 51 | ## Duration 52 | * Left mouse button: display total time instead of remaining time 53 | -------------------------------------------------------------------------------- /modernx.lua: -------------------------------------------------------------------------------- 1 | -- mpv-osc-modern by maoiscat 2 | -- email:valarmor@163.com 3 | -- https://github.com/maoiscat/mpv-osc-modern 4 | 5 | -- fork by cyl0 6 | -- https://github.com/cyl0/ModernX/ 7 | 8 | local assdraw = require 'mp.assdraw' 9 | local msg = require 'mp.msg' 10 | local opt = require 'mp.options' 11 | local utils = require 'mp.utils' 12 | 13 | -- 14 | -- Parameters 15 | -- 16 | -- default user option values 17 | -- may change them in osc.conf 18 | local user_opts = { 19 | showwindowed = true, -- show OSC when windowed? 20 | showfullscreen = true, -- show OSC when fullscreen? 21 | idlescreen = true, -- draw logo and text when idle 22 | scalewindowed = 1.0, -- scaling of the controller when windowed 23 | scalefullscreen = 1.0, -- scaling of the controller when fullscreen 24 | scaleforcedwindow = 2.0, -- scaling when rendered on a forced window 25 | vidscale = true, -- scale the controller with the video? 26 | hidetimeout = 1500, -- duration in ms until the OSC hides if no 27 | -- mouse movement. enforced non-negative for the 28 | -- user, but internally negative is 'always-on'. 29 | fadeduration = 250, -- duration of fade out in ms, 0 = no fade 30 | minmousemove = 1, -- minimum amount of pixels the mouse has to 31 | -- move between ticks to make the OSC show up 32 | iamaprogrammer = false, -- use native mpv values and disable OSC 33 | -- internal track list management (and some 34 | -- functions that depend on it) 35 | font = 'mpv-osd-symbols', -- default osc font 36 | seekbarhandlesize = 1.0, -- size ratio of the slider handle, range 0 ~ 1 37 | seekrange = true, -- show seekrange overlay 38 | seekrangealpha = 64, -- transparency of seekranges 39 | seekbarkeyframes = true, -- use keyframes when dragging the seekbar 40 | showjump = true, -- show "jump forward/backward 5 seconds" buttons 41 | -- shift+left-click to step 1 frame and 42 | -- right-click to jump 1 minute 43 | jumpamount = 5, -- change the jump amount (in seconds by default) 44 | jumpiconnumber = true, -- show different icon when jumpamount is 5, 10, or 30 45 | jumpmode = 'exact', -- seek mode for jump buttons. e.g. 46 | -- 'exact', 'relative+keyframes', etc. 47 | title = '${media-title}', -- string compatible with property-expansion 48 | -- to be shown as OSC title 49 | showtitle = true, -- show title in OSC 50 | showonpause = true, -- whether to disable the hide timeout on pause 51 | timetotal = true, -- display total time instead of remaining time? 52 | timems = false, -- Display time down to millliseconds by default 53 | visibility = 'auto', -- only used at init to set visibility_mode(...) 54 | windowcontrols = 'auto', -- whether to show window controls 55 | greenandgrumpy = false, -- disable santa hat 56 | language = 'eng', -- eng=English, chs=Chinese 57 | volumecontrol = true, -- whether to show mute button and volume slider 58 | keyboardnavigation = false, -- enable directional keyboard navigation 59 | chapter_fmt = "Chapter: %s", -- chapter print format for seekbar-hover. "no" to disable 60 | } 61 | 62 | -- Icons for jump button depending on jumpamount 63 | local jumpicons = { 64 | [5] = {'\239\142\177', '\239\142\163'}, 65 | [10] = {'\239\142\175', '\239\142\161'}, 66 | [30] = {'\239\142\176', '\239\142\162'}, 67 | default = {'\239\142\178 ', '\239\142\178'}, -- second icon is mirrored in layout() 68 | } 69 | 70 | local icons = { 71 | previous = '\239\142\181', 72 | next = '\239\142\180', 73 | play = '\239\142\170', 74 | pause = '\239\142\167', 75 | backward = '\239\142\160', 76 | forward = '\239\142\159', 77 | audio = '\239\142\183', 78 | volume = '\239\142\188', 79 | volume_mute = '\239\142\187', 80 | sub = '\239\143\147', 81 | minimize = '\239\133\172', 82 | fullscreen = '\239\133\173', 83 | info = '', 84 | } 85 | 86 | -- Localization 87 | local language = { 88 | ['eng'] = { 89 | welcome = '{\\fs24\\1c&H0&\\1c&HFFFFFF&}Drop files or URLs to play here.', -- this text appears when mpv starts 90 | off = 'OFF', 91 | na = 'n/a', 92 | none = 'none', 93 | video = 'Video', 94 | audio = 'Audio', 95 | subtitle = 'Subtitle', 96 | available = 'Available ', 97 | track = ' Tracks:', 98 | playlist = 'Playlist', 99 | nolist = 'Empty playlist.', 100 | chapter = 'Chapter', 101 | nochapter = 'No chapters.', 102 | }, 103 | ['chs'] = { 104 | welcome = '{\\1c&H00\\bord0\\fs30\\fn微软雅黑 light\\fscx125}MPV{\\fscx100} 播放器', -- this text appears when mpv starts 105 | off = '关闭', 106 | na = 'n/a', 107 | none = '无', 108 | video = '视频', 109 | audio = '音频', 110 | subtitle = '字幕', 111 | available = '可选', 112 | track = ':', 113 | playlist = '播放列表', 114 | nolist = '无列表信息', 115 | chapter = '章节', 116 | nochapter = '无章节信息', 117 | }, 118 | ['pl'] = { 119 | welcome = '{\\fs24\\1c&H0&\\1c&HFFFFFF&}Upuść plik lub łącze URL do odtworzenia.', -- this text appears when mpv starts 120 | off = 'WYŁ.', 121 | na = 'n/a', 122 | none = 'nic', 123 | video = 'Wideo', 124 | audio = 'Ścieżka audio', 125 | subtitle = 'Napisy', 126 | available = 'Dostępne ', 127 | track = ' Ścieżki:', 128 | playlist = 'Lista odtwarzania', 129 | nolist = 'Lista odtwarzania pusta.', 130 | chapter = 'Rozdział', 131 | nochapter = 'Brak rozdziałów.', 132 | } 133 | } 134 | -- read options from config and command-line 135 | opt.read_options(user_opts, 'osc', function(list) update_options(list) end) 136 | -- apply lang opts 137 | local texts = language[user_opts.language] 138 | local osc_param = { -- calculated by osc_init() 139 | playresy = 0, -- canvas size Y 140 | playresx = 0, -- canvas size X 141 | display_aspect = 1, 142 | unscaled_y = 0, 143 | areas = {}, 144 | } 145 | 146 | local osc_styles = { 147 | TransBg = '{\\blur100\\bord150\\1c&H000000&\\3c&H000000&}', 148 | SeekbarBg = '{\\blur0\\bord0\\1c&HFFFFFF&}', 149 | SeekbarFg = '{\\blur1\\bord1\\1c&HE39C42&}', 150 | VolumebarBg = '{\\blur0\\bord0\\1c&H999999&}', 151 | VolumebarFg = '{\\blur1\\bord1\\1c&HFFFFFF&}', 152 | Ctrl1 = '{\\blur0\\bord0\\1c&HFFFFFF&\\3c&HFFFFFF&\\fs36\\fnmaterial-design-iconic-font}', 153 | Ctrl2 = '{\\blur0\\bord0\\1c&HFFFFFF&\\3c&HFFFFFF&\\fs24\\fnmaterial-design-iconic-font}', 154 | Ctrl2Flip = '{\\blur0\\bord0\\1c&HFFFFFF&\\3c&HFFFFFF&\\fs24\\fnmaterial-design-iconic-font\\fry180', 155 | Ctrl3 = '{\\blur0\\bord0\\1c&HFFFFFF&\\3c&HFFFFFF&\\fs24\\fnmaterial-design-iconic-font}', 156 | Time = '{\\blur0\\bord0\\1c&HFFFFFF&\\3c&H000000&\\fs17\\fn' .. user_opts.font .. '}', 157 | Tooltip = '{\\blur1\\bord0.5\\1c&HFFFFFF&\\3c&H000000&\\fs18\\fn' .. user_opts.font .. '}', 158 | Title = '{\\blur1\\bord0.5\\1c&HFFFFFF&\\3c&H0\\fs38\\q2\\fn' .. user_opts.font .. '}', 159 | WinCtrl = '{\\blur1\\bord0.5\\1c&HFFFFFF&\\3c&H0\\fs20\\fnmpv-osd-symbols}', 160 | elementDown = '{\\1c&H999999&}', 161 | elementHighlight = '{\\blur1\\bord1\\1c&HFFC033&}', 162 | } 163 | 164 | -- internal states, do not touch 165 | local state = { 166 | showtime, -- time of last invocation (last mouse move) 167 | osc_visible = false, 168 | anistart, -- time when the animation started 169 | anitype, -- current type of animation 170 | animation, -- current animation alpha 171 | mouse_down_counter = 0, -- used for softrepeat 172 | active_element = nil, -- nil = none, 0 = background, 1+ = see elements[] 173 | active_event_source = nil, -- the 'button' that issued the current event 174 | rightTC_trem = not user_opts.timetotal, -- if the right timecode should display total or remaining time 175 | mp_screen_sizeX, mp_screen_sizeY, -- last screen-resolution, to detect resolution changes to issue reINITs 176 | initREQ = false, -- is a re-init request pending? 177 | last_mouseX, last_mouseY, -- last mouse position, to detect significant mouse movement 178 | mouse_in_window = false, 179 | message_text, 180 | message_hide_timer, 181 | fullscreen = false, 182 | tick_timer = nil, 183 | tick_last_time = 0, -- when the last tick() was run 184 | hide_timer = nil, 185 | cache_state = nil, 186 | idle = false, 187 | enabled = true, 188 | input_enabled = true, 189 | showhide_enabled = false, 190 | dmx_cache = 0, 191 | border = true, 192 | maximized = false, 193 | osd = mp.create_osd_overlay('ass-events'), 194 | mute = false, 195 | lastvisibility = user_opts.visibility, -- save last visibility on pause if showonpause 196 | fulltime = user_opts.timems, 197 | highlight_element = 'cy_audio', 198 | chapter_list = {}, -- sorted by time 199 | } 200 | 201 | local thumbfast = { 202 | width = 0, 203 | height = 0, 204 | disabled = true, 205 | available = false 206 | } 207 | 208 | local window_control_box_width = 138 209 | local tick_delay = 0.03 210 | 211 | local is_december = os.date("*t").month == 12 212 | 213 | --- Automatically disable OSC 214 | local builtin_osc_enabled = mp.get_property_native('osc') 215 | if builtin_osc_enabled then 216 | mp.set_property_native('osc', false) 217 | end 218 | 219 | -- 220 | 221 | 222 | -- WindowControl helpers 223 | function window_controls_enabled() 224 | val = user_opts.windowcontrols 225 | if val == 'auto' then 226 | return (not state.border) or state.fullscreen 227 | else 228 | return val ~= 'no' 229 | end 230 | end 231 | 232 | 233 | 234 | function build_keyboard_controls() 235 | 236 | -- prepare the main button row 237 | local bottom_button_line = {} 238 | table.insert(bottom_button_line, 'cy_audio') 239 | table.insert(bottom_button_line, 'cy_sub') 240 | table.insert(bottom_button_line, 'pl_prev') 241 | table.insert(bottom_button_line, 'skipback') 242 | if user_opts.showjump then 243 | table.insert(bottom_button_line, 'jumpback') 244 | end 245 | table.insert(bottom_button_line, 'playpause') 246 | if user_opts.showjump then 247 | table.insert(bottom_button_line, 'jumpfrwd') 248 | end 249 | table.insert(bottom_button_line, 'skipfrwd') 250 | table.insert(bottom_button_line, 'pl_next') 251 | table.insert(bottom_button_line, 'tog_info') 252 | table.insert(bottom_button_line, 'tog_fs') 253 | 254 | -- build up the main mapping object 255 | local mapping = {} 256 | if window_controls_enabled() then 257 | table.insert(mapping, { 258 | 'minimize', 259 | 'maximize', 260 | 'close' 261 | }) 262 | end 263 | table.insert(mapping, { 264 | 'seekbar' 265 | }) 266 | table.insert(mapping, bottom_button_line) 267 | 268 | return mapping 269 | end 270 | 271 | 272 | -- 273 | -- Helperfunctions 274 | -- 275 | 276 | function set_osd(res_x, res_y, text) 277 | if state.osd.res_x == res_x and 278 | state.osd.res_y == res_y and 279 | state.osd.data == text then 280 | return 281 | end 282 | state.osd.res_x = res_x 283 | state.osd.res_y = res_y 284 | state.osd.data = text 285 | state.osd.z = 1000 286 | state.osd:update() 287 | end 288 | 289 | -- scale factor for translating between real and virtual ASS coordinates 290 | function get_virt_scale_factor() 291 | local w, h = mp.get_osd_size() 292 | if w <= 0 or h <= 0 then 293 | return 0, 0 294 | end 295 | return osc_param.playresx / w, osc_param.playresy / h 296 | end 297 | 298 | -- return mouse position in virtual ASS coordinates (playresx/y) 299 | function get_virt_mouse_pos() 300 | if state.mouse_in_window then 301 | local sx, sy = get_virt_scale_factor() 302 | local x, y = mp.get_mouse_pos() 303 | return x * sx, y * sy 304 | else 305 | return -1, -1 306 | end 307 | end 308 | 309 | function set_virt_mouse_area(x0, y0, x1, y1, name) 310 | local sx, sy = get_virt_scale_factor() 311 | mp.set_mouse_area(x0 / sx, y0 / sy, x1 / sx, y1 / sy, name) 312 | end 313 | 314 | function scale_value(x0, x1, y0, y1, val) 315 | local m = (y1 - y0) / (x1 - x0) 316 | local b = y0 - (m * x0) 317 | return (m * val) + b 318 | end 319 | 320 | -- returns hitbox spanning coordinates (top left, bottom right corner) 321 | -- according to alignment 322 | function get_hitbox_coords(x, y, an, w, h) 323 | 324 | local alignments = { 325 | [1] = function () return x, y-h, x+w, y end, 326 | [2] = function () return x-(w/2), y-h, x+(w/2), y end, 327 | [3] = function () return x-w, y-h, x, y end, 328 | 329 | [4] = function () return x, y-(h/2), x+w, y+(h/2) end, 330 | [5] = function () return x-(w/2), y-(h/2), x+(w/2), y+(h/2) end, 331 | [6] = function () return x-w, y-(h/2), x, y+(h/2) end, 332 | 333 | [7] = function () return x, y, x+w, y+h end, 334 | [8] = function () return x-(w/2), y, x+(w/2), y+h end, 335 | [9] = function () return x-w, y, x, y+h end, 336 | } 337 | 338 | return alignments[an]() 339 | end 340 | 341 | function get_hitbox_coords_geo(geometry) 342 | return get_hitbox_coords(geometry.x, geometry.y, geometry.an, 343 | geometry.w, geometry.h) 344 | end 345 | 346 | function get_element_hitbox(element) 347 | return element.hitbox.x1, element.hitbox.y1, 348 | element.hitbox.x2, element.hitbox.y2 349 | end 350 | 351 | function mouse_hit(element) 352 | return mouse_hit_coords(get_element_hitbox(element)) 353 | end 354 | 355 | function mouse_hit_coords(bX1, bY1, bX2, bY2) 356 | local mX, mY = get_virt_mouse_pos() 357 | return (mX >= bX1 and mX <= bX2 and mY >= bY1 and mY <= bY2) 358 | end 359 | 360 | function limit_range(min, max, val) 361 | if val > max then 362 | val = max 363 | elseif val < min then 364 | val = min 365 | end 366 | return val 367 | end 368 | 369 | -- translate value into element coordinates 370 | function get_slider_ele_pos_for(element, val) 371 | 372 | local ele_pos = scale_value( 373 | element.slider.min.value, element.slider.max.value, 374 | element.slider.min.ele_pos, element.slider.max.ele_pos, 375 | val) 376 | 377 | return limit_range( 378 | element.slider.min.ele_pos, element.slider.max.ele_pos, 379 | ele_pos) 380 | end 381 | 382 | -- translates global (mouse) coordinates to value 383 | function get_slider_value_at(element, glob_pos) 384 | 385 | local val = scale_value( 386 | element.slider.min.glob_pos, element.slider.max.glob_pos, 387 | element.slider.min.value, element.slider.max.value, 388 | glob_pos) 389 | 390 | return limit_range( 391 | element.slider.min.value, element.slider.max.value, 392 | val) 393 | end 394 | 395 | -- get value at current mouse position 396 | function get_slider_value(element) 397 | return get_slider_value_at(element, get_virt_mouse_pos()) 398 | end 399 | 400 | function countone(val) 401 | if not (user_opts.iamaprogrammer) then 402 | val = val + 1 403 | end 404 | return val 405 | end 406 | 407 | -- multiplies two alpha values, formular can probably be improved 408 | function mult_alpha(alphaA, alphaB) 409 | return 255 - (((1-(alphaA/255)) * (1-(alphaB/255))) * 255) 410 | end 411 | 412 | function add_area(name, x1, y1, x2, y2) 413 | -- create area if needed 414 | if (osc_param.areas[name] == nil) then 415 | osc_param.areas[name] = {} 416 | end 417 | table.insert(osc_param.areas[name], {x1=x1, y1=y1, x2=x2, y2=y2}) 418 | end 419 | 420 | function ass_append_alpha(ass, alpha, modifier) 421 | local ar = {} 422 | 423 | for ai, av in pairs(alpha) do 424 | av = mult_alpha(av, modifier) 425 | if state.animation then 426 | av = mult_alpha(av, state.animation) 427 | end 428 | ar[ai] = av 429 | end 430 | 431 | ass:append(string.format('{\\1a&H%X&\\2a&H%X&\\3a&H%X&\\4a&H%X&}', 432 | ar[1], ar[2], ar[3], ar[4])) 433 | end 434 | 435 | function ass_draw_cir_cw(ass, x, y, r) 436 | ass:round_rect_cw(x-r, y-r, x+r, y+r, r) 437 | end 438 | 439 | function ass_draw_rr_h_cw(ass, x0, y0, x1, y1, r1, hexagon, r2) 440 | if hexagon then 441 | ass:hexagon_cw(x0, y0, x1, y1, r1, r2) 442 | else 443 | ass:round_rect_cw(x0, y0, x1, y1, r1, r2) 444 | end 445 | end 446 | 447 | function ass_draw_rr_h_ccw(ass, x0, y0, x1, y1, r1, hexagon, r2) 448 | if hexagon then 449 | ass:hexagon_ccw(x0, y0, x1, y1, r1, r2) 450 | else 451 | ass:round_rect_ccw(x0, y0, x1, y1, r1, r2) 452 | end 453 | end 454 | 455 | 456 | -- 457 | -- Tracklist Management 458 | -- 459 | 460 | local nicetypes = {video = texts.video, audio = texts.audio, sub = texts.subtitle} 461 | 462 | -- updates the OSC internal playlists, should be run each time the track-layout changes 463 | function update_tracklist() 464 | local tracktable = mp.get_property_native('track-list', {}) 465 | 466 | -- by osc_id 467 | tracks_osc = {} 468 | tracks_osc.video, tracks_osc.audio, tracks_osc.sub = {}, {}, {} 469 | -- by mpv_id 470 | tracks_mpv = {} 471 | tracks_mpv.video, tracks_mpv.audio, tracks_mpv.sub = {}, {}, {} 472 | for n = 1, #tracktable do 473 | if not (tracktable[n].type == 'unknown') then 474 | local type = tracktable[n].type 475 | local mpv_id = tonumber(tracktable[n].id) 476 | 477 | -- by osc_id 478 | table.insert(tracks_osc[type], tracktable[n]) 479 | 480 | -- by mpv_id 481 | tracks_mpv[type][mpv_id] = tracktable[n] 482 | tracks_mpv[type][mpv_id].osc_id = #tracks_osc[type] 483 | end 484 | end 485 | end 486 | 487 | -- return a nice list of tracks of the given type (video, audio, sub) 488 | function get_tracklist(type) 489 | local msg = texts.available .. nicetypes[type] .. texts.track 490 | if #tracks_osc[type] == 0 then 491 | msg = msg .. texts.none 492 | else 493 | for n = 1, #tracks_osc[type] do 494 | local track = tracks_osc[type][n] 495 | local lang, title, selected = 'unknown', '', '○' 496 | if not(track.lang == nil) then lang = track.lang end 497 | if not(track.title == nil) then title = track.title end 498 | if (track.id == tonumber(mp.get_property(type))) then 499 | selected = '●' 500 | end 501 | msg = msg..'\n'..selected..' '..n..': ['..lang..'] '..title 502 | end 503 | end 504 | return msg 505 | end 506 | 507 | -- relatively change the track of given by tracks 508 | --(+1 -> next, -1 -> previous) 509 | function set_track(type, next) 510 | local current_track_mpv, current_track_osc 511 | if (mp.get_property(type) == 'no') then 512 | current_track_osc = 0 513 | else 514 | current_track_mpv = tonumber(mp.get_property(type)) 515 | current_track_osc = tracks_mpv[type][current_track_mpv].osc_id 516 | end 517 | local new_track_osc = (current_track_osc + next) % (#tracks_osc[type] + 1) 518 | local new_track_mpv 519 | if new_track_osc == 0 then 520 | new_track_mpv = 'no' 521 | else 522 | new_track_mpv = tracks_osc[type][new_track_osc].id 523 | end 524 | 525 | mp.commandv('set', type, new_track_mpv) 526 | 527 | -- if (new_track_osc == 0) then 528 | -- show_message(nicetypes[type] .. ' Track: none') 529 | -- else 530 | -- show_message(nicetypes[type] .. ' Track: ' 531 | -- .. new_track_osc .. '/' .. #tracks_osc[type] 532 | -- .. ' ['.. (tracks_osc[type][new_track_osc].lang or 'unknown') ..'] ' 533 | -- .. (tracks_osc[type][new_track_osc].title or '')) 534 | -- end 535 | end 536 | 537 | -- get the currently selected track of , OSC-style counted 538 | function get_track(type) 539 | local track = mp.get_property(type) 540 | if track ~= 'no' and track ~= nil then 541 | local tr = tracks_mpv[type][tonumber(track)] 542 | if tr then 543 | return tr.osc_id 544 | end 545 | end 546 | return 0 547 | end 548 | 549 | -- 550 | -- Element Management 551 | -- 552 | 553 | local elements = {} 554 | 555 | function prepare_elements() 556 | 557 | -- remove elements without layout or invisble 558 | local elements2 = {} 559 | for n, element in pairs(elements) do 560 | if not (element.layout == nil) and (element.visible) then 561 | table.insert(elements2, element) 562 | end 563 | end 564 | elements = elements2 565 | 566 | function elem_compare (a, b) 567 | return a.layout.layer < b.layout.layer 568 | end 569 | 570 | table.sort(elements, elem_compare) 571 | 572 | 573 | for _,element in pairs(elements) do 574 | 575 | local elem_geo = element.layout.geometry 576 | 577 | -- Calculate the hitbox 578 | local bX1, bY1, bX2, bY2 = get_hitbox_coords_geo(elem_geo) 579 | element.hitbox = {x1 = bX1, y1 = bY1, x2 = bX2, y2 = bY2} 580 | 581 | local style_ass = assdraw.ass_new() 582 | 583 | -- prepare static elements 584 | style_ass:append('{}') -- hack to troll new_event into inserting a \n 585 | style_ass:new_event() 586 | style_ass:pos(elem_geo.x, elem_geo.y) 587 | style_ass:an(elem_geo.an) 588 | style_ass:append(element.layout.style) 589 | 590 | element.style_ass = style_ass 591 | 592 | local static_ass = assdraw.ass_new() 593 | 594 | 595 | if (element.type == 'box') then 596 | --draw box 597 | static_ass:draw_start() 598 | ass_draw_rr_h_cw(static_ass, 0, 0, elem_geo.w, elem_geo.h, 599 | element.layout.box.radius, element.layout.box.hexagon) 600 | static_ass:draw_stop() 601 | 602 | elseif (element.type == 'slider') then 603 | --draw static slider parts 604 | local slider_lo = element.layout.slider 605 | -- calculate positions of min and max points 606 | element.slider.min.ele_pos = user_opts.seekbarhandlesize * elem_geo.h / 2 607 | element.slider.max.ele_pos = elem_geo.w - element.slider.min.ele_pos 608 | element.slider.min.glob_pos = element.hitbox.x1 + element.slider.min.ele_pos 609 | element.slider.max.glob_pos = element.hitbox.x1 + element.slider.max.ele_pos 610 | 611 | static_ass:draw_start() 612 | -- a hack which prepares the whole slider area to allow center placements such like an=5 613 | static_ass:rect_cw(0, 0, elem_geo.w, elem_geo.h) 614 | static_ass:rect_ccw(0, 0, elem_geo.w, elem_geo.h) 615 | -- marker nibbles 616 | if not (element.slider.markerF == nil) and (slider_lo.gap > 0) then 617 | local markers = element.slider.markerF() 618 | for _,marker in pairs(markers) do 619 | if (marker >= element.slider.min.value) and (marker <= element.slider.max.value) then 620 | local s = get_slider_ele_pos_for(element, marker) 621 | if (slider_lo.gap > 5) then -- draw triangles 622 | --top 623 | if (slider_lo.nibbles_top) then 624 | static_ass:move_to(s - 3, slider_lo.gap - 5) 625 | static_ass:line_to(s + 3, slider_lo.gap - 5) 626 | static_ass:line_to(s, slider_lo.gap - 1) 627 | end 628 | --bottom 629 | if (slider_lo.nibbles_bottom) then 630 | static_ass:move_to(s - 3, elem_geo.h - slider_lo.gap + 5) 631 | static_ass:line_to(s, elem_geo.h - slider_lo.gap + 1) 632 | static_ass:line_to(s + 3, elem_geo.h - slider_lo.gap + 5) 633 | end 634 | else -- draw 2x1px nibbles 635 | --top 636 | if (slider_lo.nibbles_top) then 637 | static_ass:rect_cw(s - 1, 0, s + 1, slider_lo.gap); 638 | end 639 | --bottom 640 | if (slider_lo.nibbles_bottom) then 641 | static_ass:rect_cw(s - 1, elem_geo.h-slider_lo.gap, s + 1, elem_geo.h); 642 | end 643 | end 644 | end 645 | end 646 | end 647 | end 648 | 649 | element.static_ass = static_ass 650 | 651 | -- if the element is supposed to be disabled, 652 | -- style it accordingly and kill the eventresponders 653 | if not (element.enabled) then 654 | element.layout.alpha[1] = 136 655 | element.eventresponder = nil 656 | end 657 | -- gray out the element if it is toggled off 658 | if (element.off) then 659 | element.layout.alpha[1] = 136 660 | end 661 | 662 | end 663 | end 664 | 665 | -- 666 | -- Element Rendering 667 | -- 668 | 669 | -- returns nil or a chapter element from the native property chapter-list 670 | function get_chapter(possec) 671 | local cl = state.chapter_list -- sorted, get latest before possec, if any 672 | 673 | for n=#cl,1,-1 do 674 | if possec >= cl[n].time then 675 | return cl[n] 676 | end 677 | end 678 | end 679 | 680 | function render_elements(master_ass) 681 | -- when the slider is dragged or hovered and we have a target chapter name 682 | -- then we use it instead of the normal title. we calculate it before the 683 | -- render iterations because the title may be rendered before the slider. 684 | state.forced_title = nil 685 | if thumbfast.disabled then 686 | local se, ae = state.slider_element, elements[state.active_element] 687 | if user_opts.chapter_fmt ~= "no" and se and (ae == se or (not ae and mouse_hit(se))) then 688 | local dur = mp.get_property_number("duration", 0) 689 | if dur > 0 then 690 | local possec = get_slider_value(se) * dur / 100 -- of mouse pos 691 | local ch = get_chapter(possec) 692 | if ch and ch.title and ch.title ~= "" then 693 | state.forced_title = string.format(user_opts.chapter_fmt, ch.title) 694 | end 695 | end 696 | end 697 | end 698 | 699 | for n=1, #elements do 700 | local element = elements[n] 701 | local style_ass = assdraw.ass_new() 702 | style_ass:merge(element.style_ass) 703 | ass_append_alpha(style_ass, element.layout.alpha, 0) 704 | 705 | if element.eventresponder and (state.active_element == n) then 706 | -- run render event functions 707 | if not (element.eventresponder.render == nil) then 708 | element.eventresponder.render(element) 709 | end 710 | if mouse_hit(element) then 711 | -- mouse down styling 712 | if (element.styledown) then 713 | style_ass:append(osc_styles.elementDown) 714 | end 715 | if (element.softrepeat) and (state.mouse_down_counter >= 15 716 | and state.mouse_down_counter % 5 == 0) then 717 | 718 | element.eventresponder[state.active_event_source..'_down'](element) 719 | end 720 | state.mouse_down_counter = state.mouse_down_counter + 1 721 | end 722 | end 723 | 724 | if user_opts.keyboardnavigation and state.highlight_element == element.name then 725 | style_ass:append(osc_styles.elementHighlight) 726 | end 727 | 728 | local elem_ass = assdraw.ass_new() 729 | elem_ass:merge(style_ass) 730 | 731 | if not (element.type == 'button') then 732 | elem_ass:merge(element.static_ass) 733 | end 734 | 735 | if (element.type == 'slider') then 736 | 737 | local slider_lo = element.layout.slider 738 | local elem_geo = element.layout.geometry 739 | local s_min = element.slider.min.value 740 | local s_max = element.slider.max.value 741 | -- draw pos marker 742 | local pos = element.slider.posF() 743 | local seekRanges = element.slider.seekRangesF() 744 | local rh = user_opts.seekbarhandlesize * elem_geo.h / 2 -- Handle radius 745 | local xp 746 | 747 | if pos then 748 | xp = get_slider_ele_pos_for(element, pos) 749 | ass_draw_cir_cw(elem_ass, xp, elem_geo.h/2, rh) 750 | elem_ass:rect_cw(0, slider_lo.gap, xp, elem_geo.h - slider_lo.gap) 751 | end 752 | 753 | if seekRanges then 754 | elem_ass:draw_stop() 755 | elem_ass:merge(element.style_ass) 756 | ass_append_alpha(elem_ass, element.layout.alpha, user_opts.seekrangealpha) 757 | elem_ass:merge(element.static_ass) 758 | 759 | for _,range in pairs(seekRanges) do 760 | local pstart = get_slider_ele_pos_for(element, range['start']) 761 | local pend = get_slider_ele_pos_for(element, range['end']) 762 | elem_ass:rect_cw(pstart - rh, slider_lo.gap, pend + rh, elem_geo.h - slider_lo.gap) 763 | end 764 | end 765 | 766 | elem_ass:draw_stop() 767 | 768 | -- add tooltip 769 | if not (element.slider.tooltipF == nil) then 770 | if mouse_hit(element) then 771 | local sliderpos = get_slider_value(element) 772 | local tooltiplabel = element.slider.tooltipF(sliderpos) 773 | local an = slider_lo.tooltip_an 774 | local ty 775 | if (an == 2) then 776 | ty = element.hitbox.y1 777 | else 778 | ty = element.hitbox.y1 + elem_geo.h/2 779 | end 780 | 781 | local tx = get_virt_mouse_pos() 782 | if (slider_lo.adjust_tooltip) then 783 | if (an == 2) then 784 | if (sliderpos < (s_min + 3)) then 785 | an = an - 1 786 | elseif (sliderpos > (s_max - 3)) then 787 | an = an + 1 788 | end 789 | elseif (sliderpos > (s_max-s_min)/2) then 790 | an = an + 1 791 | tx = tx - 5 792 | else 793 | an = an - 1 794 | tx = tx + 10 795 | end 796 | end 797 | 798 | -- tooltip label 799 | elem_ass:new_event() 800 | elem_ass:pos(tx, ty) 801 | elem_ass:an(an) 802 | elem_ass:append(slider_lo.tooltip_style) 803 | ass_append_alpha(elem_ass, slider_lo.alpha, 0) 804 | elem_ass:append(tooltiplabel) 805 | 806 | -- thumbnail 807 | if not thumbfast.disabled then 808 | local osd_w = mp.get_property_number("osd-width") 809 | if osd_w then 810 | local r_w, r_h = get_virt_scale_factor() 811 | 812 | local tooltip_font_size = 18 813 | local thumbPad = 4 814 | local thumbMarginX = 18 / r_w 815 | local thumbMarginY = tooltip_font_size + thumbPad + 2 / r_h 816 | local tooltipBgColor = "FFFFFF" 817 | local tooltipBgAlpha = 80 818 | local thumbX = math.min(osd_w - thumbfast.width - thumbMarginX, math.max(thumbMarginX, tx / r_w - thumbfast.width / 2)) 819 | local thumbY = (ty - thumbMarginY) / r_h - thumbfast.height 820 | 821 | thumbX = math.floor(thumbX + 0.5) 822 | thumbY = math.floor(thumbY + 0.5) 823 | 824 | elem_ass:new_event() 825 | elem_ass:pos(thumbX * r_w, ty - thumbMarginY - thumbfast.height * r_h) 826 | elem_ass:an(7) 827 | elem_ass:append(osc_styles.Tooltip) 828 | elem_ass:draw_start() 829 | elem_ass:rect_cw(-thumbPad * r_w, -thumbPad * r_h, (thumbfast.width + thumbPad) * r_w, (thumbfast.height + thumbPad) * r_h) 830 | elem_ass:draw_stop() 831 | 832 | mp.commandv("script-message-to", "thumbfast", "thumb", 833 | mp.get_property_number("duration", 0) * (sliderpos / 100), 834 | thumbX, 835 | thumbY 836 | ) 837 | 838 | local se, ae = state.slider_element, elements[state.active_element] 839 | if user_opts.chapter_fmt ~= "no" and se and (ae == se or (not ae and mouse_hit(se))) then 840 | local dur = mp.get_property_number("duration", 0) 841 | if dur > 0 then 842 | local possec = get_slider_value(se) * dur / 100 -- of mouse pos 843 | local ch = get_chapter(possec) 844 | if ch and ch.title and ch.title ~= "" then 845 | elem_ass:new_event() 846 | elem_ass:pos((thumbX + thumbfast.width / 2) * r_w, thumbY * r_h - tooltip_font_size) 847 | elem_ass:an(an) 848 | elem_ass:append(slider_lo.tooltip_style) 849 | ass_append_alpha(elem_ass, slider_lo.alpha, 0) 850 | elem_ass:append(string.format(user_opts.chapter_fmt, ch.title)) 851 | end 852 | end 853 | end 854 | end 855 | end 856 | else 857 | if thumbfast.available then 858 | mp.commandv("script-message-to", "thumbfast", "clear") 859 | end 860 | end 861 | end 862 | 863 | elseif (element.type == 'button') then 864 | 865 | local buttontext 866 | if type(element.content) == 'function' then 867 | buttontext = element.content() -- function objects 868 | elseif not (element.content == nil) then 869 | buttontext = element.content -- text objects 870 | end 871 | 872 | buttontext = buttontext:gsub(':%((.?.?.?)%) unknown ', ':%(%1%)') --gsub('%) unknown %(\'', '') 873 | 874 | local maxchars = element.layout.button.maxchars 875 | -- 认为1个中文字符约等于1.5个英文字符 876 | -- local charcount = buttontext:len()- (buttontext:len()-select(2, buttontext:gsub('[^\128-\193]', '')))/1.5 877 | local charcount = (buttontext:len() + select(2, buttontext:gsub('[^\128-\193]', ''))*2) / 3 878 | if not (maxchars == nil) and (charcount > maxchars) then 879 | local limit = math.max(0, maxchars - 3) 880 | if (charcount > limit) then 881 | while (charcount > limit) do 882 | buttontext = buttontext:gsub('.[\128-\191]*$', '') 883 | charcount = (buttontext:len() + select(2, buttontext:gsub('[^\128-\193]', ''))*2) / 3 884 | end 885 | buttontext = buttontext .. '...' 886 | end 887 | end 888 | 889 | elem_ass:append(buttontext) 890 | 891 | -- add tooltip 892 | if not (element.tooltipF == nil) and element.enabled then 893 | if mouse_hit(element) then 894 | local tooltiplabel = element.tooltipF 895 | local an = 1 896 | local ty = element.hitbox.y1 897 | local tx = get_virt_mouse_pos() 898 | 899 | if ty < osc_param.playresy / 2 then 900 | ty = element.hitbox.y2 901 | an = 7 902 | end 903 | 904 | -- tooltip label 905 | if type(element.tooltipF) == 'function' then 906 | tooltiplabel = element.tooltipF() 907 | else 908 | tooltiplabel = element.tooltipF 909 | end 910 | elem_ass:new_event() 911 | elem_ass:pos(tx, ty) 912 | elem_ass:an(an) 913 | elem_ass:append(element.tooltip_style) 914 | elem_ass:append(tooltiplabel) 915 | end 916 | end 917 | end 918 | 919 | master_ass:merge(elem_ass) 920 | end 921 | end 922 | 923 | -- 924 | -- Message display 925 | -- 926 | 927 | -- pos is 1 based 928 | function limited_list(prop, pos) 929 | local proplist = mp.get_property_native(prop, {}) 930 | local count = #proplist 931 | if count == 0 then 932 | return count, proplist 933 | end 934 | 935 | local fs = tonumber(mp.get_property('options/osd-font-size')) 936 | local max = math.ceil(osc_param.unscaled_y*0.75 / fs) 937 | if max % 2 == 0 then 938 | max = max - 1 939 | end 940 | local delta = math.ceil(max / 2) - 1 941 | local begi = math.max(math.min(pos - delta, count - max + 1), 1) 942 | local endi = math.min(begi + max - 1, count) 943 | 944 | local reslist = {} 945 | for i=begi, endi do 946 | local item = proplist[i] 947 | item.current = (i == pos) and true or nil 948 | table.insert(reslist, item) 949 | end 950 | return count, reslist 951 | end 952 | 953 | function get_playlist() 954 | local pos = mp.get_property_number('playlist-pos', 0) + 1 955 | local count, limlist = limited_list('playlist', pos) 956 | if count == 0 then 957 | return texts.nolist 958 | end 959 | 960 | local message = string.format(texts.playlist .. ' [%d/%d]:\n', pos, count) 961 | for i, v in ipairs(limlist) do 962 | local title = v.title 963 | local _, filename = utils.split_path(v.filename) 964 | if title == nil then 965 | title = filename 966 | end 967 | message = string.format('%s %s %s\n', message, 968 | (v.current and '●' or '○'), title) 969 | end 970 | return message 971 | end 972 | 973 | function get_chapterlist() 974 | local pos = mp.get_property_number('chapter', 0) + 1 975 | local count, limlist = limited_list('chapter-list', pos) 976 | if count == 0 then 977 | return texts.nochapter 978 | end 979 | 980 | local message = string.format(texts.chapter.. ' [%d/%d]:\n', pos, count) 981 | for i, v in ipairs(limlist) do 982 | local time = mp.format_time(v.time) 983 | local title = v.title 984 | if title == nil then 985 | title = string.format(texts.chapter .. ' %02d', i) 986 | end 987 | message = string.format('%s[%s] %s %s\n', message, time, 988 | (v.current and '●' or '○'), title) 989 | end 990 | return message 991 | end 992 | 993 | function show_message(text, duration) 994 | 995 | --print('text: '..text..' duration: ' .. duration) 996 | if duration == nil then 997 | duration = tonumber(mp.get_property('options/osd-duration')) / 1000 998 | elseif not type(duration) == 'number' then 999 | print('duration: ' .. duration) 1000 | end 1001 | 1002 | -- cut the text short, otherwise the following functions 1003 | -- may slow down massively on huge input 1004 | text = string.sub(text, 0, 4000) 1005 | 1006 | -- replace actual linebreaks with ASS linebreaks 1007 | text = string.gsub(text, '\n', '\\N') 1008 | 1009 | state.message_text = text 1010 | 1011 | if not state.message_hide_timer then 1012 | state.message_hide_timer = mp.add_timeout(0, request_tick) 1013 | end 1014 | state.message_hide_timer:kill() 1015 | state.message_hide_timer.timeout = duration 1016 | state.message_hide_timer:resume() 1017 | request_tick() 1018 | end 1019 | 1020 | function render_message(ass) 1021 | if state.message_hide_timer and state.message_hide_timer:is_enabled() and 1022 | state.message_text 1023 | then 1024 | local _, lines = string.gsub(state.message_text, '\\N', '') 1025 | 1026 | local fontsize = tonumber(mp.get_property('options/osd-font-size')) 1027 | local outline = tonumber(mp.get_property('options/osd-border-size')) 1028 | local maxlines = math.ceil(osc_param.unscaled_y*0.75 / fontsize) 1029 | local counterscale = osc_param.playresy / osc_param.unscaled_y 1030 | 1031 | fontsize = fontsize * counterscale / math.max(0.65 + math.min(lines/maxlines, 1), 1) 1032 | outline = outline * counterscale / math.max(0.75 + math.min(lines/maxlines, 1)/2, 1) 1033 | 1034 | local style = '{\\bord' .. outline .. '\\fs' .. fontsize .. '}' 1035 | 1036 | 1037 | ass:new_event() 1038 | ass:append(style .. state.message_text) 1039 | else 1040 | state.message_text = nil 1041 | end 1042 | end 1043 | 1044 | -- 1045 | -- Initialisation and Layout 1046 | -- 1047 | 1048 | function new_element(name, type) 1049 | elements[name] = {} 1050 | elements[name].type = type 1051 | elements[name].name = name 1052 | 1053 | -- add default stuff 1054 | elements[name].eventresponder = {} 1055 | elements[name].visible = true 1056 | elements[name].enabled = true 1057 | elements[name].softrepeat = false 1058 | elements[name].styledown = (type == 'button') 1059 | elements[name].state = {} 1060 | 1061 | if (type == 'slider') then 1062 | elements[name].slider = {min = {value = 0}, max = {value = 100}} 1063 | end 1064 | 1065 | 1066 | return elements[name] 1067 | end 1068 | 1069 | function add_layout(name) 1070 | if not (elements[name] == nil) then 1071 | -- new layout 1072 | elements[name].layout = {} 1073 | 1074 | -- set layout defaults 1075 | elements[name].layout.layer = 50 1076 | elements[name].layout.alpha = {[1] = 0, [2] = 255, [3] = 255, [4] = 255} 1077 | 1078 | if (elements[name].type == 'button') then 1079 | elements[name].layout.button = { 1080 | maxchars = nil, 1081 | } 1082 | elseif (elements[name].type == 'slider') then 1083 | -- slider defaults 1084 | elements[name].layout.slider = { 1085 | border = 1, 1086 | gap = 1, 1087 | nibbles_top = true, 1088 | nibbles_bottom = true, 1089 | adjust_tooltip = true, 1090 | tooltip_style = '', 1091 | tooltip_an = 2, 1092 | alpha = {[1] = 0, [2] = 255, [3] = 88, [4] = 255}, 1093 | } 1094 | elseif (elements[name].type == 'box') then 1095 | elements[name].layout.box = {radius = 0, hexagon = false} 1096 | end 1097 | 1098 | return elements[name].layout 1099 | else 1100 | msg.error('Can\'t add_layout to element \''..name..'\', doesn\'t exist.') 1101 | end 1102 | end 1103 | 1104 | -- Window Controls 1105 | function window_controls() 1106 | local wc_geo = { 1107 | x = 0, 1108 | y = 32, 1109 | an = 1, 1110 | w = osc_param.playresx, 1111 | h = 32, 1112 | } 1113 | 1114 | local controlbox_w = window_control_box_width 1115 | local titlebox_w = wc_geo.w - controlbox_w 1116 | 1117 | -- Default alignment is 'right' 1118 | local controlbox_left = wc_geo.w - controlbox_w 1119 | local titlebox_left = wc_geo.x 1120 | local titlebox_right = wc_geo.w - controlbox_w 1121 | 1122 | add_area('window-controls', 1123 | get_hitbox_coords(controlbox_left, wc_geo.y, wc_geo.an, 1124 | controlbox_w, wc_geo.h)) 1125 | 1126 | local lo 1127 | 1128 | local button_y = wc_geo.y - (wc_geo.h / 2) 1129 | local first_geo = 1130 | {x = controlbox_left + 27, y = button_y, an = 5, w = 40, h = wc_geo.h} 1131 | local second_geo = 1132 | {x = controlbox_left + 69, y = button_y, an = 5, w = 40, h = wc_geo.h} 1133 | local third_geo = 1134 | {x = controlbox_left + 115, y = button_y, an = 5, w = 40, h = wc_geo.h} 1135 | 1136 | -- Window control buttons use symbols in the custom mpv osd font 1137 | -- because the official unicode codepoints are sufficiently 1138 | -- exotic that a system might lack an installed font with them, 1139 | -- and libass will complain that they are not present in the 1140 | -- default font, even if another font with them is available. 1141 | 1142 | -- Close: ?? 1143 | ne = new_element('close', 'button') 1144 | ne.content = '\238\132\149' 1145 | ne.eventresponder['mbtn_left_up'] = 1146 | function () mp.commandv('quit') end 1147 | lo = add_layout('close') 1148 | lo.geometry = third_geo 1149 | lo.style = osc_styles.WinCtrl 1150 | lo.alpha[3] = 0 1151 | 1152 | -- Minimize: ?? 1153 | ne = new_element('minimize', 'button') 1154 | ne.content = '\\n\238\132\146' 1155 | ne.eventresponder['mbtn_left_up'] = 1156 | function () mp.commandv('cycle', 'window-minimized') end 1157 | lo = add_layout('minimize') 1158 | lo.geometry = first_geo 1159 | lo.style = osc_styles.WinCtrl 1160 | lo.alpha[3] = 0 1161 | 1162 | -- Maximize: ?? /?? 1163 | ne = new_element('maximize', 'button') 1164 | if state.maximized or state.fullscreen then 1165 | ne.content = '\238\132\148' 1166 | else 1167 | ne.content = '\238\132\147' 1168 | end 1169 | ne.eventresponder['mbtn_left_up'] = 1170 | function () 1171 | if state.fullscreen then 1172 | mp.commandv('cycle', 'fullscreen') 1173 | else 1174 | mp.commandv('cycle', 'window-maximized') 1175 | end 1176 | end 1177 | lo = add_layout('maximize') 1178 | lo.geometry = second_geo 1179 | lo.style = osc_styles.WinCtrl 1180 | lo.alpha[3] = 0 1181 | end 1182 | 1183 | -- 1184 | -- Layouts 1185 | -- 1186 | 1187 | local layouts = {} 1188 | 1189 | -- Default layout 1190 | layouts = function () 1191 | 1192 | local osc_geo = {w, h} 1193 | 1194 | osc_geo.w = osc_param.playresx 1195 | osc_geo.h = 180 1196 | 1197 | -- origin of the controllers, left/bottom corner 1198 | local posX = 0 1199 | local posY = osc_param.playresy 1200 | 1201 | osc_param.areas = {} -- delete areas 1202 | 1203 | -- area for active mouse input 1204 | add_area('input', get_hitbox_coords(posX, posY, 1, osc_geo.w, 104)) 1205 | 1206 | -- area for show/hide 1207 | add_area('showhide', 0, 0, osc_param.playresx, osc_param.playresy) 1208 | 1209 | -- fetch values 1210 | local osc_w, osc_h= 1211 | osc_geo.w, osc_geo.h 1212 | 1213 | -- 1214 | -- Controller Background 1215 | -- 1216 | local lo 1217 | 1218 | new_element('TransBg', 'box') 1219 | lo = add_layout('TransBg') 1220 | lo.geometry = {x = posX, y = posY, an = 7, w = osc_w, h = 1} 1221 | lo.style = osc_styles.TransBg 1222 | lo.layer = 10 1223 | lo.alpha[3] = 0 1224 | 1225 | -- 1226 | -- Alignment 1227 | -- 1228 | local refX = osc_w / 2 1229 | local refY = posY 1230 | local geo 1231 | 1232 | -- 1233 | -- Seekbar 1234 | -- 1235 | new_element('seekbarbg', 'box') 1236 | lo = add_layout('seekbarbg') 1237 | lo.geometry = {x = refX , y = refY - 96 , an = 5, w = osc_geo.w - 50, h = 2} 1238 | lo.layer = 13 1239 | lo.style = osc_styles.SeekbarBg 1240 | lo.alpha[1] = 128 1241 | lo.alpha[3] = 128 1242 | 1243 | lo = add_layout('seekbar') 1244 | lo.geometry = {x = refX, y = refY - 96 , an = 5, w = osc_geo.w - 50, h = 16} 1245 | lo.style = osc_styles.SeekbarFg 1246 | lo.slider.gap = 7 1247 | lo.slider.tooltip_style = osc_styles.Tooltip 1248 | lo.slider.tooltip_an = 2 1249 | 1250 | local showjump = user_opts.showjump 1251 | local offset = showjump and 60 or 0 1252 | 1253 | -- 1254 | -- Volumebar 1255 | -- 1256 | lo = new_element('volumebarbg', 'box') 1257 | lo.visible = (osc_param.playresx >= 750) and user_opts.volumecontrol 1258 | lo = add_layout('volumebarbg') 1259 | lo.geometry = {x = 155, y = refY - 40, an = 4, w = 80, h = 2} 1260 | lo.layer = 13 1261 | lo.style = osc_styles.VolumebarBg 1262 | 1263 | 1264 | lo = add_layout('volumebar') 1265 | lo.geometry = {x = 155, y = refY - 40, an = 4, w = 80, h = 8} 1266 | lo.style = osc_styles.VolumebarFg 1267 | lo.slider.gap = 3 1268 | lo.slider.tooltip_style = osc_styles.Tooltip 1269 | lo.slider.tooltip_an = 2 1270 | 1271 | -- buttons 1272 | lo = add_layout('pl_prev') 1273 | lo.geometry = {x = refX - 120 - offset, y = refY - 40 , an = 5, w = 30, h = 24} 1274 | lo.style = osc_styles.Ctrl2 1275 | 1276 | lo = add_layout('skipback') 1277 | lo.geometry = {x = refX - 60 - offset, y = refY - 40 , an = 5, w = 30, h = 24} 1278 | lo.style = osc_styles.Ctrl2 1279 | 1280 | 1281 | if showjump then 1282 | lo = add_layout('jumpback') 1283 | lo.geometry = {x = refX - 60, y = refY - 40 , an = 5, w = 30, h = 24} 1284 | lo.style = osc_styles.Ctrl2 1285 | end 1286 | 1287 | lo = add_layout('playpause') 1288 | lo.geometry = {x = refX, y = refY - 40 , an = 5, w = 45, h = 45} 1289 | lo.style = osc_styles.Ctrl1 1290 | 1291 | if showjump then 1292 | lo = add_layout('jumpfrwd') 1293 | lo.geometry = {x = refX + 60, y = refY - 40 , an = 5, w = 30, h = 24} 1294 | 1295 | -- HACK: jumpfrwd's icon must be mirrored for nonstandard # of seconds 1296 | -- as the font only has an icon without a number for rewinding 1297 | lo.style = (user_opts.jumpiconnumber and jumpicons[user_opts.jumpamount] ~= nil) and osc_styles.Ctrl2 or osc_styles.Ctrl2Flip 1298 | end 1299 | 1300 | lo = add_layout('skipfrwd') 1301 | lo.geometry = {x = refX + 60 + offset, y = refY - 40 , an = 5, w = 30, h = 24} 1302 | lo.style = osc_styles.Ctrl2 1303 | 1304 | lo = add_layout('pl_next') 1305 | lo.geometry = {x = refX + 120 + offset, y = refY - 40 , an = 5, w = 30, h = 24} 1306 | lo.style = osc_styles.Ctrl2 1307 | 1308 | 1309 | -- Time 1310 | lo = add_layout('tc_left') 1311 | lo.geometry = {x = 25, y = refY - 84, an = 7, w = 64, h = 20} 1312 | lo.style = osc_styles.Time 1313 | 1314 | 1315 | lo = add_layout('tc_right') 1316 | lo.geometry = {x = osc_geo.w - 25 , y = refY -84, an = 9, w = 64, h = 20} 1317 | lo.style = osc_styles.Time 1318 | 1319 | lo = add_layout('cy_audio') 1320 | lo.geometry = {x = 37, y = refY - 40, an = 5, w = 24, h = 24} 1321 | lo.style = osc_styles.Ctrl3 1322 | lo.visible = (osc_param.playresx >= 540) 1323 | 1324 | lo = add_layout('cy_sub') 1325 | lo.geometry = {x = 87, y = refY - 40, an = 5, w = 24, h = 24} 1326 | lo.style = osc_styles.Ctrl3 1327 | lo.visible = (osc_param.playresx >= 600) 1328 | 1329 | lo = add_layout('vol_ctrl') 1330 | lo.geometry = {x = 137, y = refY - 40, an = 5, w = 24, h = 24} 1331 | lo.style = osc_styles.Ctrl3 1332 | lo.visible = (osc_param.playresx >= 650) 1333 | 1334 | lo = add_layout('tog_fs') 1335 | lo.geometry = {x = osc_geo.w - 37, y = refY - 40, an = 5, w = 24, h = 24} 1336 | lo.style = osc_styles.Ctrl3 1337 | lo.visible = (osc_param.playresx >= 540) 1338 | 1339 | lo = add_layout('tog_info') 1340 | lo.geometry = {x = osc_geo.w - 87, y = refY - 40, an = 5, w = 24, h = 24} 1341 | lo.style = osc_styles.Ctrl3 1342 | lo.visible = (osc_param.playresx >= 600) 1343 | 1344 | geo = { x = 25, y = refY - 132, an = 1, w = osc_geo.w - 50, h = 48 } 1345 | lo = add_layout('title') 1346 | lo.geometry = geo 1347 | lo.style = string.format('%s{\\clip(%f,%f,%f,%f)}', osc_styles.Title, 1348 | geo.x, geo.y - geo.h, geo.x + geo.w , geo.y + 5) 1349 | lo.alpha[3] = 0 1350 | lo.button.maxchars = geo.w / 23 1351 | end 1352 | 1353 | -- Validate string type user options 1354 | function validate_user_opts() 1355 | if user_opts.windowcontrols ~= 'auto' and 1356 | user_opts.windowcontrols ~= 'yes' and 1357 | user_opts.windowcontrols ~= 'no' then 1358 | msg.warn('windowcontrols cannot be \'' .. 1359 | user_opts.windowcontrols .. '\'. Ignoring.') 1360 | user_opts.windowcontrols = 'auto' 1361 | end 1362 | end 1363 | 1364 | function update_options(list) 1365 | validate_user_opts() 1366 | request_tick() 1367 | visibility_mode(user_opts.visibility, true) 1368 | update_duration_watch() 1369 | request_init() 1370 | end 1371 | 1372 | -- OSC INIT 1373 | function osc_init() 1374 | msg.debug('osc_init') 1375 | 1376 | -- set canvas resolution according to display aspect and scaling setting 1377 | local baseResY = 720 1378 | local display_w, display_h, display_aspect = mp.get_osd_size() 1379 | local scale = 1 1380 | 1381 | if (mp.get_property('video') == 'no') then -- dummy/forced window 1382 | scale = user_opts.scaleforcedwindow 1383 | elseif state.fullscreen then 1384 | scale = user_opts.scalefullscreen 1385 | else 1386 | scale = user_opts.scalewindowed 1387 | end 1388 | 1389 | if user_opts.vidscale then 1390 | osc_param.unscaled_y = baseResY 1391 | else 1392 | osc_param.unscaled_y = display_h 1393 | end 1394 | osc_param.playresy = osc_param.unscaled_y / scale 1395 | if (display_aspect > 0) then 1396 | osc_param.display_aspect = display_aspect 1397 | end 1398 | osc_param.playresx = osc_param.playresy * osc_param.display_aspect 1399 | 1400 | -- stop seeking with the slider to prevent skipping files 1401 | state.active_element = nil 1402 | 1403 | elements = {} 1404 | 1405 | -- some often needed stuff 1406 | local pl_count = mp.get_property_number('playlist-count', 0) 1407 | local have_pl = (pl_count > 1) 1408 | local pl_pos = mp.get_property_number('playlist-pos', 0) + 1 1409 | local have_ch = (mp.get_property_number('chapters', 0) > 0) 1410 | local loop = mp.get_property('loop-playlist', 'no') 1411 | 1412 | local ne 1413 | 1414 | -- playlist buttons 1415 | -- prev 1416 | ne = new_element('pl_prev', 'button') 1417 | 1418 | ne.content = icons.previous 1419 | ne.enabled = (pl_pos > 1) or (loop ~= 'no') 1420 | ne.eventresponder['mbtn_left_up'] = 1421 | function () 1422 | mp.commandv('playlist-prev', 'weak') 1423 | end 1424 | ne.eventresponder['mbtn_right_up'] = 1425 | function () show_message(get_playlist()) end 1426 | 1427 | --next 1428 | ne = new_element('pl_next', 'button') 1429 | 1430 | ne.content = icons.next 1431 | ne.enabled = (have_pl and (pl_pos < pl_count)) or (loop ~= 'no') 1432 | ne.eventresponder['mbtn_left_up'] = 1433 | function () 1434 | mp.commandv('playlist-next', 'weak') 1435 | end 1436 | ne.eventresponder['mbtn_right_up'] = 1437 | function () show_message(get_playlist()) end 1438 | 1439 | 1440 | --play control buttons 1441 | --playpause 1442 | ne = new_element('playpause', 'button') 1443 | 1444 | ne.content = function () 1445 | if mp.get_property('pause') == 'yes' then 1446 | return (icons.play) 1447 | else 1448 | return (icons.pause) 1449 | end 1450 | end 1451 | ne.eventresponder['mbtn_left_up'] = 1452 | function () mp.commandv('cycle', 'pause') end 1453 | --ne.eventresponder['mbtn_right_up'] = 1454 | -- function () mp.commandv('script-binding', 'open-file-dialog') end 1455 | 1456 | if user_opts.showjump then 1457 | local jumpamount = user_opts.jumpamount 1458 | local jumpmode = user_opts.jumpmode 1459 | local icons = jumpicons.default 1460 | if user_opts.jumpiconnumber then 1461 | icons = jumpicons[jumpamount] or jumpicons.default 1462 | end 1463 | 1464 | --jumpback 1465 | ne = new_element('jumpback', 'button') 1466 | 1467 | ne.softrepeat = true 1468 | ne.content = icons[1] 1469 | ne.eventresponder['mbtn_left_down'] = 1470 | --function () mp.command('seek -5') end 1471 | function () mp.commandv('seek', -jumpamount, jumpmode) end 1472 | ne.eventresponder['shift+mbtn_left_down'] = 1473 | function () mp.commandv('frame-back-step') end 1474 | ne.eventresponder['mbtn_right_down'] = 1475 | --function () mp.command('seek -60') end 1476 | function () mp.commandv('seek', -60, jumpmode) end 1477 | ne.eventresponder['enter'] = 1478 | --function () mp.command('seek -5') end 1479 | function () mp.commandv('seek', -jumpamount, jumpmode) end 1480 | 1481 | 1482 | --jumpfrwd 1483 | ne = new_element('jumpfrwd', 'button') 1484 | 1485 | ne.softrepeat = true 1486 | ne.content = icons[2] 1487 | ne.eventresponder['mbtn_left_down'] = 1488 | --function () mp.command('seek +5') end 1489 | function () mp.commandv('seek', jumpamount, jumpmode) end 1490 | ne.eventresponder['shift+mbtn_left_down'] = 1491 | function () mp.commandv('frame-step') end 1492 | ne.eventresponder['mbtn_right_down'] = 1493 | --function () mp.command('seek +60') end 1494 | function () mp.commandv('seek', 60, jumpmode) end 1495 | ne.eventresponder['enter'] = 1496 | --function () mp.command('seek +5') end 1497 | function () mp.commandv('seek', jumpamount, jumpmode) end 1498 | end 1499 | 1500 | 1501 | --skipback 1502 | ne = new_element('skipback', 'button') 1503 | 1504 | ne.softrepeat = true 1505 | ne.content = icons.backward 1506 | ne.enabled = (have_ch) -- disables button when no chapters available. 1507 | ne.eventresponder['mbtn_left_down'] = 1508 | --function () mp.command('seek -5') end 1509 | --function () mp.commandv('seek', -5, 'relative', 'keyframes') end 1510 | function () mp.commandv("add", "chapter", -1) end 1511 | --ne.eventresponder['shift+mbtn_left_down'] = 1512 | --function () mp.commandv('frame-back-step') end 1513 | ne.eventresponder['mbtn_right_down'] = 1514 | function () show_message(get_chapterlist()) end 1515 | --function () mp.command('seek -60') end 1516 | --function () mp.commandv('seek', -60, 'relative', 'keyframes') end 1517 | ne.eventresponder['enter'] = 1518 | --function () mp.command('seek -5') end 1519 | --function () mp.commandv('seek', -5, 'relative', 'keyframes') end 1520 | function () mp.commandv("add", "chapter", -1) end 1521 | 1522 | --skipfrwd 1523 | ne = new_element('skipfrwd', 'button') 1524 | 1525 | ne.softrepeat = true 1526 | ne.content = icons.forward 1527 | ne.enabled = (have_ch) -- disables button when no chapters available. 1528 | ne.eventresponder['mbtn_left_down'] = 1529 | --function () mp.command('seek +5') end 1530 | --function () mp.commandv('seek', 5, 'relative', 'keyframes') end 1531 | function () mp.commandv("add", "chapter", 1) end 1532 | --ne.eventresponder['shift+mbtn_left_down'] = 1533 | --function () mp.commandv('frame-step') end 1534 | ne.eventresponder['mbtn_right_down'] = 1535 | function () show_message(get_chapterlist()) end 1536 | --function () mp.command('seek +60') end 1537 | --function () mp.commandv('seek', 60, 'relative', 'keyframes') end 1538 | ne.eventresponder['enter'] = 1539 | --function () mp.command('seek +5') end 1540 | --function () mp.commandv('seek', 5, 'relative', 'keyframes') end 1541 | function () mp.commandv("add", "chapter", 1) end 1542 | 1543 | -- 1544 | update_tracklist() 1545 | 1546 | --cy_audio 1547 | ne = new_element('cy_audio', 'button') 1548 | ne.enabled = (#tracks_osc.audio > 0) 1549 | ne.off = (get_track('audio') == 0) 1550 | ne.visible = (osc_param.playresx >= 540) 1551 | ne.content = icons.audio 1552 | ne.tooltip_style = osc_styles.Tooltip 1553 | ne.tooltipF = function () 1554 | local msg = texts.off 1555 | if not (get_track('audio') == 0) then 1556 | msg = (texts.audio .. ' [' .. get_track('audio') .. ' ∕ ' .. #tracks_osc.audio .. '] ') 1557 | local prop = mp.get_property('current-tracks/audio/title') --('current-tracks/audio/lang') 1558 | if not prop then 1559 | prop = texts.na 1560 | end 1561 | msg = msg .. '[' .. prop .. ']' 1562 | prop = mp.get_property('current-tracks/audio/lang') --('current-tracks/audio/title') 1563 | if prop then 1564 | msg = msg .. ' ' .. prop 1565 | end 1566 | return msg 1567 | end 1568 | return msg 1569 | end 1570 | ne.eventresponder['mbtn_left_up'] = 1571 | function () set_track('audio', 1) end 1572 | ne.eventresponder['mbtn_right_up'] = 1573 | function () set_track('audio', -1) end 1574 | ne.eventresponder['shift+mbtn_left_down'] = 1575 | function () show_message(get_tracklist('audio')) end 1576 | ne.eventresponder['enter'] = 1577 | function () set_track('audio', 1); show_message(get_tracklist('audio')) end 1578 | 1579 | --cy_sub 1580 | ne = new_element('cy_sub', 'button') 1581 | ne.enabled = (#tracks_osc.sub > 0) 1582 | ne.off = (get_track('sub') == 0) 1583 | ne.visible = (osc_param.playresx >= 600) 1584 | ne.content = icons.sub 1585 | ne.tooltip_style = osc_styles.Tooltip 1586 | ne.tooltipF = function () 1587 | local msg = texts.off 1588 | if not (get_track('sub') == 0) then 1589 | msg = (texts.subtitle .. ' [' .. get_track('sub') .. ' ∕ ' .. #tracks_osc.sub .. '] ') 1590 | local prop = mp.get_property('current-tracks/sub/lang') 1591 | if not prop then 1592 | prop = texts.na 1593 | end 1594 | msg = msg .. '[' .. prop .. ']' 1595 | prop = mp.get_property('current-tracks/sub/title') 1596 | if prop then 1597 | msg = msg .. ' ' .. prop 1598 | end 1599 | return msg 1600 | end 1601 | return msg 1602 | end 1603 | ne.eventresponder['mbtn_left_up'] = 1604 | function () set_track('sub', 1) end 1605 | ne.eventresponder['mbtn_right_up'] = 1606 | function () set_track('sub', -1) end 1607 | ne.eventresponder['shift+mbtn_left_down'] = 1608 | function () show_message(get_tracklist('sub')) end 1609 | ne.eventresponder['enter'] = 1610 | function () set_track('sub', 1); show_message(get_tracklist('sub')) end 1611 | 1612 | -- vol_ctrl 1613 | ne = new_element('vol_ctrl', 'button') 1614 | ne.enabled = (get_track('audio')>0) 1615 | ne.visible = (osc_param.playresx >= 650) and user_opts.volumecontrol 1616 | ne.content = function () 1617 | if (state.mute) then 1618 | return (icons.volume_mute) 1619 | else 1620 | return (icons.volume) 1621 | end 1622 | end 1623 | ne.eventresponder['mbtn_left_up'] = 1624 | function () mp.commandv('cycle', 'mute') end 1625 | ne.eventresponder["wheel_up_press"] = 1626 | function () mp.commandv("osd-auto", "add", "volume", 5) end 1627 | ne.eventresponder["wheel_down_press"] = 1628 | function () mp.commandv("osd-auto", "add", "volume", -5) end 1629 | 1630 | --tog_fs 1631 | ne = new_element('tog_fs', 'button') 1632 | ne.content = function () 1633 | if (state.fullscreen) then 1634 | return (icons.minimize) 1635 | else 1636 | return (icons.fullscreen) 1637 | end 1638 | end 1639 | ne.visible = (osc_param.playresx >= 540) 1640 | ne.eventresponder['mbtn_left_up'] = 1641 | function () mp.commandv('cycle', 'fullscreen') end 1642 | 1643 | --tog_info 1644 | ne = new_element('tog_info', 'button') 1645 | ne.content = icons.info 1646 | ne.visible = (osc_param.playresx >= 600) 1647 | ne.eventresponder['mbtn_left_up'] = 1648 | function () mp.commandv('script-binding', 'stats/display-stats-toggle') end 1649 | 1650 | -- title 1651 | ne = new_element('title', 'button') 1652 | ne.content = function () 1653 | local title = state.forced_title or 1654 | mp.command_native({"expand-text", user_opts.title}) 1655 | if state.paused then 1656 | title = title:gsub('\\n', ' '):gsub('\\$', ''):gsub('{','\\{') 1657 | else 1658 | title = title:gsub('\\n', ' '):gsub('\\$', ''):gsub('{','\\{') --title = ' ' 1659 | end 1660 | return not (title == '') and title or ' ' 1661 | end 1662 | ne.visible = osc_param.playresy >= 320 and user_opts.showtitle 1663 | 1664 | --seekbar 1665 | ne = new_element('seekbar', 'slider') 1666 | 1667 | ne.enabled = not (mp.get_property('percent-pos') == nil) 1668 | state.slider_element = ne.enabled and ne or nil -- used for forced_title 1669 | ne.slider.markerF = function () 1670 | local duration = mp.get_property_number('duration', nil) 1671 | if not (duration == nil) then 1672 | local chapters = mp.get_property_native('chapter-list', {}) 1673 | local markers = {} 1674 | for n = 1, #chapters do 1675 | markers[n] = (chapters[n].time / duration * 100) 1676 | end 1677 | return markers 1678 | else 1679 | return {} 1680 | end 1681 | end 1682 | ne.slider.posF = 1683 | function () return mp.get_property_number('percent-pos', nil) end 1684 | ne.slider.tooltipF = function (pos) 1685 | local duration = mp.get_property_number('duration', nil) 1686 | if not ((duration == nil) or (pos == nil)) then 1687 | possec = duration * (pos / 100) 1688 | return mp.format_time(possec) 1689 | else 1690 | return '' 1691 | end 1692 | end 1693 | ne.slider.seekRangesF = function() 1694 | if not user_opts.seekrange then 1695 | return nil 1696 | end 1697 | local cache_state = state.cache_state 1698 | if not cache_state then 1699 | return nil 1700 | end 1701 | local duration = mp.get_property_number('duration', nil) 1702 | if (duration == nil) or duration <= 0 then 1703 | return nil 1704 | end 1705 | local ranges = cache_state['seekable-ranges'] 1706 | if #ranges == 0 then 1707 | return nil 1708 | end 1709 | local nranges = {} 1710 | for _, range in pairs(ranges) do 1711 | nranges[#nranges + 1] = { 1712 | ['start'] = 100 * range['start'] / duration, 1713 | ['end'] = 100 * range['end'] / duration, 1714 | } 1715 | end 1716 | return nranges 1717 | end 1718 | ne.eventresponder['mouse_move'] = --keyframe seeking when mouse is dragged 1719 | function (element) 1720 | if not element.state.mbtnleft then return end -- allow drag for mbtnleft only! 1721 | -- mouse move events may pile up during seeking and may still get 1722 | -- sent when the user is done seeking, so we need to throw away 1723 | -- identical seeks 1724 | local seekto = get_slider_value(element) 1725 | if (element.state.lastseek == nil) or 1726 | (not (element.state.lastseek == seekto)) then 1727 | local flags = 'absolute-percent' 1728 | if not user_opts.seekbarkeyframes then 1729 | flags = flags .. '+exact' 1730 | end 1731 | mp.commandv('seek', seekto, flags) 1732 | element.state.lastseek = seekto 1733 | end 1734 | 1735 | end 1736 | ne.eventresponder['mbtn_left_down'] = --exact seeks on single clicks 1737 | function (element) 1738 | mp.commandv('seek', get_slider_value(element), 'absolute-percent', 'exact') 1739 | element.state.mbtnleft = true 1740 | end 1741 | ne.eventresponder['mbtn_left_up'] = 1742 | function (element) element.state.mbtnleft = false end 1743 | ne.eventresponder['mbtn_right_down'] = --seeks to chapter start 1744 | function (element) 1745 | local duration = mp.get_property_number('duration', nil) 1746 | if not (duration == nil) then 1747 | local chapters = mp.get_property_native('chapter-list', {}) 1748 | if #chapters > 0 then 1749 | local pos = get_slider_value(element) 1750 | local ch = #chapters 1751 | for n = 1, ch do 1752 | if chapters[n].time / duration * 100 >= pos then 1753 | ch = n - 1 1754 | break 1755 | end 1756 | end 1757 | mp.commandv('set', 'chapter', ch - 1) 1758 | --if chapters[ch].title then show_message(chapters[ch].time) end 1759 | end 1760 | end 1761 | end 1762 | ne.eventresponder['reset'] = 1763 | function (element) element.state.lastseek = nil end 1764 | 1765 | --volumebar 1766 | ne = new_element('volumebar', 'slider') 1767 | ne.visible = (osc_param.playresx >= 700) and user_opts.volumecontrol 1768 | ne.enabled = (get_track('audio')>0) 1769 | ne.slider.markerF = function () 1770 | return {} 1771 | end 1772 | ne.slider.seekRangesF = function() 1773 | return nil 1774 | end 1775 | ne.slider.posF = 1776 | function () 1777 | local val = mp.get_property_number('volume', nil) 1778 | return val*val/100 1779 | end 1780 | ne.eventresponder['mouse_move'] = 1781 | function (element) 1782 | if not element.state.mbtnleft then return end -- allow drag for mbtnleft only! 1783 | local seekto = get_slider_value(element) 1784 | if (element.state.lastseek == nil) or 1785 | (not (element.state.lastseek == seekto)) then 1786 | mp.commandv('set', 'volume', 10*math.sqrt(seekto)) 1787 | element.state.lastseek = seekto 1788 | end 1789 | end 1790 | ne.eventresponder['mbtn_left_down'] = --exact seeks on single clicks 1791 | function (element) 1792 | local seekto = get_slider_value(element) 1793 | mp.commandv('set', 'volume', 10*math.sqrt(seekto)) 1794 | element.state.mbtnleft = true 1795 | end 1796 | ne.eventresponder['mbtn_left_up'] = 1797 | function (element) element.state.mbtnleft = false end 1798 | ne.eventresponder['reset'] = 1799 | function (element) element.state.lastseek = nil end 1800 | ne.eventresponder["wheel_up_press"] = 1801 | function () mp.commandv("osd-auto", "add", "volume", 5) end 1802 | ne.eventresponder["wheel_down_press"] = 1803 | function () mp.commandv("osd-auto", "add", "volume", -5) end 1804 | 1805 | -- tc_left (current pos) 1806 | ne = new_element('tc_left', 'button') 1807 | ne.content = function () 1808 | if (state.fulltime) then 1809 | return (mp.get_property_osd('playback-time/full')) 1810 | else 1811 | return (mp.get_property_osd('playback-time')) 1812 | end 1813 | end 1814 | ne.eventresponder["mbtn_left_up"] = function () 1815 | state.fulltime = not state.fulltime 1816 | request_init() 1817 | end 1818 | -- tc_right (total/remaining time) 1819 | ne = new_element('tc_right', 'button') 1820 | ne.content = function () 1821 | if (mp.get_property_number('duration', 0) <= 0) then return '--:--:--' end 1822 | if (state.rightTC_trem) then 1823 | if (state.fulltime) then 1824 | return ('-'..mp.get_property_osd('playtime-remaining/full')) 1825 | else 1826 | return ('-'..mp.get_property_osd('playtime-remaining')) 1827 | end 1828 | else 1829 | if (state.fulltime) then 1830 | return (mp.get_property_osd('duration/full')) 1831 | else 1832 | return (mp.get_property_osd('duration')) 1833 | end 1834 | 1835 | end 1836 | end 1837 | ne.eventresponder['mbtn_left_up'] = 1838 | function () state.rightTC_trem = not state.rightTC_trem end 1839 | 1840 | -- load layout 1841 | layouts() 1842 | 1843 | -- load window controls 1844 | if window_controls_enabled() then 1845 | window_controls() 1846 | end 1847 | 1848 | --do something with the elements 1849 | prepare_elements() 1850 | end 1851 | 1852 | function shutdown() 1853 | 1854 | end 1855 | 1856 | -- 1857 | -- Other important stuff 1858 | -- 1859 | 1860 | 1861 | function show_osc() 1862 | -- show when disabled can happen (e.g. mouse_move) due to async/delayed unbinding 1863 | if not state.enabled then return end 1864 | 1865 | msg.trace('show_osc') 1866 | --remember last time of invocation (mouse move) 1867 | state.showtime = mp.get_time() 1868 | 1869 | osc_visible(true) 1870 | 1871 | if user_opts.keyboardnavigation == true then 1872 | osc_enable_key_bindings() 1873 | end 1874 | 1875 | if (user_opts.fadeduration > 0) then 1876 | state.anitype = nil 1877 | end 1878 | end 1879 | 1880 | function hide_osc() 1881 | msg.trace('hide_osc') 1882 | if not state.enabled then 1883 | -- typically hide happens at render() from tick(), but now tick() is 1884 | -- no-op and won't render again to remove the osc, so do that manually. 1885 | state.osc_visible = false 1886 | render_wipe() 1887 | if user_opts.keyboardnavigation == true then 1888 | osc_disable_key_bindings() 1889 | end 1890 | elseif (user_opts.fadeduration > 0) then 1891 | if not(state.osc_visible == false) then 1892 | state.anitype = 'out' 1893 | request_tick() 1894 | end 1895 | else 1896 | osc_visible(false) 1897 | end 1898 | end 1899 | 1900 | function osc_visible(visible) 1901 | if state.osc_visible ~= visible then 1902 | state.osc_visible = visible 1903 | end 1904 | request_tick() 1905 | end 1906 | 1907 | function pause_state(name, enabled) 1908 | state.paused = enabled 1909 | mp.add_timeout(0.1, function() state.osd:update() end) 1910 | if user_opts.showonpause then 1911 | if enabled then 1912 | state.lastvisibility = user_opts.visibility 1913 | visibility_mode("always", true) 1914 | show_osc() 1915 | else 1916 | visibility_mode(state.lastvisibility, true) 1917 | end 1918 | end 1919 | request_tick() 1920 | end 1921 | 1922 | function cache_state(name, st) 1923 | state.cache_state = st 1924 | request_tick() 1925 | end 1926 | 1927 | -- Request that tick() is called (which typically re-renders the OSC). 1928 | -- The tick is then either executed immediately, or rate-limited if it was 1929 | -- called a small time ago. 1930 | function request_tick() 1931 | if state.tick_timer == nil then 1932 | state.tick_timer = mp.add_timeout(0, tick) 1933 | end 1934 | 1935 | if not state.tick_timer:is_enabled() then 1936 | local now = mp.get_time() 1937 | local timeout = tick_delay - (now - state.tick_last_time) 1938 | if timeout < 0 then 1939 | timeout = 0 1940 | end 1941 | state.tick_timer.timeout = timeout 1942 | state.tick_timer:resume() 1943 | end 1944 | end 1945 | 1946 | function mouse_leave() 1947 | if get_hidetimeout() >= 0 then 1948 | hide_osc() 1949 | end 1950 | -- reset mouse position 1951 | state.last_mouseX, state.last_mouseY = nil, nil 1952 | state.mouse_in_window = false 1953 | end 1954 | 1955 | function request_init() 1956 | state.initREQ = true 1957 | request_tick() 1958 | end 1959 | 1960 | -- Like request_init(), but also request an immediate update 1961 | function request_init_resize() 1962 | request_init() 1963 | -- ensure immediate update 1964 | state.tick_timer:kill() 1965 | state.tick_timer.timeout = 0 1966 | state.tick_timer:resume() 1967 | end 1968 | 1969 | function render_wipe() 1970 | msg.trace('render_wipe()') 1971 | state.osd:remove() 1972 | end 1973 | 1974 | function render() 1975 | msg.trace('rendering') 1976 | local current_screen_sizeX, current_screen_sizeY, aspect = mp.get_osd_size() 1977 | local mouseX, mouseY = get_virt_mouse_pos() 1978 | local now = mp.get_time() 1979 | 1980 | -- check if display changed, if so request reinit 1981 | if not (state.mp_screen_sizeX == current_screen_sizeX 1982 | and state.mp_screen_sizeY == current_screen_sizeY) then 1983 | 1984 | request_init_resize() 1985 | 1986 | state.mp_screen_sizeX = current_screen_sizeX 1987 | state.mp_screen_sizeY = current_screen_sizeY 1988 | end 1989 | 1990 | -- init management 1991 | if state.active_element then 1992 | -- mouse is held down on some element - keep ticking and igore initReq 1993 | -- till it's released, or else the mouse-up (click) will misbehave or 1994 | -- get ignored. that's because osc_init() recreates the osc elements, 1995 | -- but mouse handling depends on the elements staying unmodified 1996 | -- between mouse-down and mouse-up (using the index active_element). 1997 | request_tick() 1998 | elseif state.initREQ then 1999 | osc_init() 2000 | state.initREQ = false 2001 | 2002 | -- store initial mouse position 2003 | if (state.last_mouseX == nil or state.last_mouseY == nil) 2004 | and not (mouseX == nil or mouseY == nil) then 2005 | 2006 | state.last_mouseX, state.last_mouseY = mouseX, mouseY 2007 | end 2008 | end 2009 | 2010 | 2011 | -- fade animation 2012 | if not(state.anitype == nil) then 2013 | 2014 | if (state.anistart == nil) then 2015 | state.anistart = now 2016 | end 2017 | 2018 | if (now < state.anistart + (user_opts.fadeduration/1000)) then 2019 | 2020 | if (state.anitype == 'in') then --fade in 2021 | osc_visible(true) 2022 | state.animation = scale_value(state.anistart, 2023 | (state.anistart + (user_opts.fadeduration/1000)), 2024 | 255, 0, now) 2025 | elseif (state.anitype == 'out') then --fade out 2026 | state.animation = scale_value(state.anistart, 2027 | (state.anistart + (user_opts.fadeduration/1000)), 2028 | 0, 255, now) 2029 | end 2030 | 2031 | else 2032 | if (state.anitype == 'out') then 2033 | osc_visible(false) 2034 | end 2035 | state.anistart = nil 2036 | state.animation = nil 2037 | state.anitype = nil 2038 | end 2039 | else 2040 | state.anistart = nil 2041 | state.animation = nil 2042 | state.anitype = nil 2043 | end 2044 | 2045 | --mouse show/hide area 2046 | for k,cords in pairs(osc_param.areas['showhide']) do 2047 | set_virt_mouse_area(cords.x1, cords.y1, cords.x2, cords.y2, 'showhide') 2048 | end 2049 | if osc_param.areas['showhide_wc'] then 2050 | for k,cords in pairs(osc_param.areas['showhide_wc']) do 2051 | set_virt_mouse_area(cords.x1, cords.y1, cords.x2, cords.y2, 'showhide_wc') 2052 | end 2053 | else 2054 | set_virt_mouse_area(0, 0, 0, 0, 'showhide_wc') 2055 | end 2056 | do_enable_keybindings() 2057 | 2058 | --mouse input area 2059 | local mouse_over_osc = false 2060 | 2061 | for _,cords in ipairs(osc_param.areas['input']) do 2062 | if state.osc_visible then -- activate only when OSC is actually visible 2063 | set_virt_mouse_area(cords.x1, cords.y1, cords.x2, cords.y2, 'input') 2064 | end 2065 | if state.osc_visible ~= state.input_enabled then 2066 | if state.osc_visible then 2067 | mp.enable_key_bindings('input') 2068 | else 2069 | mp.disable_key_bindings('input') 2070 | end 2071 | state.input_enabled = state.osc_visible 2072 | end 2073 | 2074 | if (mouse_hit_coords(cords.x1, cords.y1, cords.x2, cords.y2)) then 2075 | mouse_over_osc = true 2076 | end 2077 | end 2078 | 2079 | if osc_param.areas['window-controls'] then 2080 | for _,cords in ipairs(osc_param.areas['window-controls']) do 2081 | if state.osc_visible then -- activate only when OSC is actually visible 2082 | set_virt_mouse_area(cords.x1, cords.y1, cords.x2, cords.y2, 'window-controls') 2083 | mp.enable_key_bindings('window-controls') 2084 | else 2085 | mp.disable_key_bindings('window-controls') 2086 | end 2087 | 2088 | if (mouse_hit_coords(cords.x1, cords.y1, cords.x2, cords.y2)) then 2089 | mouse_over_osc = true 2090 | end 2091 | end 2092 | end 2093 | 2094 | if osc_param.areas['window-controls-title'] then 2095 | for _,cords in ipairs(osc_param.areas['window-controls-title']) do 2096 | if (mouse_hit_coords(cords.x1, cords.y1, cords.x2, cords.y2)) then 2097 | mouse_over_osc = true 2098 | end 2099 | end 2100 | end 2101 | 2102 | -- autohide 2103 | if not (state.showtime == nil) and (get_hidetimeout() >= 0) then 2104 | local timeout = state.showtime + (get_hidetimeout()/1000) - now 2105 | if timeout <= 0 then 2106 | if (state.active_element == nil) and not (mouse_over_osc) then 2107 | hide_osc() 2108 | end 2109 | else 2110 | -- the timer is only used to recheck the state and to possibly run 2111 | -- the code above again 2112 | if not state.hide_timer then 2113 | state.hide_timer = mp.add_timeout(0, tick) 2114 | end 2115 | state.hide_timer.timeout = timeout 2116 | -- re-arm 2117 | state.hide_timer:kill() 2118 | state.hide_timer:resume() 2119 | end 2120 | end 2121 | 2122 | 2123 | -- actual rendering 2124 | local ass = assdraw.ass_new() 2125 | 2126 | -- Messages 2127 | render_message(ass) 2128 | 2129 | -- actual OSC 2130 | if state.osc_visible then 2131 | render_elements(ass) 2132 | end 2133 | 2134 | -- submit 2135 | set_osd(osc_param.playresy * osc_param.display_aspect, 2136 | osc_param.playresy, ass.text) 2137 | end 2138 | 2139 | -- 2140 | -- Eventhandling 2141 | -- 2142 | 2143 | local function element_has_action(element, action) 2144 | return element and element.eventresponder and 2145 | element.eventresponder[action] 2146 | end 2147 | 2148 | function process_event(source, what) 2149 | local action = string.format('%s%s', source, 2150 | what and ('_' .. what) or '') 2151 | 2152 | if what == 'down' or what == 'press' then 2153 | 2154 | for n = 1, #elements do 2155 | 2156 | if mouse_hit(elements[n]) and 2157 | elements[n].eventresponder and 2158 | (elements[n].eventresponder[source .. '_up'] or 2159 | elements[n].eventresponder[action]) then 2160 | 2161 | if what == 'down' then 2162 | state.active_element = n 2163 | state.active_event_source = source 2164 | end 2165 | -- fire the down or press event if the element has one 2166 | if element_has_action(elements[n], action) then 2167 | elements[n].eventresponder[action](elements[n]) 2168 | end 2169 | 2170 | end 2171 | end 2172 | 2173 | elseif what == 'up' then 2174 | 2175 | if elements[state.active_element] then 2176 | local n = state.active_element 2177 | 2178 | if n == 0 then 2179 | --click on background (does not work) 2180 | elseif element_has_action(elements[n], action) and 2181 | mouse_hit(elements[n]) then 2182 | 2183 | elements[n].eventresponder[action](elements[n]) 2184 | end 2185 | 2186 | --reset active element 2187 | if element_has_action(elements[n], 'reset') then 2188 | elements[n].eventresponder['reset'](elements[n]) 2189 | end 2190 | 2191 | end 2192 | state.active_element = nil 2193 | state.mouse_down_counter = 0 2194 | 2195 | elseif source == 'mouse_move' then 2196 | 2197 | state.mouse_in_window = true 2198 | 2199 | local mouseX, mouseY = get_virt_mouse_pos() 2200 | if (user_opts.minmousemove == 0) or 2201 | (not ((state.last_mouseX == nil) or (state.last_mouseY == nil)) and 2202 | ((math.abs(mouseX - state.last_mouseX) >= user_opts.minmousemove) 2203 | or (math.abs(mouseY - state.last_mouseY) >= user_opts.minmousemove) 2204 | ) 2205 | ) then 2206 | show_osc() 2207 | end 2208 | state.last_mouseX, state.last_mouseY = mouseX, mouseY 2209 | 2210 | local n = state.active_element 2211 | if element_has_action(elements[n], action) then 2212 | elements[n].eventresponder[action](elements[n]) 2213 | end 2214 | end 2215 | 2216 | -- ensure rendering after any (mouse) event - icons could change etc 2217 | request_tick() 2218 | end 2219 | 2220 | local logo_lines = { 2221 | -- White border 2222 | "{\\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}", 2223 | -- Purple fill 2224 | "{\\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}", 2225 | -- Darker fill 2226 | "{\\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}", 2227 | -- White fill 2228 | "{\\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}", 2229 | -- Triangle 2230 | "{\\c&H691F69&\\p6}m 762 1113 l 762 708 b 881 776 1000 843 1119 911 1000 978 881 1046 762 1113{\\p0}", 2231 | } 2232 | 2233 | local santa_hat_lines = { 2234 | -- Pompoms 2235 | "{\\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}", 2236 | "{\\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}", 2237 | -- Main cap 2238 | "{\\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}", 2239 | -- Cap shadow 2240 | "{\\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}", 2241 | -- Brim and tip pompom 2242 | "{\\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}", 2243 | } 2244 | 2245 | -- called by mpv on every frame 2246 | function tick() 2247 | if (not state.enabled) then return end 2248 | 2249 | if (state.idle) then 2250 | 2251 | -- render idle message 2252 | msg.trace('idle message') 2253 | local _, _, display_aspect = mp.get_osd_size() 2254 | local display_h = 360 2255 | local display_w = display_h * display_aspect 2256 | -- logo is rendered at 2^(6-1) = 32 times resolution with size 1800x1800 2257 | local icon_x, icon_y = (display_w - 1800 / 32) / 2, 140 2258 | local line_prefix = ('{\\rDefault\\an7\\1a&H00&\\bord0\\shad0\\pos(%f,%f)}'):format(icon_x, icon_y) 2259 | 2260 | local ass = assdraw.ass_new() 2261 | -- mpv logo 2262 | if user_opts.idlescreen then 2263 | for i, line in ipairs(logo_lines) do 2264 | ass:new_event() 2265 | ass:append(line_prefix .. line) 2266 | end 2267 | end 2268 | 2269 | -- Santa hat 2270 | if is_december and user_opts.idlescreen and not user_opts.greenandgrumpy then 2271 | for i, line in ipairs(santa_hat_lines) do 2272 | ass:new_event() 2273 | ass:append(line_prefix .. line) 2274 | end 2275 | end 2276 | 2277 | if user_opts.idlescreen then 2278 | ass:new_event() 2279 | ass:pos(display_w / 2, icon_y + 65) 2280 | ass:an(8) 2281 | ass:append(texts.welcome) 2282 | end 2283 | set_osd(display_w, display_h, ass.text) 2284 | 2285 | if state.showhide_enabled then 2286 | mp.disable_key_bindings('showhide') 2287 | mp.disable_key_bindings('showhide_wc') 2288 | state.showhide_enabled = false 2289 | end 2290 | 2291 | 2292 | elseif (state.fullscreen and user_opts.showfullscreen) 2293 | or (not state.fullscreen and user_opts.showwindowed) then 2294 | 2295 | -- render the OSC 2296 | render() 2297 | else 2298 | -- Flush OSD 2299 | set_osd(osc_param.playresy, osc_param.playresy, '') 2300 | end 2301 | 2302 | state.tick_last_time = mp.get_time() 2303 | 2304 | if state.anitype ~= nil then 2305 | -- state.anistart can be nil - animation should now start, or it can 2306 | -- be a timestamp when it started. state.idle has no animation. 2307 | if not state.idle and 2308 | (not state.anistart or 2309 | mp.get_time() < 1 + state.anistart + user_opts.fadeduration/1000) 2310 | then 2311 | -- animating or starting, or still within 1s past the deadline 2312 | request_tick() 2313 | else 2314 | kill_animation() 2315 | end 2316 | end 2317 | end 2318 | 2319 | function do_enable_keybindings() 2320 | if state.enabled then 2321 | if not state.showhide_enabled then 2322 | mp.enable_key_bindings('showhide', 'allow-vo-dragging+allow-hide-cursor') 2323 | mp.enable_key_bindings('showhide_wc', 'allow-vo-dragging+allow-hide-cursor') 2324 | end 2325 | state.showhide_enabled = true 2326 | end 2327 | end 2328 | 2329 | function enable_osc(enable) 2330 | state.enabled = enable 2331 | if enable then 2332 | do_enable_keybindings() 2333 | else 2334 | hide_osc() -- acts immediately when state.enabled == false 2335 | if state.showhide_enabled then 2336 | mp.disable_key_bindings('showhide') 2337 | mp.disable_key_bindings('showhide_wc') 2338 | end 2339 | state.showhide_enabled = false 2340 | end 2341 | end 2342 | 2343 | -- duration is observed for the sole purpose of updating chapter markers 2344 | -- positions. live streams with chapters are very rare, and the update is also 2345 | -- expensive (with request_init), so it's only observed when we have chapters 2346 | -- and the user didn't disable the livemarkers option (update_duration_watch). 2347 | function on_duration() request_init() end 2348 | 2349 | local duration_watched = false 2350 | function update_duration_watch() 2351 | local want_watch = user_opts.livemarkers and 2352 | (mp.get_property_number("chapters", 0) or 0) > 0 and 2353 | true or false -- ensure it's a boolean 2354 | 2355 | if (want_watch ~= duration_watched) then 2356 | if want_watch then 2357 | mp.observe_property("duration", nil, on_duration) 2358 | else 2359 | mp.unobserve_property(on_duration) 2360 | end 2361 | duration_watched = want_watch 2362 | end 2363 | end 2364 | 2365 | validate_user_opts() 2366 | update_duration_watch() 2367 | 2368 | mp.register_event('shutdown', shutdown) 2369 | mp.register_event('start-file', request_init) 2370 | mp.observe_property('track-list', nil, request_init) 2371 | mp.observe_property('playlist', nil, request_init) 2372 | mp.observe_property("chapter-list", "native", function(_, list) 2373 | list = list or {} -- safety, shouldn't return nil 2374 | table.sort(list, function(a, b) return a.time < b.time end) 2375 | state.chapter_list = list 2376 | update_duration_watch() 2377 | request_init() 2378 | end) 2379 | 2380 | mp.register_script_message('osc-message', show_message) 2381 | mp.register_script_message('osc-chapterlist', function(dur) 2382 | show_message(get_chapterlist(), dur) 2383 | end) 2384 | mp.register_script_message('osc-playlist', function(dur) 2385 | show_message(get_playlist(), dur) 2386 | end) 2387 | mp.register_script_message('osc-tracklist', function(dur) 2388 | local msg = {} 2389 | for k,v in pairs(nicetypes) do 2390 | table.insert(msg, get_tracklist(k)) 2391 | end 2392 | show_message(table.concat(msg, '\n\n'), dur) 2393 | end) 2394 | 2395 | mp.observe_property('fullscreen', 'bool', 2396 | function(name, val) 2397 | state.fullscreen = val 2398 | request_init_resize() 2399 | end 2400 | ) 2401 | mp.observe_property('mute', 'bool', 2402 | function(name, val) 2403 | state.mute = val 2404 | end 2405 | ) 2406 | mp.observe_property('border', 'bool', 2407 | function(name, val) 2408 | state.border = val 2409 | request_init_resize() 2410 | end 2411 | ) 2412 | mp.observe_property('window-maximized', 'bool', 2413 | function(name, val) 2414 | state.maximized = val 2415 | request_init_resize() 2416 | end 2417 | ) 2418 | mp.observe_property('idle-active', 'bool', 2419 | function(name, val) 2420 | state.idle = val 2421 | request_tick() 2422 | end 2423 | ) 2424 | mp.observe_property('pause', 'bool', pause_state) 2425 | mp.observe_property('demuxer-cache-state', 'native', cache_state) 2426 | mp.observe_property('vo-configured', 'bool', function(name, val) 2427 | request_tick() 2428 | end) 2429 | mp.observe_property('playback-time', 'number', function(name, val) 2430 | request_tick() 2431 | end) 2432 | mp.observe_property('osd-dimensions', 'native', function(name, val) 2433 | -- (we could use the value instead of re-querying it all the time, but then 2434 | -- we might have to worry about property update ordering) 2435 | request_init_resize() 2436 | end) 2437 | 2438 | -- mouse show/hide bindings 2439 | mp.set_key_bindings({ 2440 | {'mouse_move', function(e) process_event('mouse_move', nil) end}, 2441 | {'mouse_leave', mouse_leave}, 2442 | }, 'showhide', 'force') 2443 | mp.set_key_bindings({ 2444 | {'mouse_move', function(e) process_event('mouse_move', nil) end}, 2445 | {'mouse_leave', mouse_leave}, 2446 | }, 'showhide_wc', 'force') 2447 | do_enable_keybindings() 2448 | 2449 | --mouse input bindings 2450 | mp.set_key_bindings({ 2451 | {"mbtn_left", function(e) process_event("mbtn_left", "up") end, 2452 | function(e) process_event("mbtn_left", "down") end}, 2453 | {"shift+mbtn_left", function(e) process_event("shift+mbtn_left", "up") end, 2454 | function(e) process_event("shift+mbtn_left", "down") end}, 2455 | {"mbtn_right", function(e) process_event("mbtn_right", "up") end, 2456 | function(e) process_event("mbtn_right", "down") end}, 2457 | -- alias to shift_mbtn_left for single-handed mouse use 2458 | {"mbtn_mid", function(e) process_event("shift+mbtn_left", "up") end, 2459 | function(e) process_event("shift+mbtn_left", "down") end}, 2460 | {"wheel_up", function(e) process_event("wheel_up", "press") end}, 2461 | {"wheel_down", function(e) process_event("wheel_down", "press") end}, 2462 | {"mbtn_left_dbl", "ignore"}, 2463 | {"shift+mbtn_left_dbl", "ignore"}, 2464 | {"mbtn_right_dbl", "ignore"}, 2465 | }, "input", "force") 2466 | mp.enable_key_bindings('input') 2467 | 2468 | mp.set_key_bindings({ 2469 | {'mbtn_left', function(e) process_event('mbtn_left', 'up') end, 2470 | function(e) process_event('mbtn_left', 'down') end}, 2471 | }, 'window-controls', 'force') 2472 | mp.enable_key_bindings('window-controls') 2473 | 2474 | function get_hidetimeout() 2475 | if user_opts.visibility == 'always' then 2476 | return -1 -- disable autohide 2477 | end 2478 | return user_opts.hidetimeout 2479 | end 2480 | 2481 | function always_on(val) 2482 | if state.enabled then 2483 | if val then 2484 | show_osc() 2485 | else 2486 | hide_osc() 2487 | end 2488 | end 2489 | end 2490 | 2491 | -- mode can be auto/always/never/cycle 2492 | -- the modes only affect internal variables and not stored on its own. 2493 | function visibility_mode(mode, no_osd) 2494 | if mode == "cycle" then 2495 | if not state.enabled then 2496 | mode = "auto" 2497 | elseif user_opts.visibility ~= "always" then 2498 | mode = "always" 2499 | else 2500 | mode = "never" 2501 | end 2502 | end 2503 | 2504 | if mode == 'auto' then 2505 | always_on(false) 2506 | enable_osc(true) 2507 | elseif mode == 'always' then 2508 | enable_osc(true) 2509 | always_on(true) 2510 | elseif mode == 'never' then 2511 | enable_osc(false) 2512 | else 2513 | msg.warn('Ignoring unknown visibility mode \"' .. mode .. '\"') 2514 | return 2515 | end 2516 | 2517 | user_opts.visibility = mode 2518 | mp.set_property_native("user-data/osc/visibility", user_opts.visibility) 2519 | 2520 | if not no_osd and tonumber(mp.get_property('osd-level')) >= 1 then 2521 | mp.osd_message('OSC visibility: ' .. mode) 2522 | end 2523 | 2524 | -- Reset the input state on a mode change. The input state will be 2525 | -- recalcuated on the next render cycle, except in 'never' mode where it 2526 | -- will just stay disabled. 2527 | mp.disable_key_bindings('input') 2528 | mp.disable_key_bindings('window-controls') 2529 | state.input_enabled = false 2530 | request_tick() 2531 | end 2532 | 2533 | 2534 | -- KeyboardControl 2535 | -- 2536 | 2537 | local osc_key_bindings = {} 2538 | 2539 | function osc_kb_control_up() 2540 | visibility_mode('always', true) 2541 | local keyboard_controls = build_keyboard_controls() 2542 | local rows = {} 2543 | local active_row_index = 0 2544 | local active_row_name = nil 2545 | 2546 | local row_index = -1 2547 | for row_name, row_controls in pairs(keyboard_controls) do 2548 | row_index = row_index + 1 2549 | rows[row_index] = row_name 2550 | for i, control in pairs(row_controls) do 2551 | if control == state.highlight_element then 2552 | active_row_index = row_index 2553 | active_row_name = row_name 2554 | end 2555 | end 2556 | end 2557 | 2558 | if active_row_index - 1 < 0 then 2559 | return 2560 | end 2561 | 2562 | local next_row_index = active_row_index - 1 2563 | 2564 | local new_active_row_name = rows[next_row_index] 2565 | local new_active_row = keyboard_controls[new_active_row_name] 2566 | 2567 | for i, control in pairs(new_active_row) do 2568 | state.highlight_element = control 2569 | return 2570 | end 2571 | end 2572 | 2573 | function osc_kb_control_down() 2574 | visibility_mode('always', true) 2575 | local keyboard_controls = build_keyboard_controls() 2576 | local rows = {} 2577 | local active_row_index = 0 2578 | local active_row_name = nil 2579 | 2580 | local row_index = -1 2581 | for row_name, row_controls in pairs(keyboard_controls) do 2582 | row_index = row_index + 1 2583 | rows[row_index] = row_name 2584 | for i, control in pairs(row_controls) do 2585 | if control == state.highlight_element then 2586 | active_row_index = row_index 2587 | active_row_name = row_name 2588 | end 2589 | end 2590 | end 2591 | 2592 | if active_row_index + 1 > #rows then 2593 | return 2594 | end 2595 | 2596 | local next_row_index = active_row_index + 1 2597 | 2598 | local new_active_row_name = rows[next_row_index] 2599 | local new_active_row = keyboard_controls[new_active_row_name] 2600 | 2601 | for i, control in pairs(new_active_row) do 2602 | state.highlight_element = control 2603 | return 2604 | end 2605 | 2606 | end 2607 | 2608 | function osc_kb_control_left() 2609 | visibility_mode('always', true) 2610 | local keyboard_controls = build_keyboard_controls() 2611 | 2612 | local active_control_name = nil 2613 | for row_name, row_controls in pairs(keyboard_controls) do 2614 | local controls = {} 2615 | local controls_index = -1 2616 | for i, control in pairs(row_controls) do 2617 | controls_index = controls_index + 1 2618 | controls[controls_index] = control 2619 | if control == state.highlight_element then 2620 | active_control_index = controls_index 2621 | active_control_name = control 2622 | end 2623 | end 2624 | 2625 | if active_control_name == 'seekbar' then 2626 | mp.commandv('seek', -5, 'exact', 'keyframes') 2627 | return 2628 | end 2629 | 2630 | if active_control_name then 2631 | if active_control_index - 1 < 0 then 2632 | return 2633 | end 2634 | 2635 | local next_control_index = active_control_index - 1 2636 | state.highlight_element = controls[next_control_index] 2637 | return 2638 | end 2639 | end 2640 | 2641 | end 2642 | 2643 | function osc_kb_control_right() 2644 | visibility_mode('always', true) 2645 | local keyboard_controls = build_keyboard_controls() 2646 | 2647 | local active_control_name = nil 2648 | for row_name, row_controls in pairs(keyboard_controls) do 2649 | local controls = {} 2650 | local controls_index = -1 2651 | for i, control in pairs(row_controls) do 2652 | controls_index = controls_index + 1 2653 | controls[controls_index] = control 2654 | if control == state.highlight_element then 2655 | active_control_index = controls_index 2656 | active_control_name = control 2657 | end 2658 | end 2659 | 2660 | if active_control_name == 'seekbar' then 2661 | mp.commandv('seek', 5, 'exact', 'keyframes') 2662 | return 2663 | end 2664 | 2665 | if active_control_name then 2666 | if active_control_index + 1 > #controls then 2667 | return 2668 | end 2669 | 2670 | local next_control_index = active_control_index + 1 2671 | state.highlight_element = controls[next_control_index] 2672 | return 2673 | end 2674 | end 2675 | 2676 | end 2677 | 2678 | function osc_kb_control_back() 2679 | visibility_mode('auto', true) 2680 | end 2681 | 2682 | function osc_kb_control_enter() 2683 | visibility_mode('always', true) 2684 | for n = 1, #elements do 2685 | if elements[n].name == state.highlight_element then 2686 | 2687 | local action = 'enter' 2688 | if element_has_action(elements[n], action) then 2689 | elements[n].eventresponder[action](elements[n]) 2690 | return 2691 | end 2692 | 2693 | local action = 'mbtn_left_up' 2694 | if element_has_action(elements[n], action) then 2695 | elements[n].eventresponder[action](elements[n]) 2696 | return 2697 | end 2698 | end 2699 | end 2700 | 2701 | end 2702 | 2703 | function osc_add_key_binding(key, name, fn, flags) 2704 | osc_key_bindings[#osc_key_bindings + 1] = name 2705 | mp.add_forced_key_binding(key, name, fn, flags) 2706 | end 2707 | 2708 | -- This is based on code from https://github.com/darsain/uosc 2709 | function osc_enable_key_bindings() 2710 | osc_key_bindings = {} 2711 | -- The `mp.set_key_bindings()` method would be easier here, but that 2712 | -- doesn't support 'repeatable' flag, so we are stuck with this monster. 2713 | osc_add_key_binding('up', 'osc-kb-control-prev1', osc_kb_control_up, 'repeatable') 2714 | osc_add_key_binding('down', 'osc-kb-control-next1', osc_kb_control_down, 'repeatable') 2715 | osc_add_key_binding('left', 'osc-kb-control-left1', osc_kb_control_left, 'repeatable') 2716 | osc_add_key_binding('right', 'osc-kb-control-right1', osc_kb_control_right, 'repeatable') 2717 | osc_add_key_binding('enter', 'osc-kb-control-select-alt3', osc_kb_control_enter, 'repeatable') 2718 | osc_add_key_binding('esc', 'osc-kb-control-close', osc_kb_control_back, 'repeatable') 2719 | end 2720 | 2721 | function osc_disable_key_bindings() 2722 | for _, name in ipairs(osc_key_bindings) do mp.remove_key_binding(name) end 2723 | osc_key_bindings = {} 2724 | end 2725 | 2726 | 2727 | 2728 | visibility_mode(user_opts.visibility, true) 2729 | mp.register_script_message('osc-visibility', visibility_mode) 2730 | mp.add_key_binding(nil, 'visibility', function() visibility_mode('cycle') end) 2731 | 2732 | mp.register_script_message("thumbfast-info", function(json) 2733 | local data = utils.parse_json(json) 2734 | if type(data) ~= "table" or not data.width or not data.height then 2735 | msg.error("thumbfast-info: received json didn't produce a table with thumbnail information") 2736 | else 2737 | thumbfast = data 2738 | end 2739 | end) 2740 | 2741 | set_virt_mouse_area(0, 0, 0, 0, 'input') 2742 | set_virt_mouse_area(0, 0, 0, 0, 'window-controls') 2743 | -------------------------------------------------------------------------------- /preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyl0/ModernX/3f2ed6b993059c6986bf34be3998048c50547187/preview.png --------------------------------------------------------------------------------