Video

mpv-gallery-view

Rina Kawakita 2020. 1. 8. 21:31

Gallery-view scripts for mpv

 

https://github.com/occivink/mpv-gallery-view

 

occivink/mpv-gallery-view

Gallery-view scripts for mpv. Contribute to occivink/mpv-gallery-view development by creating an account on GitHub.

github.com

mpv-gallery-view-v2

 


mpv-gallery-view-v1


~/.config/mpv/script-opts/gallery.conf

# thumbnail directory in which to look for and create thumbnails
# this option is the only one whose default value depends on the platform
# on unix-like platforms
thumbs_dir=~/.mpv_thumbs_dir
# on windows
#thumbs_dir=%APPDATA%\mpv\gallery-thumbs-dir
# note that not all env vars get expanded, only '~' and 'APPDATA' do

# generate thumbnails automatically
# if this is set to no, generator scripts are not necessary
auto_generate_thumbnails=yes
# use mpv instead of ffmpeg for thumbnail generation. Not recommended (slower, no transparency)
generate_thumbnails_with_mpv=no

# static thumbnail size
thumbnail_width=192
thumbnail_height=108

# this option lets you define the thumbnail size dynamically depending on the window size
# if it is defined, the two previous options are ignored
# its syntax is the following
# PIXELS_1,WIDTH_1,HEIGHT_1;PIXELS_2,WIDTH_2,HEIGHT_2;...
# such that if the window is smaller than PIXELS_1 the thumbnails are WIDTH_1 x HEIGHT_1
# otherwise if the window is smaller than PIXELS_2 the thumbnails are WIDTH_2 x HEIGHT_2
# ...and so on
# the more presets (PIXELS,WIDTH,HEIGHT triple) you have, the more thumbnails will be generated
# the value of PIXELS in the last preset should be bigger than your display's resolution. 1e20 can be used for that purpose
# examples:
# 3 presets for 1080p
#dynamic_thumbnail_size=691200,192,108 ; 1382400,256,144 ; 1e20,320,180
# 4 presets for 1440p
#dynamic_thumbnail_size=921600,192,108 ; 1843200,256,144 ; 2764800,320,180 ; 1e20,384,216
# empty by default
dynamic_thumbnail_size=

# the time at which to take the thumbnail
# a trailing '%' indicate that it's in percentage of the video duration
take_thumbnail_at=20%
# otherwise it's in seconds, like so
#take_thumbnail_at=150

# when starting an entry from the gallery, resume the position it was at when entering the gallery
resume_when_picking=yes
# start gallery view when current file is over
start_gallery_on_file_end=no
# if yes, pressing 'g' during gallery view opens the selection (same as ENTER)
# if no, it goes back to the previous file (same as ESC)
toggle_behaves_as_accept=yes

# minimum size (in pixels) of the margins.
# the real margins may be bigger in order to evenly space out the thumbnails
# left-right margin between thumbnails
margin_x=15
# up-down margin
margin_y=15
# limit the number of thumbnails visible, even if more could be shown
# 64 is the maximum due to limitations by mpv
max_thumbnails=64

# whether to show a minimal scrollbar
show_scrollbar=yes
# left or right
scrollbar_side=left
# in percentage of the max size
scrollbar_min_size=10

# whether to show placeholders for missing thumbnails
show_placeholders=yes
# the color of the placeholders (BGR hexadecimal)
#placeholder_color=222222
#placeholder_color=312e39
placeholder_color=392e31
# show placeholders even when the corresponding thumbnail is loaded
# this matters for thumbnails that have some form of transparency
always_show_placeholders=no

# whether to show the filename of the current selection
show_filename=yes
strip_directory=yes
strip_extension=yes
# size of the text. The up-down margin will be expanded if needed
text_size=28

# maximum generators, could be useful with different profiles (not sure how)
max_generators=8

# click on entries and scroll with the wheel
mouse_support=yes
# default bindings in gallery mode, their meaning should be self-explanatory
UP=UP
DOWN=DOWN
LEFT=LEFT
RIGHT=RIGHT
PAGE_UP=PGUP
PAGE_DOWN=PGDWN
FIRST=HOME
LAST=END
ACCEPT=ENTER
RANDOM=r
CANCEL=ESC
# simply removes entry from playlist, not the file
REMOVE=DEL

