summaryrefslogtreecommitdiff
path: root/mac/.config/mpv/script-modules/gallery.lua
diff options
context:
space:
mode:
Diffstat (limited to 'mac/.config/mpv/script-modules/gallery.lua')
-rw-r--r--mac/.config/mpv/script-modules/gallery.lua581
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 }