gallery.lua (33679B)
1 local utils = require 'mp.utils' 2 local msg = require 'mp.msg' 3 local assdraw = require 'mp.assdraw' 4 5 local on_windows = (package.config:sub(1,1) ~= "/") 6 7 local opts = { 8 thumbs_dir = "/tmp/thumb", 9 auto_generate_thumbnails = true, 10 generate_thumbnails_with_mpv = false, 11 12 thumbnail_width = 192, 13 thumbnail_height = 108, 14 dynamic_thumbnail_size = "", 15 16 take_thumbnail_at = "20%", 17 18 resume_when_picking = true, 19 start_gallery_on_startup = false, 20 start_gallery_on_file_end = false, 21 toggle_behaves_as_accept = true, 22 23 margin_x = 15, 24 margin_y = 15, 25 max_thumbnails = 64, 26 27 show_scrollbar = true, 28 scrollbar_side = "left", 29 scrollbar_min_size = 10, 30 31 show_placeholders = true, 32 placeholder_color = "222222", 33 always_show_placeholders = false, 34 background = "0.1", 35 36 show_filename = true, 37 show_title = true, 38 strip_directory = true, 39 strip_extension = true, 40 text_size = 28, 41 42 selected_frame_color = "DDDDDD", 43 frame_roundness = 5, 44 flagged_frame_color = "5B9769", 45 selected_flagged_frame_color = "BAFFCA", 46 flagged_file_path = "/tmp/mpv.list", 47 48 max_generators = 8, 49 50 mouse_support = true, 51 UP = "k", 52 DOWN = "j", 53 LEFT = "h", 54 RIGHT = "l", 55 PAGE_UP = "PGUP", 56 PAGE_DOWN = "PGDWN", 57 FIRST = "HOME", 58 LAST = "END", 59 RANDOM = "r", 60 ACCEPT = "ENTER", 61 CANCEL = "ESC", 62 REMOVE = "DEL", 63 FLAG = "SPACE", 64 } 65 (require 'mp.options').read_options(opts) 66 67 function split(input, char, tonum) 68 local ret = {} 69 for str in string.gmatch(input, "([^" .. char .. "]+)") do 70 ret[#ret + 1] = (not tonum and str) or tonumber(str) 71 end 72 return ret 73 end 74 opts.dynamic_thumbnail_size = split(opts.dynamic_thumbnail_size, ";", false) 75 for i = 1, #opts.dynamic_thumbnail_size do 76 local preset = split(opts.dynamic_thumbnail_size[i], ",", true) 77 if (#preset ~= 3) or not (preset[1] and preset[2] and preset[3]) then 78 msg.error(opts.dynamic_thumbnail_size[i] .. " is not a valid preset") 79 return 80 end 81 opts.dynamic_thumbnail_size[i] = preset 82 end 83 84 if on_windows then 85 opts.thumbs_dir = string.gsub(opts.thumbs_dir, "^%%APPDATA%%", os.getenv("APPDATA") or "%APPDATA%") 86 else 87 opts.thumbs_dir = string.gsub(opts.thumbs_dir, "^~", os.getenv("HOME") or "~") 88 end 89 opts.max_thumbnails = math.min(opts.max_thumbnails, 64) 90 91 local sha256 92 --[[ 93 minified code below is a combination of: 94 -sha256 implementation from 95 http://lua-users.org/wiki/SecureHashAlgorithm 96 -lua implementation of bit32 (used as fallback on lua5.1) from 97 https://www.snpedia.com/extensions/Scribunto/engines/LuaCommon/lualib/bit32.lua 98 both are licensed under the MIT below: 99 100 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 101 The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 102 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 103 --]] 104 do local b,c,d,e,f;if bit32 then b,c,d,e,f=bit32.band,bit32.rrotate,bit32.bxor,bit32.rshift,bit32.bnot else f=function(g)g=math.floor(tonumber(g))%0x100000000;return(-g-1)%0x100000000 end;local h={[0]={[0]=0,0,0,0},[1]={[0]=0,1,0,1},[2]={[0]=0,0,2,2},[3]={[0]=0,1,2,3}}local i={[0]={[0]=0,1,2,3},[1]={[0]=1,0,3,2},[2]={[0]=2,3,0,1},[3]={[0]=3,2,1,0}}local function j(k,l,m,n,o)for p=1,m do l[p]=math.floor(tonumber(l[p]))%0x100000000 end;local q=1;local r=0;for s=0,31,2 do local t=n;for p=1,m do t=o[t][l[p]%4]l[p]=math.floor(l[p]/4)end;r=r+t*q;q=q*4 end;return r end;b=function(...)return j('band',{...},select('#',...),3,h)end;d=function(...)return j('bxor',{...},select('#',...),0,i)end;e=function(g,u)g=math.floor(tonumber(g))%0x100000000;u=math.floor(tonumber(u))u=math.min(math.max(-32,u),32)return math.floor(g/2^u)%0x100000000 end;c=function(g,u)g=math.floor(tonumber(g))%0x100000000;u=-math.floor(tonumber(u))%32;local g=g*2^u;return g%0x100000000+math.floor(g/0x100000000)end end;local v={0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967,0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2}local function w(n)return string.gsub(n,".",function(t)return string.format("%02x",string.byte(t))end)end;local function x(y,z)local n=""for p=1,z do local A=y%256;n=string.char(A)..n;y=(y-A)/256 end;return n end;local function B(n,p)local z=0;for p=p,p+3 do z=z*256+string.byte(n,p)end;return z end;local function C(D,E)local F=-(E+1+8)%64;E=x(8*E,8)D=D.."\128"..string.rep("\0",F)..E;return D end;local function G(H)H[1]=0x6a09e667;H[2]=0xbb67ae85;H[3]=0x3c6ef372;H[4]=0xa54ff53a;H[5]=0x510e527f;H[6]=0x9b05688c;H[7]=0x1f83d9ab;H[8]=0x5be0cd19;return H end;local function I(D,p,H)local J={}for K=1,16 do J[K]=B(D,p+(K-1)*4)end;for K=17,64 do local L=J[K-15]local M=d(c(L,7),c(L,18),e(L,3))L=J[K-2]local N=d(c(L,17),c(L,19),e(L,10))J[K]=J[K-16]+M+J[K-7]+N end;local O,s,t,P,Q,R,S,T=H[1],H[2],H[3],H[4],H[5],H[6],H[7],H[8]for p=1,64 do local M=d(c(O,2),c(O,13),c(O,22))local U=d(b(O,s),b(O,t),b(s,t))local V=M+U;local N=d(c(Q,6),c(Q,11),c(Q,25))local W=d(b(Q,R),b(f(Q),S))local X=T+N+W+v[p]+J[p]T=S;S=R;R=Q;Q=P+X;P=t;t=s;s=O;O=X+V end;H[1]=b(H[1]+O)H[2]=b(H[2]+s)H[3]=b(H[3]+t)H[4]=b(H[4]+P)H[5]=b(H[5]+Q)H[6]=b(H[6]+R)H[7]=b(H[7]+S)H[8]=b(H[8]+T)end;local function Y(H)return w(x(H[1],4)..x(H[2],4)..x(H[3],4)..x(H[4],4)..x(H[5],4)..x(H[6],4)..x(H[7],4)..x(H[8],4))end;local Z={}sha256=function(D)D=C(D,#D)local H=G(Z)for p=1,#D,64 do I(D,p,H)end;return Y(H)end end 105 -- end of sha code 106 107 active = false 108 playlist = {} -- copy of the current "playlist" property 109 geometry = { 110 window_w = 0, 111 window_h = 0, 112 rows = 0, 113 columns = 0, 114 size_x = 0, 115 size_y = 0, 116 margin_x = 0, 117 margin_y = 0, 118 } 119 view = { -- 1-based indices into the "playlist" array 120 first = 0, -- must be equal to N*columns 121 last = 0, -- must be > first and <= first + rows*columns 122 } 123 hash_cache = {} 124 overlays = { 125 active = {}, -- array of 64 strings indicating the file associated to the current thumbnail (empty if no file) 126 missing = {}, -- maps hashes of missing thumbnails to the index they should be shown at 127 } 128 selection = { 129 old = 0, -- the playlist element selected when entering the gallery 130 now = 0, -- the currently selected element 131 } 132 pending = { 133 selection = -1, 134 window_size_changed = false, 135 deletion = false, 136 } 137 ass = { 138 selection = "", 139 scrollbar = "", 140 placeholders = "", 141 } 142 flags = {} 143 resume = {} -- maps filenames to a {time=,vid=,aid=,sid=} tuple 144 misc = { 145 old_force_window = "", 146 old_geometry = "", 147 old_osd_level = "", 148 old_background = "", 149 old_idle = "", 150 } 151 generators = {} -- list of generator scripts that have registered themselves 152 153 do 154 local inited = false 155 function init() 156 if not inited then 157 inited = true 158 if utils.file_info then -- 0.28+ 159 local res = utils.file_info(opts.thumbs_dir) 160 if not res or not res.is_dir then 161 msg.error(string.format("Thumbnail directory \"%s\" does not exist", opts.thumbs_dir)) 162 os.execute("mkdir " .. opts.thumbs_dir) 163 end 164 end 165 end 166 end 167 end 168 169 function file_exists(path) 170 if utils.file_info then -- 0.28+ 171 local info = utils.file_info(path) 172 return info ~= nil and info.is_file 173 else 174 local f = io.open(path, "r") 175 if f ~= nil then 176 io.close(f) 177 return true 178 end 179 return false 180 end 181 end 182 183 function thumbnail_size_from_presets(window_w, window_h) 184 local size = window_w * window_h 185 local picked = nil 186 for _, preset in ipairs(opts.dynamic_thumbnail_size) do 187 picked = { preset[2], preset[3] } 188 if size <= preset[1] then 189 break 190 end 191 end 192 return picked 193 end 194 195 function select_under_cursor() 196 local g = geometry 197 local mx, my = mp.get_mouse_pos() 198 if mx < 0 or my < 0 or mx > g.window_w or my > g.window_h then return end 199 local mx, my = mx - g.margin_x, my - g.margin_y 200 local on_column = (mx % (g.size_x + g.margin_x)) < g.size_x 201 local on_row = (my % (g.size_y + g.margin_y)) < g.size_y 202 if on_column and on_row then 203 local column = math.floor(mx / (g.size_x + g.margin_x)) 204 local row = math.floor(my / (g.size_y + g.margin_y)) 205 local new_sel = view.first + row * g.columns + column 206 if new_sel > view.last then return end 207 if selection.now == new_sel then 208 quit_gallery_view(selection.now) 209 else 210 selection.now = new_sel 211 pending.selection = new_sel 212 ass_show(true, false, false) 213 end 214 end 215 end 216 217 function toggle_selection_flag() 218 local name = playlist[selection.now].filename 219 if flags[name] == nil then 220 flags[name] = true 221 else 222 flags[name] = nil 223 end 224 ass_show(true, false, false) 225 end 226 227 do 228 local function increment_func(increment, clamp) 229 local new = pending.selection == -1 and selection.now or pending.selection 230 new = new + increment 231 if new <= 0 or new > #playlist then 232 if not clamp then return end 233 new = math.max(1, math.min(new, #playlist)) 234 end 235 pending.selection = new 236 end 237 238 local bindings_repeat = {} 239 bindings_repeat[opts.UP] = function() increment_func(- geometry.columns, false) end 240 bindings_repeat[opts.DOWN] = function() increment_func( geometry.columns, false) end 241 bindings_repeat[opts.LEFT] = function() increment_func(- 1, false) end 242 bindings_repeat[opts.RIGHT] = function() increment_func( 1, false) end 243 bindings_repeat[opts.PAGE_UP] = function() increment_func(- geometry.columns * geometry.rows, true) end 244 bindings_repeat[opts.PAGE_DOWN] = function() increment_func( geometry.columns * geometry.rows, true) end 245 bindings_repeat[opts.RANDOM] = function() pending.selection = math.random(1, #playlist) end 246 bindings_repeat[opts.REMOVE] = function() pending.deletion = true end 247 248 local bindings = {} 249 bindings[opts.FIRST] = function() pending.selection = 1 end 250 bindings[opts.LAST] = function() pending.selection = #playlist end 251 bindings[opts.ACCEPT] = function() quit_gallery_view(selection.now) end 252 bindings[opts.CANCEL] = function() quit_gallery_view(selection.old) end 253 bindings[opts.FLAG] = toggle_selection_flag 254 if opts.mouse_support then 255 bindings["MBTN_LEFT"] = select_under_cursor 256 bindings["WHEEL_UP"] = function() increment_func(- geometry.columns, true) end 257 bindings["WHEEL_DOWN"] = function() increment_func( geometry.columns, true) end 258 end 259 260 local function window_size_changed() 261 pending.window_size_changed = true 262 end 263 264 local function idle_handler() 265 if pending.selection ~= -1 then 266 selection.now = pending.selection 267 pending.selection = -1 268 ensure_view_valid() 269 refresh_overlays(false) 270 ass_show(true, true, true) 271 end 272 if pending.window_size_changed then 273 pending.window_size_changed = false 274 local window_w, window_h = mp.get_osd_size() 275 if window_w ~= geometry.window_w or window_h ~= geometry.window_h then 276 compute_geometry(window_w, window_h) 277 if geometry.rows <= 0 or geometry.columns <= 0 then 278 quit_gallery_view(selection.old) 279 return 280 end 281 ensure_view_valid() 282 refresh_overlays(true) 283 ass_show(true, true, true) 284 end 285 end 286 if pending.deletion then 287 pending.deletion = false 288 mp.commandv("playlist-remove", selection.now - 1) 289 selection.now = selection.now + (selection.now == #playlist and -1 or 1) 290 end 291 end 292 293 function setup_ui_handlers() 294 for key, func in pairs(bindings_repeat) do 295 mp.add_forced_key_binding(key, "gallery-view-"..key, func, {repeatable = true}) 296 end 297 for key, func in pairs(bindings) do 298 mp.add_forced_key_binding(key, "gallery-view-"..key, func) 299 end 300 for _, prop in ipairs({ "osd-width", "osd-height" }) do 301 mp.observe_property(prop, "native", window_size_changed) 302 end 303 mp.register_idle(idle_handler) 304 end 305 306 function teardown_ui_handlers() 307 for key, _ in pairs(bindings_repeat) do 308 mp.remove_key_binding("gallery-view-"..key) 309 end 310 for key, _ in pairs(bindings) do 311 mp.remove_key_binding("gallery-view-"..key) 312 end 313 mp.unobserve_property(window_size_changed) 314 mp.unregister_idle(idle_handler) 315 end 316 end 317 318 function resume_playback(select) 319 -- what a mess 320 local s = resume[playlist[select].filename] 321 local pos = mp.get_property_number("playlist-pos-1") 322 if pos == select then 323 if s and opts.resume_when_picking then 324 mp.commandv("seek", s.time, "absolute") 325 end 326 mp.set_property("vid", s and s.vid or "1") 327 mp.set_property("aid", s and s.aid or "1") 328 mp.set_property("sid", s and s.sid or "1") 329 mp.set_property_bool("pause", false) 330 else 331 if s then 332 local func 333 func = function() 334 local change_maybe = function(prop, val) 335 if val ~= mp.get_property(prop) then 336 mp.set_property(prop,val) 337 end 338 end 339 change_maybe("vid", s.vid) 340 change_maybe("aid", s.aid) 341 change_maybe("sid", s.sid) 342 if opts.resume_when_picking then 343 mp.commandv("seek", s.time, "absolute") 344 end 345 mp.unregister_event(func) 346 end 347 mp.register_event("file-loaded", func) 348 end 349 mp.set_property("playlist-pos-1", select) 350 mp.set_property("vid", "1") 351 mp.set_property("aid", "1") 352 mp.set_property("sid", "1") 353 mp.set_property_bool("pause", false) 354 end 355 end 356 357 function restore_properties() 358 mp.set_property("force-window", misc.old_force_window) 359 mp.set_property("track-auto-selection", misc.old_track_auto_selection) 360 mp.set_property("geometry", misc.old_geometry) 361 mp.set_property("osd-level", misc.old_osd_level) 362 mp.set_property("background", misc.old_background) 363 mp.set_property("idle", misc.old_idle) 364 mp.commandv("script-message", "osc-visibility", "auto", "true") 365 end 366 367 function save_properties() 368 misc.old_force_window = mp.get_property("force-window") 369 misc.old_track_auto_selection = mp.get_property("track-auto-selection") 370 misc.old_geometry = mp.get_property("geometry") 371 misc.old_osd_level = mp.get_property("osd-level") 372 misc.old_background = mp.get_property("background") 373 misc.old_idle = mp.get_property("idle") 374 mp.set_property_bool("force-window", true) 375 mp.set_property_bool("track-auto-selection", false) 376 mp.set_property_number("osd-level", 0) 377 mp.set_property("background", opts.background) 378 mp.set_property_bool("idle", true) 379 mp.commandv("no-osd", "script-message", "osc-visibility", "never", "true") 380 mp.set_property("geometry", geometry.window_w .. "x" .. geometry.window_h) 381 end 382 383 function compute_geometry(ww, wh) 384 geometry.window_w, geometry.window_h = ww, wh 385 386 local dyn_thumb_size = thumbnail_size_from_presets(ww, wh) 387 if dyn_thumb_size then 388 geometry.size_x = dyn_thumb_size[1] 389 geometry.size_y = dyn_thumb_size[2] 390 else 391 geometry.size_x = opts.thumbnail_width 392 geometry.size_y = opts.thumbnail_height 393 end 394 395 local margin_y = opts.show_filename and math.max(opts.text_size, opts.margin_y) or opts.margin_y 396 geometry.rows = math.floor((wh - margin_y) / (geometry.size_y + margin_y)) 397 geometry.columns = math.floor((ww - opts.margin_x) / (geometry.size_x + opts.margin_x)) 398 if (geometry.rows * geometry.columns > opts.max_thumbnails) then 399 local r = math.sqrt(geometry.rows * geometry.columns / opts.max_thumbnails) 400 geometry.rows = math.floor(geometry.rows / r) 401 geometry.columns = math.floor(geometry.columns / r) 402 end 403 geometry.margin_x = (ww - geometry.columns * geometry.size_x) / (geometry.columns + 1) 404 geometry.margin_y = (wh - geometry.rows * geometry.size_y) / (geometry.rows + 1) 405 end 406 407 -- makes sure that view.first and view.last are valid with regards to the playlist 408 -- and that selection.now is within the view 409 -- to be called after the playlist, view or selection was modified somehow 410 function ensure_view_valid() 411 local selection_row = math.floor((selection.now - 1) / geometry.columns) 412 local max_thumbs = geometry.rows * geometry.columns 413 414 if view.last >= #playlist then 415 view.last = #playlist 416 last_row = math.floor((view.last - 1) / geometry.columns) 417 first_row = math.max(0, last_row - geometry.rows + 1) 418 view.first = 1 + first_row * geometry.columns 419 elseif view.first == 0 or view.last == 0 or view.last - view.first + 1 ~= max_thumbs then 420 -- special case: the number of possible thumbnails was changed 421 -- just recreate the view such that the selection is in the middle row 422 local max_row = (#playlist - 1) / geometry.columns + 1 423 local row_first = selection_row - math.floor((geometry.rows - 1) / 2) 424 local row_last = selection_row + math.floor((geometry.rows - 1) / 2) + geometry.rows % 2 425 if row_first < 0 then 426 row_first = 0 427 elseif row_last > max_row then 428 row_first = max_row - geometry.rows + 1 429 end 430 view.first = 1 + row_first * geometry.columns 431 view.last = math.min(#playlist, view.first - 1 + max_thumbs) 432 return 433 end 434 435 if selection.now < view.first then 436 -- the selection is now on the first line 437 view.first = selection_row * geometry.columns + 1 438 view.last = math.min(#playlist, view.first + max_thumbs - 1) 439 elseif selection.now > view.last then 440 -- the selection is now on the last line 441 view.last = (selection_row + 1) * geometry.columns 442 view.first = math.max(1, view.last - max_thumbs + 1) 443 view.last = math.min(#playlist, view.last) 444 end 445 end 446 447 -- ass related stuff 448 do 449 local function refresh_placeholders() 450 if not opts.show_placeholders then return end 451 local a = assdraw.ass_new() 452 a:new_event() 453 a:append('{\\bord0}') 454 a:append('{\\shad0}') 455 a:append('{\\1c&' .. opts.placeholder_color .. '}') 456 a:pos(0, 0) 457 a:draw_start() 458 for i = 0, view.last - view.first do 459 if opts.always_show_placeholders or overlays.active[i + 1] == "" then 460 local x = geometry.margin_x + (geometry.margin_x + geometry.size_x) * (i % geometry.columns) 461 local y = geometry.margin_y + (geometry.margin_y + geometry.size_y) * math.floor(i / geometry.columns) 462 a:rect_cw(x, y, x + geometry.size_x, y + geometry.size_y) 463 end 464 end 465 a:draw_stop() 466 ass.placeholders = a.text 467 end 468 469 local function refresh_scrollbar() 470 if not opts.show_scrollbar then return end 471 ass.scrollbar = "" 472 local before = (view.first - 1) / #playlist 473 local after = (#playlist - view.last) / #playlist 474 -- don't show the scrollbar if everything is visible 475 if before + after == 0 then return end 476 local p = opts.scrollbar_min_size / 100 477 if before + after > 1 - p then 478 if before == 0 then 479 after = (1 - p) 480 elseif after == 0 then 481 before = (1 - p) 482 else 483 before, after = 484 before / after * (1 - p) / (1 + before / after), 485 after / before * (1 - p) / (1 + after / before) 486 end 487 end 488 local y1 = geometry.margin_y + before * (geometry.window_h - 2 * geometry.margin_y) 489 local y2 = geometry.window_h - (geometry.margin_y + after * (geometry.window_h - 2 * geometry.margin_y)) 490 local x1, x2 491 if opts.scrollbar_side == "left" then 492 x1, x2 = 4, 8 493 else 494 x1, x2 = geometry.window_w - 8, geometry.window_w - 4 495 end 496 local scrollbar = assdraw.ass_new() 497 scrollbar:new_event() 498 scrollbar:append('{\\bord0}') 499 scrollbar:append('{\\shad0}') 500 scrollbar:append('{\\1c&AAAAAA&}') 501 scrollbar:pos(0, 0) 502 scrollbar:draw_start() 503 scrollbar:round_rect_cw(x1, y1, x2, y2, opts.frame_roundness) 504 scrollbar:draw_stop() 505 ass.scrollbar = scrollbar.text 506 end 507 508 local function refresh_selection() 509 local selection_ass = assdraw.ass_new() 510 local draw_frame = function(index, color) 511 if index < view.first or index > view.last then return end 512 local i = index - view.first 513 local x = geometry.margin_x + (geometry.margin_x + geometry.size_x) * (i % geometry.columns) 514 local y = geometry.margin_y + (geometry.margin_y + geometry.size_y) * math.floor(i / geometry.columns) 515 selection_ass:new_event() 516 selection_ass:append('{\\bord5}') 517 selection_ass:append('{\\3c&'.. color ..'&}') 518 selection_ass:append('{\\1a&FF&}') 519 selection_ass:pos(0, 0) 520 selection_ass:draw_start() 521 selection_ass:round_rect_cw(x, y, x + geometry.size_x, y + geometry.size_y, opts.frame_roundness) 522 selection_ass:draw_stop() 523 end 524 for i = view.first, view.last do 525 local name = playlist[i].filename 526 if flags[name] then 527 if i == selection.now then 528 draw_frame(i, opts.selected_flagged_frame_color) 529 else 530 draw_frame(i, opts.flagged_frame_color) 531 end 532 elseif i == selection.now then 533 draw_frame(i, opts.selected_frame_color) 534 end 535 end 536 537 if opts.show_filename or opts.show_title then 538 selection_ass:new_event() 539 local i = (selection.now - view.first) 540 local an = 5 541 local x = geometry.margin_x + (geometry.margin_x + geometry.size_x) * (i % geometry.columns) + geometry.size_x / 2 542 local y = geometry.margin_y + (geometry.margin_y + geometry.size_y) * math.floor(i / geometry.columns) + geometry.size_y + geometry.margin_y / 2 543 local col = i % geometry.columns 544 if geometry.columns > 1 then 545 if col == 0 then 546 x = x - geometry.size_x / 2 547 an = 4 548 elseif col == geometry.columns - 1 then 549 x = x + geometry.size_x / 2 550 an = 6 551 end 552 end 553 selection_ass:an(an) 554 selection_ass:pos(x, y) 555 selection_ass:append(string.format("{\\fs%d}", opts.text_size)) 556 selection_ass:append("{\\bord0}") 557 local f 558 local element = playlist[selection.now] 559 if opts.show_title and element.title then 560 f = element.title 561 else 562 f = element.filename 563 if opts.strip_directory then 564 if on_windows then 565 f = string.match(f, "([^\\/]+)$") or f 566 else 567 f = string.match(f, "([^/]+)$") or f 568 end 569 end 570 if opts.strip_extension then 571 f = string.match(f, "(.+)%.[^.]+$") or f 572 end 573 end 574 selection_ass:append(f) 575 end 576 ass.selection = selection_ass.text 577 end 578 579 function ass_show(selection, scrollbar, placeholders) 580 if selection then refresh_selection() end 581 if scrollbar then refresh_scrollbar() end 582 if placeholders then refresh_placeholders() end 583 local merge = function(a, b) 584 return b ~= "" and (a .. "\n" .. b) or a 585 end 586 mp.set_osd_ass(geometry.window_w, geometry.window_h, 587 merge(merge(ass.selection, ass.scrollbar), ass.placeholders) 588 ) 589 end 590 591 function ass_hide() 592 mp.set_osd_ass(1280, 720, "") 593 end 594 end 595 596 function normalize_path(path) 597 if string.find(path, "://") then 598 return path 599 end 600 path = utils.join_path(utils.getcwd(), path) 601 if on_windows then 602 path = string.gsub(path, "\\", "/") 603 end 604 path = string.gsub(path, "/%./", "/") 605 local n 606 repeat 607 path, n = string.gsub(path, "/[^/]*/%.%./", "/", 1) 608 until n == 0 609 return path 610 end 611 612 function refresh_overlays(force) 613 local todo = {} 614 overlays.missing = {} 615 for i = 1, 64 do 616 local index = view.first + i - 1 617 if index <= view.last then 618 local filename = playlist[index].filename 619 if force or overlays.active[i] ~= filename then 620 local filename_hash = hash_cache[filename] 621 if filename_hash == nil then 622 filename_hash = string.sub(sha256(normalize_path(filename)), 1, 12) 623 hash_cache[filename] = filename_hash 624 end 625 local thumb_filename = string.format("%s_%d_%d", filename_hash, geometry.size_x, geometry.size_y) 626 local thumb_path = utils.join_path(opts.thumbs_dir, thumb_filename) 627 if file_exists(thumb_path) then 628 show_overlay(i, thumb_path) 629 overlays.active[i] = filename 630 else 631 overlays.missing[thumb_path] = { index = i, input = filename } 632 remove_overlay(i) 633 todo[#todo + 1] = { input = filename, output = thumb_path } 634 end 635 end 636 else 637 remove_overlay(i) 638 end 639 end 640 -- reverse iterate so that the first thumbnail is at the top of the stack 641 if opts.auto_generate_thumbnails and #generators >= 1 then 642 for i = #todo, 1, -1 do 643 local generator = generators[i % #generators + 1] 644 local t = todo[i] 645 mp.commandv("script-message-to", generator, "push-thumbnail-front", 646 mp.get_script_name(), 647 t.input, 648 tostring(geometry.size_x), 649 tostring(geometry.size_y), 650 opts.take_thumbnail_at, 651 t.output, 652 opts.generate_thumbnails_with_mpv and "true" or "false" 653 ) 654 end 655 end 656 end 657 658 function show_overlay(index_1, thumb_path) 659 local g = geometry 660 local index_0 = index_1 - 1 661 mp.commandv("overlay-add", 662 tostring(index_0), 663 tostring(math.floor(0.5 + g.margin_x + (g.margin_x + g.size_x) * (index_0 % g.columns))), 664 tostring(math.floor(0.5 + g.margin_y + (g.margin_y + g.size_y) * math.floor(index_0 / g.columns))), 665 thumb_path, 666 "0", 667 "bgra", 668 tostring(g.size_x), 669 tostring(g.size_y), 670 tostring(4*g.size_x)) 671 mp.osd_message("", 0.01) 672 end 673 674 function remove_overlays() 675 for i = 1, 64 do 676 remove_overlay(i) 677 end 678 overlays.missing = {} 679 end 680 681 function remove_overlay(index_1) 682 if overlays.active[index_1] == "" then return end 683 overlays.active[index_1] = "" 684 mp.command("overlay-remove " .. index_1 - 1) 685 mp.osd_message("", 0.01) 686 end 687 688 function playlist_changed(key, value) 689 local did_change = function() 690 if #playlist ~= #value then return true end 691 for i = 1, #playlist do 692 if playlist[i].filename ~= value[i].filename then return true end 693 end 694 return false 695 end 696 if not did_change() then return end 697 if #value == 0 then 698 quit_gallery_view() 699 return 700 end 701 local sel_old_file = playlist[selection.old].filename 702 local sel_new_file = playlist[selection.now].filename 703 playlist = value 704 selection.old = math.max(1, math.min(selection.old, #playlist)) 705 selection.now = math.max(1, math.min(selection.now, #playlist)) 706 for i, f in ipairs(playlist) do 707 if sel_old_file == f.filename then 708 selection.old = i 709 end 710 if sel_new_file == f.filename then 711 selection.now = i 712 end 713 end 714 ensure_view_valid() 715 refresh_overlays(false) 716 ass_show(true, true, true) 717 end 718 719 function start_gallery_view(record_time) 720 if active then return end 721 init() 722 playlist = mp.get_property_native("playlist") 723 if #playlist == 0 then return end 724 725 local ww, wh = mp.get_osd_size() 726 compute_geometry(ww, wh) 727 if geometry.rows <= 0 or geometry.columns <= 0 then return end 728 729 mp.observe_property("playlist", "native", playlist_changed) 730 save_properties() 731 732 local pos = mp.get_property_number("playlist-pos-1") 733 if pos then 734 local s = {} 735 mp.set_property_bool("pause", true) 736 if opts.resume_when_picking then 737 s.time = record_time and mp.get_property_number("time-pos") or 0 738 end 739 s.vid = mp.get_property_number("vid") or "1" 740 s.aid = mp.get_property_number("aid") or "1" 741 s.sid = mp.get_property_number("sid") or "1" 742 resume[playlist[pos].filename] = s 743 mp.set_property("vid", "no") 744 mp.set_property("aid", "no") 745 mp.set_property("sid", "no") 746 else 747 -- this may happen if we enter the gallery too fast 748 local func 749 func = function() 750 mp.set_property_bool("pause", true) 751 mp.set_property("vid", "no") 752 mp.set_property("aid", "no") 753 mp.set_property("sid", "no") 754 mp.unregister_event(func) 755 end 756 mp.register_event("file-loaded", func) 757 end 758 selection.old = pos or 1 759 selection.now = selection.old 760 ensure_view_valid() 761 setup_ui_handlers() 762 refresh_overlays(true) 763 ass_show(true, true, true) 764 active = true 765 end 766 767 function quit_gallery_view(select) 768 if not active then return end 769 teardown_ui_handlers() 770 remove_overlays() 771 mp.unobserve_property(playlist_changed) 772 ass_hide() 773 if select then 774 resume_playback(select) 775 end 776 restore_properties() 777 active = false 778 end 779 780 function toggle_gallery() 781 if not active then 782 start_gallery_view(true) 783 else 784 quit_gallery_view(opts.toggle_behaves_as_accept and selection.now or selection.old) 785 end 786 end 787 788 mp.register_script_message("thumbnail-generated", function(thumbnail_path) 789 if not active then return end 790 local missing = overlays.missing[thumbnail_path] 791 if missing == nil then return end 792 show_overlay(missing.index, thumbnail_path) 793 overlays.active[missing.index] = missing.input 794 if not opts.always_show_placeholders then 795 ass_show(false, false, true) 796 end 797 overlays.missing[thumbnail_path] = nil 798 end) 799 800 mp.register_script_message("thumbnails-generator-broadcast", function(generator_name) 801 if #generators >= opts.max_generators then return end 802 for _, g in ipairs(generators) do 803 if generator_name == g then return end 804 end 805 generators[#generators + 1] = generator_name 806 end) 807 808 function write_flag_file() 809 if next(flags) == nil then return end 810 local out = io.open(opts.flagged_file_path, "w") 811 for f, _ in pairs(flags) do 812 out:write(f .. "\n") 813 end 814 out:close() 815 end 816 817 mp.register_event("shutdown", write_flag_file) 818 819 if opts.start_gallery_on_file_end then 820 mp.observe_property("eof-reached", "bool", function(_, val) 821 if val and mp.get_property_number("playlist-count") > 1 then 822 start_gallery_view(false) 823 824 end 825 end) 826 end 827 if opts.start_gallery_on_startup then 828 local autostart 829 autostart = function() 830 if mp.get_property_number("playlist-count") == 0 then return end 831 if mp.get_property_number("osd-width") <= 0 then return end 832 start_gallery_view(false) 833 mp.unobserve_property(autostart) 834 end 835 mp.observe_property("playlist-count", "number", autostart) 836 mp.observe_property("osd-width", "number", autostart) 837 end 838 839 mp.add_key_binding("g", "gallery-view", toggle_gallery) 840 mp.add_key_binding(nil, "gallery-write-flag-file", write_flag_file)