# vim-style version
UP=k
DOWN=j
LEFT=h
RIGHT=l
PAGE_UP=CTRL+u
PAGE_DOWN=CTRL+d
FIRST=g
LAST=G
RANDOM=r
ACCEPT=i
CANCEL=ESC
REMOVE=d

 

~/.config/mpv/scripts/gallery.lua

local utils = require 'mp.utils'
local msg = require 'mp.msg'
local assdraw = require 'mp.assdraw'

local on_windows = (package.config:sub(1,1) ~= "/")

local opts = {
    thumbs_dir = on_windows and "%APPDATA%\\mpv\\gallery-thumbs-dir" or "~/.mpv_thumbs_dir/",
    auto_generate_thumbnails = true,
    generate_thumbnails_with_mpv = false,

    thumbnail_width = 192,
    thumbnail_height = 108,
    dynamic_thumbnail_size = "",

    take_thumbnail_at = "20%",

    resume_when_picking = true,
    start_gallery_on_file_end = false,
    toggle_behaves_as_accept = true,

    margin_x = 15,
    margin_y = 15,
    max_thumbnails = 64,

    show_scrollbar = true,
    scrollbar_side = "left",
    scrollbar_min_size = 10,

    show_placeholders = true,
    placeholder_color = "222222",
    always_show_placeholders = false,

    show_filename = true,
    strip_directory = true,
    strip_extension = true,
    text_size = 28,

    max_generators = 8,

    mouse_support = true,
    UP        = "UP",
    DOWN      = "DOWN",
    LEFT      = "LEFT",
    RIGHT     = "RIGHT",
    PAGE_UP   = "PGUP",
    PAGE_DOWN = "PGDWN",
    FIRST     = "HOME",
    LAST      = "END",
    RANDOM    = "r",
    ACCEPT    = "ENTER",
    CANCEL    = "ESC",
    REMOVE    = "DEL",
}
(require 'mp.options').read_options(opts)

