diff options
Diffstat (limited to 'mac/.config/mpv/script-modules/gallery.lua')
| -rw-r--r-- | mac/.config/mpv/script-modules/gallery.lua | 581 |
1 files changed, 581 insertions, 0 deletions
diff --git a/mac/.config/mpv/script-modules/gallery.lua b/mac/.config/mpv/script-modules/gallery.lua new file mode 100644 index 0000000..ee0be42 --- /dev/null +++ b/mac/.config/mpv/script-modules/gallery.lua @@ -0,0 +1,581 @@ +local utils = require("mp.utils") +local msg = require("mp.msg") +local assdraw = require("mp.assdraw") + +local gallery_mt = {} +gallery_mt.__index = gallery_mt + +function gallery_new() + local gallery = setmetatable({ + -- public, can be modified by user + items = {}, + item_to_overlay_path = function(index, item) + return "" + end, + item_to_thumbnail_params = function(index, item) + return "", 0 + end, + item_to_text = function(index, item) + return "", true + end, + item_to_border = function(index, item) + return 0, "" + end, + ass_show = function(ass) end, + config = { + background_color = "333333", + background_opacity = "33", + background_roundness = 5, + scrollbar = true, + scrollbar_left_side = false, + scrollbar_min_size = 10, + overlay_range = 0, + max_thumbnails = 64, + show_placeholders = true, + always_show_placeholders = false, + placeholder_color = "222222", + text_size = 28, + align_text = true, + accurate = false, + generate_thumbnails_with_mpv = false, + }, + + -- private, can be read but should not be modified + active = false, + geometry = { + ok = false, + position = { 0, 0 }, + size = { 0, 0 }, + min_spacing = { 0, 0 }, + thumbnail_size = { 0, 0 }, + rows = 0, + columns = 0, + effective_spacing = { 0, 0 }, + }, + view = { -- 1-based indices into the "playlist" array + first = 0, -- must be equal to N*columns + last = 0, -- must be > first and <= first + rows*columns + }, + overlays = { + active = {}, -- array of <=64 strings indicating the file associated to the current overlay (false if nothing) + missing = {}, -- associative array of thumbnail path to view index it should be shown at + }, + selection = nil, + ass = { + background = "", + selection = "", + scrollbar = "", + placeholders = "", + }, + generators = {}, -- list of generator scripts + }, gallery_mt) + + for i = 1, gallery.config.max_thumbnails do + gallery.overlays.active[i] = false + end + return gallery +end + +function gallery_mt.show_overlay(gallery, index_1, thumb_path) + local g = gallery.geometry + gallery.overlays.active[index_1] = thumb_path + local index_0 = index_1 - 1 + local x, y = gallery:view_index_position(index_0) + mp.commandv( + "overlay-add", + tostring(index_0 + gallery.config.overlay_range), + tostring(math.floor(x + 0.5)), + tostring(math.floor(y + 0.5)), + thumb_path, + "0", + "bgra", + tostring(g.thumbnail_size[1]), + tostring(g.thumbnail_size[2]), + tostring(4 * g.thumbnail_size[1]) + ) + mp.osd_message(" ", 0.01) +end + +function gallery_mt.remove_overlays(gallery) + for view_index, _ in pairs(gallery.overlays.active) do + mp.commandv("overlay-remove", gallery.config.overlay_range + view_index - 1) + gallery.overlays.active[view_index] = false + end + gallery.overlays.missing = {} +end + +local function file_exists(path) + local info = utils.file_info(path) + return info ~= nil and info.is_file +end + +function gallery_mt.refresh_overlays(gallery, force) + local todo = {} + local o = gallery.overlays + local g = gallery.geometry + o.missing = {} + for view_index = 1, g.rows * g.columns do + local index = gallery.view.first + view_index - 1 + local active = o.active[view_index] + if index > 0 and index <= #gallery.items then + local thumb_path = gallery.item_to_overlay_path(index, gallery.items[index]) + if not force and active == thumb_path then + -- nothing to do + elseif file_exists(thumb_path) then + gallery:show_overlay(view_index, thumb_path) + else + -- need to generate that thumbnail + o.active[view_index] = false + mp.commandv("overlay-remove", gallery.config.overlay_range + view_index - 1) + o.missing[thumb_path] = view_index + todo[#todo + 1] = { index = index, output = thumb_path } + end + else + -- might happen if we're close to the end of gallery.items + if active ~= false then + o.active[view_index] = false + mp.commandv("overlay-remove", gallery.config.overlay_range + view_index - 1) + end + end + end + if #gallery.generators >= 1 then + -- reverse iterate so that the first thumbnail is at the top of the stack + for i = #todo, 1, -1 do + local generator = gallery.generators[i % #gallery.generators + 1] + local t = todo[i] + local input_path, time = gallery.item_to_thumbnail_params(t.index, gallery.items[t.index]) + mp.commandv( + "script-message-to", + generator, + "push-thumbnail-front", + mp.get_script_name(), + input_path, + tostring(g.thumbnail_size[1]), + tostring(g.thumbnail_size[2]), + time, + t.output, + gallery.config.accurate and "true" or "false", + gallery.config.generate_thumbnails_with_mpv and "true" or "false" + ) + end + end +end + +function gallery_mt.index_at(gallery, mx, my) + local g = gallery.geometry + if mx < g.position[1] or my < g.position[2] then + return nil + end + mx = mx - g.position[1] + my = my - g.position[2] + if mx > g.size[1] or my > g.size[2] then + return nil + end + mx = mx - g.effective_spacing[1] + my = my - g.effective_spacing[2] + local on_column = (mx % (g.thumbnail_size[1] + g.effective_spacing[1])) < g.thumbnail_size[1] + local on_row = (my % (g.thumbnail_size[2] + g.effective_spacing[2])) < g.thumbnail_size[2] + if on_column and on_row then + local column = math.floor(mx / (g.thumbnail_size[1] + g.effective_spacing[1])) + local row = math.floor(my / (g.thumbnail_size[2] + g.effective_spacing[2])) + local index = gallery.view.first + row * g.columns + column + if index > 0 and index <= gallery.view.last then + return index + end + end + return nil +end + +function gallery_mt.compute_internal_geometry(gallery) + local g = gallery.geometry + g.rows = math.floor((g.size[2] - g.min_spacing[2]) / (g.thumbnail_size[2] + g.min_spacing[2])) + g.columns = math.floor((g.size[1] - g.min_spacing[1]) / (g.thumbnail_size[1] + g.min_spacing[1])) + if g.rows <= 0 or g.columns <= 0 then + g.rows = 0 + g.columns = 0 + g.effective_spacing[1] = g.size[1] + g.effective_spacing[2] = g.size[2] + return + end + if g.rows * g.columns > gallery.config.max_thumbnails then + local r = math.sqrt(g.rows * g.columns / gallery.config.max_thumbnails) + g.rows = math.floor(g.rows / r) + g.columns = math.floor(g.columns / r) + end + g.effective_spacing[1] = (g.size[1] - g.columns * g.thumbnail_size[1]) / (g.columns + 1) + g.effective_spacing[2] = (g.size[2] - g.rows * g.thumbnail_size[2]) / (g.rows + 1) +end + +-- makes sure that view.first and view.last are valid with regards to the playlist +-- and that selection is within the view +-- to be called after the playlist, view or selection was modified somehow +function gallery_mt.ensure_view_valid(gallery) + local g = gallery.geometry + if #gallery.items == 0 or g.rows == 0 or g.columns == 0 then + gallery.view.first = 0 + gallery.view.last = 0 + return + end + local v = gallery.view + local selection_row = math.floor((gallery.selection - 1) / g.columns) + local max_thumbs = g.rows * g.columns + local changed = false + + if v.last >= #gallery.items then + v.last = #gallery.items + if g.rows == 1 then + v.first = math.max(1, v.last - g.columns + 1) + else + local last_row = math.floor((v.last - 1) / g.columns) + local first_row = math.max(0, last_row - g.rows + 1) + v.first = 1 + first_row * g.columns + end + changed = true + elseif v.first == 0 or v.last == 0 or v.last - v.first + 1 ~= max_thumbs then + -- special case: the number of possible thumbnails was changed + -- just recreate the view such that the selection is in the middle row + local max_row = (#gallery.items - 1) / g.columns + 1 + local row_first = selection_row - math.floor((g.rows - 1) / 2) + local row_last = selection_row + math.floor((g.rows - 1) / 2) + g.rows % 2 + if row_first < 0 then + row_first = 0 + elseif row_last > max_row then + row_first = max_row - g.rows + 1 + end + v.first = 1 + row_first * g.columns + v.last = math.min(#gallery.items, v.first - 1 + max_thumbs) + return true + end + + if gallery.selection < v.first then + -- the selection is now on the first line + v.first = (g.rows == 1) and gallery.selection or selection_row * g.columns + 1 + v.last = math.min(#gallery.items, v.first + max_thumbs - 1) + changed = true + elseif gallery.selection > v.last then + v.last = (g.rows == 1) and gallery.selection or (selection_row + 1) * g.columns + v.first = math.max(1, v.last - max_thumbs + 1) + v.last = math.min(#gallery.items, v.last) + changed = true + end + return changed +end + +-- ass related stuff +function gallery_mt.refresh_background(gallery) + local g = gallery.geometry + local a = assdraw.ass_new() + a:new_event() + a:append("{\\an7}") + a:append("{\\bord0}") + a:append("{\\shad0}") + a:append("{\\1c&" .. gallery.config.background_color .. "}") + a:append("{\\1a&" .. gallery.config.background_opacity .. "}") + a:pos(0, 0) + a:draw_start() + a:round_rect_cw( + g.position[1], + g.position[2], + g.position[1] + g.size[1], + g.position[2] + g.size[2], + gallery.config.background_roundness + ) + a:draw_stop() + gallery.ass.background = a.text +end + +function gallery_mt.refresh_placeholders(gallery) + if not gallery.config.show_placeholders then + return + end + if gallery.view.first == 0 then + gallery.ass.placeholders = "" + return + end + local g = gallery.geometry + local a = assdraw.ass_new() + a:new_event() + a:append("{\\an7}") + a:append("{\\bord0}") + a:append("{\\shad0}") + a:append("{\\1c&" .. gallery.config.placeholder_color .. "}") + a:pos(0, 0) + a:draw_start() + for i = 0, gallery.view.last - gallery.view.first do + if gallery.config.always_show_placeholders or not gallery.overlays.active[i + 1] then + local x, y = gallery:view_index_position(i) + a:rect_cw(x, y, x + g.thumbnail_size[1], y + g.thumbnail_size[2]) + end + end + a:draw_stop() + gallery.ass.placeholders = a.text +end + +function gallery_mt.refresh_scrollbar(gallery) + if not gallery.config.scrollbar then + return + end + gallery.ass.scrollbar = "" + if gallery.view.first == 0 then + return + end + local g = gallery.geometry + local before = (gallery.view.first - 1) / #gallery.items + local after = (#gallery.items - gallery.view.last) / #gallery.items + -- don't show the scrollbar if everything is visible + if before + after == 0 then + return + end + local p = gallery.config.scrollbar_min_size / 100 + if before + after > 1 - p then + if before == 0 then + after = (1 - p) + elseif after == 0 then + before = (1 - p) + else + before, after = + before / after * (1 - p) / (1 + before / after), after / before * (1 - p) / (1 + after / before) + end + end + local dist_from_edge = g.size[2] * 0.015 + local y1 = g.position[2] + dist_from_edge + before * (g.size[2] - 2 * dist_from_edge) + local y2 = g.position[2] + g.size[2] - (dist_from_edge + after * (g.size[2] - 2 * dist_from_edge)) + local x1, x2 + if gallery.config.scrollbar_left_side then + x1 = g.position[1] + g.effective_spacing[1] / 2 - 2 + else + x1 = g.position[1] + g.size[1] - g.effective_spacing[1] / 2 - 2 + end + x2 = x1 + 4 + local scrollbar = assdraw.ass_new() + scrollbar:new_event() + scrollbar:append("{\\an7}") + scrollbar:append("{\\bord0}") + scrollbar:append("{\\shad0}") + scrollbar:append("{\\1c&AAAAAA&}") + scrollbar:pos(0, 0) + scrollbar:draw_start() + scrollbar:rect_cw(x1, y1, x2, y2) + scrollbar:draw_stop() + gallery.ass.scrollbar = scrollbar.text +end + +function gallery_mt.refresh_selection(gallery) + local v = gallery.view + if v.first == 0 then + gallery.ass.selection = "" + return + end + local selection_ass = assdraw.ass_new() + local g = gallery.geometry + local draw_frame = function(index, size, color) + local x, y = gallery:view_index_position(index - v.first) + selection_ass:new_event() + selection_ass:append("{\\an7}") + selection_ass:append("{\\bord" .. size .. "}") + selection_ass:append("{\\3c&" .. color .. "&}") + selection_ass:append("{\\1a&FF&}") + selection_ass:pos(0, 0) + selection_ass:draw_start() + selection_ass:rect_cw(x, y, x + g.thumbnail_size[1], y + g.thumbnail_size[2]) + selection_ass:draw_stop() + end + for i = v.first, v.last do + local size, color = gallery.item_to_border(i, gallery.items[i]) + if size > 0 then + draw_frame(i, size, color) + end + end + + for index = v.first, v.last do + local text = gallery.item_to_text(index, gallery.items[index]) + if text ~= "" then + selection_ass:new_event() + local an = 5 + local x, y = gallery:view_index_position(index - v.first) + x = x + g.thumbnail_size[1] / 2 + y = y + g.thumbnail_size[2] + gallery.config.text_size * 0.75 + if gallery.config.align_text then + local col = (index - v.first) % g.columns + if g.columns > 1 then + if col == 0 then + x = x - g.thumbnail_size[1] / 2 + an = 4 + elseif col == g.columns - 1 then + x = x + g.thumbnail_size[1] / 2 + an = 6 + end + end + end + selection_ass:an(an) + selection_ass:pos(x, y) + selection_ass:append(string.format("{\\fs%d}", gallery.config.text_size)) + selection_ass:append("{\\bord0}") + selection_ass:append(text) + end + end + gallery.ass.selection = selection_ass.text +end + +function gallery_mt.ass_refresh(gallery, selection, scrollbar, placeholders, background) + if not gallery.active then + return + end + if selection then + gallery:refresh_selection() + end + if scrollbar then + gallery:refresh_scrollbar() + end + if placeholders then + gallery:refresh_placeholders() + end + if background then + gallery:refresh_background() + end + gallery.ass_show(table.concat({ + gallery.ass.background, + gallery.ass.placeholders, + gallery.ass.selection, + gallery.ass.scrollbar, + }, "\n")) +end + +function gallery_mt.set_selection(gallery, selection) + if not selection or selection ~= selection then + return + end + local new_selection = math.max(1, math.min(selection, #gallery.items)) + if gallery.selection == new_selection then + return + end + gallery.selection = new_selection + if gallery.active then + if gallery:ensure_view_valid() then + gallery:refresh_overlays(false) + gallery:ass_refresh(true, true, true, false) + else + gallery:ass_refresh(true, false, false, false) + end + end +end + +function gallery_mt.set_geometry(gallery, x, y, w, h, sw, sh, tw, th) + if w <= 0 or h <= 0 or tw <= 0 or th <= 0 then + msg.warn("Invalid coordinates") + return + end + gallery.geometry.position = { x, y } + gallery.geometry.size = { w, h } + gallery.geometry.min_spacing = { sw, sh } + gallery.geometry.thumbnail_size = { tw, th } + gallery.geometry.ok = true + if not gallery.active then + return + end + if not gallery:enough_space() then + msg.warn("Not enough space to display something") + end + local old_total = gallery.geometry.rows * gallery.geometry.columns + gallery:compute_internal_geometry() + gallery:ensure_view_valid() + local new_total = gallery.geometry.rows * gallery.geometry.columns + for view_index = new_total + 1, old_total do + if gallery.overlays.active[view_index] then + mp.commandv("overlay-remove", gallery.config.overlay_range + view_index - 1) + gallery.overlays.active[view_index] = false + end + end + gallery:refresh_overlays(true) + gallery:ass_refresh(true, true, true, true) +end + +function gallery_mt.items_changed(gallery, new_sel) + gallery.selection = math.max(1, math.min(new_sel, #gallery.items)) + if not gallery.active then + return + end + gallery:ensure_view_valid() + gallery:refresh_overlays(false) + gallery:ass_refresh(true, true, true, false) +end + +function gallery_mt.thumbnail_generated(gallery, thumb_path) + if not gallery.active then + return + end + local view_index = gallery.overlays.missing[thumb_path] + if view_index == nil then + return + end + gallery:show_overlay(view_index, thumb_path) + if not gallery.config.always_show_placeholders then + gallery:ass_refresh(false, false, true, false) + end + gallery.overlays.missing[thumb_path] = nil +end + +function gallery_mt.add_generator(gallery, generator_name) + for _, g in ipairs(gallery.generators) do + if generator_name == g then + return + end + end + gallery.generators[#gallery.generators + 1] = generator_name +end + +function gallery_mt.view_index_position(gallery, index_0) + local g = gallery.geometry + return math.floor( + g.position[1] + g.effective_spacing[1] + (g.effective_spacing[1] + g.thumbnail_size[1]) * (index_0 % g.columns) + ), + math.floor( + g.position[2] + + g.effective_spacing[2] + + (g.effective_spacing[2] + g.thumbnail_size[2]) * math.floor(index_0 / g.columns) + ) +end + +function gallery_mt.enough_space(gallery) + if gallery.geometry.size[1] < gallery.geometry.thumbnail_size[1] + 2 * gallery.geometry.min_spacing[1] then + return false + end + if gallery.geometry.size[2] < gallery.geometry.thumbnail_size[2] + 2 * gallery.geometry.min_spacing[2] then + return false + end + return true +end + +function gallery_mt.activate(gallery) + if gallery.active then + return false + end + if not gallery:enough_space() then + msg.warn("Not enough space, refusing to start") + return false + end + if not gallery.geometry.ok then + msg.warn("Gallery geometry unitialized, refusing to start") + return false + end + gallery.active = true + if not gallery.selection then + gallery:set_selection(1) + end + gallery:compute_internal_geometry() + gallery:ensure_view_valid() + gallery:refresh_overlays(false) + gallery:ass_refresh(true, true, true, true) + return true +end + +function gallery_mt.deactivate(gallery) + if not gallery.active then + return + end + gallery.active = false + gallery:remove_overlays() + gallery.ass_show("") +end + +return { gallery_new = gallery_new } |
