main.lua 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140
  1. --[[ uosc | https://github.com/tomasklaen/uosc ]]
  2. local uosc_version = '5.2.0'
  3. mp.commandv('script-message', 'uosc-version', uosc_version)
  4. assdraw = require('mp.assdraw')
  5. opt = require('mp.options')
  6. utils = require('mp.utils')
  7. msg = require('mp.msg')
  8. osd = mp.create_osd_overlay('ass-events')
  9. QUARTER_PI_SIN = math.sin(math.pi / 4)
  10. require('lib/std')
  11. --[[ OPTIONS ]]
  12. defaults = {
  13. timeline_style = 'line',
  14. timeline_line_width = 2,
  15. timeline_size = 40,
  16. progress = 'windowed',
  17. progress_size = 2,
  18. progress_line_width = 20,
  19. timeline_persistency = '',
  20. timeline_border = 1,
  21. timeline_step = 5,
  22. timeline_cache = true,
  23. controls =
  24. 'menu,gap,subtitles,<has_many_audio>audio,<has_many_video>video,<has_many_edition>editions,<stream>stream-quality,gap,space,speed,space,shuffle,loop-playlist,loop-file,gap,prev,items,next,gap,fullscreen',
  25. controls_size = 32,
  26. controls_margin = 8,
  27. controls_spacing = 2,
  28. controls_persistency = '',
  29. volume = 'right',
  30. volume_size = 40,
  31. volume_persistency = '',
  32. volume_border = 1,
  33. volume_step = 1,
  34. speed_persistency = '',
  35. speed_step = 0.1,
  36. speed_step_is_factor = false,
  37. menu_item_height = 36,
  38. menu_min_width = 260,
  39. menu_padding = 4,
  40. menu_type_to_search = true,
  41. top_bar = 'no-border',
  42. top_bar_size = 40,
  43. top_bar_persistency = '',
  44. top_bar_controls = true,
  45. top_bar_title = 'yes',
  46. top_bar_alt_title = '',
  47. top_bar_alt_title_place = 'below',
  48. top_bar_flash_on = 'video,audio',
  49. window_border_size = 1,
  50. autoload = false,
  51. autoload_types = 'video,audio,image',
  52. shuffle = false,
  53. scale = 1,
  54. scale_fullscreen = 1.3,
  55. font_scale = 1,
  56. text_border = 1.2,
  57. border_radius = 4,
  58. color = '',
  59. opacity = '',
  60. animation_duration = 100,
  61. refine = '',
  62. pause_on_click_shorter_than = 0, -- deprecated by below
  63. click_threshold = 0,
  64. click_command = 'cycle pause; script-binding uosc/flash-pause-indicator',
  65. flash_duration = 1000,
  66. proximity_in = 40,
  67. proximity_out = 120,
  68. total_time = false, -- deprecated by below
  69. destination_time = 'playtime-remaining',
  70. time_precision = 0,
  71. font_bold = false,
  72. autohide = false,
  73. buffered_time_threshold = 60,
  74. pause_indicator = 'flash',
  75. stream_quality_options = '4320,2160,1440,1080,720,480,360,240,144',
  76. video_types =
  77. '3g2,3gp,asf,avi,f4v,flv,h264,h265,m2ts,m4v,mkv,mov,mp4,mp4v,mpeg,mpg,ogm,ogv,rm,rmvb,ts,vob,webm,wmv,y4m',
  78. audio_types =
  79. 'aac,ac3,aiff,ape,au,cue,dsf,dts,flac,m4a,mid,midi,mka,mp3,mp4a,oga,ogg,opus,spx,tak,tta,wav,weba,wma,wv',
  80. image_types = 'apng,avif,bmp,gif,j2k,jp2,jfif,jpeg,jpg,jxl,mj2,png,svg,tga,tif,tiff,webp',
  81. subtitle_types = 'aqt,ass,gsub,idx,jss,lrc,mks,pgs,pjs,psb,rt,sbv,slt,smi,sub,sup,srt,ssa,ssf,ttxt,txt,usf,vt,vtt',
  82. default_directory = '~/',
  83. show_hidden_files = false,
  84. use_trash = false,
  85. adjust_osd_margins = true,
  86. chapter_ranges = 'openings:30abf964,endings:30abf964,ads:c54e4e80',
  87. chapter_range_patterns = 'openings:オープニング;endings:エンディング',
  88. languages = 'slang,en',
  89. disable_elements = '',
  90. }
  91. options = table_copy(defaults)
  92. opt.read_options(options, 'uosc', function(changed_options)
  93. if changed_options.time_precision then
  94. timestamp_zero_rep_clear_cache()
  95. end
  96. update_config()
  97. update_human_times()
  98. Manager:disable('user', options.disable_elements)
  99. Elements:trigger('options')
  100. Elements:update_proximities()
  101. request_render()
  102. end)
  103. -- Normalize values
  104. options.proximity_out = math.max(options.proximity_out, options.proximity_in + 1)
  105. if options.chapter_ranges:sub(1, 4) == '^op|' then options.chapter_ranges = defaults.chapter_ranges end
  106. if options.pause_on_click_shorter_than > 0 and options.click_threshold == 0 then
  107. msg.warn('`pause_on_click_shorter_than` is deprecated. Use `click_threshold` and `click_command` instead.')
  108. options.click_threshold = options.pause_on_click_shorter_than
  109. end
  110. if options.total_time and options.destination_time == 'playtime-remaining' then
  111. msg.warn('`total_time` is deprecated. Use `destination_time` instead.')
  112. options.destination_time = 'total'
  113. elseif not itable_index_of({'total', 'playtime-remaining', 'time-remaining'}, options.destination_time) then
  114. options.destination_time = 'playtime-remaining'
  115. end
  116. -- Ensure required environment configuration
  117. if options.autoload then mp.commandv('set', 'keep-open-pause', 'no') end
  118. --[[ INTERNATIONALIZATION ]]
  119. local intl = require('lib/intl')
  120. t = intl.t
  121. require('lib/char_conv')
  122. --[[ CONFIG ]]
  123. local config_defaults = {
  124. color = {
  125. foreground = serialize_rgba('ffffff').color,
  126. foreground_text = serialize_rgba('000000').color,
  127. background = serialize_rgba('000000').color,
  128. background_text = serialize_rgba('ffffff').color,
  129. curtain = serialize_rgba('111111').color,
  130. success = serialize_rgba('a5e075').color,
  131. error = serialize_rgba('ff616e').color,
  132. },
  133. opacity = {
  134. timeline = 0.9,
  135. position = 1,
  136. chapters = 0.8,
  137. slider = 0.9,
  138. slider_gauge = 1,
  139. controls = 0,
  140. speed = 0.6,
  141. menu = 1,
  142. submenu = 0.4,
  143. border = 1,
  144. title = 1,
  145. tooltip = 1,
  146. thumbnail = 1,
  147. curtain = 0.8,
  148. idle_indicator = 0.8,
  149. audio_indicator = 0.5,
  150. buffering_indicator = 0.3,
  151. playlist_position = 0.8,
  152. },
  153. }
  154. config = {
  155. version = uosc_version,
  156. open_subtitles_api_key = 'b0rd16N0bp7DETMpO4pYZwIqmQkZbYQr',
  157. open_subtitles_agent = 'uosc v' .. uosc_version,
  158. -- sets max rendering frequency in case the
  159. -- native rendering frequency could not be detected
  160. render_delay = 1 / 60,
  161. font = mp.get_property('options/osd-font'),
  162. osd_margin_x = mp.get_property('osd-margin-x'),
  163. osd_margin_y = mp.get_property('osd-margin-y'),
  164. osd_alignment_x = mp.get_property('osd-align-x'),
  165. osd_alignment_y = mp.get_property('osd-align-y'),
  166. refine = create_set(comma_split(options.refine)),
  167. types = {
  168. video = comma_split(options.video_types),
  169. audio = comma_split(options.audio_types),
  170. image = comma_split(options.image_types),
  171. subtitle = comma_split(options.subtitle_types),
  172. media = comma_split(options.video_types .. ',' .. options.audio_types .. ',' .. options.image_types),
  173. autoload = (function()
  174. ---@type string[]
  175. local option_values = {}
  176. for _, name in ipairs(comma_split(options.autoload_types)) do
  177. local value = options[name .. '_types']
  178. if type(value) == 'string' then option_values[#option_values + 1] = value end
  179. end
  180. return comma_split(table.concat(option_values, ','))
  181. end)(),
  182. },
  183. stream_quality_options = comma_split(options.stream_quality_options),
  184. top_bar_flash_on = comma_split(options.top_bar_flash_on),
  185. chapter_ranges = (function()
  186. ---@type table<string, string[]> Alternative patterns.
  187. local alt_patterns = {}
  188. if options.chapter_range_patterns and options.chapter_range_patterns ~= '' then
  189. for _, definition in ipairs(split(options.chapter_range_patterns, ';+ *')) do
  190. local name_patterns = split(definition, ' *:')
  191. local name, patterns = name_patterns[1], name_patterns[2]
  192. if name and patterns then alt_patterns[name] = split(patterns, ',') end
  193. end
  194. end
  195. ---@type table<string, {color: string; opacity: number; patterns?: string[]}>
  196. local ranges = {}
  197. if options.chapter_ranges and options.chapter_ranges ~= '' then
  198. for _, definition in ipairs(split(options.chapter_ranges, ' *,+ *')) do
  199. local name_color = split(definition, ' *:+ *')
  200. local name, color = name_color[1], name_color[2]
  201. if name and color
  202. and name:match('^[a-zA-Z0-9_]+$') and color:match('^[a-fA-F0-9]+$')
  203. and (#color == 6 or #color == 8) then
  204. local range = serialize_rgba(name_color[2])
  205. range.patterns = alt_patterns[name]
  206. ranges[name_color[1]] = range
  207. end
  208. end
  209. end
  210. return ranges
  211. end)(),
  212. color = table_copy(config_defaults.color),
  213. opacity = table_copy(config_defaults.opacity),
  214. cursor_leave_fadeout_elements = {'timeline', 'volume', 'top_bar', 'controls'},
  215. }
  216. -- Updates config with values dependent on options
  217. function update_config()
  218. -- Adds `{element}_persistency` config properties with forced visibility states (e.g.: `{paused = true}`)
  219. for _, name in ipairs({'timeline', 'controls', 'volume', 'top_bar', 'speed'}) do
  220. local option_name = name .. '_persistency'
  221. local value, flags = options[option_name], {}
  222. if type(value) == 'string' then
  223. for _, state in ipairs(comma_split(value)) do flags[state] = true end
  224. end
  225. config[option_name] = flags
  226. end
  227. -- Opacity
  228. config.opacity = table_assign({}, config_defaults.opacity, serialize_key_value_list(options.opacity,
  229. function(value, key)
  230. return clamp(0, tonumber(value) or config.opacity[key], 1)
  231. end
  232. ))
  233. -- Color
  234. config.color = table_assign({}, config_defaults.color, serialize_key_value_list(options.color, function(value)
  235. return serialize_rgba(value).color
  236. end))
  237. -- Global color shorthands
  238. fg, bg = config.color.foreground, config.color.background
  239. fgt, bgt = config.color.foreground_text, config.color.background_text
  240. end
  241. update_config()
  242. -- Default menu items
  243. function create_default_menu_items()
  244. return {
  245. {title = t('Subtitles'), value = 'script-binding uosc/subtitles'},
  246. {title = t('Audio tracks'), value = 'script-binding uosc/audio'},
  247. {title = t('Stream quality'), value = 'script-binding uosc/stream-quality'},
  248. {title = t('Playlist'), value = 'script-binding uosc/items'},
  249. {title = t('Chapters'), value = 'script-binding uosc/chapters'},
  250. {
  251. title = t('Navigation'),
  252. items = {
  253. {
  254. title = t('Next'),
  255. hint = t('playlist or file'),
  256. value =
  257. 'script-binding uosc/next',
  258. },
  259. {
  260. title = t('Prev'),
  261. hint = t('playlist or file'),
  262. value =
  263. 'script-binding uosc/prev',
  264. },
  265. {title = t('Delete file & Next'), value = 'script-binding uosc/delete-file-next'},
  266. {title = t('Delete file & Prev'), value = 'script-binding uosc/delete-file-prev'},
  267. {title = t('Delete file & Quit'), value = 'script-binding uosc/delete-file-quit'},
  268. {title = t('Open file'), value = 'script-binding uosc/open-file'},
  269. },
  270. },
  271. {
  272. title = t('Utils'),
  273. items = {
  274. {
  275. title = t('Aspect ratio'),
  276. items = {
  277. {title = t('Default'), value = 'set video-aspect-override "-1"'},
  278. {title = '16:9', value = 'set video-aspect-override "16:9"'},
  279. {title = '4:3', value = 'set video-aspect-override "4:3"'},
  280. {title = '2.35:1', value = 'set video-aspect-override "2.35:1"'},
  281. },
  282. },
  283. {title = t('Audio devices'), value = 'script-binding uosc/audio-device'},
  284. {title = t('Editions'), value = 'script-binding uosc/editions'},
  285. {title = t('Screenshot'), value = 'async screenshot'},
  286. {title = t('Key bindings'), value = 'script-binding uosc/keybinds'},
  287. {title = t('Show in directory'), value = 'script-binding uosc/show-in-directory'},
  288. {title = t('Open config folder'), value = 'script-binding uosc/open-config-directory'},
  289. {title = t('Update uosc'), value = 'script-binding uosc/update'},
  290. },
  291. },
  292. {title = t('Quit'), value = 'quit'},
  293. }
  294. end
  295. --[[ STATE ]]
  296. display = {width = 1280, height = 720, initialized = false}
  297. cursor = require('lib/cursor')
  298. state = {
  299. platform = (function()
  300. local platform = mp.get_property_native('platform')
  301. if platform then
  302. if itable_index_of({'windows', 'darwin'}, platform) then return platform end
  303. else
  304. if os.getenv('windir') ~= nil then return 'windows' end
  305. local homedir = os.getenv('HOME')
  306. if homedir ~= nil and string.sub(homedir, 1, 6) == '/Users' then return 'darwin' end
  307. end
  308. return 'linux'
  309. end)(),
  310. cwd = mp.get_property('working-directory'),
  311. path = nil, -- current file path or URL
  312. history = {}, -- history of last played files stored as full paths
  313. title = nil,
  314. alt_title = nil,
  315. time = nil, -- current media playback time
  316. speed = 1,
  317. duration = nil, -- current media duration
  318. time_human = nil, -- current playback time in human format
  319. destination_time_human = nil, -- depends on options.destination_time
  320. pause = mp.get_property_native('pause'),
  321. chapters = {},
  322. current_chapter = nil,
  323. chapter_ranges = {},
  324. border = mp.get_property_native('border'),
  325. title_bar = mp.get_property_native('title-bar'),
  326. fullscreen = mp.get_property_native('fullscreen'),
  327. maximized = mp.get_property_native('window-maximized'),
  328. fullormaxed = mp.get_property_native('fullscreen') or mp.get_property_native('window-maximized'),
  329. render_timer = nil,
  330. render_last_time = 0,
  331. volume = nil,
  332. volume_max = nil,
  333. mute = nil,
  334. is_idle = false,
  335. is_video = false,
  336. is_audio = false, -- true if file is audio only (mp3, etc)
  337. is_image = false,
  338. is_stream = false,
  339. has_image = false,
  340. has_audio = false,
  341. has_sub = false,
  342. has_chapter = false,
  343. has_playlist = false,
  344. shuffle = options.shuffle,
  345. ---@type nil|{pos: number; paths: string[]}
  346. shuffle_history = nil,
  347. on_shuffle = function() state.shuffle_history = nil end,
  348. mouse_bindings_enabled = false,
  349. uncached_ranges = nil,
  350. cache = nil,
  351. cache_buffering = 100,
  352. cache_underrun = false,
  353. core_idle = false,
  354. eof_reached = false,
  355. render_delay = config.render_delay,
  356. playlist_count = 0,
  357. playlist_pos = 0,
  358. margin_top = 0,
  359. margin_bottom = 0,
  360. margin_left = 0,
  361. margin_right = 0,
  362. hidpi_scale = 1,
  363. scale = 1,
  364. radius = 0,
  365. }
  366. thumbnail = {width = 0, height = 0, disabled = false}
  367. external = {} -- Properties set by external scripts
  368. key_binding_overwrites = {} -- Table of key_binding:mpv_command
  369. Elements = require('elements/Elements')
  370. Menu = require('elements/Menu')
  371. -- State dependent utilities
  372. require('lib/utils')
  373. require('lib/text')
  374. require('lib/ass')
  375. require('lib/menus')
  376. -- Determine path to ziggy
  377. do
  378. local bin = 'ziggy-' .. (state.platform == 'windows' and 'windows.exe' or state.platform)
  379. config.ziggy_path = os.getenv('MPV_UOSC_ZIGGY') or join_path(mp.get_script_directory(), join_path('bin', bin))
  380. end
  381. --[[ STATE UPDATERS ]]
  382. function update_display_dimensions()
  383. state.scale = (state.hidpi_scale or 1) * (state.fullormaxed and options.scale_fullscreen or options.scale)
  384. state.radius = round(options.border_radius * state.scale)
  385. local real_width, real_height = mp.get_osd_size()
  386. if real_width <= 0 then return end
  387. display.width, display.height = real_width, real_height
  388. display.initialized = true
  389. -- Tell elements about this
  390. Elements:trigger('display')
  391. -- Some elements probably changed their rectangles as a reaction to `display`
  392. Elements:update_proximities()
  393. request_render()
  394. end
  395. function update_fullormaxed()
  396. state.fullormaxed = state.fullscreen or state.maximized
  397. update_display_dimensions()
  398. Elements:trigger('prop_fullormaxed', state.fullormaxed)
  399. cursor:leave()
  400. end
  401. function update_human_times()
  402. if state.time then
  403. state.time_human = format_time(state.time, state.duration)
  404. if state.duration then
  405. local speed = state.speed or 1
  406. if options.destination_time == 'playtime-remaining' then
  407. state.destination_time_human = format_time((state.time - state.duration) / speed, state.duration)
  408. elseif options.destination_time == 'total' then
  409. state.destination_time_human = format_time(state.duration, state.duration)
  410. else
  411. state.destination_time_human = format_time(state.time - state.duration, state.duration)
  412. end
  413. else
  414. state.destination_time_human = nil
  415. end
  416. else
  417. state.time_human = nil
  418. end
  419. end
  420. -- Notifies other scripts such as console about where the unoccupied parts of the screen are.
  421. function update_margins()
  422. if display.height == 0 then return end
  423. local function causes_margin(element)
  424. return element and element.enabled and (element:is_persistent() or element.min_visibility > 0.5)
  425. end
  426. local timeline, top_bar, controls, volume = Elements.timeline, Elements.top_bar, Elements.controls, Elements.volume
  427. -- margins are normalized to window size
  428. local left, right, top, bottom = 0, 0, 0, 0
  429. if causes_margin(controls) then
  430. bottom = (display.height - controls.ay) / display.height
  431. elseif causes_margin(timeline) then
  432. bottom = (display.height - timeline.ay) / display.height
  433. end
  434. if causes_margin(top_bar) then top = top_bar.title_by / display.height end
  435. if causes_margin(volume) then
  436. if options.volume == 'left' then
  437. left = volume.bx / display.width
  438. elseif options.volume == 'right' then
  439. right = volume.ax / display.width
  440. end
  441. end
  442. if top == state.margin_top and bottom == state.margin_bottom and
  443. left == state.margin_left and right == state.margin_right then
  444. return
  445. end
  446. state.margin_top = top
  447. state.margin_bottom = bottom
  448. state.margin_left = left
  449. state.margin_right = right
  450. if utils.shared_script_property_set then
  451. utils.shared_script_property_set('osc-margins', string.format('%f,%f,%f,%f', 0, 0, top, bottom))
  452. end
  453. mp.set_property_native('user-data/osc/margins', {l = left, r = right, t = top, b = bottom})
  454. if not options.adjust_osd_margins then return end
  455. local osd_margin_y, osd_margin_x, osd_factor_x = 0, 0, display.width / display.height * 720
  456. if config.osd_alignment_y == 'bottom' then
  457. osd_margin_y = round(bottom * 720)
  458. elseif config.osd_alignment_y == 'top' then
  459. osd_margin_y = round(top * 720)
  460. end
  461. if config.osd_alignment_x == 'left' then
  462. osd_margin_x = round(left * osd_factor_x)
  463. elseif config.osd_alignment_x == 'right' then
  464. osd_margin_x = round(right * osd_factor_x)
  465. end
  466. mp.set_property_native('osd-margin-y', osd_margin_y + config.osd_margin_y)
  467. mp.set_property_native('osd-margin-x', osd_margin_x + config.osd_margin_x)
  468. end
  469. function create_state_setter(name, callback)
  470. return function(_, value)
  471. set_state(name, value)
  472. if callback then callback() end
  473. request_render()
  474. end
  475. end
  476. function set_state(name, value)
  477. state[name] = value
  478. call_maybe(state['on_' .. name], value)
  479. Elements:trigger('prop_' .. name, value)
  480. end
  481. function handle_file_end()
  482. local resume = false
  483. if not state.loop_file then
  484. if state.has_playlist then
  485. resume = state.shuffle and navigate_playlist(1)
  486. else
  487. resume = options.autoload and navigate_directory(1)
  488. end
  489. end
  490. -- Resume only when navigation happened
  491. if resume then mp.command('set pause no') end
  492. end
  493. local file_end_timer = mp.add_timeout(1, handle_file_end)
  494. file_end_timer:kill()
  495. function load_file_index_in_current_directory(index)
  496. if not state.path or is_protocol(state.path) then return end
  497. local serialized = serialize_path(state.path)
  498. if serialized and serialized.dirname then
  499. local files = read_directory(serialized.dirname, {
  500. types = config.types.autoload,
  501. hidden = options.show_hidden_files,
  502. })
  503. if not files then return end
  504. sort_strings(files)
  505. if index < 0 then index = #files + index + 1 end
  506. if files[index] then
  507. mp.commandv('loadfile', join_path(serialized.dirname, files[index]))
  508. end
  509. end
  510. end
  511. function update_render_delay(name, fps)
  512. if fps then state.render_delay = 1 / fps end
  513. end
  514. function observe_display_fps(name, fps)
  515. if fps then
  516. mp.unobserve_property(update_render_delay)
  517. mp.unobserve_property(observe_display_fps)
  518. mp.observe_property('display-fps', 'native', update_render_delay)
  519. end
  520. end
  521. function select_current_chapter()
  522. local current_chapter
  523. if state.time and state.chapters then
  524. _, current_chapter = itable_find(state.chapters, function(c) return state.time >= c.time end, #state.chapters, 1)
  525. end
  526. set_state('current_chapter', current_chapter)
  527. end
  528. --[[ STATE HOOKS ]]
  529. -- Click detection
  530. if options.click_threshold > 0 then
  531. -- Executes custom command for clicks shorter than `options.click_threshold`
  532. -- while filtering out double clicks.
  533. local click_time = options.click_threshold / 1000
  534. local doubleclick_time = mp.get_property_native('input-doubleclick-time') / 1000
  535. local last_down, last_up = 0, 0
  536. local click_timer = mp.add_timeout(math.max(click_time, doubleclick_time), function()
  537. local delta = last_up - last_down
  538. if delta > 0 and delta < click_time and delta > 0.02 then mp.command(options.click_command) end
  539. end)
  540. click_timer:kill()
  541. local function handle_up() last_up = mp.get_time() end
  542. local function handle_down()
  543. last_down = mp.get_time()
  544. if click_timer:is_enabled() then click_timer:kill() else click_timer:resume() end
  545. end
  546. -- If this function exists, it'll be called at the beginning of render().
  547. function setup_click_detection()
  548. local hitbox = {ax = 0, ay = 0, bx = display.width, by = display.height, window_drag = true}
  549. cursor:zone('primary_down', hitbox, handle_down)
  550. cursor:zone('primary_up', hitbox, handle_up)
  551. end
  552. end
  553. mp.observe_property('osc', 'bool', function(name, value) if value == true then mp.set_property('osc', 'no') end end)
  554. mp.register_event('file-loaded', function()
  555. local path = normalize_path(mp.get_property_native('path'))
  556. itable_delete_value(state.history, path)
  557. state.history[#state.history + 1] = path
  558. set_state('path', path)
  559. -- Flash top bar on requested file types
  560. for _, type in ipairs(config.top_bar_flash_on) do
  561. if state['is_' .. type] then
  562. Elements:flash({'top_bar'})
  563. break
  564. end
  565. end
  566. end)
  567. mp.register_event('end-file', function(event)
  568. set_state('path', nil)
  569. if event.reason == 'eof' then
  570. file_end_timer:kill()
  571. handle_file_end()
  572. end
  573. end)
  574. -- Top bar titles
  575. do
  576. local function update_state_with_template(prop, template)
  577. -- escape ASS, and strip newlines and trailing slashes and trim whitespace
  578. local tmp = mp.command_native({'expand-text', template}):gsub('\\n', ' '):gsub('[\\%s]+$', ''):gsub('^%s+', '')
  579. set_state(prop, ass_escape(tmp))
  580. end
  581. local function add_template_listener(template, callback)
  582. local props = get_expansion_props(template)
  583. for prop, _ in pairs(props) do
  584. mp.observe_property(prop, 'native', callback)
  585. end
  586. if not next(props) then callback() end
  587. end
  588. local function remove_template_listener(callback) mp.unobserve_property(callback) end
  589. -- Main title
  590. if #options.top_bar_title > 0 and options.top_bar_title ~= 'no' then
  591. if options.top_bar_title == 'yes' then
  592. local template = nil
  593. local function update_title() update_state_with_template('title', template) end
  594. mp.observe_property('title', 'string', function(_, title)
  595. remove_template_listener(update_title)
  596. template = title
  597. if template then
  598. if template:sub(-6) == ' - mpv' then template = template:sub(1, -7) end
  599. add_template_listener(template, update_title)
  600. end
  601. end)
  602. elseif type(options.top_bar_title) == 'string' then
  603. add_template_listener(options.top_bar_title, function()
  604. update_state_with_template('title', options.top_bar_title)
  605. end)
  606. end
  607. end
  608. -- Alt title
  609. if #options.top_bar_alt_title > 0 and options.top_bar_alt_title ~= 'no' then
  610. add_template_listener(options.top_bar_alt_title, function()
  611. update_state_with_template('alt_title', options.top_bar_alt_title)
  612. end)
  613. end
  614. end
  615. mp.observe_property('playback-time', 'number', create_state_setter('time', function()
  616. -- Create a file-end event that triggers right before file ends
  617. file_end_timer:kill()
  618. if state.duration and state.time and not state.pause then
  619. local remaining = (state.duration - state.time) / state.speed
  620. if remaining < 5 then
  621. local timeout = remaining - 0.02
  622. if timeout > 0 then
  623. file_end_timer.timeout = timeout
  624. file_end_timer:resume()
  625. else
  626. handle_file_end()
  627. end
  628. end
  629. end
  630. update_human_times()
  631. select_current_chapter()
  632. end))
  633. mp.observe_property('duration', 'number', create_state_setter('duration', update_human_times))
  634. mp.observe_property('speed', 'number', create_state_setter('speed', update_human_times))
  635. mp.observe_property('track-list', 'native', function(name, value)
  636. -- checks the file dispositions
  637. local types = {sub = 0, image = 0, audio = 0, video = 0}
  638. for _, track in ipairs(value) do
  639. if track.type == 'video' then
  640. if track.image or track.albumart then
  641. types.image = types.image + 1
  642. else
  643. types.video = types.video + 1
  644. end
  645. elseif types[track.type] then
  646. types[track.type] = types[track.type] + 1
  647. end
  648. end
  649. set_state('is_audio', types.video == 0 and types.audio > 0)
  650. set_state('is_image', types.image > 0 and types.video == 0 and types.audio == 0)
  651. set_state('has_image', types.image > 0)
  652. set_state('has_audio', types.audio > 0)
  653. set_state('has_many_audio', types.audio > 1)
  654. set_state('has_sub', types.sub > 0)
  655. set_state('has_many_sub', types.sub > 1)
  656. set_state('is_video', types.video > 0)
  657. set_state('has_many_video', types.video > 1)
  658. Elements:trigger('dispositions')
  659. end)
  660. mp.observe_property('editions', 'number', function(_, editions)
  661. if editions then set_state('has_many_edition', editions > 1) end
  662. Elements:trigger('dispositions')
  663. end)
  664. mp.observe_property('chapter-list', 'native', function(_, chapters)
  665. local chapters, chapter_ranges = serialize_chapters(chapters), {}
  666. if chapters then chapters, chapter_ranges = serialize_chapter_ranges(chapters) end
  667. set_state('chapters', chapters)
  668. set_state('chapter_ranges', chapter_ranges)
  669. set_state('has_chapter', #chapters > 0)
  670. select_current_chapter()
  671. Elements:trigger('dispositions')
  672. end)
  673. mp.observe_property('border', 'bool', create_state_setter('border'))
  674. mp.observe_property('title-bar', 'bool', create_state_setter('title_bar'))
  675. mp.observe_property('loop-file', 'native', create_state_setter('loop_file'))
  676. mp.observe_property('ab-loop-a', 'number', create_state_setter('ab_loop_a'))
  677. mp.observe_property('ab-loop-b', 'number', create_state_setter('ab_loop_b'))
  678. mp.observe_property('playlist-pos-1', 'number', create_state_setter('playlist_pos'))
  679. mp.observe_property('playlist-count', 'number', function(_, value)
  680. set_state('playlist_count', value)
  681. set_state('has_playlist', value > 1)
  682. Elements:trigger('dispositions')
  683. end)
  684. mp.observe_property('fullscreen', 'bool', create_state_setter('fullscreen', update_fullormaxed))
  685. mp.observe_property('window-maximized', 'bool', create_state_setter('maximized', update_fullormaxed))
  686. mp.observe_property('idle-active', 'bool', function(_, idle)
  687. set_state('is_idle', idle)
  688. Elements:trigger('dispositions')
  689. end)
  690. mp.observe_property('pause', 'bool', create_state_setter('pause', function() file_end_timer:kill() end))
  691. mp.observe_property('volume', 'number', create_state_setter('volume'))
  692. mp.observe_property('volume-max', 'number', create_state_setter('volume_max'))
  693. mp.observe_property('mute', 'bool', create_state_setter('mute'))
  694. mp.observe_property('osd-dimensions', 'native', function(name, val)
  695. update_display_dimensions()
  696. request_render()
  697. end)
  698. mp.observe_property('display-hidpi-scale', 'native', create_state_setter('hidpi_scale', update_display_dimensions))
  699. mp.observe_property('cache', 'string', create_state_setter('cache'))
  700. mp.observe_property('cache-buffering-state', 'number', create_state_setter('cache_buffering'))
  701. mp.observe_property('demuxer-via-network', 'native', create_state_setter('is_stream', function()
  702. Elements:trigger('dispositions')
  703. end))
  704. mp.observe_property('demuxer-cache-state', 'native', function(prop, cache_state)
  705. local cached_ranges, bof, eof, uncached_ranges = nil, nil, nil, nil
  706. if cache_state then
  707. cached_ranges, bof, eof = cache_state['seekable-ranges'], cache_state['bof-cached'], cache_state['eof-cached']
  708. set_state('cache_underrun', cache_state['underrun'])
  709. else
  710. cached_ranges = {}
  711. end
  712. if not (state.duration and (#cached_ranges > 0 or state.cache == 'yes' or
  713. (state.cache == 'auto' and state.is_stream))) then
  714. if state.uncached_ranges then set_state('uncached_ranges', nil) end
  715. return
  716. end
  717. -- Normalize
  718. local ranges = {}
  719. for _, range in ipairs(cached_ranges) do
  720. ranges[#ranges + 1] = {
  721. math.max(range['start'] or 0, 0),
  722. math.min(range['end'] or state.duration, state.duration),
  723. }
  724. end
  725. table.sort(ranges, function(a, b) return a[1] < b[1] end)
  726. if bof then ranges[1][1] = 0 end
  727. if eof then ranges[#ranges][2] = state.duration end
  728. -- Invert cached ranges into uncached ranges, as that's what we're rendering
  729. local inverted_ranges = {{0, state.duration}}
  730. for _, cached in pairs(ranges) do
  731. inverted_ranges[#inverted_ranges][2] = cached[1]
  732. inverted_ranges[#inverted_ranges + 1] = {cached[2], state.duration}
  733. end
  734. uncached_ranges = {}
  735. local last_range = nil
  736. for _, range in ipairs(inverted_ranges) do
  737. if last_range and last_range[2] + 0.5 > range[1] then -- fuse ranges
  738. last_range[2] = range[2]
  739. else
  740. if range[2] - range[1] > 0.5 then -- skip short ranges
  741. uncached_ranges[#uncached_ranges + 1] = range
  742. last_range = range
  743. end
  744. end
  745. end
  746. set_state('uncached_ranges', uncached_ranges)
  747. end)
  748. mp.observe_property('display-fps', 'native', observe_display_fps)
  749. mp.observe_property('estimated-display-fps', 'native', update_render_delay)
  750. mp.observe_property('eof-reached', 'native', create_state_setter('eof_reached'))
  751. mp.observe_property('core-idle', 'native', create_state_setter('core_idle'))
  752. --[[ KEY BINDS ]]
  753. -- Adds a key binding that respects rerouting set by `key_binding_overwrites` table.
  754. ---@param name string
  755. ---@param callback fun(event: table)
  756. ---@param flags nil|string
  757. function bind_command(name, callback, flags)
  758. mp.add_key_binding(nil, name, function(...)
  759. if key_binding_overwrites[name] then
  760. mp.command(key_binding_overwrites[name])
  761. else
  762. callback(...)
  763. end
  764. end, flags)
  765. end
  766. bind_command('toggle-ui', function() Elements:toggle({'timeline', 'controls', 'volume', 'top_bar'}) end)
  767. bind_command('flash-ui', function() Elements:flash({'timeline', 'controls', 'volume', 'top_bar'}) end)
  768. bind_command('flash-timeline', function() Elements:flash({'timeline'}) end)
  769. bind_command('flash-top-bar', function() Elements:flash({'top_bar'}) end)
  770. bind_command('flash-volume', function() Elements:flash({'volume'}) end)
  771. bind_command('flash-speed', function() Elements:flash({'speed'}) end)
  772. bind_command('flash-pause-indicator', function() Elements:flash({'pause_indicator'}) end)
  773. bind_command('flash-progress', function() Elements:flash({'progress'}) end)
  774. bind_command('toggle-progress', function() Elements:maybe('timeline', 'toggle_progress') end)
  775. bind_command('toggle-title', function() Elements:maybe('top_bar', 'toggle_title') end)
  776. bind_command('decide-pause-indicator', function() Elements:maybe('pause_indicator', 'decide') end)
  777. bind_command('menu', function() toggle_menu_with_items() end)
  778. bind_command('menu-blurred', function() toggle_menu_with_items({mouse_nav = true}) end)
  779. bind_command('keybinds', function()
  780. if Menu:is_open('keybinds') then
  781. Menu:close()
  782. else
  783. open_command_menu({type = 'keybinds', items = get_keybinds_items(), search_style = 'palette'})
  784. end
  785. end)
  786. bind_command('download-subtitles', open_subtitle_downloader)
  787. bind_command('load-subtitles', create_track_loader_menu_opener({
  788. name = 'subtitles', prop = 'sub', allowed_types = itable_join(config.types.video, config.types.subtitle),
  789. }))
  790. bind_command('load-audio', create_track_loader_menu_opener({
  791. name = 'audio', prop = 'audio', allowed_types = itable_join(config.types.video, config.types.audio),
  792. }))
  793. bind_command('load-video', create_track_loader_menu_opener({
  794. name = 'video', prop = 'video', allowed_types = config.types.video,
  795. }))
  796. bind_command('subtitles', create_select_tracklist_type_menu_opener(
  797. t('Subtitles'), 'sub', 'sid', 'script-binding uosc/load-subtitles', 'script-binding uosc/download-subtitles'
  798. ))
  799. bind_command('audio', create_select_tracklist_type_menu_opener(
  800. t('Audio'), 'audio', 'aid', 'script-binding uosc/load-audio'
  801. ))
  802. bind_command('video', create_select_tracklist_type_menu_opener(
  803. t('Video'), 'video', 'vid', 'script-binding uosc/load-video'
  804. ))
  805. bind_command('playlist', create_self_updating_menu_opener({
  806. title = t('Playlist'),
  807. type = 'playlist',
  808. list_prop = 'playlist',
  809. serializer = function(playlist)
  810. local items = {}
  811. for index, item in ipairs(playlist) do
  812. local is_url = is_protocol(item.filename)
  813. local item_title = type(item.title) == 'string' and #item.title > 0 and item.title or false
  814. items[index] = {
  815. title = item_title or (is_url and item.filename or serialize_path(item.filename).basename),
  816. hint = tostring(index),
  817. active = item.current,
  818. value = index,
  819. }
  820. end
  821. return items
  822. end,
  823. on_select = function(index) mp.commandv('set', 'playlist-pos-1', tostring(index)) end,
  824. on_move_item = function(from, to)
  825. mp.commandv('playlist-move', tostring(math.max(from, to) - 1), tostring(math.min(from, to) - 1))
  826. end,
  827. on_delete_item = function(index) mp.commandv('playlist-remove', tostring(index - 1)) end,
  828. }))
  829. bind_command('chapters', create_self_updating_menu_opener({
  830. title = t('Chapters'),
  831. type = 'chapters',
  832. list_prop = 'chapter-list',
  833. active_prop = 'chapter',
  834. serializer = function(chapters, current_chapter)
  835. local items = {}
  836. chapters = normalize_chapters(chapters)
  837. for index, chapter in ipairs(chapters) do
  838. items[index] = {
  839. title = chapter.title or '',
  840. hint = format_time(chapter.time, state.duration),
  841. value = index,
  842. active = index - 1 == current_chapter,
  843. }
  844. end
  845. return items
  846. end,
  847. on_select = function(index) mp.commandv('set', 'chapter', tostring(index - 1)) end,
  848. }))
  849. bind_command('editions', create_self_updating_menu_opener({
  850. title = t('Editions'),
  851. type = 'editions',
  852. list_prop = 'edition-list',
  853. active_prop = 'current-edition',
  854. serializer = function(editions, current_id)
  855. local items = {}
  856. for _, edition in ipairs(editions or {}) do
  857. local edition_id_1 = tostring(edition.id + 1)
  858. items[#items + 1] = {
  859. title = edition.title or t('Edition %s', edition_id_1),
  860. hint = edition_id_1,
  861. value = edition.id,
  862. active = edition.id == current_id,
  863. }
  864. end
  865. return items
  866. end,
  867. on_select = function(id) mp.commandv('set', 'edition', id) end,
  868. }))
  869. bind_command('show-in-directory', function()
  870. -- Ignore URLs
  871. if not state.path or is_protocol(state.path) then return end
  872. if state.platform == 'windows' then
  873. utils.subprocess_detached({args = {'explorer', '/select,', state.path .. ' '}, cancellable = false})
  874. elseif state.platform == 'darwin' then
  875. utils.subprocess_detached({args = {'open', '-R', state.path}, cancellable = false})
  876. elseif state.platform == 'linux' then
  877. local result = utils.subprocess({args = {'nautilus', state.path}, cancellable = false})
  878. -- Fallback opens the folder with xdg-open instead
  879. if result.status ~= 0 then
  880. utils.subprocess({args = {'xdg-open', serialize_path(state.path).dirname}, cancellable = false})
  881. end
  882. end
  883. end)
  884. bind_command('stream-quality', open_stream_quality_menu)
  885. bind_command('open-file', open_open_file_menu)
  886. bind_command('shuffle', function() set_state('shuffle', not state.shuffle) end)
  887. bind_command('items', function()
  888. if state.has_playlist then
  889. mp.command('script-binding uosc/playlist')
  890. else
  891. mp.command('script-binding uosc/open-file')
  892. end
  893. end)
  894. bind_command('next', function() navigate_item(1) end)
  895. bind_command('prev', function() navigate_item(-1) end)
  896. bind_command('next-file', function() navigate_directory(1) end)
  897. bind_command('prev-file', function() navigate_directory(-1) end)
  898. bind_command('first', function()
  899. if state.has_playlist then
  900. mp.commandv('set', 'playlist-pos-1', '1')
  901. else
  902. load_file_index_in_current_directory(1)
  903. end
  904. end)
  905. bind_command('last', function()
  906. if state.has_playlist then
  907. mp.commandv('set', 'playlist-pos-1', tostring(state.playlist_count))
  908. else
  909. load_file_index_in_current_directory(-1)
  910. end
  911. end)
  912. bind_command('first-file', function() load_file_index_in_current_directory(1) end)
  913. bind_command('last-file', function() load_file_index_in_current_directory(-1) end)
  914. bind_command('delete-file-prev', function() delete_file_navigate(-1) end)
  915. bind_command('delete-file-next', function() delete_file_navigate(1) end)
  916. bind_command('delete-file-quit', function()
  917. mp.command('stop')
  918. if state.path and not is_protocol(state.path) then delete_file(state.path) end
  919. mp.command('quit')
  920. end)
  921. bind_command('audio-device', create_self_updating_menu_opener({
  922. title = t('Audio devices'),
  923. type = 'audio-device-list',
  924. list_prop = 'audio-device-list',
  925. active_prop = 'audio-device',
  926. serializer = function(audio_device_list, current_device)
  927. current_device = current_device or 'auto'
  928. local ao = mp.get_property('current-ao') or ''
  929. local items = {}
  930. for _, device in ipairs(audio_device_list) do
  931. if device.name == 'auto' or string.match(device.name, '^' .. ao) then
  932. local hint = string.match(device.name, ao .. '/(.+)')
  933. if not hint then hint = device.name end
  934. items[#items + 1] = {
  935. title = device.description:sub(1, 7) == 'Default'
  936. and t('Default %s', device.description:sub(9))
  937. or device.description,
  938. hint = hint,
  939. active = device.name == current_device,
  940. value = device.name,
  941. }
  942. end
  943. end
  944. return items
  945. end,
  946. on_select = function(name) mp.commandv('set', 'audio-device', name) end,
  947. }))
  948. bind_command('open-config-directory', function()
  949. local config_path = mp.command_native({'expand-path', '~~/mpv.conf'})
  950. local config = serialize_path(normalize_path(config_path))
  951. if config then
  952. local args
  953. if state.platform == 'windows' then
  954. args = {'explorer', '/select,', config.path}
  955. elseif state.platform == 'darwin' then
  956. args = {'open', '-R', config.path}
  957. elseif state.platform == 'linux' then
  958. args = {'xdg-open', config.dirname}
  959. end
  960. utils.subprocess_detached({args = args, cancellable = false})
  961. else
  962. msg.error('Couldn\'t serialize config path "' .. config_path .. '".')
  963. end
  964. end)
  965. bind_command('update', function()
  966. if not Elements:has('updater') then require('elements/Updater'):new() end
  967. end)
  968. --[[ MESSAGE HANDLERS ]]
  969. mp.register_script_message('show-submenu', function(id) toggle_menu_with_items({submenu = id}) end)
  970. mp.register_script_message('show-submenu-blurred', function(id)
  971. toggle_menu_with_items({submenu = id, mouse_nav = true})
  972. end)
  973. mp.register_script_message('open-menu', function(json, submenu_id)
  974. local data = utils.parse_json(json)
  975. if type(data) ~= 'table' or type(data.items) ~= 'table' then
  976. msg.error('open-menu: received json didn\'t produce a table with menu configuration')
  977. else
  978. open_command_menu(data, {submenu = submenu_id, on_close = data.on_close})
  979. end
  980. end)
  981. mp.register_script_message('update-menu', function(json)
  982. local data = utils.parse_json(json)
  983. if type(data) ~= 'table' or type(data.items) ~= 'table' then
  984. msg.error('update-menu: received json didn\'t produce a table with menu configuration')
  985. else
  986. local menu = data.type and Menu:is_open(data.type)
  987. if menu then menu:update(data) end
  988. end
  989. end)
  990. mp.register_script_message('close-menu', function(type)
  991. if Menu:is_open(type) then Menu:close() end
  992. end)
  993. mp.register_script_message('thumbfast-info', function(json)
  994. local data = utils.parse_json(json)
  995. if type(data) ~= 'table' or not data.width or not data.height then
  996. thumbnail.disabled = true
  997. msg.error('thumbfast-info: received json didn\'t produce a table with thumbnail information')
  998. else
  999. thumbnail = data
  1000. request_render()
  1001. end
  1002. end)
  1003. mp.register_script_message('set', function(name, value)
  1004. external[name] = value
  1005. Elements:trigger('external_prop_' .. name, value)
  1006. end)
  1007. mp.register_script_message('toggle-elements', function(elements) Elements:toggle(comma_split(elements)) end)
  1008. mp.register_script_message('set-min-visibility', function(visibility, elements)
  1009. local fraction = tonumber(visibility)
  1010. local ids = comma_split(elements and elements ~= '' and elements or 'timeline,controls,volume,top_bar')
  1011. if fraction then Elements:set_min_visibility(clamp(0, fraction, 1), ids) end
  1012. end)
  1013. mp.register_script_message('flash-elements', function(elements) Elements:flash(comma_split(elements)) end)
  1014. mp.register_script_message('overwrite-binding', function(name, command) key_binding_overwrites[name] = command end)
  1015. mp.register_script_message('disable-elements', function(id, elements) Manager:disable(id, elements) end)
  1016. --[[ ELEMENTS ]]
  1017. -- Dynamic elements
  1018. local constructors = {
  1019. window_border = require('elements/WindowBorder'),
  1020. buffering_indicator = require('elements/BufferingIndicator'),
  1021. pause_indicator = require('elements/PauseIndicator'),
  1022. top_bar = require('elements/TopBar'),
  1023. timeline = require('elements/Timeline'),
  1024. controls = options.controls and options.controls ~= 'never' and require('elements/Controls'),
  1025. volume = itable_index_of({'left', 'right'}, options.volume) and require('elements/Volume'),
  1026. }
  1027. -- Required elements
  1028. require('elements/Curtain'):new()
  1029. -- Element manager
  1030. -- Handles creating and destroying elements based on disabled_elements user+script config.
  1031. Manager = {
  1032. -- Managed disable-able element IDs
  1033. _ids = itable_join(table_keys(constructors), {'idle_indicator', 'audio_indicator'}),
  1034. ---@type table<string, string[]> A map of clients and a list of element ids they disable
  1035. _disabled_by = {},
  1036. ---@type table<string, boolean>
  1037. disabled = {},
  1038. }
  1039. -- Set client and which elements it wishes disabled. To undo just pass an empty `element_ids` for the same `client`.
  1040. ---@param client string
  1041. ---@param element_ids string|string[]|nil `foo,bar` or `{'foo', 'bar'}`.
  1042. function Manager:disable(client, element_ids)
  1043. self._disabled_by[client] = comma_split(element_ids)
  1044. self.disabled = create_set(itable_join(unpack(table_values(self._disabled_by))))
  1045. self:_commit()
  1046. end
  1047. function Manager:_commit()
  1048. -- Create and destroy elements as needed
  1049. for _, id in ipairs(self._ids) do
  1050. local constructor = constructors[id]
  1051. if not self.disabled[id] then
  1052. if not Elements:has(id) and constructor then constructor:new() end
  1053. else
  1054. Elements:maybe(id, 'destroy')
  1055. end
  1056. end
  1057. -- We use `on_display` event to tell elements to update their dimensions
  1058. Elements:trigger('display')
  1059. end
  1060. -- Initial commit
  1061. Manager:disable('user', options.disable_elements)