function split(input, char, tonum)
    local ret = {}
    for str in string.gmatch(input, "([^" .. char .. "]+)") do
        ret[#ret + 1] = (not tonum and str) or tonumber(str)
    end
    return ret
end
opts.dynamic_thumbnail_size = split(opts.dynamic_thumbnail_size, ";", false)
for i = 1, #opts.dynamic_thumbnail_size do
    local preset = split(opts.dynamic_thumbnail_size[i], ",", true)
    if (#preset ~= 3) or not (preset[1] and preset[2] and preset[3]) then
        msg.error(opts.dynamic_thumbnail_size[i] .. " is not a valid preset")
        return
    end
    opts.dynamic_thumbnail_size[i] = preset
end

if on_windows then
    opts.thumbs_dir = string.gsub(opts.thumbs_dir, "^%%APPDATA%%", os.getenv("APPDATA") or "%APPDATA%")
else
    opts.thumbs_dir = string.gsub(opts.thumbs_dir, "^~", os.getenv("HOME") or "~")
end
opts.max_thumbnails = math.min(opts.max_thumbnails, 64)

local sha256
--[[
minified code below is a combination of:
-sha256 implementation from
http://lua-users.org/wiki/SecureHashAlgorithm
-lua implementation of bit32 (used as fallback on lua5.1) from
https://www.snpedia.com/extensions/Scribunto/engines/LuaCommon/lualib/bit32.lua
both are licensed under the MIT below:

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:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
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.
--]]
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
-- end of sha code

active = false
geometry = {
    window_w = 0,
    window_h = 0,
    rows = 0,
    columns = 0,
    size_x = 0,
    size_y = 0,
    margin_x = 0,
    margin_y = 0,
}
playlist = {}
view = { -- 1-based indices into the "playlist" array
    first = 0, -- must be equal to N*columns
    last = 0, -- must be > first_visible and <= first_visible + rows*columns
}
overlays = {
    active = {}, -- array of 64 booleans indicated whether the corresponding overlay is shown
    missing = {}, -- maps hashes of missing thumbnails to the index they should be shown at
}
selection = {
    old = 0, -- the playlist element selected when entering the gallery
    now = 0, -- the currently selected element
}
pending = {
    selection = 0,
    window_size_changed = false,
    deletion = false,
}
ass = {
    selection = "",
    scrollbar = "",
    placeholders = "",
}
resume = {} -- maps filename to the time-pos it was at when starting the gallery
misc = {
    old_idle = "",
    old_force_window = "",
    old_geometry = "",
    old_osd_level = "",
}
generators = {} -- list of generator scripts that have registered themselves

do
    local inited = false
    function init()
        if not inited then
            inited = true
            if utils.file_info then -- 0.28+
                local res = utils.file_info(opts.thumbs_dir)
                if not res or not res.is_dir then
                    msg.error(string.format("Thumbnail directory \"%s\" does not exist", opts.thumbs_dir))
                end
            end
            if opts.auto_generate_thumbnails and #generators == 0 then
                msg.error("Auto-generation on, but no generators registered")
            end
        end
    end
end

function file_exists(path)
    local f = io.open(path, "r")
    if f ~= nil then
        io.close(f)
        return true
    else
        return false
    end
end

function thumbnail_size_from_presets(window_w, window_h)
    local size = window_w * window_h
    local picked = nil
    for _, preset in ipairs(opts.dynamic_thumbnail_size) do
        picked = { preset[2], preset[3] }
        if size <= preset[1] then
            break
        end
    end
    return picked
end

function select_under_cursor()
    local g = geometry
    local mx, my = mp.get_mouse_pos()
    if mx < 0 or my < 0 or mx > g.window_w or my > g.window_h then return end
    local mx, my = mx - g.margin_x, my - g.margin_y
    local on_column = (mx % (g.size_x + g.margin_x)) < g.size_x
    local on_row = (my % (g.size_y + g.margin_y)) < g.size_y
    if on_column and on_row then
        local column = math.floor(mx / (g.size_x + g.margin_x))
        local row = math.floor(my / (g.size_y + g.margin_y))
        local new_sel = view.first + row * g.columns + column
        if new_sel > view.last then return end
        if selection.now == new_sel then
            quit_gallery_view(selection.now)
        else
            selection.now = new_sel
            pending.selection = new_sel
            ass_show(true, false, false)
        end
    end
end

do
    local function increment_func(increment, clamp)
        local new = pending.selection + increment
        if new <= 0 or new > #playlist then
            if not clamp then return end
            new = math.max(1, math.min(new, #playlist))
        end
        pending.selection = new
    end

    local bindings_repeat = {}
        bindings_repeat[opts.UP]        = function() increment_func(- geometry.columns, false) end
        bindings_repeat[opts.DOWN]      = function() increment_func(  geometry.columns, false) end
        bindings_repeat[opts.LEFT]      = function() increment_func(- 1, false) end
        bindings_repeat[opts.RIGHT]     = function() increment_func(  1, false) end
        bindings_repeat[opts.PAGE_UP]   = function() increment_func(- geometry.columns * geometry.rows, true) end
        bindings_repeat[opts.PAGE_DOWN] = function() increment_func(  geometry.columns * geometry.rows, true) end
        bindings_repeat[opts.RANDOM]    = function() pending.selection = math.random(1, #playlist) end
        bindings_repeat[opts.REMOVE]    = function() pending.deletion = true end

    local bindings = {}
        bindings[opts.FIRST]  = function() pending.selection = 1 end
        bindings[opts.LAST]   = function() pending.selection = #playlist end
        bindings[opts.ACCEPT] = function() quit_gallery_view(selection.now) end
        bindings[opts.CANCEL] = function() quit_gallery_view(selection.old) end
    if opts.mouse_support then
        bindings["MBTN_LEFT"]  = select_under_cursor
        bindings["WHEEL_UP"]   = function() increment_func(- geometry.columns, false) end
        bindings["WHEEL_DOWN"] = function() increment_func(  geometry.columns, false) end
    end

    local function window_size_changed()
        pending.window_size_changed = true
    end

    local function file_started()
        quit_gallery_view(nil)
    end

    local function idle_handler()
        if pending.selection ~= selection.now then
            increment_selection(pending.selection)
        end
        if pending.window_size_changed then
            pending.window_size_changed = false
            local actually_changed = false
            local window_w, window_h = mp.get_osd_size()
            local new_thumb_size = thumbnail_size_from_presets(window_w, window_h)
            if new_thumb_size and (new_thumb_size[1] ~= opts.thumbnail_width or
                new_thumb_size[2] ~= opts.thumbnail_height)
            then
                opts.thumbnail_width = new_thumb_size[1]
                opts.thumbnail_height = new_thumb_size[2]
                actually_changed = true
            end
            if window_w ~= geometry.window_w or window_h ~= geometry.window_h then
                actually_changed = true
            end
            if actually_changed then
                resize_gallery(window_w, window_h)
            end
        end
        if pending.deletion then
            pending.deletion = false
            remove_selected()
        end
    end

    function setup_handlers()
        for key, func in pairs(bindings_repeat) do
            mp.add_forced_key_binding(key, "gallery-view-"..key, func, {repeatable = true})
        end
        for key, func in pairs(bindings) do
            mp.add_forced_key_binding(key, "gallery-view-"..key, func)
        end
        for _, prop in ipairs({ "osd-width", "osd-height" }) do
            mp.observe_property(prop, bool, window_size_changed)
        end
        mp.register_idle(idle_handler)
        mp.register_event("start-file", file_started)
    end

    function teardown_handlers()
        for key, _ in pairs(bindings_repeat) do
            mp.remove_key_binding("gallery-view-"..key)
        end
        for key, _ in pairs(bindings) do
            mp.remove_key_binding("gallery-view-"..key)
        end
        mp.unobserve_property(window_size_changed)
        mp.unregister_idle(idle_handler)
        mp.unregister_event(file_started)
    end
end

function save_and_clear_playlist()
    playlist = {}
    local cwd = utils.getcwd()
    for _, f in ipairs(mp.get_property_native("playlist")) do
        f = utils.join_path(cwd, f.filename)
        -- attempt basic path normalization
        if on_windows then
            f = string.gsub(f, "\\", "/")
        end
        f = string.gsub(f, "/%./", "/")
        local n
        repeat
            f, n = string.gsub(f, "/[^/]*/%.%./", "/", 1)
        until n == 0
        playlist[#playlist + 1]  = f
    end
    if opts.resume_when_picking then
        resume[playlist[mp.get_property_number("playlist-pos-1")]] = mp.get_property_number("time-pos")
    end
    mp.command("stop")
end

function restore_playlist_and_select(select)
    mp.set_property_bool("pause", false)
    mp.commandv("loadfile", playlist[select], "replace")
    if opts.resume_when_picking then
        local time = resume[playlist[select]]
        if time then
            local tmp
            local func = function()
                mp.commandv("seek", time, "absolute")
                mp.unregister_event(tmp)
            end
            tmp = func
            mp.register_event("file-loaded", func)
        end
    end
    for i = 1, select - 1 do
        mp.commandv("loadfile", playlist[i], "append")
    end
    for i = select + 1, #playlist do
        mp.commandv("loadfile", playlist[i], "append")
    end
    mp.commandv("playlist-move", 0, select)
end

function restore_properties()
    mp.set_property("idle", misc.old_idle)
    mp.set_property("force-window", misc.old_force_window)
    mp.set_property("geometry", misc.old_geometry)
    mp.set_property("osd-level", misc.old_osd_level)
    mp.commandv("script-message", "osc-visibility", "auto", "true")
end

function save_properties()
    misc.old_idle = mp.get_property("idle")
    misc.old_force_window = mp.get_property("force-window")
    misc.old_geometry = mp.get_property("geometry")
    misc.old_osd_level = mp.get_property("osd-level")
    mp.set_property_bool("idle", true)
    mp.set_property_bool("force-window", true)
    mp.set_property_number("osd-level", 0)
    mp.commandv("no-osd", "script-message", "osc-visibility", "never", "true")
    mp.set_property("geometry", geometry.window_w .. "x" .. geometry.window_h)
end

function get_geometry(window_w, window_h)
    local margin_y = opts.show_filename and math.max(opts.text_size, opts.margin_y) or opts.margin_y
    geometry.window_w, geometry.window_h = window_w, window_h
    geometry.size_x = opts.thumbnail_width
    geometry.size_y = opts.thumbnail_height
    geometry.rows = math.floor((geometry.window_h - margin_y) / (geometry.size_y + margin_y))
    geometry.columns = math.floor((geometry.window_w - opts.margin_x) / (geometry.size_x + opts.margin_x))
    if (geometry.rows * geometry.columns > opts.max_thumbnails) then
        local r = math.sqrt(geometry.rows * geometry.columns / opts.max_thumbnails)
        geometry.rows = math.floor(geometry.rows / r)
        geometry.columns = math.floor(geometry.columns / r)
    end
    geometry.margin_x = (geometry.window_w - geometry.columns * geometry.size_x) / (geometry.columns + 1)
    geometry.margin_y = (geometry.window_h - geometry.rows * geometry.size_y) / (geometry.rows + 1)
end

function increment_selection(inc)
    selection.now = inc
    pending.selection = inc
    max_thumbs = geometry.rows * geometry.columns
    if selection.now < view.first or selection.now > view.last then
        if selection.now < view.first then
            view.first = math.floor((selection.now - 1) / geometry.columns) * geometry.columns + 1
            view.last = math.min(view.first + max_thumbs - 1, #playlist)
        else
            view.last = (math.floor((selection.now - 1) / geometry.columns) + 1) * geometry.columns
            view.first = view.last - max_thumbs + 1
            if view.last > #playlist then
                remove_overlays(max_thumbs - (view.last - #playlist) + 1, max_thumbs)
                view.last = #playlist
            end
        end
        show_overlays(1, view.last - view.first + 1)
    end
    ass_show(true, true, true)
end

function resize_gallery(window_w, window_h)
    local old_max_thumbs = geometry.rows * geometry.columns
    get_geometry(window_w, window_h)
    local max_thumbs = geometry.rows * geometry.columns
    if geometry.rows <= 0 or geometry.columns <= 0 then
        quit_gallery_view(selection.old)
        return
    elseif max_thumbs ~= old_max_thumbs then
        center_view_on_selection()
        remove_overlays(view.last - view.first + 2, old_max_thumbs)
    end
    show_overlays(1, view.last - view.first + 1)
    ass_show(true, true, true)
end

function remove_selected()
    if #playlist < 2 then return end
    table.remove(playlist, selection.now)
    selection.old = math.min(selection.old, #playlist)
    view.last = math.min(view.last, #playlist)
    if selection.now > #playlist then
        increment_selection(selection.now - 1)
    else
        show_overlays(selection.now - view.first + 1, view.last - view.first + 1)
        ass_show(true, true, true)
    end
    remove_overlay(view.last - view.first + 2)
end

function center_view_on_selection()
    view.first = math.floor((selection.now - 1) / geometry.columns) * geometry.columns + 1
    view.last = view.first + geometry.rows * geometry.columns - 1
    if view.last > #playlist then
        local last_row = math.floor((#playlist - 1) / geometry.columns)
        view.last = #playlist
        view.first = math.max(1, (last_row - geometry.rows + 1) * geometry.columns + 1)
    end
end

-- ass related stuff
do
    local function refresh_placeholders()
        if not opts.show_placeholders then return end
        local a = assdraw.ass_new()
        a:new_event()
        a:append('{\\bord0}')
        a:append('{\\shad0}')
        a:append('{\\1c&' .. opts.placeholder_color .. '}')
        a:pos(0, 0)
        a:draw_start()
        for i = 0, view.last - view.first do
            if opts.always_show_placeholders or (not overlays.active[i + 1]) then
                local x = geometry.margin_x + (geometry.margin_x + geometry.size_x) * (i % geometry.columns)
                local y = geometry.margin_y + (geometry.margin_y + geometry.size_y) * math.floor(i / geometry.columns)
                a:rect_cw(x, y, x + geometry.size_x, y + geometry.size_y)
            end
        end
        a:draw_stop()
        ass.placeholders = a.text
    end

    local function refresh_scrollbar()
        if not opts.show_scrollbar then return end
        ass.scrollbar = ""
        local before = (view.first - 1) / #playlist
        local after = (#playlist - view.last) / #playlist
        -- don't show the scrollbar if everything is visible
        if before + after == 0 then return end
        local p = opts.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 y1 = geometry.margin_y + before * (geometry.window_h - 2 * geometry.margin_y)
        local y2 = geometry.window_h - (geometry.margin_y + after * (geometry.window_h - 2 * geometry.margin_y))
        local x1, x2
        if opts.scrollbar_side == "left" then
            x1, x2 = 4, 8
        else
            x1, x2 = geometry.window_w - 8, geometry.window_w - 4
        end
        local scrollbar = assdraw.ass_new()
        scrollbar:new_event()
        scrollbar:append('{\\bord0}')
        scrollbar:append('{\\shad0}')
        scrollbar:append('{\\1c&AAAAAA&}')
        scrollbar:pos(0, 0)
        scrollbar:draw_start()
        scrollbar:round_rect_cw(x1, y1, x2, y2, 2)
        scrollbar:draw_stop()
        ass.scrollbar = scrollbar.text
    end

    local function refresh_selection()
        local i = selection.now - view.first
        local col = (i % geometry.columns)
        local x = geometry.margin_x + (geometry.margin_x + geometry.size_x) * col
        local y = geometry.margin_y + (geometry.margin_y + geometry.size_y) * math.floor(i / geometry.columns)
        local box = assdraw.ass_new()
        box:new_event()
        box:append('{\\bord6}')
        box:append('{\\3c&DDDDDD&}')
        box:append('{\\1a&FF&}')
        box:pos(0, 0)
        box:draw_start()
        box:round_rect_cw(x + 1, y + 1, x + geometry.size_x - 1, y + geometry.size_y - 1, 2)
        box:draw_stop()
        if opts.show_filename then
            box:new_event()
            local an = 5
            y = y + geometry.size_y + geometry.margin_y / 2
            x = x + geometry.size_x / 2
            if geometry.columns > 1 then
                if col == 0 then
                    x = x - geometry.size_x / 2
                    an = 4
                elseif col == geometry.columns - 1 then
                    x = x + geometry.size_x / 2
                    an = 6
                end
            end
            box:an(an)
            box:pos(x, y)
            box:append(string.format("{\\fs%d}", opts.text_size))
            box:append("{\\bord0}")
            local f = playlist[selection.now]
            if opts.strip_directory then
                if on_windows then
                    f = string.match(f, "([^\\/]+)$") or f
                else
                    f = string.match(f, "([^/]+)$") or f
                end
            end
            if opts.strip_extension then
                f = string.match(f, "(.+)%.[^.]+$") or f
            end
            box:append(f)
        end
        ass.selection = box.text
    end

    function ass_show(selection, scrollbar, placeholders)
        local merge = function(a, b)
            return b ~= "" and (a .. "\n" .. b) or a
        end
        if selection then refresh_selection() end
        if scrollbar then refresh_scrollbar() end
        if placeholders then refresh_placeholders() end
        mp.set_osd_ass(geometry.window_w, geometry.window_h,
            merge(merge(ass.selection, ass.scrollbar), ass.placeholders)
        )
    end

    function ass_hide()
        mp.set_osd_ass(1280, 720, "")
    end
end

-- 1-based indices
function show_overlays(from, to)
    local todo = {}
    overlays.missing = {}
    for i = from, to do
        local filename = playlist[view.first + i - 1]
        local filename_hash = string.sub(sha256(filename), 1, 12)
        local thumb_filename = string.format("%s_%d_%d", filename_hash, geometry.size_x, geometry.size_y)
        local thumb_path = utils.join_path(opts.thumbs_dir, thumb_filename)
        if file_exists(thumb_path) then
            show_overlay(i, thumb_path)
        else
            remove_overlay(i)
            todo[#todo + 1] = { index = i, input = filename, output = thumb_path }
        end
    end
    -- reverse iterate so that the first thumbnail is at the top of the stack
    if opts.auto_generate_thumbnails and #generators >= 1 then
        for i = #todo, 1, -1 do
            local generator = generators[i % #generators + 1]
            local t = todo[i]
            overlays.missing[t.output] = t.index
            mp.commandv("script-message-to", generator, "push-thumbnail-front",
                mp.get_script_name(),
                t.input,
                tostring(opts.thumbnail_width),
                tostring(opts.thumbnail_height),
                opts.take_thumbnail_at,
                t.output,
                opts.generate_thumbnails_with_mpv and "true" or "false"
            )
        end
    end
end

function show_overlay(index_1, thumb_path)
    local g = geometry
    local index_0 = index_1 - 1
    overlays.active[index_1] = true
    mp.command(string.format("overlay-add %i %i %i %s 0 bgra %i %i %i;",
        index_0,
        g.margin_x + (g.margin_x + g.size_x) * (index_0 % g.columns),
        g.margin_y + (g.margin_y + g.size_y) * math.floor(index_0 / g.columns),
        thumb_path,
        g.size_x, g.size_y, 4*g.size_x
    ))
    mp.osd_message("", 0.01)
end

-- 1-based indices
function remove_overlays(from, to)
    for i = to, from, -1 do
        remove_overlay(i)
    end
end

function remove_overlay(index_1)
    if overlays.active[index_1] then
        overlays.active[index_1] = false
        mp.command("overlay-remove " .. index_1 - 1)
        mp.osd_message("", 0.01)
    end
end

function start_gallery_view()
    init()
    if mp.get_property_number("playlist-count") == 0 then return end
    local w, h = mp.get_osd_size()
    local new_thumb_size = thumbnail_size_from_presets(w, h)
    if new_thumb_size then
        opts.thumbnail_width = new_thumb_size[1]
        opts.thumbnail_height = new_thumb_size[2]
    end
    local old_max_thumbs = geometry.rows * geometry.columns
    get_geometry(w, h)
    if geometry.rows <= 0 or geometry.columns <= 0 then return end
    save_properties()
    selection.old = mp.get_property_number("playlist-pos-1")
    selection.now = selection.old
    pending.selection = selection.now
    local old_playlist_size = #playlist
    local max_thumbs = geometry.rows * geometry.columns
    save_and_clear_playlist()
    local selection_row = math.floor((selection.now - 1) / geometry.columns)
    if max_thumbs ~= old_max_thumbs or old_playlist_size ~= #playlist then
        center_view_on_selection()
    elseif selection.now < view.first then
        -- the selection is now on the first line
        view.first = selection_row * geometry.columns + 1
        view.last = math.min(#playlist, view.first + max_thumbs - 1)
    elseif selection.now > view.last then
        -- the selection is now on the last line
        view.last = (selection_row + 1) * geometry.columns
        view.first = math.max(1, view.last - max_thumbs + 1)
        view.last = math.min(#playlist, view.last)
    end
    setup_handlers()
    show_overlays(1, view.last - view.first + 1)
    ass_show(true, true, true)
    active = true
end

function quit_gallery_view(select)
    teardown_handlers()
    remove_overlays(1, view.last - view.first + 1)
    ass_hide()
    if select then
        restore_playlist_and_select(select)
    end
    restore_properties()
    active = false
end

function toggle_gallery()
    if not active then
        start_gallery_view()
    else
        quit_gallery_view(opts.toggle_behaves_as_accept and selection.new or selection.old)
    end
end

mp.register_script_message("thumbnail-generated", function(thumbnail_path)
    if not active then return end
    local index_missing = overlays.missing[thumbnail_path]
    if index_missing == nil then return end
    show_overlay(index_missing, thumbnail_path)
    if not opts.always_show_placeholders then
        ass_show(false, false, true)
    end
    overlays.missing[thumbnail_path] = nil
end)

mp.register_script_message("thumbnails-generator-broadcast", function(generator_name)
    if #generators >= opts.max_generators then return end
    for _, g in ipairs(generators) do
        if generator_name == g then return end
    end
    generators[#generators + 1] = generator_name
end)

if opts.start_gallery_on_file_end then
    mp.register_event("end-file", function()
        if not active and mp.get_property_number("playlist-count") > 1 then
            start_gallery_view()
        end
    end)
end

mp.add_key_binding("g", "gallery-view", toggle_gallery)