From a16e0eead7dc06df8b06ff19f630d54eeaa01ede Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Wed, 22 Mar 2023 11:14:25 +0100 Subject: [PATCH 01/27] wezterm: Add cursive italics Set up wezterm to continue to use Iosevka for everything *except* italics (in all weights) which will instead be displayed by the Victor font. This ultimately results in cursive fonts for italics and Iosevka for everything else, very pretty. --- bootstrap/packages_stable.tsv | 1 + terminal/.config/wezterm/wezterm.lua | 31 +++++++++++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/bootstrap/packages_stable.tsv b/bootstrap/packages_stable.tsv index 5897b7b..ba437ae 100644 --- a/bootstrap/packages_stable.tsv +++ b/bootstrap/packages_stable.tsv @@ -301,6 +301,7 @@ ttf-comic-neue Comic Neue aspires to be the casual script choice for everyone in ttf-heuristica A serif latin & cyrillic font, derived from the "Adobe Utopia" font by Apanov A ttf-iosevka-nerd Patched font Iosevka from nerd fonts library R ttf-signika Sans-serif typeface from Google by Anna Giedryś A +ttf-victor-mono-nerd Patched font Victor Mono from nerd fonts library R tuir Browse Reddit from your terminal A tut A TUI for Mastodon with vim inspired keys A typescript-language-server Language Server Protocol (LSP) implementation for TypeScript using tsserver R diff --git a/terminal/.config/wezterm/wezterm.lua b/terminal/.config/wezterm/wezterm.lua index fc58b76..fc9fb24 100644 --- a/terminal/.config/wezterm/wezterm.lua +++ b/terminal/.config/wezterm/wezterm.lua @@ -36,6 +36,35 @@ local settings = { -- default_prog = {"nu"}, scrollback_lines = 10000, font = wezterm.font('Iosevka Nerd Font'), + -- add cursive italic font from Victor font for all weights + font_rules = { + { + intensity = 'Bold', + italic = true, + font = wezterm.font { + family = 'VictorMono Nerd Font', + weight = 'Bold', + style = 'Italic', + }, + }, + { + italic = true, + intensity = 'Half', + font = wezterm.font { + family = 'VictorMono Nerd Font', + weight = 'DemiBold', + style = 'Italic', + }, + }, + { + italic = true, + intensity = 'Normal', + font = wezterm.font { + family = 'VictorMono Nerd Font', + style = 'Italic', + }, + }, + }, line_height = 1.0, leader = { key = 'a', mods = 'CTRL', timeout_milliseconds = 1500 }, keys = maps.keys, @@ -45,7 +74,7 @@ local settings = { event = { Up = { streak = 1, button = 'Left' } }, mods = 'NONE', action = wezterm.action - .CompleteSelectionOrOpenLinkAtMouseCursor 'ClipboardAndPrimarySelection' + .CompleteSelectionOrOpenLinkAtMouseCursor 'ClipboardAndPrimarySelection' } } } From 337b250ababeb6fab49439bf23b0c22e71e5335f Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Wed, 22 Mar 2023 11:48:57 +0100 Subject: [PATCH 02/27] qutebrowser: Fix bare redirect exception --- qutebrowser/.config/qutebrowser/redirects.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/qutebrowser/.config/qutebrowser/redirects.py b/qutebrowser/.config/qutebrowser/redirects.py index caaba00..8323b9d 100644 --- a/qutebrowser/.config/qutebrowser/redirects.py +++ b/qutebrowser/.config/qutebrowser/redirects.py @@ -1,6 +1,7 @@ import random import re from qutebrowser.api import interceptor +from qutebrowser.extensions.interceptors import RedirectException redirects = { "youtube": { @@ -184,6 +185,7 @@ redirects = { }, } + def rewrite(request: interceptor.Request): if ( request.resource_type != interceptor.ResourceType.main_frame @@ -194,17 +196,17 @@ def rewrite(request: interceptor.Request): url = request.request_url for service in redirects.values(): - matched=False + matched = False for source in service["source"]: if re.search(source, url.host()): - matched=True + matched = True if matched: - target = service["target"][random.randint(0, len(service["target"])-1)] + target = service["target"][random.randint(0, len(service["target"]) - 1)] if target is not None and url.setHost(target) is not False: try: request.redirect(url) - except: + except RedirectException: pass break From f6a9006c077f569ab0427b1736739f108912c2df Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Wed, 22 Mar 2023 11:52:14 +0100 Subject: [PATCH 03/27] mpv: Make mpv automatic quality setting explicit --- multimedia/.config/mpv/scripts/battery.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/multimedia/.config/mpv/scripts/battery.lua b/multimedia/.config/mpv/scripts/battery.lua index 31d1c10..f14be30 100644 --- a/multimedia/.config/mpv/scripts/battery.lua +++ b/multimedia/.config/mpv/scripts/battery.lua @@ -20,6 +20,7 @@ local function adjust() mp.set_property("profile", lqprofile) else mp.msg.info("Not running on battery, setting high-quality options.") + mp.set_property("profile", hqprofile) end end mp.add_hook("on_load", 1, adjust) From 88336a433a53b5715ac48d8669101e0a4c0312c6 Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Wed, 22 Mar 2023 11:53:08 +0100 Subject: [PATCH 04/27] bootstrap: Disable system USB mouse wakeups My (un-branded) usb mouse prevented the system from going into suspend/hibernation by sending intermittent wakeup signals. This system configuration option simply disables the device from sending those wakeup signals. Used with superuser stow installation method. --- .../system-packages/etc/udev/rules.d/usb-optical-mouse.rules | 1 + 1 file changed, 1 insertion(+) create mode 100644 bootstrap/system-packages/etc/udev/rules.d/usb-optical-mouse.rules diff --git a/bootstrap/system-packages/etc/udev/rules.d/usb-optical-mouse.rules b/bootstrap/system-packages/etc/udev/rules.d/usb-optical-mouse.rules new file mode 100644 index 0000000..1ce944b --- /dev/null +++ b/bootstrap/system-packages/etc/udev/rules.d/usb-optical-mouse.rules @@ -0,0 +1 @@ +ACTION=="add", SUBSYSTEM=="usb", DRIVERS=="usb", ATTRS{idVendor}=="0000", ATTRS{idProduct}=="3825", ATTR{power/wakeup}="disabled" From f156292e8fa3fcc447bb3648d9d8872f99f91731 Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Wed, 22 Mar 2023 15:58:18 +0100 Subject: [PATCH 05/27] nvim: Add file tree In addition to my standard file manager, vifm, being integrated into neovim, I have now also added a side-pane file tree (akin to nerdtree) that is easily reachable to get a quick overview of a file layout. For now, I do not intend to do much more with the plugin, only keep it for those rare cases I want to have a view on my file layout at the same time as working in a buffer. For all other things (file operations especially) I still have vifm. --- nvim/.config/nvim/lazy-lock.json | 2 ++ nvim/.config/nvim/lua/maps.lua | 4 ++++ nvim/.config/nvim/lua/plugins.lua | 16 +++++++++++----- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/nvim/.config/nvim/lazy-lock.json b/nvim/.config/nvim/lazy-lock.json index 423307a..341c8c7 100644 --- a/nvim/.config/nvim/lazy-lock.json +++ b/nvim/.config/nvim/lazy-lock.json @@ -49,11 +49,13 @@ "nvim-notify": { "branch": "master", "commit": "bdd647f61a05c9b8a57c83b78341a0690e9c29d7" }, "nvim-surround": { "branch": "main", "commit": "b98862527c4727c171f8626e04d8ae88ef6cb736" }, "nvim-toggleterm.lua": { "branch": "main", "commit": "a5638b2206c3930a16a24e5c184dddd572f8cd34" }, + "nvim-tree.lua": { "branch": "master", "commit": "aa9971768a08caa4f10f94ab84e48d2ceb30b1c0" }, "nvim-treesitter": { "branch": "master", "commit": "c38646edf2bdfac157ca619697ecad9ea87fd469" }, "nvim-treesitter-context": { "branch": "master", "commit": "88d1627285f7477883516ef60521601862dae7a1" }, "nvim-treesitter-textsubjects": { "branch": "master", "commit": "b913508f503527ff540f7fe2dcf1bf1d1f259887" }, "nvim-ts-context-commentstring": { "branch": "main", "commit": "729d83ecb990dc2b30272833c213cc6d49ed5214" }, "nvim-ts-rainbow2": { "branch": "master", "commit": "cee4601ff8aac73dee4afa1074814343bb5a0b80" }, + "nvim-web-devicons": { "branch": "master", "commit": "95b1e300699be8eb6b5be1758a9d4d69fe93cc7f" }, "otter.nvim": { "branch": "main", "commit": "cfb548957aed403d9838febd7223595d47b32031" }, "playground": { "branch": "master", "commit": "4044b53c4d4fcd7a78eae20b8627f78ce7dc6f56" }, "plenary.nvim": { "branch": "master", "commit": "253d34830709d690f013daf2853a9d21ad7accab" }, diff --git a/nvim/.config/nvim/lua/maps.lua b/nvim/.config/nvim/lua/maps.lua index 30ca8ff..513ae9e 100644 --- a/nvim/.config/nvim/lua/maps.lua +++ b/nvim/.config/nvim/lua/maps.lua @@ -211,6 +211,10 @@ map('n', 'ss', ":lua MiniStarter.open()", { desc = 'show startpage' map('n', 'so', 'SymbolsOutline', { silent = true, desc = 'toggle symbol outline' }) +-- PLUGIN: nvim-tree +map('n', 'se', 'NvimTreeToggle', + { silent = true, desc = 'toggle filetree' }) + -- PLUGIN: easy-align -- Start interactive EasyAlign in visual mode (e.g. vipga) map('x', 'ga', '(EasyAlign)') diff --git a/nvim/.config/nvim/lua/plugins.lua b/nvim/.config/nvim/lua/plugins.lua index d833ab9..596b5cb 100644 --- a/nvim/.config/nvim/lua/plugins.lua +++ b/nvim/.config/nvim/lua/plugins.lua @@ -1,18 +1,24 @@ local writing_ft = { "quarto", "pandoc", "markdown", "text", "tex" } return { - -- vim plugs -- essential { 'numToStr/Navigator.nvim', branch = "master", config = true }, -- allow seamless navigation between vim buffers and tmux/wezterm splits { 'jeffkreeftmeijer/vim-numbertoggle', event = "BufEnter" }, -- toggles numbers to absolute for all buffers but the current which is relative { 'ojroques/vim-oscyank', event = "VeryLazy" }, -- yank from *anywhere* (even ssh session) to clipboard, using :OSCYank { 'ggandor/lightspeed.nvim', event = "VeryLazy" }, -- jump between letters with improved fFtT quicksearch, mimics sneak - -- files - { 'vifm/vifm.vim' }, -- integrate file manager { 'lewis6991/gitsigns.nvim', -- show vcs changes on left-hand gutter opts = { numhl = true, signcolumn = false }, event = "BufRead" + }, + -- files + { 'vifm/vifm.vim' }, -- integrate file manager + { + 'nvim-tree/nvim-tree.lua', -- integrate file tree + config = true, + dependencies = { 'nvim-tree/nvim-web-devicons', config = true }, + cmd = "NvimTreeToggle" + }, }, { 'RRethy/nvim-base16', event = "BufWinEnter", @@ -66,7 +72,7 @@ return { }, { 'edKotinsky/Arduino.nvim', ft = 'arduino', config = true }, -- statusline { 'nvim-lualine/lualine.nvim', - requires = { 'kyazdani42/nvim-web-devicons', opt = true }, + requires = { 'nvim-tree/nvim-web-devicons', config = true }, config = function() require('plug._lualine') end }, -- writing { 'vim-pandoc/vim-criticmarkup', ft = writing_ft }, { @@ -136,7 +142,7 @@ return { vim.g.magma_image_provider = "kitty" vim.g.magma_automatically_open_output = false end - }, -- nvim plugs + }, { 'echasnovski/mini.nvim', version = '*', From e95bd9b25225c81829a7f3ea4e2fb76a61204635 Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Wed, 22 Mar 2023 16:14:10 +0100 Subject: [PATCH 06/27] nvim: Add smartcolumn plugin --- nvim/.config/nvim/lazy-lock.json | 1 + nvim/.config/nvim/lua/plugins.lua | 1 + 2 files changed, 2 insertions(+) diff --git a/nvim/.config/nvim/lazy-lock.json b/nvim/.config/nvim/lazy-lock.json index 341c8c7..8158b5a 100644 --- a/nvim/.config/nvim/lazy-lock.json +++ b/nvim/.config/nvim/lazy-lock.json @@ -62,6 +62,7 @@ "popup.nvim": { "branch": "master", "commit": "b7404d35d5d3548a82149238289fa71f7f6de4ac" }, "quarto-nvim": { "branch": "main", "commit": "91c82b96660d0b2d830c668365719b295272432d" }, "significant.nvim": { "branch": "main", "commit": "5450e9d5917dc6aa9afb0fcbe32355799b8303fb" }, + "smartcolumn.nvim": { "branch": "main", "commit": "0c572e3eae48874f25b74394a486f38cadb5c958" }, "spellsitter.nvim": { "branch": "master", "commit": "4af8640d9d706447e78c13150ef7475ea2c16b30" }, "symbols-outline.nvim": { "branch": "master", "commit": "512791925d57a61c545bc303356e8a8f7869763c" }, "telescope-fzf-native.nvim": { "branch": "main", "commit": "580b6c48651cabb63455e97d7e131ed557b8c7e2" }, diff --git a/nvim/.config/nvim/lua/plugins.lua b/nvim/.config/nvim/lua/plugins.lua index 596b5cb..acd61a4 100644 --- a/nvim/.config/nvim/lua/plugins.lua +++ b/nvim/.config/nvim/lua/plugins.lua @@ -11,6 +11,7 @@ return { opts = { numhl = true, signcolumn = false }, event = "BufRead" }, + { "m4xshen/smartcolumn.nvim", config = true }, -- auto-hiding colorcolumn -- files { 'vifm/vifm.vim' }, -- integrate file manager { From d0e536b798e4eb62a3ddde4ac1fb08c48c1ba8f0 Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Wed, 22 Mar 2023 16:29:10 +0100 Subject: [PATCH 07/27] nvim: Update colorizer git address --- nvim/.config/nvim/lazy-lock.json | 4 ++-- nvim/.config/nvim/lua/plugins.lua | 11 ++++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/nvim/.config/nvim/lazy-lock.json b/nvim/.config/nvim/lazy-lock.json index 8158b5a..200333b 100644 --- a/nvim/.config/nvim/lazy-lock.json +++ b/nvim/.config/nvim/lazy-lock.json @@ -42,9 +42,9 @@ "mini.nvim": { "branch": "main", "commit": "427751024313e2270ca723eb16af7b218c83a7fc" }, "neural": { "branch": "main", "commit": "155618730b87a67655bdde373ee27bfce8b07ac9" }, "nui.nvim": { "branch": "main", "commit": "0dc148c6ec06577fcf06cbab3b7dac96d48ba6be" }, - "nvim-base16": { "branch": "master", "commit": "22bad36cd64e85afb0c9d0e9b92106b5ea6dabc6" }, + "nvim-base16": { "branch": "master", "commit": "db9ac827d833236b2b7bbacf6ec3a92f96b88890" }, "nvim-cmp": { "branch": "main", "commit": "777450fd0ae289463a14481673e26246b5e38bf2" }, - "nvim-colorizer.lua": { "branch": "master", "commit": "36c610a9717cc9ec426a07c8e6bf3b3abcb139d6" }, + "nvim-colorizer.lua": { "branch": "master", "commit": "dde3084106a70b9a79d48f426f6d6fec6fd203f7" }, "nvim-lspconfig": { "branch": "master", "commit": "0f94c5fded29c0024254259f3d8a0284bfb507ea" }, "nvim-notify": { "branch": "master", "commit": "bdd647f61a05c9b8a57c83b78341a0690e9c29d7" }, "nvim-surround": { "branch": "main", "commit": "b98862527c4727c171f8626e04d8ae88ef6cb736" }, diff --git a/nvim/.config/nvim/lua/plugins.lua b/nvim/.config/nvim/lua/plugins.lua index acd61a4..750125e 100644 --- a/nvim/.config/nvim/lua/plugins.lua +++ b/nvim/.config/nvim/lua/plugins.lua @@ -20,12 +20,13 @@ return { dependencies = { 'nvim-tree/nvim-web-devicons', config = true }, cmd = "NvimTreeToggle" }, + -- colors + { + 'RRethy/nvim-base16', + event = "BufWinEnter", + dependencies = { 'rktjmp/fwatch.nvim' } }, { - 'RRethy/nvim-base16', - event = "BufWinEnter", - dependencies = { 'rktjmp/fwatch.nvim' } -}, { - 'norcalli/nvim-colorizer.lua', -- color hex, named colors in the correct preview scheme + 'NvChad/nvim-colorizer.lua', -- color hex, named colors in the correct preview scheme config = true, event = "VeryLazy" }, { From aa68137ff81ae83ab20accd5fb390d22e6d158fe Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Wed, 22 Mar 2023 17:01:02 +0100 Subject: [PATCH 08/27] nvim: Add zk mappings --- nvim/.config/nvim/lazy-lock.json | 2 +- nvim/.config/nvim/lua/maps.lua | 29 ++++++++++++++++++++++++++--- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/nvim/.config/nvim/lazy-lock.json b/nvim/.config/nvim/lazy-lock.json index 200333b..48726bb 100644 --- a/nvim/.config/nvim/lazy-lock.json +++ b/nvim/.config/nvim/lazy-lock.json @@ -80,5 +80,5 @@ "wrapping.nvim": { "branch": "master", "commit": "a4013c377e2ffa3be00fb67791d3605ae3115acb" }, "zen-mode.nvim": { "branch": "main", "commit": "d907e638c879642d226d27469b53db6925f69d4c" }, "zettelkasten.nvim": { "branch": "main", "commit": "0e77624689b470410f5355b613d45219c9350264" }, - "zk-nvim": { "branch": "main", "commit": "0413c52500cd0133b0cd8e7e7d43084855ac1760" } + "zk-nvim": { "branch": "main", "commit": "50fc25b88fb28829ec7f5e5a4d4b458fca21a550" } } \ No newline at end of file diff --git a/nvim/.config/nvim/lua/maps.lua b/nvim/.config/nvim/lua/maps.lua index 513ae9e..e6538c3 100644 --- a/nvim/.config/nvim/lua/maps.lua +++ b/nvim/.config/nvim/lua/maps.lua @@ -236,9 +236,32 @@ map("v", "g", 'g(dial-increment)') -- PLUGIN: zettelkasten.nvim map('n', '', [[:silent lua require 'zettelkasten'.link_follow()]]) map('v', '', [[:lua require 'zettelkasten'.link_follow(true)]]) -prefix({ ['w'] = { name = '+wiki' } }) -map('n', 'ww', [[:lua require 'zettelkasten'.index_open() ]], - { desc = "open wiki" }) +prefix({ ['n'] = { name = '+notes' } }) +map('n', 'ni', [[:lua require 'zettelkasten'.index_open() ]], + { desc = "index page" }) +-- PLUGIN: zk +map('n', 'nn', "ZkNotes { sort = { 'modified' } }", + { desc = "note list" }) +map("n", "nf", "ZkNotes { sort = { 'modified' }, match = { vim.fn.input('Search: ') } }", + { desc = "note search" }) +map('n', 'nt', "ZkTags", + { desc = "note tags" }) +map('n', 'nc', "ZkCd", + { desc = "notes directory" }) +prefix({ ['n'] = { name = '+note' } }) +map('n', 'nl', "ZkLinks", + { desc = "note links" }) +map('n', 'nb', "ZkLinks", + { desc = "note backlinks" }) +map('n', 'nn', "ZkNew { title = vim.fn.input('Title: ') }", + { desc = "new note" }) +prefix({ ['n'] = { name = '+note', mode = "v" } }) +map('v', 'nn', ":ZkNewFromTitleSelection", + { desc = "title from selection" }) +map('v', 'nN', ":ZkNewFromContentSelection", + { desc = "content from selection" }) +map('v', 'nf', ":ZkMatch", + { desc = "find note from selection" }) -- PLUGIN: toggleterm.nvim -- create a lazygit window, set up in toggleterm settings From f1b7c02aee6e7c07c7529f6515d9a2543deb4ab0 Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Wed, 22 Mar 2023 17:29:41 +0100 Subject: [PATCH 09/27] nvim: Make cursorword highlights more inconspicuous --- nvim/.config/nvim/lua/plug/_mini.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nvim/.config/nvim/lua/plug/_mini.lua b/nvim/.config/nvim/lua/plug/_mini.lua index 83d618a..58872cf 100644 --- a/nvim/.config/nvim/lua/plug/_mini.lua +++ b/nvim/.config/nvim/lua/plug/_mini.lua @@ -34,3 +34,5 @@ starter.setup({ starter.gen_hook.aligning('center', 'center') } }) + +vim.api.nvim_set_hl(0, 'MiniCursorword', { bold = true }) From 2b64b4b750625f1beb89396b6087eac528de07f0 Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Wed, 22 Mar 2023 17:31:49 +0100 Subject: [PATCH 10/27] nvim: Update lazy lockfile --- nvim/.config/nvim/lazy-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nvim/.config/nvim/lazy-lock.json b/nvim/.config/nvim/lazy-lock.json index 48726bb..c1db2c7 100644 --- a/nvim/.config/nvim/lazy-lock.json +++ b/nvim/.config/nvim/lazy-lock.json @@ -32,13 +32,13 @@ "lazy.nvim": { "branch": "main", "commit": "887eb75591520a01548134c4623617b639289d0b" }, "lightspeed.nvim": { "branch": "main", "commit": "299eefa6a9e2d881f1194587c573dad619fdb96f" }, "lsp-format.nvim": { "branch": "master", "commit": "ca0df5c8544e51517209ea7b86ecc522c98d4f0a" }, - "lsp-zero.nvim": { "branch": "v2.x", "commit": "7cb74b241c9d99b5d44f2111696cd9306848b25b" }, + "lsp-zero.nvim": { "branch": "v2.x", "commit": "3beb377de24b81ba3c6e719b3c15cf1f50536338" }, "lsp_signature.nvim": { "branch": "master", "commit": "4665921ff8e30601c7c1328625b3abc1427a6143" }, "lualine.nvim": { "branch": "master", "commit": "e99d733e0213ceb8f548ae6551b04ae32e590c80" }, "magma-nvim-goose": { "branch": "main", "commit": "5d916c39c1852e09fcd39eab174b8e5bbdb25f8f" }, "markdown-preview.nvim": { "branch": "master", "commit": "9becceee5740b7db6914da87358a183ad11b2049" }, "mason-lspconfig.nvim": { "branch": "main", "commit": "2b811031febe5f743e07305738181ff367e1e452" }, - "mason.nvim": { "branch": "main", "commit": "a192887fd0c29275cf2acb4a83bcbdf63399f8df" }, + "mason.nvim": { "branch": "main", "commit": "9f6fd51ce6a3381fbed5fe33169ff20b5bd8f00b" }, "mini.nvim": { "branch": "main", "commit": "427751024313e2270ca723eb16af7b218c83a7fc" }, "neural": { "branch": "main", "commit": "155618730b87a67655bdde373ee27bfce8b07ac9" }, "nui.nvim": { "branch": "main", "commit": "0dc148c6ec06577fcf06cbab3b7dac96d48ba6be" }, @@ -47,7 +47,7 @@ "nvim-colorizer.lua": { "branch": "master", "commit": "dde3084106a70b9a79d48f426f6d6fec6fd203f7" }, "nvim-lspconfig": { "branch": "master", "commit": "0f94c5fded29c0024254259f3d8a0284bfb507ea" }, "nvim-notify": { "branch": "master", "commit": "bdd647f61a05c9b8a57c83b78341a0690e9c29d7" }, - "nvim-surround": { "branch": "main", "commit": "b98862527c4727c171f8626e04d8ae88ef6cb736" }, + "nvim-surround": { "branch": "main", "commit": "056f69ed494198ff6ea0070cfc66997cfe0a6c8b" }, "nvim-toggleterm.lua": { "branch": "main", "commit": "a5638b2206c3930a16a24e5c184dddd572f8cd34" }, "nvim-tree.lua": { "branch": "master", "commit": "aa9971768a08caa4f10f94ab84e48d2ceb30b1c0" }, "nvim-treesitter": { "branch": "master", "commit": "c38646edf2bdfac157ca619697ecad9ea87fd469" }, From d2e101b8220b13ac3ad971f5c177a94061cb0683 Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Thu, 23 Mar 2023 07:34:08 +0100 Subject: [PATCH 11/27] nvim: Add latex notation concealing with nabla Notations will be concealed automatically on entering a textual buffer and `$...$` style notations are contained. Concealing can be turned off with sV, which will toggle concealing on or off for all notations in the file. Additionally, the notation under curser can be viewed in a popup with sv. --- nvim/.config/nvim/lazy-lock.json | 1 + nvim/.config/nvim/lua/maps.lua | 6 +++++- nvim/.config/nvim/lua/plugins.lua | 6 ++++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/nvim/.config/nvim/lazy-lock.json b/nvim/.config/nvim/lazy-lock.json index c1db2c7..43a8dbf 100644 --- a/nvim/.config/nvim/lazy-lock.json +++ b/nvim/.config/nvim/lazy-lock.json @@ -40,6 +40,7 @@ "mason-lspconfig.nvim": { "branch": "main", "commit": "2b811031febe5f743e07305738181ff367e1e452" }, "mason.nvim": { "branch": "main", "commit": "9f6fd51ce6a3381fbed5fe33169ff20b5bd8f00b" }, "mini.nvim": { "branch": "main", "commit": "427751024313e2270ca723eb16af7b218c83a7fc" }, + "nabla.nvim": { "branch": "master", "commit": "4870fce48aa4ce3565fafb0e778378d728ad02b0" }, "neural": { "branch": "main", "commit": "155618730b87a67655bdde373ee27bfce8b07ac9" }, "nui.nvim": { "branch": "main", "commit": "0dc148c6ec06577fcf06cbab3b7dac96d48ba6be" }, "nvim-base16": { "branch": "master", "commit": "db9ac827d833236b2b7bbacf6ec3a92f96b88890" }, diff --git a/nvim/.config/nvim/lua/maps.lua b/nvim/.config/nvim/lua/maps.lua index e6538c3..b0b6d43 100644 --- a/nvim/.config/nvim/lua/maps.lua +++ b/nvim/.config/nvim/lua/maps.lua @@ -267,9 +267,13 @@ map('v', 'nf', ":ZkMatch", -- create a lazygit window, set up in toggleterm settings map('n', 'G', ':Lazygit') +prefix({ ['s'] = { name = '+set' } }) -- PLUGIN: wrapping.nvim map('n', 'sw', [[:lua require('wrapping').toggle_wrap_mode() ]], { silent = true, desc = 'toggle wrap mode' }) - -- PLUGIN: easyread.nvim map('n', 'ss', ':EasyreadToggle', { silent = true, desc = 'toggle speedreading' }) +-- PLUGIN: nabla.nvim +map('n', 'sv', 'lua require("nabla").popup()', { silent = true, desc = 'latex formula popup' }) +map('n', 'sV', 'lua require("nabla").toggle_virt({autogen = true, silent = true})', + { silent = true, desc = 'toggle formula notation' }) diff --git a/nvim/.config/nvim/lua/plugins.lua b/nvim/.config/nvim/lua/plugins.lua index 750125e..ced2b51 100644 --- a/nvim/.config/nvim/lua/plugins.lua +++ b/nvim/.config/nvim/lua/plugins.lua @@ -78,6 +78,12 @@ return { config = function() require('plug._lualine') end }, -- writing { 'vim-pandoc/vim-criticmarkup', ft = writing_ft }, { + 'jbyuki/nabla.nvim', + ft = writing_ft, + config = function() + require('nabla').enable_virt({ autogen = true, silent = true }) + end +}, { 'mickael-menu/zk-nvim', config = function() require('zk').setup({ picker = "telescope" }) end }, { From 3a6c7717c875f9195bbba464b8b0bf45466144f7 Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Thu, 23 Mar 2023 08:29:12 +0100 Subject: [PATCH 12/27] nvim: Set up gitsigns mappings --- nvim/.config/nvim/lua/plug/_gitsigns.lua | 84 +++++++++++++----------- nvim/.config/nvim/lua/plugins.lua | 7 +- 2 files changed, 48 insertions(+), 43 deletions(-) diff --git a/nvim/.config/nvim/lua/plug/_gitsigns.lua b/nvim/.config/nvim/lua/plug/_gitsigns.lua index 378a12b..6cbb465 100644 --- a/nvim/.config/nvim/lua/plug/_gitsigns.lua +++ b/nvim/.config/nvim/lua/plug/_gitsigns.lua @@ -1,40 +1,44 @@ --- require('gitsigns').setup { --- on_attach = function(bufnr) --- local gs = package.loaded.gitsigns --- --- local function map(mode, l, r, opts) --- opts = opts or {} --- opts.buffer = bufnr --- vim.keymap.set(mode, l, r, opts) --- end --- --- -- Navigation --- map('n', ']h', function() --- if vim.wo.diff then return ']h' end --- vim.schedule(function() gs.next_hunk() end) --- return '' --- end, {expr = true}) --- --- map('n', '[h', function() --- if vim.wo.diff then return '[h' end --- vim.schedule(function() gs.prev_hunk() end) --- return '' --- end, {expr = true}) --- --- -- Actions --- map({'n', 'v'}, 'hs', ':Gitsigns stage_hunk') --- map('n', 'hS', gs.stage_buffer) --- map({'n', 'v'}, 'hr', ':Gitsigns reset_hunk') --- map('n', 'hR', gs.reset_buffer) --- map('n', 'hu', gs.undo_stage_hunk) --- map('n', 'hp', gs.preview_hunk) --- map('n', 'hb', function() gs.blame_line {full = true} end) --- map('n', 'hB', gs.toggle_current_line_blame) --- map('n', 'hd', gs.diffthis) --- map('n', 'hD', function() gs.diffthis('~') end) --- map('n', 'hdd', gs.toggle_deleted) --- --- -- Text object --- map({'o', 'x'}, 'ih', ':Gitsigns select_hunk') --- end --- } +require('gitsigns').setup { + numhl = true, + signcolumn = false, + on_attach = function(bufnr) + local gs = package.loaded.gitsigns + + local function map(mode, l, r, opts) + opts = opts or {} + opts.buffer = bufnr + vim.keymap.set(mode, l, r, opts) + end + + -- Navigation + map('n', ']h', function() + if vim.wo.diff then return ']h' end + vim.schedule(function() gs.next_hunk() end) + return '' + end, { expr = true }) + + map('n', '[h', function() + if vim.wo.diff then return '[h' end + vim.schedule(function() gs.prev_hunk() end) + return '' + end, { expr = true }) + + -- Actions + require('which-key').register({ ['h'] = { name = '+git' } }) + map({ 'n', 'v' }, 'hs', ':Gitsigns stage_hunk', { desc = 'stage hunk' }) + map({ 'n', 'v' }, 'hr', ':Gitsigns reset_hunk', { desc = 'reset hunk' }) + map('n', 'hS', gs.stage_buffer, { desc = 'stage buffer' }) + map('n', 'hu', gs.undo_stage_hunk, { desc = 'undo stage hunk' }) + map('n', 'hR', gs.reset_buffer, { desc = 'reset buffer' }) + map('n', 'hp', gs.preview_hunk, { desc = 'preview hunk' }) + map('n', 'hb', function() gs.blame_line { full = true } end, { desc = 'blame line' }) + map('n', 'hB', gs.toggle_current_line_blame, { desc = 'toggle blame' }) + map('n', 'hd', gs.diffthis, { desc = 'diffthis' }) + map('n', 'hD', function() gs.diffthis('~') end, { desc = 'diffbase' }) + map('n', 'ht', gs.toggle_deleted, { desc = 'toggle deleted' }) + + -- Text object + map({ 'o', 'x' }, 'ih', ':Gitsigns select_hunk') + map({ 'o', 'x' }, 'ah', ':Gitsigns select_hunk') + end +} diff --git a/nvim/.config/nvim/lua/plugins.lua b/nvim/.config/nvim/lua/plugins.lua index ced2b51..ca2b57d 100644 --- a/nvim/.config/nvim/lua/plugins.lua +++ b/nvim/.config/nvim/lua/plugins.lua @@ -8,10 +8,11 @@ return { { 'ggandor/lightspeed.nvim', event = "VeryLazy" }, -- jump between letters with improved fFtT quicksearch, mimics sneak { 'lewis6991/gitsigns.nvim', -- show vcs changes on left-hand gutter - opts = { numhl = true, signcolumn = false }, + config = function() + require('plug._gitsigns') + end, event = "BufRead" - }, - { "m4xshen/smartcolumn.nvim", config = true }, -- auto-hiding colorcolumn + }, { "m4xshen/smartcolumn.nvim", config = true }, -- auto-hiding colorcolumn -- files { 'vifm/vifm.vim' }, -- integrate file manager { From 8606652b934ead34c468ed484bbd34a42995221b Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Thu, 23 Mar 2023 08:30:01 +0100 Subject: [PATCH 13/27] nvim: Fix colorizer setup and add mappings --- nvim/.config/nvim/lua/maps.lua | 6 +++++- nvim/.config/nvim/lua/plugins.lua | 8 +++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/nvim/.config/nvim/lua/maps.lua b/nvim/.config/nvim/lua/maps.lua index b0b6d43..2597fd5 100644 --- a/nvim/.config/nvim/lua/maps.lua +++ b/nvim/.config/nvim/lua/maps.lua @@ -4,7 +4,7 @@ local prefix = require('which-key').register -- The general ideas behind these mappings: -- -- * Leader prefix is the generally preferred way to map new things, however --- only for those that affect all of vim of work in a supra-buffer way. +-- only for those that affect all of vim or work in a supra-buffer way. -- -- * Localleader prefix is used for mappings which only affect single buffers. -- In other words mostly filetype specific mappings @@ -277,3 +277,7 @@ map('n', 'ss', ':EasyreadToggle', { silent = true, desc = 'togg map('n', 'sv', 'lua require("nabla").popup()', { silent = true, desc = 'latex formula popup' }) map('n', 'sV', 'lua require("nabla").toggle_virt({autogen = true, silent = true})', { silent = true, desc = 'toggle formula notation' }) +-- PLUGIN: nvim-colorizer +map('n', 'sc', 'ColorizerToggle', { silent = true, desc = 'toggle colorizer' }) +map('n', 'sC', 'lua require("colorizer").attach_to_buffer(0, {mode = "background"} )', + { silent = true, desc = 'colorize background' }) diff --git a/nvim/.config/nvim/lua/plugins.lua b/nvim/.config/nvim/lua/plugins.lua index ca2b57d..70df9a8 100644 --- a/nvim/.config/nvim/lua/plugins.lua +++ b/nvim/.config/nvim/lua/plugins.lua @@ -28,7 +28,13 @@ return { dependencies = { 'rktjmp/fwatch.nvim' } }, { 'NvChad/nvim-colorizer.lua', -- color hex, named colors in the correct preview scheme - config = true, + config = function() + require('colorizer').setup({ + user_default_options = { + mode = 'virtualtext' + } + }) + end, event = "VeryLazy" }, { 'mhartington/formatter.nvim', -- auto formatting on save From 42e8504b1b239390fb655ffd0c69620a824955d9 Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Mon, 15 May 2023 09:38:20 +0200 Subject: [PATCH 14/27] waybar: Add river modules,Update for new nerdfonts Added a window entry on the bar for the currently displayed window. If it annoys me I will delete it again but it helps distinguish the active and the inactive output. Also added the current river *mode* which is a lovely feature to have, though I would like to hide it if no mode (other than normal) is currently active. --- desktop/.config/waybar/config | 27 +++++++++++++++++---------- desktop/.config/waybar/style.css | 10 ++++++++++ 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/desktop/.config/waybar/config b/desktop/.config/waybar/config index 6ead0b9..54794f6 100644 --- a/desktop/.config/waybar/config +++ b/desktop/.config/waybar/config @@ -1,13 +1,13 @@ { "layer": "top", - "modules-left": ["river/tags", "custom/events", "custom/vidl"], + "modules-left": ["river/tags", "custom/events", "custom/vidl", "river/window"], "modules-center": ["clock", "custom/media"], - "modules-right": ["custom/wireguard", "custom/archupdates", "pulseaudio", "backlight", "network", "cpu", "memory", "temperature", "battery", "tray"], + "modules-right": ["river/mode", "custom/wireguard", "custom/archupdates", "pulseaudio", "backlight", "network", "cpu", "memory", "temperature", "battery", "tray"], "custom/archupdates": { "format": "{} {icon}", "format-alt-click": "right", "format-icons": { - "default": "" + "default": "" }, "return-type": "json", "exec": "~/.config/waybar/modules/archupdates 5 json", @@ -56,7 +56,8 @@ }, "exec": "~/.config/waybar/modules/khal.py 2>/dev/null", "exec-if": "command -v khal >/dev/null 2>&1", - "return-type": "json" + "return-type": "json", + "on-click": "$TERMINAL start --class float ikhal" }, "memory": { "interval": 30, @@ -105,14 +106,13 @@ "on-click-right": "playerctl stop", }, "network": { - "interface": "wlp58s0", "format": "{ifname}", "format-wifi": "{signalStrength}% ", - "format-ethernet": "{ipaddr}/{cidr} ", - "format-disconnected": "睊", - "tooltip-format": "{ifname} via {gwaddr} ", + "format-ethernet": "{ipaddr}/{cidr} 󰈀", + "format-disconnected": "󰖪", + "tooltip-format": "{ifname} via {gwaddr} 󰈁", "tooltip-format-wifi": "{essid}: {bandwidthDownBits}-{bandwidthUpBits} ({signalStrength}%)  {ifname}", - "tooltip-format-ethernet": "{ifname} ", + "tooltip-format-ethernet": "{ifname} 󰈀", "tooltip-format-disconnected": "Disconnected", "max-length": 50, "on-click": "$TERMINAL start --class float nmtui", @@ -140,6 +140,13 @@ "num-tags": 10, "tag-labels": [ "", "", "", "", "", "", "", "", "", "" ] }, + "river/mode": { + "format": "{} 󱐁", + }, + "river/window": { + "format": " {}", + "max-length": 70 + }, "temperature": { // "thermal-zone": 2, "hwmon-path": "/sys/class/hwmon/hwmon5/temp1_input", @@ -171,7 +178,7 @@ "format": "{} {icon}", "format-alt-click": "right", "format-icons": { - "default": "" + "default": "" }, "exec": "wc -l ~/.local/share/vidl/vidl_queue | cut -d' ' -f1", "exec-if": "[ -f ~/.local/share/vidl/vidl_queue ]", diff --git a/desktop/.config/waybar/style.css b/desktop/.config/waybar/style.css index 9ec5143..465be18 100644 --- a/desktop/.config/waybar/style.css +++ b/desktop/.config/waybar/style.css @@ -2,6 +2,7 @@ border: none; border-radius: 0; min-height: 0; + font-family: Iosevka Nerd Font, Iosevka, monospace; } window#waybar { @@ -57,6 +58,7 @@ window#waybar.hidden { #mode, #idle_inhibitor, #mpd, +#window, #custom-archupdates, #custom-wireguard, #custom-events, @@ -82,6 +84,14 @@ window#waybar.hidden { background-color: @base02; } +/* Mark active output through highlighted window background */ +#window { + background-color: transparent; +} +#window.focused { + background-color: @base01; +} + #battery.warning { background-color: @base09; color: @base00; From 8a0fd5364744406741ea71879f8da6ca21983999 Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Tue, 23 May 2023 15:26:34 +0200 Subject: [PATCH 15/27] waybar: Fix remaining status bar icons --- desktop/.config/waybar/config | 6 +++--- desktop/.config/waybar/modules/wireguard | 2 +- desktop/.config/waybar/style.css | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/desktop/.config/waybar/config b/desktop/.config/waybar/config index 54794f6..ccb3871 100644 --- a/desktop/.config/waybar/config +++ b/desktop/.config/waybar/config @@ -120,12 +120,12 @@ }, "pulseaudio": { "format": "{volume}% {icon}", - "format-bluetooth": "{volume}% {icon}", + "format-bluetooth": "{volume}% {icon} ", "format-muted": "", "format-icons": { "headphone": "", "hands-free": "", - "headset": "", + "headset": "󰋎", "phone": "", "portable": "", "car": "", @@ -166,7 +166,7 @@ }, "custom/wireguard": { "format-icons": { - "default": "嬨" + "default": "󰖂" }, "exec": "~/.config/waybar/modules/wireguard json", "exec-if": "command -v nmcli >/dev/null 2>&1", diff --git a/desktop/.config/waybar/modules/wireguard b/desktop/.config/waybar/modules/wireguard index 82256b8..fcdca46 100755 --- a/desktop/.config/waybar/modules/wireguard +++ b/desktop/.config/waybar/modules/wireguard @@ -41,7 +41,7 @@ connected=() available=() function print_as_json() { - text="嬨" # only prints a single icon when connected + text="󰖂" # only prints a single icon when connected # text="${1}" # use this line to show all output in text alt="${1}" tooltip="${1}" diff --git a/desktop/.config/waybar/style.css b/desktop/.config/waybar/style.css index 465be18..348a579 100644 --- a/desktop/.config/waybar/style.css +++ b/desktop/.config/waybar/style.css @@ -2,7 +2,7 @@ border: none; border-radius: 0; min-height: 0; - font-family: Iosevka Nerd Font, Iosevka, monospace; + font-family: Cantarell, Signika, Iosevka Nerd Font, Iosevka, monospace; } window#waybar { From 01ca98aa4be58e17c3230bde205ac7cc21f176d9 Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Tue, 23 May 2023 15:31:17 +0200 Subject: [PATCH 16/27] mpv: Update gui interface --- multimedia/.config/mpv/fonts/uosc_icons.otf | Bin 0 -> 400360 bytes .../.config/mpv/fonts/uosc_textures.ttf | Bin 0 -> 38228 bytes multimedia/.config/mpv/input.conf | 6 +- multimedia/.config/mpv/scripts/battery.lua | 13 +- .../.config/mpv/scripts/copy_videotime.lua | 80 + multimedia/.config/mpv/scripts/gallery-dl.lua | 13 +- .../mpv/scripts/sponsorblock_minimal.lua | 6 +- multimedia/.config/mpv/scripts/thumbfast.lua | 975 ++++ multimedia/.config/mpv/scripts/uosc.lua | 4716 ++++------------- .../elements/BufferingIndicator.lua | 37 + .../scripts/uosc_shared/elements/Button.lua | 90 + .../scripts/uosc_shared/elements/Controls.lua | 329 ++ .../scripts/uosc_shared/elements/Curtain.lua | 35 + .../uosc_shared/elements/CycleButton.lua | 64 + .../scripts/uosc_shared/elements/Element.lua | 154 + .../scripts/uosc_shared/elements/Elements.lua | 125 + .../mpv/scripts/uosc_shared/elements/Menu.lua | 854 +++ .../uosc_shared/elements/PauseIndicator.lua | 80 + .../scripts/uosc_shared/elements/Speed.lua | 192 + .../scripts/uosc_shared/elements/Timeline.lua | 430 ++ .../scripts/uosc_shared/elements/TopBar.lua | 253 + .../scripts/uosc_shared/elements/Volume.lua | 252 + .../uosc_shared/elements/WindowBorder.lua | 33 + .../mpv/scripts/uosc_shared/lib/ass.lua | 170 + .../mpv/scripts/uosc_shared/lib/menus.lua | 292 + .../mpv/scripts/uosc_shared/lib/std.lua | 181 + .../mpv/scripts/uosc_shared/lib/text.lua | 461 ++ .../mpv/scripts/uosc_shared/lib/utils.lua | 609 +++ .../.config/mpv/scripts/uosc_shared/main.lua | 5 + 29 files changed, 6886 insertions(+), 3569 deletions(-) create mode 100644 multimedia/.config/mpv/fonts/uosc_icons.otf create mode 100644 multimedia/.config/mpv/fonts/uosc_textures.ttf create mode 100644 multimedia/.config/mpv/scripts/copy_videotime.lua create mode 100644 multimedia/.config/mpv/scripts/thumbfast.lua create mode 100644 multimedia/.config/mpv/scripts/uosc_shared/elements/BufferingIndicator.lua create mode 100644 multimedia/.config/mpv/scripts/uosc_shared/elements/Button.lua create mode 100644 multimedia/.config/mpv/scripts/uosc_shared/elements/Controls.lua create mode 100644 multimedia/.config/mpv/scripts/uosc_shared/elements/Curtain.lua create mode 100644 multimedia/.config/mpv/scripts/uosc_shared/elements/CycleButton.lua create mode 100644 multimedia/.config/mpv/scripts/uosc_shared/elements/Element.lua create mode 100644 multimedia/.config/mpv/scripts/uosc_shared/elements/Elements.lua create mode 100644 multimedia/.config/mpv/scripts/uosc_shared/elements/Menu.lua create mode 100644 multimedia/.config/mpv/scripts/uosc_shared/elements/PauseIndicator.lua create mode 100644 multimedia/.config/mpv/scripts/uosc_shared/elements/Speed.lua create mode 100644 multimedia/.config/mpv/scripts/uosc_shared/elements/Timeline.lua create mode 100644 multimedia/.config/mpv/scripts/uosc_shared/elements/TopBar.lua create mode 100644 multimedia/.config/mpv/scripts/uosc_shared/elements/Volume.lua create mode 100644 multimedia/.config/mpv/scripts/uosc_shared/elements/WindowBorder.lua create mode 100644 multimedia/.config/mpv/scripts/uosc_shared/lib/ass.lua create mode 100644 multimedia/.config/mpv/scripts/uosc_shared/lib/menus.lua create mode 100644 multimedia/.config/mpv/scripts/uosc_shared/lib/std.lua create mode 100644 multimedia/.config/mpv/scripts/uosc_shared/lib/text.lua create mode 100644 multimedia/.config/mpv/scripts/uosc_shared/lib/utils.lua create mode 100644 multimedia/.config/mpv/scripts/uosc_shared/main.lua diff --git a/multimedia/.config/mpv/fonts/uosc_icons.otf b/multimedia/.config/mpv/fonts/uosc_icons.otf new file mode 100644 index 0000000000000000000000000000000000000000..4c4e0dcfe20684e18da5fd6f4dfbd489ee947c66 GIT binary patch literal 400360 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

Evhfm%Hl zoDENxyc5S`F;Df2;I3_y5365-BLHViOb<9!a@qsVHju01>fIfk-MiTlyQoou-G+KjFF8gQ0iZ|J4RR%s33ju)KV@uVNqdKX*(Z$H*tjBUw>r0MA3 z9AHu@O(~;Zm=+Amz$B@CdW3PKK)S~ovMDezUDM+0fM0b|>rSf5%s`8w-gJ>H$Dh`s*l^XZvnWnFl0L{u7QG)94)j(Q090MHdiN}MZ z=gyuf4tV-+hD}24mb-!;5iUi-Kn-f6(x4r1vRB3@bt2z#*b-fGi3OpyUX9w!n&LUpQfgsWIhIt(oFuPH$n?e*=9WO zp2a!69*-b-1?vvYD&S&!?6OhhJhq_Jyk&9DGH3Gt6Tnd`k|$Xc-^n#J7+-wFnx~gB zwvRc}?PI0g;7SV8H&q6lS6OVh>LJaeLfyDE5j=Tu8L0O<5B1xHH!6p z4ZqTqilr}+xHr2pu+A%VQc!F@1D0~cQF9(HEmpLu%d7x4&7+Qb<3?a1u#wIJ-1)PP z_o)-PdL(nqdHw}CzRcXZ2Q1s{Vz0S-#FD!xo!3~bqagLKvv$#nAj7rziEl{&7wcTw zq@v(7WP*|-n6OAuzlwu{fTzc}huwrIQyK`HNus?+Gzkd%&z5jB$j!Sm@R9vb;D5=$ z^Eq;D{QPE%Cr?uIMJELS8~HVcUoNB$i}ee8N8Qd=kgm&schc+h5Btj4Gq&$9?G+FaUaT2vf{!2cL5VmLh~+oUcQfCjM7oydh`U)gXPf_m5}CbZZknqb zou@u)4<+1K#QOJYTf%Xv;}L4FIA4yI;hWWb3%o$IoKoieDz-U_&e7ZYf6!ZwaB;9N z%ETUOinoF~6COoSV9U53VibVmn9{RM`0_yx$|SS~FqBosEc_Z|#GL`xsi&5i3fyRL zo}0^{WS5#>Gs>iJPh5}LZp)Hi<%c~8QQ!|sY%RTniuz4CNZaIZb6w$zKKsZ;Mjl<%=LRH!)ILf zosn~~xz?89Tn<}Kn?}!oINDzFYDsVu^3jwW>S9`Um*F9g(ff(*Sv{1Z{5XBjk&ogr zTqVcepj7I(I$Bc8rCO3ZP%5eK$v@PS*RwUKEo!T&K&ea`Fjh#L7c!TiR8r1eEaUPk z)baGx8FgsPD5bZ{`8u)ob9bd?Daxg4y~ww~JL3q-@zmn~u^jtSY;H!m%sx;v*jXL) zm1p!|#h&4}elp8(W+93y?TBGzt|_tQGRKqNHmO!>H8i#3#Hbv{DIR-wW`^^4nTeon z9fopA>ebPbc-r;@y0B3lU1&W{J7THvY)6!|G)I3ZOMITAN}Wdi&bd4CLxuSVO8zme zxsH*xun;BrWFkj~`ciZ~?xJ(R%utj}?T_;%+*wVuD+&E?F4$|ym`^Aj;Gdu^X88Cd zV+m?jN)q|S-8%~μ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%0Aaf{ouBq_<$=_pQ`g# zHrR}jhG0XzxxS^r+aPz@B9-y3O6et_WkRAR_!rscsF3)K_w`xxO0>&FwZIUfMJAi+vysL3a0Tv3E_fPFCel zcDO$dvAr2}>$34l%JEZ9pIIVTsHbM2buP_L{dWaoi7@&Q!}qv!bk3#PKS)Ij4COVI z6>ajC%3XUBN~EJb-5Kr(XZ$I*@fp~q{Ekfyyp-?yDtxua7kh24Y>g zdtZHZuh10>3^v5B-)mAYfdFH}Is{eyJgC~xwLauTP&JVas-`HY+BFqaMXM(0VY1aF zLEMFt#B6Ex zu}!c_h#2x!!<_@iHXgSS+wB)Ozi>7DmYij)keVjC+mBW;?xj*pM8Pb7=93@Y!evsTk+?tU4&SrW^s_0Ml+!oE zx=0Q5Cs`~zlv|y$_v^3QUR`ZR+?!tafIV3qM%+6T&xImaa$q1CN9fxX%B@N{a5zHW z&Rja&iNh1V+oEjM6B0X z=55GJQ&`2R#x3v;h~`XaA;MdgF^aP*Rf+#nnL|0OBSRMm0yxdGUul zL`3HB_v3NwZpn0tv~r;87)=x`qg5+cRW!H=lg-0VUe*&R9d(1${oJ@#Zay?!AsddH(fut(2F? zGKhY&_5%@-uxM01@wyNUwuM4wwLnbRBW}0+n`4#H`39;EOGWn#B(WYMvFe>$SJRNQUdq?M$6?M~pmF=YUs2q@ z^5qK`KDqoQz-V&fF|RwMJfwX^aD}`sr^V|HMOqd6@X~<26tHHm-)nJs;&B|Jenp6d zP$9ySr~Qd7ll(R?P!Sqn#6Li5;NS53PX)-a{)2TfF-h@bYRL(tzp_oVv+@iaunkb) zENPwSX!oHnpJq|TsgfXwDy|brnyNZd9jjBK%{{(kDjE&JRx^4!{GDB#Z>9dzZIZ|1 zcZS`@Xj2H(EP7D+fS9y}Jsx@KQe;VC>-(NwYKnT=5ePpZlI&H#x8L2PcrtZihn<1b zZMp+D6Bq)47V280@OW^JD^HcortZ#4eK2XNJUFFr`4E<456%Ebz2Zyu$Cm|f2wi>i z!fRTM-)*_+CvTbex9{oOF8}vM(}-uNwi%&cWYm}BbsMYhDl}{7ynU5=PDuXm;4gm} z`n^&8yUol^&xrzalVyR{6!o#7P9g67t^E9N{qWYY=eNoQ$h^6` zI=i~OT^>`rFX@cfhHf(Xf+2szl2F4W;6yl@W>CTz>Wq9YSuGO@OGEe?Su10iekL@m zE@7V&#zxkb@Z(8XTrFqEZg%UdY5z=>=kpj_%yEKPjihtdf)L zIFV%7cp@#yR&l>$=cYAg?buvB^j=s49TZ(Jj4`yqsG)A-5q1W3)Lt<(+uSynGDn*) zcwGT!#C=MeZ?ebRT^Wn|if%>eWxT(KXcBj_jZ2Kc?$A9E1jUOb0$kxmA_^#vQ-UiMLxKxcdLl@0d5cI+mhBuv$Lf+w^9549`H_-Sf{I}A(pFkCZ%nT2Dhs&)|mUartA(hso!tOG$*Xy zT=km|O1By6ni}hZO1E~;i9ok{kI)lr>O?}SRJ%s1So=1-BsCz(0XOVHu&J@(YWTSQ~iden(TMB zfP9o&9v`mVYuU%H6HK%we2Hba&QA={`i`bJ>;t zTx%Hp?dZ1!ZDh9o+wZUxe{2dpUtKG`Bc5FF8mOfCm&%ukHkY%x$qXn4bMzje=|oVB z4rj&|hlCH{c*Q5i<{M7TE;=zz2#TCJCk)RWe)O57tZfv>)Q!?Jj>9W%HP4@;ZWPD0 z-%9MT$=U2A&599`4Tfi-f{^=}o&7!g9ov(Ssn0?^ZOVl~)Qh4xfR-uRyd3rg?^pH( z^&=vMkb}^%#FTTv^x$A+J%z!r#?TkUWs!Sf=^Y|fbD}z8Q7rv;B4+|p`v%R1dr*dk z?atYwSX7mggp>Rz+CFAYGC+kwaA+=IkBpd37*tD6QMsfn2xDRjA0_9_(a+-eQ^Jg3 z;pEsHE5AccMmKv8@5BMZX2{KlFM70=EZ zPZMW@RVOIZfx)RKK?TkrsG7(p;{=MOzmQIA%MdF@t*OnhobaQ#{}# zL-_M{lTmS!l7LXSfl><01bGZUV8zu?dJdN@s-Z{$GK;8k#Ky!Iy#(Qt5`Wz#N^psi zzwQzUc&Pb*-6dw>60`oQOQ2lR-2ZKzn$fVS46dSG9Mn!u= zY)soaW%V*cXS%&pQQr_dt?5R2IdY>6`bxO&tUlfJ?#_fj)Gu(|YV9s*`5eLPXbPvy zuWmyXO_VKGo;)Q)k{v#~`TFf*Jc?4sEPVR~E>C0cB%ZeTBTvgy$B+~yj|3y1a_;)5 zrggO#@PWTT8;H4(bgM7e!Y^>z)ce?}3KIN%k4N#-|9nO&6WEHfQ@O2P6bUq!7!c(S z$K*DuE*NZM|D(Evp-^wHDG~`M;%4IUG}~NOpX_Pzc-+Y!6lyrmxb{``tES(m|0rP*>gn3k77T#UE5g?hyQK;g+!Ff2V7#pbSg*7J-1ffG((bRy_Ce{zL6l)>k@#?H14a0%f;GaMO*Mc zYMK0FF%wBAk}`76aB1b2QM%pliz?x0EEKoA3`+zpTPY?S5u|n&O}8v~`W7kDVeM>e z(Z1C1T}>F&{;4zXrJGJPy@BlzZ7{w+ z&$lPMFTFQcD{2K+p5TjnHknHAvo3F6+V$O^Ec#{Bars3E?n zhbU>l%_AvXvIohx*(EkXZP_iW^R;t?xG#ZnKk9t-98-HNmhLdCyU5k41c&HCNz2pn zfCC_@?flnFPAF8-4S+px8lPFBUZf(_z>o;Ki^x_-Mvwqv8Isk?eBh!^fa}0)wG-IE zXPp3iSbYR-Axn<=bgH*6T0&_2rnd5(t$U2UiSA6hoZcXW z?d={2ZiJ)lzIN+=Qveb;%Ko-pWgE=v%gWXVWo>yM7yp4;!Ty~fVL@9%$;-*A5@h2& zYyq5i3MyoHCoq-J=~eRlFTS|~)heWej&`eM=^0x*6=sAx!E_7;(iJA`9dOp}AjgFZSw1H2?iSotzHOCe%^(&p7u*ca z0^NQDkd5O{K{W=_nYrQT4%{ICjFinN{Xs;o)*F*cjtc(m;qCE(&TZT4cRESn3`B?+ zh!Bxb5EXf~9Cd|>g&o_(q$_D}lWT)p67~CN7N5=sQc(xGu?P@zy0DkP&^gOdtuf5U z=pHWu)w~$6{`_$@9|WKi5~Kd)jX`%Ne+-E!q{1G-TC3)-6&-Gm-D zQjVB>a2%5>!S|NWxVc(xsOfCPxt)FTJJV3U8bcA&A?q@t9QwoTXPH5yP6+3Xoi|xzO;6IroT*4 z-KtyYFUzc7Yo<6YTk8i--z&Ko1J!upcUD|`vw6;&H~sq8Z~o-e3oG8Z@Hg@Ya3MgvNQ0mx z1o80`iMQS|X(j@>rZe(y-qCYK3toTyH4aeEoxfcgU9)Dj@TNGbRY_~s1l}BFWjT>< zJS>%#@?diAa9Pxo3Gjui9Q}9RF@4bo(yy=pTY+lI zK8ec{9eeZ=>5D#y--3+ssCw`UZ&2Lw0-J6#67XUVW&r` z68a7m8y=!gFkXUW=r} z!2^hV62$r5hjfVZBC+_8&Q<24z<6xs%9VP0feVR^jhPmS4hE4vd|1b>tvz7!{aEU) zt*sSCMa`5mDze|dStn_whW-gbT{))RZ)moI1fbln%@VY$rrGl;HDRCrOTDT9EJir$ z+9F2Y=OP9qkX7H*+@Pq1v$MhmQ=id1|Nn2`x)PL^@cKZDS-lszkZ73IWcZz{yMJBLBQK;C1e5phRqCG0*APGje%d+TTkNL>Q<0Q1Obv=9E3t zVvJz_p;=JZkf)?LsecJQjWFy?ONu30Ch?g15S{Ls1YXx!Sx4G+ zXGSI|Qi&LB9)GhsQi)wtEnK0G7L0$Q!qF&%(hw#$o$@WFIZxHT(bqY?DZMO2W zvqvtX(j{lV5cFtvoyztzy}Uo=WP3N;+sgOP9_8AXatOMag>N(c88zSWg*#&M?Eae2 zXc*`#Ty_*`^R8zp>FgWA)U%^~{J0roa6ZBSr2UwcilKZmge2j1vA2sgJ&O{1k4e*H zWw2?1QBFW@6Q}CBGPJz0DO@)pxjeC0o6@UVpk-3m-r1%M{piq(kDfF_%rqFW#G-C@ zt5T;~gr9*6=5u(J8?}F2z0g);^tIqW2JM(d*fpKGOIZX~O+Y91s>mWR!Qw=*bP6ut z9lKX-7GUku&iZ+(*JzvOAhE6`h&pai3+G6qV%nKPHeeR~UAq;kGTGHfStnW(cEXFp z*0=$re~hvu@1HbeEL&sujI@*GG~>5v_B0y9u9_kt9xiu9Wq0zkbD&!QFMXS z>VY^clhois9NNVcCq(ByD>ikIJgNHSM^to3#?wI^!X>lk!LS&wp>7|b*o{`gLp`k8 z5uH0M<`LhHKBg?%yX-w(jQ6AuK)k0jP?{{;$>~~5*N9OvAdpU8D2Zt!wtWg@1d4Ue z6|1pv%{_&2g-1O_oHXR4aO46BnRnV6-}QHVeP@Fg<%JWk+E-J_f#d`s>mg*J?{O0x@}7yq3Fo2JvAr z8j6IW47pTYD*VrK>Do6A9%M+6gzM;CCLlnXovyWONfr*eFziO-AV;wNDMFfqeS+7> zeimcuvu`H(n^E)m&-wZrgzgEwZKwkr<0qhKJbx;PM?#^67=dir*6l^Ulq(SXEcwWL zT!3-H`aOH=N#6Z4nyG(Md{%1_R?#0IPj6;|{~UuS#GV7meb(uD05Y);3TREEh_iMv zlpC{q44GyZv-$#L7Lcac#aMu|4}iZ|T`LHoWiO+T0~%K#R`T}q;e!`Hb2MR4(J+~h zr{oPfnH?AWofnV+M=5$tX!2ah5=CJo27;E+T?u|#9*l5y{^X>79h7au2clMT=D5p4 z?|*bKK~L$=e4fqEVN(*w2b!~#mYv3Tf6~hF3RVtmgKX)bb&(c}tP&d5MKXZoIg|SA z$1J5QQ@_m92U#9a0KDn^#cKNW%bV(wd_F}8PkzSqr+G1s`M_dJypK=t1115&;GM+W z957I+^GL|nF6U`~*1JgW>O3rH`dmHZ)lef}(Hp4`F)hK{cp^J#_1?}-?vq$Bi37%6 zO$?M{dVd_726+J7#loxVNqsKO?2PPG#|>y$UtHVIb_$;j2eR_>_XZmjU z>ROk-)e-X=A78h>;0NX(Y$zyPC%e6Ej=0}w9U69{n2GeiszJ@IQLMi#_~|vhEN68) z4BVDLpw&|EsdRQIPp><;DBqmFcG1#xvZE-|HvaE-Uc0cBg~@Bw zK_a;RoYbRj@^yyWscJBx>TfSJUB}D_!exI=o;@EC8RQaLHk3Iv8bFuXJ6IA+`Bn+M~{jFN52LLXGy4oli+j#~ zSj^J1n7}k|D(8<%ZRxLIc#w8XH!xF)ioJo#i5exDC)D z?`j-fZCcK;Vow?ltr5B)N7mVOXw>v1V8T%5d{P`e1VKxPmvyc=WYYdc`IQcPXuwz4YQTVv>MMnY4o`=(-C=w~)E+5-J3{K53TR>Wq&<)IK-*Sr=to2)41`<8)=m=H&4BgCyg+(Rv5emly{|}p-mZpF zO{m##bvt}4r4n#lZj~cYY6d(b(w6W=jo!E) zQuBL8cItE%vTWZRjtLx>GnV8VMx5(T0BKaC9U#zIWe)apMc=<1245 zSGuaK^)i%XoFIeZj?tL0dmuTy+x&}#&tL6e3pGI@J2EZ)4D3!d2h$4?{e=2;u@cF% zL*|pOz4orGeJy9|(Udv6PZ29pe`NVMs>iHbRhg{mXjBFU1V_??d_QFQpjls8*5t7| z9P&W5(9x0X8|0hJqd5x}FV0`vS+ac_s*wtHy|x|0W-8@;{K%?hrM9hgb@D)!uzh>S zz60jh7C)QMR?b4g=FJiBKC0snZd|zVC7pn9fi#A+1-s<1uU@K2&>q4HlIyHIXkqb_Y%;c##vn zJ#sccE>udWLJoS}gIkC1DKrtOKYE9zDI73*E<^7 zLOBm35+v%eVxcV@AYKk3&dJ~IF(ALARvEO{9+ccHLFsk{Y#^mOOVnaTSKo{p3-z^2$M_9UyCn-!+mgRPOo zbTZA!fga0VL~;lW5A2b4N@_@3fnB{51u4>99gdg{h+ay*Ga+HJnwU)kBhSh_5*Do{ z-$josSUG5O!T{6P-Nc@hRNSBlHs4S%!*1_WAE7KZEcFQ_&zuRNdcZaUzW5>@<{(%U zMLBK}x7bT-s$_>r+TOK7dl~ z8RD(6)|fniv#RXZ657qYY721GyAUDH|TbATvpR*J>FN&Hvy#ju9L1N|B zpDzHOh~z~|N(uLd z9(EjGGs;3_pDLvFU)qll-qT4aw>)p!>MV`c^i~e~_IK+SAz|gJkZjZ*L$AWUMEByX@4@BjE2&lr1o!Wg~^q!4Y;geE>@U4 zj*vC!!GafxCfj>V+Ifti2ND^^EJj=31Pf$SCS{6Yf&ny+rHsYp$U}vp&iGi_s2(xX zXt;Cdm1jq?j_t|WFy$#Mz`ka5IjDZ)?GP1+;bgU~np4_+FdU5sASOJ#;h`t>%92Nx z=#?e4>kQ6TZz>WaV|fx0vI+GvvEhln@o>zjEs59ORczMo7i;5nX{)@6C2|?4J*nn9 zrqjB6#B@5|(`$ZJT)m-lM}?xTG(5)gyIit@_)>jTOmzH;2jTg7K4pTK*V(&v7`I<50Ipj1ntdK1Z9^5X6Nd3wb$H0li@i zCO%V;Udc1CBJ)hw9joUgRk*}kq)<*Fpe5vak?jhSA%krpWzM+ZvlzLBuVxH}2&rGH zbq|&Ud#p{+uk3;PBq(9?X^HfY^U zb%Xd&$>@C{`L59FwRfWw`k9C0FH%XSB?=Eh)lo4`7~d2r1NEDR{;uBspu9cQU%Sn$ zZ4mF6~@ z<8hK{q(W~`L#8q6Yj;@Iv0Z%d^uJ1(NPDKOvkmlRZ@l7&$%pzFUU$VRlaG~lE#Fx^ zoRoi|ZI-J0tEwu>?+%pj%&i)zsuG%;>s)n~#^y-erYty@oK^U19lua|TopPstzo->7f>gsyxg(z^uR3yX^a7T^x-F5YN87Lb9 zs}>?aQDaX}T@QW*+ZGz5TcuYS2aa&R%Jl{U-zI=#R^HGSN?R)oOh?QO{hy%<(TSf- zpnGTsPQmYSNY`xLAtqu3(}f_As#HjH3CIM|CHRHeH;?Abxb%!0V!R;F1|TA>)HT`y z?5f}}(7G*Si{@xqQlTQH51jzUgr{KgVb`Tpq)gmaN-(HEv{I7d;41@iB8U&Pm!*6+SsPjWbg;oFxP%%p-A68@a z%#omN!sGCe86F3qOqNADN!F;o@>1mrJyBQ99z$PC66(8{_H2DY4-ub;Jw0KEvpR>A z5TeuBLN_Q^Px4?rvfZ>EFr)>xQku{RzOgtyYi{-{vOZGWtp=NdwU%AQQ5LRX-m+i0bEINcwmlGxhuHJG0f#gGvh3&%eQ|pATlMpEq__ zv+C#Xskcbudxc~?+MYH)AwJ*t)4=mJ#@j_}6!~s>e3Q`CJQ(cg*s;Te7{<=}PVIak zQP%_@H1U=C#z1*jgK@{!j;6tut_(AbSZ0w~8(%hP&04cw=$5N*sLg7#N&E5)>9Kr_ z4^Vcn>oQY?9`38ZkznHzZ~V3=OG}?x&T);oTA>=0j%xp4_{K$CwG^yiDnO(PssM3| zqarVOvG65e*hswf#)W#l4Z7w<>VHexMfmR@)PE3OW6L~)QXxxCV~hsXR4Dv)Q8jC3 zox!f=sd;2_qdDBlCrju*)R!ccaOt`psaZ9f7O*XZh8q&x6i!6)11WHt9z=$G&7h$W zm_~SYejObwLXErZ^ktB^AbXxV%mgvEEW8N)!ss8EaWm3TUp7!b@ffARFC(#V#@ zi-${}eRgCE7C+Jf_zf?0@*RDT(4=nZI|i0B|Bfy{H!||y-w@y?Pa%&%nuN}}FAQA( z2#RpQUuq!vx}Ph;On@rvoXj&oKTz(BJSt|mGDGY~kTecO{Rvmw?eXQ!_m-geswd=~ z(9Sc3-N`^Cb^^80)GM{WmueneJ+^Yd8}vdUz5rjleGzBeqF!%6t<9KU32u}$lL0Dt zC^i0$c7+i3Bw8bp2aa?;<9x~(ZS{t|mJiSSPig(VjY|^CjNUe=#amPp1Wpq%pK_Uc zg)izP8PqM>dZ};4q2gmzkrvQaA`|L)CSI*v!)onyLqtNo7M07Ma6OrMXm11}_x^~n zKs}G|qPg?6d45mG8RrFL3o&13Ry!f^ysX+_+udZ?x)X37Tf}C$++eeLoi1f-xo{IV z;68E1$w$V6%35Semdozp#b%iCgG=uE=q1b8B`LYiSm`rtTUx}Z{zN{CvhYjJ5C~EW zb1IhIw)Lrsp=CqIpBlPtN5ydld;jxk7_yr_Vl6Y$Yc?y7EB5B>U)a52!@~LvvU)%F zr**Yg1)hOs(kzlEYV$eF4ahU-2qR^(!N3(Ukk;ioy8w}@mu_QXdhihmzc;~>`3%Ga za8_bkCY>ug3^3$J!|5M_lzEAOdPgWFi})Lnd$}SO?F@xt+MRa9F%jQuOw|S&?YZw^ z9j(X+%=bkd599(_iiM>D;fFE4r#O@mV&S;gWnKaPjaM%#NQf)S%rS8f>@V}#g29A8 zn8`TQB*grQNZfo(j6(&%FIO}P{i615b;X%2$|arwzG|K9q zT7$|E7V@vpSI-}g2SZ6uv|xflf?T|qwmyuq*wV1oDS9;ZC{Qo%q6? zCJIO!h&wWs!gIvz30uM*cf_nMZl@6?tpoB;G3eXyo9s?lgHE^IWp&!2XWN3(e8yIs zDWf`HOCpwvwbhj@0pntLGrZ8T<(2=;-JgftwD{EKfTV#x?ke%a^m=^pD6KJDshqP=(m3{=Lu~g$Sg%^&+SP zl4O*6!&>czwSoih$zg80NYDXM7d|6gm-_M2H!W|!{PNpx-|+I406gW~vhvvT&mW_I zD{r}F<;q(iH8S~6d%!7|IS8c<$f`!wO9m^0jm`K<%d zns>W@M&MYh{>g zMKy`l_DWxEo3YIYJpeORXCbJ6JQRn(O-ZI~N8?_j_Rqv7_)kMeFcZ$mUx)6D)9M8Q z_A3I4Yk0k8?NX5dl~kEfP7$wwaH6o=Pknsh7gAms8tfiSDC)V?S-Q>*dr*#()h;zS zc9r&Qa&Pn2)_S)Y*SJPDj9MmhVAC)cgS;s6su1|Vs;Mo5r|5C|m5p;zRdbrO+lLZD z(|@TJapMOkgOa4*jKOuOtoDKiE}TBcZlS}3iF^AHR= z>(YVp6B(}p@-3?J7_oM=nUP8i1S}|K9rDn_X|f)xxmR7eejWw1uCK9L6d`h=q}< zDq1z_8QW!hlf6Ueve%}nEfjA@JUZl7R_XSKo*$Ts)>%2YbIv%6!e*USa$YeA5fz2m zt>Zk|XQ~afTJ7C2_1$8_2RYiifC^IR|5t|1MU0h~bSFHSU^HQT`pu{RU5Gj}PU~|utW>>@Z>Qv3eYo!9Bo`+D4Lx&K1skxRIq3MI>@*Tm;B1~=Bd3x zl3iqUQCFQ&{gOp*vmueJJ&G(?3*K{96)CHdgu4P<{gL8N?_F~CaUb{D$DK>9nduWj zdLhgCB-aPxs?7D_`xD#02z`nnN#tK7&3ISn^vn;xp*bXL$Ob~CBThTaL#}-#K!Z5i z5|A%Xr`p?F6vuOSJ$!4@xb14GU_vA^uRMq_vbD|bu$t|0XR1|x$N9<)@ym?%V8WF` z`Sny9a(C)^SH7*CmtsJdd2mCh1Ob9ESQEMBW)*)~@xskRdB)$N%Q11r5#Z0dd4Js_ z<>M`&rHJsklX926E>&gO{?_Mw$kt!|MGy9E^%v6RN8Mo;s=?%5ez)5faKwDMzrH)_ zLlLHM?yrx=!l(-z&OM<1!{6rNw0YWEsbHT_DpLB^FF#r)@OwSF%M=TtP`Y_oEZFGw zqd}ouXf)+ay|e$TF39r6lSqIGV6* z8qTA#;bccc3JK^1F*|)(%w4k=?Cpd&%$0MX>b&v-$ixyD4{Q_SIy4a zlLYme2?3LfXP=SHxb*b}Cn*@t;snmq1=9C4NoaN^>$)uJ6^5>4PsXWyPnBF5B-EO7 z4&|GGeRwRVnn zjtO~0@v);F(X3r{p$l&)wvM9k#1SUPXbgU8HmN6f8I1O z5O@HG46r-lMyzedeyHpQ8te{cGG3={klKRLXd5WzxlKK;fqwHqe4wLSjznAWaBwY% z-RyDZ)@`$wmYTPuN_)1+QCE9wJZ4mD)LN;f1rOkix^s7I%B)^vUSln;*erWoZFu51 zAS1E3anESSk%!Gk8-BETRQAMeZ4mU(cC| zV-Qdi^ldD+Vz+f{)I6F#G_*&?M<@_xRI9WqDel6h6Fu9L!-3(JVdrq&b`MnITyA44 z^j%sV5l?PMZM>?+T;-~QmZ6tFc*(D%y_f0*=bvEO+VQJ66;lQ4%xF?CMIwWf|&e*EG1%Xv14tFc0HVCm3j~dl$AD3bt8ib*s zDHd;Scg3!IUNZ!~SK!29aTmN=95(Uj$-u-4TJQlb{g@J%2msCFg>_lla8RD1+y=vq zI2BCj(I)r9z9icXzAV|evri9*Va!$VNk2Nx$BKxVvsLPXQ`kji6;(Erz(s1YFKEc75epR>Xa%uIkY z2^~&oRA`S(PD|3w6*ufE-zW3DBMTa%${qWQcWl47Z`aQKP%PtBUXK+?#p|%6p0-}#(z@*gi=890$_a8n7XOhP%tUY2QY`N`X$-7H6Mz76ioX$ zVrHn;#Xnrlx7d(+)(~;1=TxNMV4&9>#*#(<1@~2J&+&DuRaFVyJBuH+4X2i1?x4 zM>iSb2_X`0MQLUH+3NFERhjB~41l1KJM9mnE*5HZyIiIW3!77%Ty7R!ak%JIB+Jh< zcCi}Qp=|?{VQBODyuMa<#20s(nhaA+>JB9T1h2OZU!XB<;V_o+9@bNz3P$PC{!8L@ z?7CXTitN?_N_Z){fX#y*Sji z)6rKmvL$|B^E#issRhV1zIgmjf?jx;wmqPpua*h*$0Y3rZBKSP{$G7N)^1SuaDWTj zT0R$D@XymFkS_sccT{X|3V9szh8qO8AMebveT~7^;g2Te{VzkA*&BCRwy~x6bChZx z!Q#d=ezPgSO;~L76reh54&tyg|0XYtmK~dNg2*E?kA>5r`}lf(5K*yrY2vU-xF1s` znr1r1Vvq}bQQWpoQ1^nzfqIMjN!kfrkOKvOpdQG5L_H=?-Cd|&D}DcG*~AdTZvE#K z*f0PuPhiijIU@n3ZO}HbaO3Clhi;tG+|7U805|3n_ z`b?rsANChsCJBz}CwOCQD1BN;@iv4M^Iu0fMcyuo6>?RlGuvgPhRrC?^Qlwm<4SA1 zrG_K`um2g*!I{~&$8X1KgoPq^3=ImIO7TrY*?frZ4+W*C=4$!7P^4ujo$>O`uTBpKQ0&ljT#7^Qpb~z2g4l;C|Pr zcW2c=d2D^Wq_(-#*aYSK4jpjR9%%>Io%!e>bt7712SHf7Rr@!=+L+kdYk`z*Z(>Ip z`i&>Tun&tR@hj`EyKx5Ra2VgZ z`s%B7J_mpK#`oXXqrdas*8?GRll>^ik`>kloEgRL+dJervq_tqG`I=jjJo$7vU)9K z=YC)yWx}AcscT==sAY8Dz@9E89K^O_QNQt|cXR$5lxJ2YCMxY|7#U&e^q z;4Kz{Oc0Yu2|{MT63#?3@pj|r(BYwDmScx2)(k0e)anX18zK51v=G+_AhSxAYa2ej z94T}#!%q~vbPdY=1Sw*Hf)hKWfnDkS2hG}qp?L&_LfjFT1#7sjI-u+@1V-n(B1(Bv z!>0I#SVe!$kmo_iq2b7w@gYN`c(nAP#89-qFE$h#cI>R}GHRYH5t#WXG?1iUKJppf z5rQ!J+^Hwz#_&h#JPvdAhIUL(A>@NSNNIEehuFmR5PZqG~i3iAyU4=t4F&n!lmv(q=tl=>j*2_Hy;<;P*U1sDS<+ydj49};Y$P(A@%a7O)Th3FXAKuZ#CfK5eSY< zdRe=;MqGB!s$~e%@+>DIzmVlTB_6-$$TE2XOz~pM+~`2Df+AoQk(gl~7W)IRFnR-W zLGdY()rCV+e*ez0je6YIeAV#YuGflA&ow2huJyAU7ENSRS` zoH1#$!G4M`>WTa!#AFC~%q9YXk;wz(-r@u~4{Qrfke8_|v*TQ{%4qQXjoW$p?$h6s z=FNkCmX(!6o`2sT?uX|}@4t@|(yT`D`&`*` z9u;f0W|X}p^X5$@7s*)Tp;j^kvqF~)PNv7HDNRjQAF(wSXbMY+$#(b-DB)g&Bmg-p za>_uZ-4#2>zL9H<9sL9?Bq@073`%T=bOO`0ksSc^WKV`rhMb&nGJGnIrDG?fSP5VH z>`d+88)&22jzPsCZ%7NZnt$-J5Df!oYEE(F5M3NC@fp@06t<5u_lw;CH%u{~o!tPd zsZZ4YRW%9fgCG72(7~{E5^F|HT3UzaCB(KsTbsGAcgoB|PeJAEPuib!JiT_Gdc9QN zE%?wCxM9&m*ibP9U0Fb7p?y!dEcWE+hcE;G&ijAAgHK-$h0#}<3ZA&mvUtVDl@&^X z=dQv<<{O{9?Il?qHc-O!iTe*1hS|>gspjl^M1EOh*ieo#xhU|cgbWWP_6&`JgR*v& zk}^<~m7=a}x_sW7YKt|@f99j#4?A=E`HvN-*u~W(M6=^ z5b%b-`Fj&DO-mPmpChq{mU+jNSGcTI%I?iVG}9UGv`~^tMs41LjAD9~|$M zW8p|VVeV*3HhbjT%2zMD$$aO)z5BPw>uGJI#S=7cp0t@1skj_dULc1+LDrs7IeF7L z=Mk~5u48MY%s6iFj@LX;I%M3>JnGN;LN&v!(){@+3dG<44#5%>OY!~gn-M4=E7ThLMV&ovskM&6i&Z^Ft{mZw$`t&J2zqIHew$tz0t0LdVhI^q0%jrGJr^8mgC8y7q?s`~Xa&NwQ>yB)gg~hTEhh!Q*B;xPK zj!qhT08WPBVg?n`_~~rYwG_C=Zrr-#$1jj|3Floqq8*S{A5yPZuRpXJwcxI2fP)fP z036Ypjj&}Aug9R@B%Gcl&V>%pC&lz85P~GTijxCoMhrjs8yZG4Ly(^q(p#t9FRkxi zUoC*$52&H~tq-(siB6~29iiOjFiH{fNtd>oP(X;&w_Jwz=S@#hS}i_2|MdBKkA9$? zFCADbY-5dvs}YGPX^049pWp~Ff3Gm8*N#gyeZo)3l-AWI&jV^w0PcDElN$_qlY;s> z6Jb)c*G27I^;{u5eZVTVH59_0pF-|jWjN|6dH}(MqV;z>l&hvK`(GLEPL8if;9&fL zqe_h-^9p}gsGS!ma3D#mEu5DIV^Dakk{?9XxAFt-dHTCbc_j5I}e2| zjd1zShDGJi&fb4Y-j1ev3C%>FCd&pzKOiiQgCWJ_(?DSGWEbc#BEXGv4Iii=(9fbx z{1s_#5pC)mHZqq!FGK=}0gA9!(IQB&>({_W;&G1?k!iOT+16e&~VP|HzgTXDeC3K219%n@~j z)6TBi^w!MYei+S-_9km{lPBR%wWW-^g4_FcS-R2Bp>Rxzg%iMeuWNNKDg=i*6mcou zW>=+u+mN@fIaRl3VBkP#+=y`yiCPXCl5osg6_56Jm(ADC3(=NI{la6w1nSCqxU}iP zA_8iAmb4a}LXNZmQ<199>3lsX=<0mEn718Rz%K(ab8seh?x2{6q+)Jm>2YLjU`ugTu_!PAZUvPd z22aCgrt?+V77iyFfj>HwA+btO4DK;j3*lpl@BD9W4ZG^BG~T1mdQ>EZQTd1JMG`FbR;AioU+<|hqA;A> zVWC_nM1=xM;^4^0D4^VMaxJtJIKU$GE1l{&lHKKWw8}R*?^xmbq0!Oebm1(r0op|J z0F>0PUVQQGq>*{_?7florS~S|gaEyL0&bMSec#$L?b* zW49PxZ81+Qd)ynhK6h>QID~WKY~A^-dQd{)B3Fx2b=}i}beDjNJ8fZ0JmSVju8MfM z;0n?T>hlLfEsC{sX<*>OyV2MSAFYS9U z(Xe#8g37>kO9R$Ufr`eUE*P#DRO02X3c=M9amOv;P+KUdq`RIDRN?9^QDh9mxIjq3 zrJloJPNdqD_BKb#hT(u4hWx$|*FY`Aw3z$BefNHFfzWR44W!fU=|Hco-MA1pCet?U zPkEBc^5vU%KlP*#wcA5BOYx>n#jc<;=vLOQS$9CsCNy?7 zy}A`b0e>t&9zqSf)f!=wuD`vw>Vs0V}qm15%-3i-fVrNli*|* zAd(fRerlZ%_J>jW=cxmxzEhw5I1tgvS_~V?VLec!1Coph7{h&r@yUVlu@hBA*fIeR zfPs85kC}ZSvWOW1r_4;4D^mD~jn%!BbiDK)+MIAC=Dck?SEl)+QW9sHaPfM zu1FTd0p2}qmIdv|ba!^f)xX;qp>vMH0o$e&6@YEuEnZmv{oKUsJ zMAfoE2oY0(~EWd zPzy?ar`-p@Wylu~#*Lvw8^R-rRyaDSN&(DeI1x^ILyrHCy!QaF;ylxavk5cj*l{-5 zU4}^K>^FlGCov{C#ei+>0B%3V*aq8R8`C7ZB#L@HI=$=kuHKM95*;Kkm}1xKliNQAwbWHHX{U3>}_!M;q*ha00zNobrV1iHdMpLusJQUE!$IpWcd-Y#y8x z@+5FlW=C|#cEfhp4$BVF6URxRL?qz}+x3lwvBDxlk+ZPAkgbVRs9H{4`Pov;*3r== z`eSWj)i_kt1J%k<`7DJqvuiYq7i*P!qw!)avvU>+J)IRLB^C6iv!|!Cvq!q1Y=w_A z!0&mzbz^`i_l*<$2?-j{JdBjDDa^e z^mDNaP|ll#l3UoDSX<}|t(N~JL^Gi#UyEn&mj3n8MaiX0lZ&G3`?u`%wD_BXnVA0A zlS`L!W~fxx`qq0kZE>x)EjqUJ#pK@Dj{fl8K(nvOnb99V#wusqss|c-4dkdooHs7V zxgR)?cEJ*@l2nwHu@p=KpsLHZfhl!5BIlPeL!(T0lb~F$TtAdYxqL1X3BmX6EouZk z6fLe((DlF1Z?l)TLh-e{rqnKdUkd{u@^DFQJ=lm@xM9kqY)MAe_{W4q73y&E3Fm~F zm_XBF2M|@P)flY1OX{Qw5*GTPiX2ueLxj*kujRIF8*c$Fs`166!9 zbUpf#JcU&{Sm~N{f1-$y{7vZ@-Duuddx3n!PWN$1lg{W?TVdK>^ypiSr{IK zBRiA_+>7d`T>3)y@E0bN7fNd5;FP-L74QsKZ=UOi;QP>*xb#}=vwgb8DMmUXZ-`5G z#Cz=h);)zg)`cF9KDjjdaCqJJ!oAjhhzWOQhVm}O4JcnZC~Pk8>g(&GKjoV@mzQr= zeOT07T>BxzFn{~QF#=1fl9M_v{-f}|?esm<4bvaEXQoY*e@z4N5AtbYb-ossJD8u7 zvH?ttG)_aaEv2t+Yir|@$%YmihG}hs)uxwsOtM)UYLhm7TQxqJ4UpX=avv=Xsigkk zpq6Y~J!r+z01cy-{0X7D4Fqa*vU>2~Y7H|qcrXXIX@RGiti#^w>YPXAB&t{G{@}jA z7gevT-d~@8P%WR!&0noPL9SEz;+3V$cqM%7zcQ90X#wDVYWftVx>RGxoNL5?k%=6Q z;2~c{@EH``T>EWC`gui7;nl!8bf2@pw~&V`<&mhXmPyp&eJU&!;_KPD47ydUEKTW~ zDV*v%dZ-8C^KdL~Xs$`tJH$r|*FJQw;okiZJzpp`I$D#>hPWq&qTW?Kn~$C{02444 zJNxL|xsN`YaSlPU4<_7x|Eur5`zrmp|MuJOzyEd?=Xy#W5K7`@-GM$md$Se(wfm14 zl8%HefI`aFsrV{iFeulu*Szlh@8vIg#e^$vhag+e^eZE0LuzRnMd(@^!TnFswDVVs zHlRh+Jo+ch)D#(HGx_leqf@qWwuABC>f~!CfD?R>NLzUbjuMx*aYj;1_PY*Q)ngeHM$s zrLzQ}2`7fE5nId=a{v_W*1J<_U!yUZ2!s+6*7`7D`1mgjD*&r{+^~-K_#t#4`aDc6 z6#sbqdY{ML0Pn#S2)iW+1xKJ)8S#OjOiwIjF+)^`+Ot4RJP2*M&)PRHE(I`%U?Wl? z@7-bTt?2~BGr+4A04hwsqBF+{sJ{#UF{po8S!HW+YD+kG0Om9xRL+=PDniBs&=c_U z`K3bL7H_e&q6RqDfKDwkP)h|WDst3n0j<8EyR4?tTI}7DTgWHSJah#t`lH;DJNeL- zRB>x%S(#Pi}aiJWcr8C_d+L61Hrz?=JAmSbIexfoR$t{iWjk4^R6;jgr z>#q!@t^erCklwoM5#3bJ$zDU+2lZ-o8)9jg5fB2D-k5a7Va+3y0~kGo(WPZ)u1`+^ zmPf9~#6fvtK0h*nt<+G&%l>c@)sT>hv z2_G>1O1=yvVAP5N5Jh)OPG$`CzEZb*18B2+^)Y*l3J+r)Dz_E`MohfGC>Ckm6em+0 zj{tsP+UlrNWdw6S{=7U*Q0cioJ)IYuP!PL%Nw>SCe`8QA4V2egEA{n`R9&+%9t%Z7 zh>XP|aUKZ<&)r!K`Av{xro&W(?`X(XY&EX?nZWWfr@I+!xMmOJZpl8gS5P8))dqgMv3?G zXn9dh6*loRaPL@fMvd8Lnd1`bwJV??pe4Md3-X63qo%xNCxvU(B2aqZCtngMuz!iY z)moF|b@@R3Jj&a#i_uFn{IB-%AAbS#kR#i4=Vo$v$CoLgwTpuIFohC25IUm7TDeia zk(0kLqCCmZJg2pe=2@tRv8N`L4=v(1LqT9%w?1{&zMDsNwOM zjHdcvxL$hu+#Sr&~RdmFaLNfL;V22rFZ&YA=_5{%0ZF z9BK+=^sj9^de5y`s8y?kD%0LS`jgjoive@U6t2@R-CVHbK|{76?S}==F6|Q!D&GQ+ zG?8UL|N2nJ-hpnhCXy&=Fq-P@mF0%z`_>_1zVAqvy~ETjHY9plBho-w$ND_2Dr2z# zfl8|KC08zA=9Lg0VGbDu zs1wX0yh70d*CJh+ORAPs6LM@MXq8PJz}1n*jH!)+%%5BxC7;D&1he!%D`%3=P8b($ z|HR@b^>zRACaPosG0p!(DvA702D>AFbM~me`Kcc5LTPZ#w!=>aL(Y?m}EC7>)ZA=69B!xo@vt+4AvRVSfLzSFCUNP>N^w zIZ(eCuxDl+oA=tfJ=IT}PlWWd-rcfQtAfYCAeAi@Q+Fv95n1S%{wisWV995dkAqNs zS^}B|26aAWY%7qHbB7&CPgvixXYZ@;8U{_B#f49-x~)Qdui}YAg~no2MgHx3@9)_l zp=!zI_UNtl(y9W(^8IU%>}*drAhf;n$o~CDc9phU6R^dO36%p|PMtQSf~ZLn+nvum z8Ty*d>+oWx-gXnhDWEcp#G$H%DcJ3FNdt?}y_=#@uu`TIP(!l=jWzJ%mB-1Sn-KcT zGsoLwAnpjdtcLkp7FQNQ+Q1RAvHTdaiBH2;hP8Zkhz?sJuZnSW=`Oeh7m^n-Bbm%n zR7GAgW5X%|97Ji6la+aV)aQyg!I94(KP*`SuagQifGr|p z6l?1+94Owop2`4eq$D~7`S1VuTltG=miI6lt#*giFWqFnea^Rx_dl}h_(o}f8bqy6 zVWwLatOY|L1#$a@gRigonek%b1m*9)b+htCi)8U9tr=q~5l1fNcZqj@`bXodFBCkv z8!pSJrH4E<8KQueW`m3ybT8%Kd*DuE_1A@DBA!ep95&IR>+&>0^DSog)Wh5JIzlKg zj=OA$8l$P)QBl?E*k_iG*JesK8yc5#1VFkQ;%iF0))c5+RLR$HyX0jWBVICojY($P&$kCQen=cegS9NKp* zA*F)xSPCpuRg#SExa2bJUb2w}6?3PGh$ zRXYuz`UgV7l|bw)y=q=&hH*uH^_I;NxSu*>z52$mEn_l5f!ywsEUV1(W*Anq-lYI4_3TLst9@YO%#zZ2T2ukTgnKS1M zPgf6a=@T1$ot|E&zR{C`s-aS##I6*Mp?7$l4AZahRx3t`bjx%E*FDHNDPPecmzZ#f zKc5bud&-kC%3pz^csLQ4e*SZa*MS1rAb&-N9rn059a~uHu~F$@)p+)}e&y3ESE87J zc8eHqF2-E0xv!$OtsFPb)(_> zb2C2r!#i)DPl>^n>P($YZ>gztl^Yj4d3?Al+L3OR5{Y+V5?~b)=hRa12nKh z*#eSfO109cA3^J~JJHR-*+D)uyA|a-!0Dk{OzOQx57DkLdI%&b3Q0|oUW#mERJ)H| zhsGI;qlj`pP+Zub54IcpOAjf5jBsp;02u=AM~lCN2r4rIs0W;dKY@d=hA0Ufo}EEK z<3y<)7L8@DF5Q$0Mb8T3SQLoSoVAmB*-Pz3TpegtBnz({OEPSYY)zJlfB1boo^D7u zb5E^!?KRGVmSvL(5oOX$1eb*-BNpyT>u$Zx;jsAavE0Q+rcL7>+yA5WXN}~4l1GlO z^SE*q;xTG_d>}2}PLl{)g@yIW&(Uq%v1KjPaOz2@lDwdDvCeY-#0Z1cQQC^;IYirpy2aJGHM@`vQl*3WW!o?b0 zJcf9J7UBvl^iSRo*sRFNqmR;eaU-0(v({4&%{5eeYFxGAbA?>GH@XArAYZuk*UIOT zu)hCYxNZLF_uf49%31JF=#==-@BJv&kWGMi+2h2NlV5fzFd9i(fbWnztv|Gz2=B1?bHYOR%>@M(yk9R z2b-G>=O29MmR_+cT4|~&UCGc4AUbrW*~Nl}(i{aq5=)r#2~n zm%jO@bAD@i)&5H7;e!wz>Z!)6UFWmeinipLmN8L^PuDWD;dae9cWwrFRwroxBF}Qk z7V1KGVF@fd_cK1zzAfHk#Ox7^`Ta3To~lJcE{jII!&}>2om~bsm+)sOyq-gy1mq(o zv47>yK_9Op)ZWx#Y#U3w*YK;>6s`0~OWgWJQFn}0n$I7s2j_tO;HF?d>OO+ z%q#WcA&;1i!O}e`HY+ZH{`{EYGo}8;XA$J5?4L%ONb`!g1fkum63D1H2-HxgsEYQ$ zZc;fL72AmyC#rhoIfvInD$U;(9B{47ePZBW#9=@O^63EMFo5X5ZH-EqLPjym^@O zrL8fG&+2nngO&NG2v%PD@RoSRPKF8umr76pT)c&XFjbxv5}z`T*roq?NV$J7Rn3)Nog17E5}X`u2efOZvCW$MhR(lUTj6|}4B z&uXC!XjhR>kQd1h{trCZm2Zr~8D^MJW`(RP3L}!GL~A2~Y*|oGx*&XpCV89P zpb#TgRr+k|0+@BWk1x$1lyiu&H zb@0TFv}gPFJ>r{uCKFA@r8j=VMdP7l!tgvM9z3D+PVDd=15j@E&MK zN$$g7B8+7Adc>V5&6tpVQF7Ip1&F5{fl z+<)g>2xYU&YI8cfEStmG02;b{XUDc3mcwXlXN?*8ME?;v1F` z`zuc!e>kOAdO!XLR&8_%$e{=z&W~V(uWnhly;xN4;z98?@yf$a;;==_!Bl_b=!-8N z1=TQB#JdqHV9~xibJgtU;$rS3!2FSz7etTCw&%R5138188HK|aSAbcCK%V2_ zpbYSpAe0--;c;-#gz_%3zSMnEv{=DU7#mKM{T1krGrq&3;kphJ-V^GxHz>0ICk3R%e8o+0wkBZB!+sd{qs1%3tM& z@;SEk(5lPuU4VmWbw~2+Q#F>EuhPk9=Fv&=SDD%O2f0nywd;CFH7+L;SayKh1rlfGoFAf~& zEw%xa%rMm&SS^|C-vkD4`A+3dc0~WCE05?c#u5J_$Ho%Hwq!kmZnK4D?b`B%}t20Z|CJoH=1{grrF< z%bJ--;Vq12au-dl zmV=#`Y^lW$kb?wIvVi$`;wJhSN%H29rOTEz{+ZSQ5TX=lET>4cW$~O-_<|?6dK+WX zypBcyGkf!Gf32l%Mwx=5%C}cCa5M$<3P7XBTc_REpi)DYuw;0 zXy?zo1a9ICcDly}<_-pLr_;f}z(iG}Y>vBw7Ou_8W`6%#ZX20zf8(uKw0pE| zGCW~6r(F*p7hrFnv9^UnO-sOlwxJKC8WSYnLN?0GLBXpl-NuE~n94^pg;L#$r`a7T z#xo3j!f06|kmEjsS^$R};0c~p$LuaZ=O@MNZ08>{E+k1EjqJnaQ`Vb|XLApL>SF;LYB;E@H94mx^ z<(xISJ9P3Bib*(gMpN0IcP3jFjhGth+)h04#N&kE#LwDqmXeiP;$bT7;StQCofkt9keFN#nuHEnWk(+V zROG9V2FmG|MgoywSWNXFO*o(N>@3+{7+x1Cs<0O6 zE$*ZxO+gw8S|Lh@!TAAoel_T%{8g)YvsiV6t0ZLrNp2s;LlyEDZ>nI+Jc~_Y@?nT2 z7Q0j@C(=|S(dL=*O(Uo^q4|%L%@*+eMeSQ*tAeD-X*Jatl+)U*&mwEt;v96e4<;x) z-<{-|VyUJUgB%>jP{7psf!Yk%&6;Cm^dIt`&uPEG27oRkkAv91xcRX9g4&#{g(K+& z$0wRu(o-GyT4WBZn7nv0hBjHTQmxKxanw~Cs^hg8i&&?Q8XDj*bDE*dPPSXCy~Ugs zThzs`Qn_Z-B5P@avumoal_1``l9~=`*D#O^bk>wLNyTZ9yM|3%pu?|LL>CIsbaP<% zu;KU@yf^vf5~ehFggN0N<~phYpV`ScaOh|0AUZ}A|Ik5BaZ~32W>t9oI73>El3ce_ zTBCDD+%d7gvWS;|Fe0o`x8BMw#R`E2`gQ1BG3kH~^q)@Ao(ECG&I-O3WfDuG95|oc zcKe#OISZLs+W!3zON(TDB$n3Ut7*xKrJ;NbVrkz{#nQHA#nNh9E16iD3^|D_a77AP zc30Vo;)Tl)NQ@=+vTg8ysr(4EYyb#k&p^l;QN-+T*}bqRCS7IQ)$a4@j-%-^jjrwD zA5|vE*EKvUPvCx|Oc3PH^Q(TrH91X*I^(VUbIJtnFGZmBgE+MOpbU}dO-czAqm>7B zGPZGd%2DAtoOvr>XR@1|(yCiHTtMr&G=qSfP3}i1UCWUv{TQ4SDDTrCES<7Wh})6i zG%8=!A_GkF5c8ykA_qA1NiB2ojep~O%6^F4Q1qErDT#ToY6cI{y$Z6ww7^jZY`W}G zo!SzQh>=*?NzbC5q7Lo?wE%}$zgRs8Zp^@axG}2-!I)mG+$U7k*VVY;tm{TKAu&x> zN6gLbhId#k8c{cik2jxWi#HXejr~GWlx`vv(w_}8L~UzlfHnpd={ zR&qpaAzM%{Puiq5oAnm} zdmgOIZ|0XxVE-&RDsA|Moj@BdR*m~yR23~& z7fSpnFIFvrT;;ZT!pRwk`>S6N%e~q`E}Z0Fy6R-sLMP$)SN@I=Xk~3@v zh3|^u#~N1jsz2-N%S)XlM%J6H6l9Y+_`V(SOs{mZc(~zYuio6d!_mj$$60%iSI!R! z0ls%zs-;az#88Xx)YrCcv-TR*O>t~WWH$x)1M=6nP~6vK3Ku7G{-DB>7HCN>8eSN- z`LI@Kk-1S1qEA-E-SVkWQX_Hp)*y}{vCbn9|P*eC!0-Ybp3BBS7k zMJyiEv>q4yK>W8?TR^b_zBxNNZQ5+xToJ3|n-!y^Dx(Z4@)(ol=(Yj)2+%2#6O-A3 zNpxT{XV?&RA!+TJ#(GU!yQPUj&0|p){I*lq*oe|p1Elxp#^PMKSrm~TAEB1yn?Rn4 zZ74GtUC1f4qQJ69FuU;Ljab0aZcC!xsF+Du$)X7>q3V+fE8!tfqQY)Z!b&iv*u%)v zbYYN>M#OyiOPtb*b^nSR$!4&~T=qap+ebq0yBw-Fq6wp%$0y=0M@Z_uorAGR^YK-l zW|aaqjOk+v<0&TaH2NIVb*qRj!#nN?vH^OCzl^^A=r*Mq@83Br7dLb+;FK12*UKiW z`SbtujfUsX+^@1z^$}QdEvg-F2Qi?U%PMq{3|E7)2Cm%?-H*^IV>N} zIiuVxusIH^6XrNpJ`4Y)ghtQ^>a|zzDm*9_6>(NMk8MjG+xB0(Ce6I# zyC=JRNe$=mq2R{gh`QpUTn&}YXguNzgATW(RlASYKb7N5i06`EAajw&`l5VLo!Ps` z(>+M@5addqJehUKbB8DZ^AvqJ8ozRYk}$4s+b( z^!QxHnH#t|@T=Auo*awzoY3uWDp2mawFPMwq)u{C{%nm|(_a10YrEW?jgc+}Zl$-T zaZQ7gy){_AT`sLrRpnXu#_EUCUUn*zB2rmvN5}e`=y;PaT!4)5H7w+L*>0K&*zW&s zOJ8*+c$Tg{d|?4kRc2QVRhkpn-t4{q|Mb4B*C8HEK_enSqv{0d&bx)h%687|Bejqq zpG?G(L20*q^PxQMdgeB&e&;9lL#T>bgJMynxUIgYVpoo(tt?jMxP4=JSm1cqMeq6zB=uAtgvZI z_lb~Y(A~1C%(u(evCG}i9B3|&wzP($sfMt>#noKckU~+9ZE& z@A3mn6)uooAFteZB(uJ8?HwI~+F;GGXl;RhV6$Lz0Ph*6Ke#1s%i`Y%!ekkcfc;V{ z3bN!CS{_~AtGF2Y3B^7C6-7zA{-K9od6waO)8RMB34m{T-Jy-t7fq6#drU{$Nx?!=?pRz5RiG*|DS%LiI@k5K{ zf#<-67(|ksN4^I!n`{)CqCZK*g($gRcutr$XZEydKb`%-hd+JmgAeAs^=%-+Cy=8= zk)rWb+!w!d@oU1?#!ZPL$n`+d&*%ZsnAcelDQGBgdt>f|2X&5tBG)GS*7{tErWhZY zUUzH!R((T*KVaOqb#Lt+3nQ$N;wij+20Ekxzi0_tqSh#sMQJ%gF;)l?f`JCV;f=zd zHT+~JBYf}&eL-joLRk>35Ehr!W%1w#oDws~0P|I4N%yArw(Q$Z*K9B*epmM1lW*d_ z4Su6iqrf4rZr`@8wY{sewW_qVrm8}cZ=6(7!?tyHsoTmK_OoYLP}a%oI5-0gRVU9< zW^v86+Z&o&2F_|7i@I}bW?M68l-M1Vyxnk`cfu@NX5REXX*_*rg)<{ddQ3OUeU^iA)c^oy8 z{J($7h>fTa3}?a z5ChonGp^XMVa4*j>z;o?iaSaAK`(DZ1y-9WR!8nNI=;T^fccpF{fvhtu0i)0SYhz0O)y>tFA*)#ju(CQRl^YjwG^Douki;14+Kofe1nK*@SytHdg0OK&|tMk@5t<~0AZ)L1T-?Z+KX=i&Z zOi{CdHvlPst~JNZ`@06a8c#I85lT5zpfn0uoL&oSXCqh}+S_b38L6Uqn|Zr&`?gGH zvy^FVOST8}J*J16R~c75R99@0mIee$#8cutn@C4cWD>~}5hovU`fb?0bq}Bn!=MaM zH;x?$LALw&I_ZrzGCDL!ry!17r zeFfudR!MKJnK~7moY$P63KaY`=g-sMQ_m~cph4lykRaZ^z2>q$QE?~^uB^|zcb@^( z#&d*q>oeV(k-e53T=(Y8`gMkBvu96Z`JlfNM59{wo13|go$399+G3c$)>2t0%C`V8 z{tEv~=-z)UToiy*Mv*6qej3bi$KjPZWd-&5D>VXx!-I|8yTn5)fmEh(LJ1A9QYz2M zcM603l&ZOF)LZ?;RvDojnp*BffDqIaB6*}@r2gO6q@}v^H*uZ2G6xQ6O}c80xuQUn zzo&c;q?^j?@+4trA-82^{o?eZ=9#bFKT`LiQCg zWzlngqoe&#G%58e>WS24B>BXI`yP4ZzWZKyql7kH>JvR@c7RihfLRP36CC4?Ln!-% zR%i>Bkl6ro%|;<5CR3suX4NNJu$3XHGzb}zM`WDJCq`6wAOO&p8LvnzGLRsm4aC@j zv=C)iF`f6l3tmc*t7D%?SwH-x>wT~an3xipP!vl6)=q)LfH0$D)DAwgir|MKnV zS!a&jZ4MDv;Fk0?l*OnVpwuw(bnL?+^+6vO`UFG#&}Gj-@@bSu6hA|Lg=YAsi}|Sk zB#y-GTXU3WXsO1>JjOrs)MY@J=@+MGK}Vc&=?iPHC>m(9)uzoyk`yUa-3V|}2X%;? z-Nw(PI-`6$f8ud23AI|aQE$-!C?l7tDQRU6AR)BEf15w~1eegE{{zjFMJ#VYji0*E z{rcA=w+6xqmCap0XD-m7>a*Di6yP|#A_t_^(NO~gV311;2?oibV*CCHZ1kwOY2-&y z3#Gzo@Y5_|?whzOTYUj9h}(a4Rlzsf55c#MjAVhk`1c@LAiRMde6e5Z+e~Z>cvh^- zxxb!7x^NJY(^hoW=K5cKUsWB=m7yZ19qH%kwz$T}_bYb@zr+!wT$0<=1RWUl>goq$ z)n4;K?5*iQxD_|Fws2WC<@A{_YdUH5!YczH? zb+o&r$}^9joY$eJvVls2U#Auu{6SeLn3k_!zucrZdu*0^Lj^boqvDy4lP5dQ=-Z>F zj!L5+v22?e6@#}ag3xG7*GI&KZL6Mp)$r<}=U!?P8zW-{2h)omoHf-j^@p>5UMzOF zI+{8eaf9XJBH`ofboFMNIV4W)xbNJb41ap<-1!bM6Ux}?4MUTZZ?GVaD1U=*#KpW) z4RNQo>dhfD_A7N0a6moQl92dz>kog}`i?#m!s=mEZs09`OjvZ2i>Roppb!ZohZwD& zr`#ngNxBql3IM~zE4gt#`(+51lr13|zqr%W5XmURPEAlgD3U)w9j7V@aQqV<$wJox zUTr)ipHkIVY@^Ax7Ud$!#HpOk;WV4U`Kv)F7#b50ip9E&Gh>S*J$b$kX!$y8ZjUmg z77h(!nnL#W%>k7Fl!|Uiisl%fH9qpCS%gi*fWYg%!1h8=xQJsMid^bMqo8turqz`Xc~@^3=3i}Pd}y-mi(#!xsTL8rn` z%AwwRt4Hsut@YLzt7}71C@AcO7!zZj3?&lbI5MfZ%l3RY=Vx4Xdt_&K;~PUcU73Ds zyd+khli1u>FAQAB~V ztB8CRaKa`yEMoL|sb8s-6e>7Yo%~cZ_JyAdwRITmEz6(Iv7{T?;Pt25{3)w`_eS}D z$^R>rYHLVa5r0oxp^R(++53&`5&x#&g;Ev)c7zI50?dhMvn?)`uT>>z7QF;=^GG@Y z{_c;U9>%;8m`+sN&AgE@064JE_X|~-@c4RCVhI9VN)yL~lNT$sBR*nS0kjDbuFb+iM^$!bZjv^fNf#0NbnTHJI>#-XNqs z&>_dM5~~}i){-{RfoeS_bt;-*817~Ku*ySB^;3aiP*r^;z9#=_pqVSTQEPlt9um5` z7*@GCS6#1aYjZ0qe)U~W`O?dRvP4-zU}vw_Fjs{&{WQyFwM1U>-CuL^SCoyy@+DiJ zAZ$3j^v?dKmZUX~DBQpR*9DI8QiJl+FNDgpxw+G@|3J&0X0bEbmq{BEwdoSE*lPA0 zo%VW^b2?(aOf0Dng*tnV7?1ATx?xRz&4xOu%D&lbHMkP4j7!|4;$Uw?_&qZ&p=;I> zx*orqC3Go;%W(VS5~;xS!@E({Bri$mQs#G5DMU-?X3L@&X`#|~LFj#l17GfFLf4Lj z?yPJ=_o^&isx_~o2giDnx(d!w)(QLINM!3chCwFy3337Sg+BB3A5k_73txhaa|4(n zU&0I#O?zyHb>sn|f5PRF7Odlu1$U_k+q39uGiPwjkwQ2gOiGYhdQe^@Fj*52f4kg< zr4*}5iN^q23x&f>*5r=2Ir(eV6wai`$8)m?XAy6awT7m0P`ph73v7?hd=mc}}xvVk8s#RbC%dKx0Wh$kZ26DpOM zLlKp)=E!fnjnp?>K6@h7=!Q)u`U2H$w(h{T+>}3&NP%_)lnVa(XnoSE5Bl0_lBNFA z+;fv~j+M?41C1`6r5`&BC(${gA2iokO9D72Mdwh*m?3uw%H&r#fX~>3iSdV(A|7*t z|I-x$4=5NNS{faarMtLsv#ZoyRpl;q6|`;HVbhy+%H#*RYKNt!3O47~uJRq?&KZ|d z{hk9$VprF85c;=UTB{Rc<)gw{9?52a=HQhh>S=aJYk8ZuF`SU(8;0c@7I2<~x86pm zReXB5>T`JIx|?r=G5>`Rr^*xLFa7?v@^w@M#)_pDpdw(w5SORigjMyXJl*gR35C$P zivig~ae``V7@QH?avo@@ldG9m!MNMdpNhv6iOT9uN47Y4`UHZ##(JCt@J&~FbjSj*t z=5CJ*=1d-x5lNtK#Y&8Ei0{|{|0gUMUX^$e#1YoyWT>lAgegx72MPz)t|=;7BO*{% zQBzYi{0++F25HSe(SZX40|!KTg08c*wX?Ierb7Id zF5vNkvX$h(h=rUgQvwY#4yU(&f%Hn5OZ(n!Im%n(dgxpHsG76t$m-=U%3svjk`?@@ zO}cayLH@1q$RiEYURczB_PLi|+p}nC{(bd#o+>$$c>3w#;a2(X@KHT`^<^IA?wSP) zs7VN2&Nw8H@F$Z5!$sw1PfCk8=@a(MK*)s^#*U&; z$A#{)tmJ1YbXSPbMIj5_(ZdnG7LSl!67X;sm?+7`(_MYn9Se_$cRdP^2Q1t4n9CBL?VNb*Z zzDo~%9Z+eIuoNRPrtl_&t;LQG%rt5(wuo>V)v~B`uwrEgI4Q~On3y*No3l`!lCP6D zT@ad@R7T5OQ)9v!rP!(s8lg~KZdufZ}n- zH#LPq(T0#Wx2-DVZE$ndhQ4)uC!5yV;35{xW*A zzx?I|Zr15(ceRQN6_~;8@wtsl28F}C{8c$;Z7GdM&D-Uxflc0QF`Mg#qxLGxw`?9KVkR`)0XyS`sy!d5d z-?|%aV700sM{>;6W5X!qeVTfdSa%*b#t!r%!qvLqOxi!cIFunuk zPheK16jTS~LS8(0>{O%Jq$9(YkXfQh*Wzh*G+L)lMIDbDY(DGMq~bn}!RN>k?gr{e zv{n}8f&7i8DM%A^(PA?`9|8peaDabNd#THcA8BTiaRd?p2$i8oozdD-CP3?U7q6c? zR>P=m)2v11$qmA@&%S-fk|nSzsptI_L}%*6{4L$k5cg-b%m=m6AF^r~rcZ$AE*&BQ zh@%y`fLeyRzTi#C?#x&Bp)}z$?~(oT_XS`H&6%nOi~&LSNI#iHViD0Y;&3?z2U z>0ITXKul86$)A0rv*PZ%v~jaon0uFE{MPNK?|<*T)33h!$Gc?XT&V+)xdr)yKg#;^ z51#PjtI@QJPDq`B+{mH)ijS0u#>o%NRIUw5%C&(%ul~_c!-5gx@bK`+FiMq%Km`UM zI6&VA3G_{n>(vDXDyP^)FdeY?1o|n=9H!Z*g{~y|^Kz~54drIkN~WrjZ^`3ZQf-Yk z=^OIR_H<2y-NFWn59zP0O2ue50MEp9p>RukZ@pBzhx5c;QD?Y5Zt|O5H$R>O78a(1 zMI(reAn{qj4&XW`ePV{?H4NSSTKd=Z1c`ZwbN7vFtLS z*`IZBZvquC1VAjbGgbnV9IHHlQAdR}s=ZSHEvu#)KeLTccOevR z)*dxr>)JZ|5vBSmkjNF~UGg9&ew;Ii>`wOX&ugyXYWYj4=UYnX&bhcVzBaqXC*QdR zcP4V;y97U&+T8}S^2jtLv()4E`#oaJ>x|kVsb#Z3hb!jQzVu&(T7JVHxn^fwx(30F z(QxQ4eqiPe4UP^jNOO@IF?et+V9?C(o6Obo_M>ZdFAeG!hMst8p*m%sRf605|nU@SG5LhTOwuAk=y|}byQ*cwTxYs|R-|O4ax7WD6tEMyo8-C0p zzH3m}%-0QZKZ1McGlBS{u`1W<3|S0+$+2h2d1D%zJb81a@rM zU|7Fp>w1U;+&LMERe9n9Zs?p2g>k?)(O^D(ah;%)C{LL!vGO)wt~|H)2IU6MP0Z`= zs599Ri|Qc~^;9P9?5YmtDsx-@O}>GPMEyxu#1*rFQk+?DH^Qa(e}>aPsPqfxz`Kd} zoRLQbfNao*@O_14oT&g1gWBn?Af|~1E@H%tM2H8 zrz9p{*@_nz%RRz?F6m7;oz!_^Z3$u zH-04m*LLFTk`^Bko_Lu*@i3>nJR%IwoQdZnk8p_MyIKR0`A7*YX?RG0Dg;eIj2I(t z1kJ&g!YPw=vW4+ivw3Z_s7mR-%7e>yAc`VS>)?saRBYzg$oF&Y?%KvG<3nR(`xU-V z$z4&uVXfn-T*M}9J2tl!!Y{9^^pxsXJNh>sF!pxS8!fz2ep5a^!l*v7t;(oAs8!j* zj|aqz&niA=&$!AEM6(tU*8!!XReCn5asj1c$K{wWfcb<`Wuq#Gt^{l%c|;xH=LU`l zr88(Daxp2WDJtelR-gn`g~6RzgbH`%OLvXUQg85Nc(CYM5#v5tzHjXjiPFgcEp@9i zAY8sIR_PO=wB-wgCKbjA;6a*TjE&N>2okY%A5c?#KX`H!c|^LMycdw!5^(^Yx~mOT3LP`Q_LrOY9#>$YV%zf+qP-2G&8-lV(rs4Z&tx@@_X>s{8cIqAxM zf7z|eIGf93j#@j`=R|E8Z!-G+(VUyquf#r2O8TAd{bfm4CKOHOcI=HNJsDdx_tvBD zA5BKX8CQ}8jV7Yd?L0j=Qn;XVX407mvaqu41dz-Kd7syz9xdifL9SGOP?xYqoKErn z`?9oMmJF;!)ko63*%}hh0e*m{^lh{nI@wdN^Cu18SRSDicb%4wvK{ zWnLu>esP)Za1WP^W;{_ynR?Aumwsi*;gXaqWXAi_dXaz`bpOLo-m!GwqMy#ox*b0~ zv+u=YhPNL+em@AGS)E1JzGdC)m+~6H$yfpP>DtOzxv{*`X{nX$E=vR0QWAltWL)3U z-JcpT4)j}VyQP!^6R2IUI9>v$65G4;z=`(fjL#phDm)-19RcihI?aAZOy66W*Sx~G zBCoEvP;xk-0>G%*RCwDbDgVqx%%6rpmRE}c%94DK@^xU>)^7#+aQ*hdY6+b&4!-dR zSd7LamHuqBqKX32OmsXub?0|7#WMss)GmT*bqv;WsY18p;Hr~QCN0`gw%e(nkMJou z@))^l0!XtU7M$jH$98mQ2J|Om2M;}K=%gSVdH}@xItOppl_hGrdkymONyRl~Wp?ph z5HzzJ<3n7}*Y$HTCLyZmdihK&0bx^uDw1_rmmpPC<%Mye?+d!7O`J0v^WepFH0|O+ z!23j#MkPF{nS2EW`T+ajJMy>-h$ONbAyD=K3mmcKglgThwofNv8ZA%q`!{hwg(3~{ z-AkAxehp6vDwN{>e(rc4Y5Op$e71-)nWBW9!tcWa_ZZx_7b-_T%U5}dLvO^ z#2AYP;j^_gwU|I9AN4^sC>jh!q*OfI=+5XJJ*}x!ERyl0b8lUFWhVLY$wC?LNxPEv zq%C2KTEhBn6p4pfT34*rd3^z|v9iKmS1Wm;{p0m)PUjS+*4J7J-E|E$aTAn;>!WfGOf-z}lx^=H*r=>54v_!+<*4FqoU$?#u!F)sw zqkbsx8kM@IKuGQhLr5snKw*Fs62Kn)nbK18CTnRTS2ifu32q;xdV`qW!oH||TdF^^ zO~38_u+?q0cyoK;_M3NcO#Iakl)VHTeZX(L>~BBxsevLk7GzN83spm7I_0s)q-Il~ zw7yP_d0<_z+5A46w8*-AnRStEar+bd3iSy`#A-1-rA9`hyg8n6G#i^U@eIh*Rj!z4 zbm<6Ky~Y0CwgZO^@6LbqHZa#&go@|*wfmZycW>OiyQyyNT5hqf<9aCG;-fx9-?jm? z%UXtj9^Lo~5ADYWBb7Q6&7L#}iG!>jAIuzeI|7F^PcfZ#tTtI3Edn(NYW?Faz?7Jp zi`^v+WyU8~?K@_dQpj%EjApCV9F&re9$fKkonC1gvkor|vbrzTB9zU#A7_i|RGN)D z-e!s70;)M7R$8p|6oWt?9(B2f;qhXmQPf$2%KNyZC>EXFBiAc7%ytwZ0VSp}M+L@a z1B?GG8?}Im@`EkH_!>*b0(fCECNwpmKoc$%fryWznvW_(r1NAzvHOEcmiZIXTwTm= z4O@&JSEzv}$!O(s)QEZ))4u(PrwH)TVH!)UH|TWwNhWkX>{r9S2; zufUH59hJE@q*F407etatsjI8WG=?^$TYx(QIy0+j@rgf zc@A(auzxA2P4Ql(q)%uZ;=%zW$&9kdN5WoDKx)e4Tp^dkN$TqkcO3K}@gKl~fxQWf zYBVJ$&xYC)+XiD!OWP~|ggE@vjtew>0?CHZI#~xZ;54hweTVE*BmP9PqjFFHG$LML z-IS<#C7{N@z5zBe6$^Gf3}D92F;nH^pk7RDmf!f^?aJp@Hs|*gN*gv1MHL`hDqd{Q zo}7}MgLQ|E&pgw%cb~L(U)wX!7yv<|pf`f@{JY9i2y{)MKw`iPjWmBG6w;q~eR_hk zd(ueV$)Eo{7Y;}K*ahIH*Q*ZGjMb!kc$WC{US(3(ab2i_<>r;1wgZxab6N-<_=xJg*pf$)SCS+Y0KD-c3r8dwz$aP zLSPNU*{^d`0O!{;g#$Z#_a=ay;@2m379TJ|DenfMy|r$8iJhL*6U=q@6g9X@Abafh zcymh=bz9p}3EJTG7^-TTxAw$CKK$>`jg|~FM0=Q0Y%sUS(Y(E?l|y16YH1KCAdPl=7Ca)@WCk!47YFzNZ8ZYw`Wh^<_%(=k~5@}C!wq4X^e&lA&H=I%Oaj&$g6oA zqMKk^QkascN3!lT!r%yux=W+{fT5}4oIwZ0IHXaGpc)Wm;7U;u&;)yhU=aO6haiD7 zc8uCfkx=|3YfCYM!xe@GkRvKo27FFzjEEgG1O`xLm&oQ5)O~b@sGjjzH5@yJN3Z&+ z7<%(z4X)?ErOf9bjo@(@AK__AWj5hn{*Ur}4zdg2-+zH0#Uar1N(d;R^odF|sPu`c z!Xq<lr@KRlDmqfhj<6t$RpJ|li)}@!TX&J5D&v&0jKx`_POD=srz2x@9KSR z$@|8=yOV7@q^3lu-3;Zf`hd-;H&>QAiW$lE*wg+A&Mq^D0Z7(g`lNc=rTYKh_;gRX zguHrHTH??pkGN{g-GBB|Jm1Ax^8Xg*o#9;2&ZD{kC@MO473RR;D-V~1Dca#n`V+x; zNUz*H#vF29%X$Ot%+lZZva0EOR+~gwJ{s6cY2fR+t?i|~UU9(NyA537t8}RRv?s-N zq=ADKdmPU&66MNQh386j4j0s;rRP85BFQF?)v#cj4mo6ek;kTRE?a###lWK2V&c(oNh-;W8dKaQq$(pG)QWj8(UHnN z2xdJR2kUX=g9i?*fvcQsh(fosAqfGP)dvqAJvwuDV(d9pTJwO7Wf~3%nzW1sCtc8x`hyi`M=?=lRtvjNHHX`lq9Ns(@aye8X{2% zqF-YHi1DBNKj|z}2BBL6&NU(r`v>@u7Kg!lE%gl#}Xk zqj(89n2+mfn(6^3`yqw^(y^3>u2h#BibY}0q+}WrA|54fA(7drjHf09ax>CP;60i2jqwNHEWnf zZ_kzQk@t=U*VNHT{%qjSQ@Ap6F~pl^~aYUKXxfmHeJsp|4f)NMYDD_ z{=@g)d+)X^qX_}q;`%ASABVB{jr^)`+ikbKmn|h=m4&~bqE)~FNUT;NX#Ndu@~O;& z>tP-&$WxUr(5NUfV!tpa0OmfvF-?O|#G`LW)jup{s3MXR&}=r_Y-V%P2A~>1ySAiR zk{|tKP3j{^8!PwBdIx>vo_A&eL_|+X6mJhH26+Ow(5_Mgh|RL$6;638F0|HMr3MfZ zpW}&Nl@WPFBbwBTEnrPf5`J=zVtD8ks2hFSc2OE<3UBi9I4U?DPKPuXEM8D@0}P%@ zP!odE$Ag3MIH&ZfgKzGzP^=rpF!lCAjTncM-fwXapT#(dB_%o{SSCSVsfYU22qq+& zs2I~<)`?}l7P_~57T+dcuHqL|H0=4~9xjTT;vD%649}NOgH|&J{k9l@p%Ey|g|SXU zwmFc98`3_cFhoz%1#xJgt0LBrK5VrF>WxtO^jIZ}+wLMIHkaLF)q89{-Cn>r5L|Xa z-VQJK{hM-%=vk`-)mA~fpjD!3t00#arB(?8g^73+gtN?(n~BzjCP3nZ#-HkTE_Hk-wV;Dsd^ zwo2BB9in7Ncc(o~dQX$DwZm{o*AZ+DH;LhryV_7P!8bgVE;{b&fEL?28BQjLb^{Vd!&AF1;q zNdHl(&Wr)RudI8^ma?+VV$lrX01<1RHK6P70k1Ts%{0l3h7V8SvBjPNAH+4(z=w66E zol3i&egr>)q9b6aYDckGLnCk`NO+ST-(12PwPkwm!1$#AJd+k$WusKl*xKZ?dIR z^fWj7T8u5t!Em!w+u3XDGrs=(^RMr#-Bkh(zyi2`(`LW*!Ou>;dbUlX0H%1)3y=Jy zrzg?WA;zl1)m3V#{C8p5(aU9H4-|{Q3^(bDZPcr8M=NDpC)-+d@Ri%1>;yfv^_#y@ zzGPIs^v&-m*IA`%Wj$BwYp>X5yf~BJ*50|zCsoVqxsU((CBf3#>FP2b*xlaOA#Lv1 zU3I|N)ft62SFAcxRl%NaQtiUuRr|*u}c`g8mxGUG1%^ z(n|NUpN2G~h>Dl4A+bJls^id5*D3u|`(N68+SuG23^hp@Od!N!>;c~ed)Sq4bd|TH zCi$DjhIZ5LNLQj=QW-3%S}WoSy8{x&7AA1L-{%KQbQYsRz)LSQzkOn=F_URZ5?PI( z?WFU(?7UzYGU0k}Fysp({)})Q#m)^VI|wEMdVuSY`^cUV###^*BUmq@D77*YL7B&p z1*=eGDkvuWfYbQ&G~U>0VFX8mV#puHUcKAz1ym(_0wc(2wq?wzdVta?;lSc*8oUNv z)c&9pc4IPe<3t4d*olmYNfGH-xVx`ypFYl_RPwb5>v)`DscmidmSb+cU-y|u z*Tuq%vxQ9=bH4|vHxMb9H*GRIJPnq(C%506*%Wr98^ZD2On-kS9&SiG!nw)|%41i) z#8qdgFaHu8k;x{fWN)iZaHg0I6h4ih;E9Xr;w{ll#w&w}{jMFBUdie(J5ahhU2(Aa z74wNdAU#JD%Cq`qSD#QmdFos{ER3IiN?fJZ3UOc)*V3Qdy-(xUShvwuyGev<0?*Y9 z?6vMUf@9-a0S6VXs(CE&aOUPW?)t6yW&3ljhj#WCUqs>kk zJ0(})g}oiycV-T5z|+*P(aYzS2%l~3M08-BI^YdwXZ)@T^~d?cgC})^Ac_=EF3>;{ zdBY19C^zcl&n4ggwfqIO))=XbQFg1WMmM869!FnpK~oWQ3GqI7;S9P2Pm7R1=aB2m z2F2KR7=~mgkFf0s9gZHaB8TKPD}{OUG@o;GpXzhsBQblWsqW-S_y_c}YPPWK<_QF2 z26RoPq{%JL-ozz!&2`CompE+=NY{xqPk9(n3@Q`J5;Pa0s$nt^RV>;FoD2EzFvqE= z8M1{Vs2pO&Qd!@gG{dkR6-9&s7(JfhgRuQH68a@zy>Qt4l1aJl_YEfbIxgwA$E_^- z@c~e`1fRJip?MCmxC>pnretHIM_LK@t<_bRt~UDk>N-n}OR8DSby=EAw;Gh$Y7$aX zrpv34KSWg>BE~iKAXf*!s)Rb9Yg9HbwOpc{YFt3Yau(5`l*-NR)0Tc2f24XKL*Hyb68ARdPpH`C1-AjO0mo1ZmT*6-IsMr8 z49Xmc6e7_Ygg(l@6JFPx>J7vgv_)IE|irpXA-?Qr; z<30Bl{9pBw*_*OtjPmw8^<4C3ipQ~Y5wQWJoi)$C_k=oAX+v$3FPNiD_}9B%4ZR!K z+qSE(l(^h*LuHy;x=i(u_Zyc)@$B zb$7$gjX9lMD39?lXM)@}-@5JD=L{pm!=pQf>nfANe$8NVIJd(%I$TkD=WTa=^9trY z5GlHD3|?L&4H>XKINGM|vL^NJSuHYEHvBEj)92wr1qqi0T;e5Gn9F z6=WSNB4*=9zB~6Y`8jkmxfi5nQn|HykQ@-Cb7|prR5H8&th_a>J``v}?Kq_hO>AF68eB`C`IzXfRl@{;*#dng;Bie!)8PS-bPCx4rNq#uxH%RydK$o2 zJSuCKu0&<+m1R2)&^7_C{eAosi33NTGW>M$(c7sc_A@6q@-fF!;CXoJEnQy5e0fJsM@q?K+Rp)w5e)<36eM*4?LhRnkH${ zQ-Ui`-UXdU3z@1ZAdXGH(}kFh(0TQzEkoHxBdk zQ=8e?-5zoV|`%$|b+yKCzjbt-vcm(qHaJF4tqUq;v=_m z&uJ0WipIh&r!Yr*<~^Pc6;@U6*F+;|B)~>->VwNEF4Wi@@r3xMmgI7UW9%p}E~Gh% z9=I@&vbX7bTu66BJS_oH7fXNc6NdZrdYA}k7CQpM^#I2_Tq@FrOU zDWrW63llwe@*b$VIRV(=w|5m}IOenqmujW&1t9V3w7Xc3Jdp1Zp;bej0!A%8OgMrb z5spmu2(@sIR{qKBJR3=PExC_-Ez&!Zz51K4YnriK{2B zgq#zFt90@L`Su%MRhLSoik9xTSLO?3ERmKdJhuXQpZtcBzLYm@C@f% zBcFb0$*QGc;h|6k2$0g}wFBMT`h&u7sDDd^0dG3*DZEQXh!`3p$Xab(ratEt`rW;` zZ3a?^!1hBvM3J@>2@Tk?3sH@W#o?(*SHpeTIUD;K?D28M>^?(pptrqQob!9N&*#k1 zH(L@lUB)GN^yyn4<*zF#X)t!^a^uoYerZuTa$&;`zsJ3HncnFN+o0rM3_408>Jk^e z?1|X@9#?@pg6djI!10927&ieP~E7B}5DT(sTaz?2>Ro>blyNnqEGbtkf z4`|+J)h&gm`64JGFe(?g)kGkpF@opBxDdy{8l!Z0Y>w=4X`H}zLgX2veDXZ;)G?87sK2HpEv4QBb0QPh=YZ}{UHPGAK5le{L!_>56iQ}J&a3!hnIzj2g*Rl1xDhJ)dV zq05wM5Pmdm=4p^%!Zo|%=Cm;Y>nj98-`_fFKeLujb?dp)jx7+BokhqHAw)t&Uh+h7 zU<&`S-8>shi`quylu&Ba=*Ns$VlmM`hnOj4w*9R=jnc+*c!dcIVi5B}z|y5$yIdV@ z8*C|}giE&07LD<_9N21L!_EIp)=ww}@QhYo3;YZlGc9*Q9dlw8T4N8^L#*dy5ePZ* zS>gQS8Q+9v<{gimxZH1?J;l~NqxGM7`zMcx>7*^!Xp|q(G}`JcOxk7QIvlTlokD>K zdEN(-k;}+0o@=F{3i6HmX&3DSwkv<`9INb8QEI42YH zN5L*5)J3H_5!%7FDJ6qpk`4uye=+uS0v_lHg(OPET4je*2m=JaN{@{JSDyCX*v*tI zO1d;kAt|K!6Aa4$LWyIEl84+y#pwissmK-(vVrCoPiB1POB1ji^eTwKo-h|Z`2*vf zi`Fl$5{q0*ix(JfdFtMmAa1K6GTf6Z4i$$)2ev$Uo93ZaI}Uop$K3lzA2lA>S-CnU z+S+1{gi*l@?MK$_eXzZE`@XIbukJb5!^L+QT!Y%}gLNAt;>y^Db*m=RAPY5ZIo6$yaXVUV%L@X=AmZxE7c^<;+YXWQ}cv z`3enWMUg#+VJGd9{x^TeHAjlLsNL5LqPw#>=uFfMRqbl(MKNW+-8xs+=q=V{ebxvk zKAY@L!;YIHEwiVZ{0>FifQ0sxL`i0iusDLkiInFf0#4))MKnr%%dF-+nmP`vo;69u zht%c*O;v_CacGnb4F@YZyo3!Dv1m5nBeb5?74O4$7*mDc;1N#s8R*pu)BqJ#8_@(2 zZ!#AB6t&IsIlW?^rf7`Yg3pN*$l1t$sOmq96G4;+5U(I#2tE^pbK~*_(&q@8{fF#L z^paX8?&j0YiDs`*?XI!aH(Q-PQ^FQ?C4I4QM*_slkS^W^kij4so#%>C`h;$`Dn=QE z7-jq;VwCQ!w%+D$MU2wr^nFx}QvT0(d0WEdb6T70?KQ4yU9&gQoCf9+jR(bWcdRqf z5stMcolzahR$4%rYZJrOiQ4wMsI|kFNJOH+q&sHqaCW-8b#b2q8Cy~)$5R;ys`!wv z5F(tCFFc>ND|-d`>+|{LYxW+d4EV!)*DPPYX3cW8V9kG~Cr4qD&^ID`ZU=-$eEh5;Wh^##;!=+PsD9uUea3X}rC-^u9XL;!c<| z#&|3oZxaus_YWO5Ld$tEA5TTv!|l3s*p@M)){M*H6<1nTZG6zUadWDxUfc~XAY-SJ zjv(>@uhiO7Nd6SIZR4u?_cVeq^(@CChLZ#WnQ^Dno5GZhWfV-cj#W&b=v>J`e9RlO>SeGyA{ccxL&{$Zs@PhRi^J6{(=9lic0gA zx`rwpN>P(gQ-Pf1siHxv2#La!Wt+~t%`v>wFm~**7hX8Fq=?cViC^*fijm^t6{U+e zMk?EDgmiV>nr%|*9)Og>E_C^ReoIZHy-}>lHSF4BU<3`YB{m{Ws!_0II^8*AI35Tf zQ!_x15&&V%)vEKgSp8mO$qJrP!ph>c?KQo+jYB5X^q`lzXK>T{n(oG}echeCzr5wA zbML;tZ1EPs(d4PCHJAcb?PWp`4~UFZudB@$(b)!E{ey;W;od~IaD3n=`+jbG@AVtM zQwCX*b(I4y0(ftn)@na>vi&4O>9@+;EuEtiY1=dwOKv7 z9vT2J!6O0TRF>;b=F(e@nY7!U5J%!%d$8TsY$#mAx7%aP6`ZuRXoAlHR9`m+vrI8J zTEtD7M;n*#y5D&J^7Ts_@oCCWEn=2q4a6E0%MqW+CsqP|k1>RXC56)ESZ+W3btQ8T zYhXTDk`JDU6|YR@coJ&PaC&Nx1$6QaZ&Bbk*jx@#lMTak8Q@F*5tTJjTT_sKhq}Z0{ccjjLMlI3|Dp)8Zl|R$Y6lBB z`C`rW$9UIB1k_+KA{#+e)3|^c)YwgIMQq9km z|Il+Wl}c6(3m)G2N+bVe2D%%M%M$Rd!WK=YGtslvu)jKIN(!s{9mB2IE&$_u1of%-&sN4>6!>^t-iQUjP3^ceBC5eA~S0K5R4p9}i8l`n4-3RlhT#+;< z8UX60Gb5B9dnO_Ua4SeG4(4ed7Gw#2Fh_{wAU|Hnk5aY(J`-q1&X966@Cy1Cb`q>h zwXOxjXX%1Kn-QH;oC)NsK2A|6HdqM7g-xrtQd77coEZSD^OO)ENTzHXKt2RS6~)ua zK&AHHAZ_4#NlLM?vxXEHYdafz#H&JlcSlEex2dDP-ejt;?=W?XZ_YU9EBW@bzH8`c z>K3njmxg&Z3vJ>ii>0Y4ZOP))bH2i<=X`hNxnE6u?%eOLl7FnQFPl>qMyLeURJ?LV zCe@zKs9#x9%^3_&dTzaRC_4Mn9NYRpu!K3CRb8^ zbX6o2J#3`*acQC@y zaqPtIZCu=EkJ*ga?#aUxWn`#Lj-i<-avI*k*@AJ1i6fbi@i_8o5px*P{u6naXMlPC z2r(Z>$%{~_SajvI(&I}NJpFgb{c$_n#EuOd`EQsNg;R0xlynR4LfRIBToI&jxpbXP z2SzyM?-8fp=5pP4pI#AKFW;f}N+|wtzIs;Q{U+bl(Akwg+2yyoT)Kvau0~F|kjsT! zu!yd)tDymzU&sZEup38pU7ZbG$o}&uLFGgvqAuO9FvTRv6lGHD+Ws=X05;VQf*NmYxlqqCdun$^e01^4&(|%QV)s zClVK=FG%-snV2oxWL&0sS(?jbBB%>!l)tO^nbL_I-)wcD4)kLATU@gzjsmFhXgC%W zlS4oACpU7AAnHsS+mW7%L!@tUDTstL8@E6Lqwg1eKj4f%KwT887;}Z*uN3)>J|I6&)UIE0U=Pfw)sCVLHOzx)7lXnsyE!V=%#@K9k61mip7NSG)OLsp z|8IUxO+dOAq9J=%^MU}5BgCd?`tc#v^$f`D0#N%R1)T5dE9J= zM4=yg9V%N+E}O+N5R0_OWT0H%qeYJ8@QHGvrm-p28ae<*BtN?8;EJ-)-h zFQqz?y6Hojn$BgzW&s3w{(@1&04hK@f5D8$VHCu{OTUVj-n@#-y0eLJHq?n(7S_q! zP_cBI57Lm1V1YSgvEp4^ti5Q!w4d`*xG~@i>YYI(ZyT=H+K?Zk$Y6cKZFV9>$DM6T zG7K^UBwA05`FF&CTC*95xe`cG5ug8}D$dh!)X6FUA3Rd?AlF2z6rwAGHXp zG>;8FvFj(sSH_mz)+;*NVpy5d$#mK+o~pdzneP~HxwCxnYRW0iH$Yk3^YK|guBcx_ z`EC0(ODc=k-(mb=(UBKwL`a*2?M92$VhM}a4!(ZR?~E@#x8w1Bl*y_Ld?t>71((q_ z)YW7@R+GQ0fU|6bu%XA=;%i1y$B}2mQq9q$s3KRYAz`>gJlGZ&piP815OMkJPOt9x zVSdpuZbWl<&EDlUyU%MEAg&2n7gr=0k0O_jqCgJZ)B#VIu^XO3T=9I zTw-dfe;bGSgc^428L-2t%~QNM+=XGubvPIb^Gfg=U@7DKk!(D{+yGMv(}!9tU{8dt zG=;O+ZPAj6ubtsndTXn;xO-Nv^we(N?CmKvmfJ0EvuO9a9d?5$V@pSbNH&`2+}2h% zGSZgYwk=e5;6Nz1&Da^Wxw0sJn@MyS;_jHeO$eLq>2jl7Hq5gF!*%=VzwE#cG2^18 zEbfXq+5}h79dfqmN~FR*8<%vaBH=_R>29}%XLr9JGr8BUrSd4Po1fWSpI{w$)qE;qzIG~_R_1;v`B<#_!eqoQx z*U1wsN-%vQ(v>jpq?xZm5h}l@v*R5xki~)6+Lhf_Ce+#*krVfJ{;aaLhX-5PlZ zE~8l7dc`7B^NjPCVJn?g%J7kRvMu3ZTZ$J;uZuuDO19DYzkWx=Erva(PWz3LWe>lw z&SrIcf^8nbcJr#zBC}f8Cq>FdNM!2roak^UE4Xrv4O2M8Ok%4KrHTzKw#_c(35spjB4`!XF%>D(v~ zl`IQ4Z?3AS?5-JbiA_zYNsd5FfxXdHU&nfE_14k)XN}K}cJ0H~fMk$5iwQ<32wnO=qp`PNuiXy^K?7c4{7p{}h6uS3=cGByMd z(Rp>P$LO;-t&-v)fl zVlWePm}oxEiP>$?AG=zCnW^#xyFAf==<>w~-Xc&%jd>q3W+kUb3&99oLCYzf`>EQt z2B83Vp~5hwgahzRPrH#`u4bw432P`_T}Z+Qb=pPu9`Ft^MxFROZhL%UiMpyNivW`7 z0mB1qC$%Kj)Sa3qi>C~32%UZS)rqg_C%$m&S&dw(kzSHt=Gr2NnHciDhU@>$<#%dn zGhs11g}($BPeH5H&4f^}zvsHMi=YUF3hsFKBq~p87YL~#iaoDKGw@V;VBjgsCP5LL z{98*MEs(eHE4Qw!cDQZPV4@&-c-?kqq%D;Q6dVq99Sp^gN9HK#Tyew`Z-wHRt6<%R z!y<_NQFjKB4EE?PI;0h8y;{3R2!wvfY49&=wRb z!c|?S{`#GIl;4S(k)7G2o> zD2(Yqd6!sBkaj=Apgp({Y=3L~K3`SEIh`#Kd`;auHGZE?QeCR3OX%OEmNKTX0{;S5&@(`cgsmn@r_;KbkLtH=awJgLrE@$dUt7KI%RJOi-+W|NMDt| z%1N&gT!_`;HI4K|`9e-I$tTsM^X7J{sY1olMI6=KJ{dx#f}XmGuYUMdHLYA;BwfSb z^3qCh4iFQv5QioP6@xah^D*uiv(x|P>W+KlJCKbCe#;N16zN{kvW4Oph~!G{rm!$^ z-VfRlEHRW{Dvnuk2}H4uX**LL*@zGYM1i&9YIb3^j6DGx(x>T6kf=W*!B=pO_C~*> zS-wENLT|Ap8ak1)b=PH_#hxJf|IY){1x`;ZGt|64ug&5-yLi_-oOJJHJpMENO3H-_ z%9cNW8JB1dG&w9#SMYj$x<}^4ngb?>rGO=(zo}6!BA26fUH;lVg=}g|Cs1UWI4uN5 zk7>hozi99}~ z0Hs|G713q(jqJuyJ|QrTRkpxdJb7qw@mWHeU6VJS12~A#TPjgK8Ol+Vn7}%j&9nMc z%&Eqn_6|i06gCJP7xq)0Z9qS%KgqhhZZMfB%QA%`E$;_THTrRGbw0BKMqrzF(_1mlS1}Tcr|!*PX;(&_U$@V{t5KA^@xl zx%QMl*PN1XmTo?KLhWA1s^YY?`0G+Fv{LJPBsc%(Z_1_q|N1WuxyoVWh<5i5lpOZ^ zOP6mVJ4oz-OqLR)V=1F}kL1SDa?hZunTR1hKkvWRj|wPG>bGO6Seh*L(gOK5J?SGD z24?f}PH88nL_?IA#*@W*Q$@>$&5SiQJe2D05e}?mNVVe8Dg6J5`M`+Ib7y&AjK^*c z&Opzz(wF?XyE$8kq8Qm!wgYSc$;D;d=7h;;GTY2<@k-gnnOv!+tnts1mk(+V%3rz4 ze^B}g^G$*7@(g>+Pe7$&Ig6^vcm9!wB98Q2rSATOWu#6D^YWLp_LLjRM66_^A2!Y1$Mvo# z^o|U8x!0fnIg6$3=bsmUcWWWhEb>gOEdy@sd~TD5X%|yI@)=}s0dJ-WVMd5xJ~x0V zgPdiQa}j5=bTwvwT#4l)9!^G`;scMMq9&DLLb9L2E9sZZt28!`)fo{{@jc{;`a_{d z9??gm?xfwgNh%-EygrL%>7XltBS5@`*T$hV#6r&_*sOR}f{?$cb;WCL3X!eT3HXEbbkOqpFuvJX`GODN_ zkFmTKKxxfBpYgBm1bAIg-8lT4H)!he!k+3OhyMW7|!>T`VKzsEIjQQ z1RyBmR!LyR_Q%4wbQj`w&MD%rU2xmA`9fExAiFNgtuq#tj2=BYN`FcgEh;Hlq^#7q z5+F6=M5^zc5(1b#Ln8v0%jM=$30W}U&!TbC?=mQy!PaCJsX;SM?Fz2Jp03FmrLtMm&Il@J5N1oUOyvxM?MB%d zHadgeIq}NI6>L{Mu1+q;(@^k~;;~b=%E{$*Y*yu;j$*X*a8fxTf-v7Geu^*Xa)1}> zr1BiT0C@c2e1s{eLd)ofa5MqdYFG(C`k8P@LNy6N&64>sMui|W;s5NDTv)MUx4Sc#m`yw@ki(ANY@f z?{oJn(Fs~Zv1AG--{L;INp8+rcw~Sl_@)rPAV`*czdZp;}7!{FG7i1)>v+LI)RQNG=92rz>87f8w zO(EkDQ@9GGml5K?V1&^|hmUw)`M|aOO)#S?9`cAw-TO-(HA+GGS}K~1lH}N{Rs$}Y znnf+prcAn{QQ?{6;SlX<3}`LO-z$IdQM%V;(6D@3nz#Hd%|ojz9&(FIJ^OI4Qs{Z0 zk~I!8!`wp^GIRrO|)AC5I_G31}+h* zQMLG=kj7Y(=rZg?3IUUKrIm+)VvL$H@oj{2Af;k$6|FzIW$_QXmL=Eku^rK!YW&HG zH;i|ycF-?>3Hf`BnecVL)Vm^N2i>jx+BKKS7xakjw>`4tdGnuZ-g>e9U0o`OQeH-R zei8qsPrJtAx8x$+kGr%l|CSG+hJf4X_MnuUNEA>w-X4;C)Zt$)r@^EJu{Y=u0`4G* zLwcYd25w~t>^fs86bOby!0Ev@NQ*&UEUXKJ0|+;FXhGp^4GBJQAiWR-^8`IXU7(l; zB=0jog`R}UNWQFvA?07@U{OZ?`T3jTB8bqPc$5cJL9o7L`Wm->l^j>I()8jT(fJ zf?fg7OY;3FSAfn!CEwPNF%%92)XqX*;s1C{2>GMVHzdDFzuTN0We-iHz0jN_o&Mrj2&4mZL3GoUjN zgVCLOg%2Qf=0-d6;|I76%E(~+pk85q`~*B;)6)1s1sFDd&?RAGcyj!pYz)RvAgn{2 zBR_u7DXnb$pk4xvpW+!F#B_WD3L4HJD;;#&DQTF&ZuyG*lR#MhzY= zAysGiNAj)G_c%wi#qV&+x60qsJDIZco}$_*(4+?$FL`po$Z3>j{rC?`vKd8lL`bj5 z@gn}u+S9Lc@~wp+3(b;l)t;WueWOqnhya*`Y~oK}_enk>H!BL(j3xuGObK}~i;S$2 zCOG~7LQ>;*Xc~HL>2@I$3IaxuzD+$=jWWwr+#S0k@^}A=X8FHpx)QzZ7L=lK2VKVJ z_VB28;0LrawDIJtsx+9Q2cWy^N(BKec<>N|HDd)!Mou22SX zT$J1(N`nVo9!U!Ozi{ZUPLwmzI zBxpbfm5c|AAYY-N)l7wPDYG|-#45;3wUdI4()Xv?A$Luo+tQb(86jobOJ2X>z4u;! z{XGG83+b%FZo&0uFkqH|8#1!M7-w;EqzmV%j4*+j#d~@NeT8}>jE=*cXeHA;e~#3G z@vBxTA%sG?28sdFv2<~;j*GBdup7XhrvK6<;d`+gs8^%+J`+yed5>W5$0#`Hf8}17 zK+Osd;QVdQ!41R%<_1?AqP?P|m%l9I4-7`H+hu6Tm|FIWuJ75)T-r@^z5tl7$GH$A=-n6N=dSI}={77~zy1M1 z;&=1W=2%O@s%x<}ALcqb_&SZVmmAmkotrjn7Om}$c6UOD>clswY_>}@qoYmFbag#_ zU#8>Ur@DGF+oJsi{XO$~YHC+%w!rYa2Yx1nkhilDctV=s-%=dXLI$#nS7@=<1bxD> zMI0~(e>i;fsGfHHHiLZaI{pfB5E+A*Eb6y3&}$6_#iPeix!n&}ei4pk@~OMwOI)G2 zuO!!w5&V`GI|)=-7arvRM)=+CMT_(>v3y>GbQQ`-EL^C10mtw)misp2TWm6ri%1UU z6&fZU%M8~4Pd55-%)z{D7dhDk9w5nP);Zrrp%Q2*k?!zYP$uw7jTFRZ6@xu*ibk#c zpFCo^cEJWNcq}3I$>m$L)4z}eZT4t#M>riIiCq{PdITKQaF%s*_o09lChVsM!j5cvzG5$gK~rOG>7-?Jxs)f!>ji# z7l>9?S=~F>6N~N{6+_{eANsj_N@C7hLuGYMCFEl;zh`1Z0Ue}8j?G8_t+XOyzK$YZ zLkd56z=fbfw)v*SpG$-M@#9jTyp`){@>iLfnoL#xrVe?l)TfsgNdL?qzbk9p^rh`X9oc?=)>I&`LkTuGIsXh_%+!Q)vOs5>uwZ1fLqm+SPu?KOxnBO-ytkRk z)V$X&Cp?FNJ>!tq2e*M_^~HA?r-5-9suVm#kepJ8-_sQL_^ozN!P=&#H6Dl6?}-&O z?Ol^~wE0u9P{H18b`OVi7k|i}U8|QLpMm4O{ubrL=3id$~3i z$Iq^z7QR05Zc>FnJ)q}#r2 z*T^>GE`ME9n|Ph2tZ1!a-S&-pc0D!tUvZ&N!;&vZ{(G}#m$&}d9Z=|ARc{hoOciV0 zZk^L@hc=hp)!7>pqrJU*<7t!-O=brTht_8rYU-PtEkcXU-XYn@3cn}sg7NT zQsTYN>LxSGDf^67m83Mp5E+@|QoITzJdxBD5l zi-bV#36)}DYzP&lJqZoe*&?k%B;<07HY!dEzyND4;4hL31u8QPEE5To!$}AumXvGj-g_b0}&*wU_-8oaXuFlj{SKrZ; zgVyK*D2EbKr(|BkT4>Rl853c@^f@jaNhdN$(X&}1;(l-dh>2h}wlF*QL1~P?>sdep z#3+()A`>uQr^kycdhB))Kn`jok^Yum-4gQ>u6k|KlXS!c`D%GCBA;g@N+z5+t5DKo zw;C4q0ydaA28_h9GUZH%e&^@;Cci8;dA+H8KCSDv{{-^678zNP)zi{E~y zN`T+%4+V7Z^uPFr_YHs2c3Lw{QQ=y-3P}JIk4H8p18qpGN~x!!8yW&RN{PJ35ut}J zKfWMLh(+rumhaGQ+&EM@WqL}kiPdPtGA@5>`Ew^O{{0r=_kAb+D18ohg(M;Fie@UU zEU1xl{6i1ZO~}4g#B_%?zIqb&Em_ZiXFwRxU3ry*u>cqp_t9I-KJl&JR|~)2dg4!K zKIR4)`4H?*Wz{97EL(9I%Z;i?6I4swHz;5+Y)LVl-bQ9KGMe|UQL>xCN*1O2i+I~_ zZhquMPU%mN@in#9#F-TWJyu6D}f>Cyfs@zP6)-{^W6 z^h>*nSgVom2SfbY1<#*4^*sGqaP74V7F^3F-u!t zhs*VmOh+(lq{cS+OrYoPsEBulyW4UFJ(_a)UwQcr=?yjMMJ)z1xT&VnT;^U=@K?KN zZ+K^Bpm(qm369e;&E(sBtdDjkhr9bkX^p&wtKU*#En^FA9AzA(&)*hvC!)SYL8bJ+ z-uqQF;!W8C1@g?~Qt6AHI26b21xw{G$}_=O^|nM^1@B!`DgQ6dXG7L~So-`6Y$I&J zLdz;;mXs)^GskCv_cekUt043cqGw1w%<7qwrIzvu1?Q}YWso^$c>ee-CTDtj7W)J@ zoJW05u*pgkS{eS!m}O^}^`~IAFb8JS0&#{yY>#2Ie2m{JBXmbUv6!vu(qdqf+;8Cw zD!|1;%PrzXY^A4=i#S*Lo)`-1_r$q?8M5U#Z|<1RM79rYRA0c)envbQsN*T(560o!4IQ=xwQPfYma_O5}gy|Jh>ZS~$# zw|IenUUz?g&-RS&szZG9j@rS>oFjrVXf|E8Jh5&y)n)+C9!z^zxG%o7J*P{_c4UpQ zNF5p4;-54vhd1mi?LmPZt)R*2v{E&|rX8<+E7J5oeuz zf?}+&^2`y%4OfM{nNyy}mgJeGEYhiZ8}dwe=j44-FTqzpNl_N9q}UEV(kJ-#N_b-C z2px;bPp&7@518;#-q)W$i#V$cVIaV;K|4~yJ$JhBPVHzZ*VUjo=acl3>dU{Rp(7yS zG>hH{qDMuAr?ry)!++&Jym8{j{1OinAm$j@q?b>rR?sQUz$O?@*t+wX!g4!OyS8uO zHf+~WA$aw#r{(=5%TI>k*l7a#&{vQOYTacuQC=2uuKd!LLRavjOilK}e zw=AcyYzF;|43x=|MJ%mvu9{6W50NOf9PYT1=7Y(*CpT1Q!BSXpQ7QodWiZWF$P7=u_kW9h|PmO_G!#tCgS z45k>?K6-HGAse`i}UmSuV{!V zu&zdatcYhqF>CfMCpVg0Xt8bvm4d!cERx^70DF6H}r1`sRQkCU~ik8ay8et?n zynEDeuwqwfL^v(4;kz1BwVMn@qhs3bWxcD?tP@V4Bez=zntFt`cs!Ue9^Q-VlNb%< zr5KST-QFGD+GmijdyNmL+C%N^fM##A8^DlO(ja~)|DVWCV7f{SgDo1GW zrj`CFNi+CVEZ*9h5Ndik3bq(Y`$nW35W~FJ&LSBT$5{V>bOG<$)|jjA&^=>+t!lZU zREwM(k4L!vbz*^`Wb@-}|524~nMX#T6y}3mhB24#q&Zw zFI0IS+*(cbz$#H3^C6xU_}W^{3T{>606`Muhk$pmLkEYHLs|JR?BPy+*O0^7C-&Mh zHCqf8B>|riJ+U>mYqw>3dlN#kZz#445l}Z(iC|_z-fR%JR`UU;BVaey)q5O`q6wuk z8j(f|dnGBH+^OjdXJaUC_0raJ`S;^LC5V`cX3PGJbn$6I0XMLlPS6LUEduu=jI8o1keCJK?-ieY~pVV7Qe>T0! zCzTLV2?}=-1uoOfd7NIS-9U*C1Ov0V_y=k8Kcszf(-U~5c$Jld%4;2@g|H^aA3b(# z;lf3Wj&c*?D8F#-8ULbj#=<;-m6NJROd4S*yfSHuL`j)Fn~ZGHH|t<;U_xZ$Yi12n&&&fL{Wtrmd8k_r5d=2!TbfP^9fr z`4UU}mOlT1zP^Gtf7{o$-`}^TK>ECVfwIIFWFF=_09Tq!o2oYTxWl^Z-;n-%u~ zEqz!1R$tw>%D<%sg5b2~u7s%c#eYI(p0%yTxUiUO38Ke^KYdGaHwzwqSk3O7E#1Yd zH^^ijevnhH2JBQ}Oq36)NT_1rR-mCX~+6iFD(PZT3-KI(}ZJJRV6QTl>5YiTz@+uq^y zx?G}Mg!qhniLzKwq=Gz^OJF=w=X@%t?=ILzgb9oNB?JyT?b8JI38xg+j#Abe65hSj7U@UUc?R@4iUI2D#Hp-v)CEnAfmSkm(D=_sUnU1<#+M0IH;_;TD_9#tVIHROz zv{rKovIy$#Jm{}`4v9R07kR2SNeZDaJ@xd9RFzVE9BBe>H#iKUb^G0Z>jn;t5X6L0 z`W*>R52Gqn3}rPy>5sY{_$+^kbF{hbb_HTV66rbD8-PD&I<5S zTD?eDzK(+X48cGg4d)aw&=v>>=*jLi*mIgt=L{sQMydN48d54A3*|%)vu?iKNCSWbv*s89bYkvjlaLE$0cEu zUNm4-V$Q<*X5~B5ogbNo((VLbg_)C&e{7=}g_%QPsco2PR3Vy{o+_GAm}!U%igHGJ zlP}*XUGNwR?s1Fd3ku42W`A<;(|0tYWZhO!Ehwo!SEpYre~v2hNf$icT|Z4b$sX6q z>!fvR;h$sI@c;ewJDTo&I{Om_KVJa{h+4zeg}?e55U#=G7od z&dOn!XfHjQpE?&QZb|&om@2)=W!{; zd9WCRIY0)_Q6!Rp`dq6uom95MJtf!$@`Fg?HkR2a>p=5Ko)JsP^j(um-Jl(cLHa*15UD$;s_=_XKlRc} z_dazCNMR*#BN4`cudTR|2x9;`u11*{;HWU#fr|ngCD-Q+eCTtIB{urGS1p^dd^Hqb z5&NF5`Z@;J3B*}O%Vmm14E7+@qBN(;hc|>esBh36RSZNu5bU%M3Qu2jQDNal3}GN? zm%4;zTDF8}1C^dQ_mnO`@bfsf7_wf984w}JLpjz`<-!C!LsnfOaM^O2qiSY?P-lf^ zC2n8G5EQ~@gmEqP6*FT!6V3cLUGuK`ra}I?mXH+4S286i6_+X9t@)3iy!;Iwn)S$6ASV!KgV^5GYFk`+*GQpLbVbsS5TV`GH@FOWf3 z^j;vEkbmx+2I3o0u#X;+AL2a9A#EBIFoT!_*>mJ65uYIz4I*PX=1xM5Sd|H+73s?u z8sooGMdhw|O9>_vTr+p>I7m0Bq2&HKR*nCDmnD7Oyz3CLAQXF8F? z!i*To$pl#t*$*gyX`vcawM;02iK=3Al^V~Aihgp)k`}W68Rwl~AiG7>v`t!vtvl;8 z^_iyjrnISFXU(~Kx(&wM^Z8H*=d#aC>1Ju%OC&4SynsfumP zK>v1qPh*z_o`5}4*S+09YoPL)kldxDk;%b@ad@g%15J^i0{d$eX+MW)HegDa= z-c!`RcqY{)S#@TYR#3cg3Rd0lDS z^5y#HwH31#XqO*OCpse8wt`zq`NPY(1)8>Oq$82mHQc~IuUWp_mUeY`vjxAonr~_| zMJ$QKhxH4z9lts^n%;2@|Gf6_a*NI6YVsDmIKnSK%ssF1Vl3G#y5+C)hYu$#5mQ@J z!2;&w4PkxX2O2S@S>F8&(k#J{^W1X}QHZT^<370@2{^!*qT=y;kY)*5Ug();)HKW0 zz_@5UeIkmPlmEiAsK-I+`+P&a&FZjwOa(qb+%`ig)L8-v=#PQKoQ%h}_3IPyaN3*k zcC@scL)Fnbi=$rW@Ht(;=D@5;rP{>!_WrFq!o%V1rtYe~^^Uc*wV%Gv=O7YnZb@ap zJi?zAGZAaWG3jdFRBd)T%pm4_Tb(+4tHWV4wj#SFkqn4|Xb`2h6J6cOWQQ)>?%c5+ zW6oiBiEf9@Yj3lLJt&{3>uT>w4;Z)iSQ?XJ+!U!a*D>YDPwW`Akp1*}f@0liCGfpd zTFw3dx2*`64-tXjgZ8kGtEGsc)_GZ7O#)R_R!vz(?6B9{PK0tI(?T zI$9l8!-xig|7}sgpqK}sm#I#8#N}2quB}f_Yw{~8;BX6mM_saN7IfDqjaMt zgKFh5@w9Xa2U1OkqrFhNMBnbT#7ry=kxlHK8tFCpHBQ>8>JXl?o~0zz)j--8Nkh4c`d8!wyu+1&6}CgHwShM;w{WPRMHtxBj7Q` z5TWY7WJ=0+q1Nx;j6X@=&nbLivb^&}Az^uwHVJg6Su~y%zolVxV{!_NGDxfVr|;$9 zoDz>(rS^_%pMDxSUcpG%@R~9pRCNn+o)*0SkVlZd3{5ts$p@n~PkvPpUF7H8#w#Qz zCf4h3h8Bu~oSHlAP&kRl3#9#K2tVf&27q?j9gyffD6Ig%mx&%Q!65D-wuwO8UUv9^|6vs#;|!rdYxajYSn%w%)lhD=IaZf#>8ahRPdm)usy8)R>pkM#^3x>dgt{Uipu$MlC0%|{m2y&8 z98k1Qv6_$-L+xX^EklhvodxoP+9~a&Ze_`mo4(6G7)K?LWH42bJr(Nh?dWwEY;|vf zATc#=H?0&Gv|ar@`E!Of*!@~uQSq6(RgrKI&#-;{RtAhwKIFI7^BOfjW#xf8jKJRZvgw=*=ma0A-l7+ zTGQFM)wZ=&w+wRf#3Y>*(^MUlIHR@*s;(1k+J-pvgSF%ALO4w=&y7K%Cf z2R^s9-d5|X_7#k3JA#=|I-*0lkgpty^11Vt_%Ar&}QEUbnW}*5lermo^0TSB*+H@on>IfD{S1sW8^jJDg*@pIly0s}-?1`2F`OsQ^ZO&Hjs`XVDjB0`% zBuS`~UjH8NGkMG|i*8R{qC3(P+*(kgS%6OJS=*V;cC>dD;G`BSA~^-}N(e>P?P;)> zOqPZM9G*(TgDEJN9sJt5bVG-!%TkcrgN%u!Ema_WRay(%3|{hlZA-`oFnX-$teJ$k z_N$7&OhfJ%NV9z-Tqx>~dm>v#^{8GPau^qDoDPr6D{d}9c+ZcKvaM9_Y(tR?2L-+XJ+9!DOSFmGa8v}hjcy*%N4?5z zaTMH-&82i(d&sq|MDGkm=(ctIV#F`bZWC@h)uxZVtsEHUx8i5P*yC=&BnTC0$d&^sim>c$R)iC?51bB=f`p@WC7OuO z6?Pg~%)-TT(}A!voMc#e?Td1w8o8wd8thS~ODKFQAo*PuANmS(oOYjcwNKwFWVEz; zMab^-%NMKBHN~7@7K&m{C~(9(m+C1e)BEXQ?i0jl+M;w_pGLY^zL+hjaGz;o7o|!y z4tM<7r;Oiv{eDoRCK#Ay)myXcHcr<^?-)dYMlw?S6eZo)VVx?8Tuua}ZDaqEQNFLI*9@7wLVe-IE+ct4~}222A& z%apou2AL~+mqSOtm{B$bW_}@09#Z3g2NmlJXc?oU!sEhKuYd5eg{1vKXwTDc9(>>L zM+RIO*hSJc(MxOr(lrYO|1AALc$PF^z6=?J{yip9dR4QwX?4{@ES&T|Feq36nMtFm zA?mrPyU8>#i9`?NX$&?OKp1c)=kY}-HA2#zfWjDfS5kz6*U0TJ9A}B6SPv=3K$^!s zp+o7@I4dVoBg~#9&m5IzD%v4WAWS{`RnDAj@SCl(=j*KwXS2nCxW9rZLdFX!Uuslz zo;K}lw?aGdV)$zEA|?AR=%bAIKx@I7`5i5(rf@^S!=)ezmz2&`CG(}D3VCh?2yBe_ zi`OwW%KT-KxTN{07;#A{ln%N6dH5BSUzJ@-N8nq`R_K4o#TP*+9~dt}woPd>QUx+u zWZo%=KYP(cDkO@MGL}Tf$z|h-r4)^$z!6EnDZzS>O$EFjVlhy-g)CZ_bmvpoV5)9( zh;ji+fmKB7lx(FlFdi9gr>miwiC;DVN|oNmW1}<@iXw#(*RR~+4!c9{U_qP98FUz{ z>S{NOCSqn{&ftlZJi>g^1jJq_@lAdlF~Uik851ktV{^5qQ6z{0iGbi~!YN1|O6rc& z_t-1*-?J&De4n?y&$`YvSjS+@CB?u{bFZnxlJTShX|Nseghs2~ZWCKKrK&p{b?q%} z=4OL6PL}_y1WtfsK4ifeI!nfzN*j^^yd$FxXS9l&k~l+XZwb*EF$A(`f1QK3vwL7* z_pVKY;O14Xr}AaQa7G_dHYH6gaEXL)+LIlg26FQ$uXnL7E#q; zpu!%DD<-~w<0^TU*M@YHXu$_>ynlVnm5h251*@cgfBzK@B5i3@V&OF_Q7PSMmD zLiqH?#h$+WNKLz$x?)!**%1+^ z*K^NH=Sn-}-?Py~YX6~WeJB#H4-9(-B(aXYyeU4wmT*PS&93O|I z$=owM(D9>Zche&6+_`LTn+aF>|K`0}`)N2$e;6kH@-n?OMY(0Fk3ZGJyu`0K`(X}h zBSDB!3$MTa`VH7jgUDjWEukXt0{^CBkkl!0{xa>&V;`d>@R7ak{CneyeW*8LX5Jmi zP74Za{8OkEsCQt)U{bG185DE0a%hy7N;F{)w^}aWrwxZV>8cT4{=DXiN1s^A$&{8b zB9)hd-%n3fCh7TU^7yOVip+kg7yotltU60>|081dOhq5E`-&SeG(pv z#(imfRG#&Y-VwI>9d3EnWqP~QXNlUQwsv1UnvCghuF|?)L8OdZ(s^Ds3vj0``j%AI z37Dx{lg&m`DKVYKl+9;!I2>k+-UAV8x3TIaO;XX{PZY@idaHt-((s6nLyni*+!p|w zt23()IGlc$5v<)!_d&6|SX+k%<{7Ga#;S-3cActbu;O7w;uSC)6D}C6eCM6%iCqrJ zLwTMYnKli%_i)tUK>%i(cg9tC#UvQ-5nV`@skL4rH>L zvhz>sP1#L;Q`1Ek=}jhoRY%jw`TA!8W}dw9YZu-4>ih50l$j{Ts!x8U_$G_VGJ4YW zL{%aR6X3^Xn%VF2J=bf9+r#KQZz}%hr3$!#Sbd)K^G5ETDfj*vt=#;#o72(^6F?vU zm{iN|MivdG%ZoYOKM!@j(LXeXOgG}R>O=3#!}IepMSswGh{6hFwCGfl#x86_Yo z&@KTg$=iJ0sOFhvNA4k7noxoo*CYauNx~$*z&s}mh7x30F>T~I!1%03K$^0Cx)G43 z_-qm0)~J^8IR`7(3((p(-bM=|gMvZLxSTZlsIn8*XCZ>g-Hh+^HY_eyIi_$1viC+{!kktr&y3Gv1!$!CWYdvw zEQao+5|8T1D9|-k$cEXbNYAkBFlN@V^gdX_5R!v!%V6*&U^Ql!JT~X|@)~JPQ}(Pl z{m!!KK>yCNvw(h`{MyfWwe>$&o~z*Dg?+=ofaE0_>4b-uUy~?^07#ui4O7d;d@V*2 z3R%#KMk9H^YW;2nI-ooc@uSthsi*?;6DQRX7UAU<1Gl6Y!=59get^!~vFgz=ARp*r z*Rdbzoe%+W7=I}(;uG0OmoMi#w07S^Vcorv2Ohf5uxPa8@vhEDJS{xF1nBWp`sb3z z^w;@VP6AR8C?|oXAh2OPADAg?J1N3pjY8XVC39g>(>fv+ z;SI)TD@I|m<6??#;xpQlSq_&_l>$Pcl#@6L%kdE}hu zL!_{0;q|NLE5rqrSn6>KrFuLC(v|YVJf(rntF`A1sj`K;1v|}bHhGC-!`OId z6yK3JHgJyMj?S1ZU2;agL{kLs@57li_1Sn=B2RNl$e4roNo01I)Z}+Ex3Lzh3&qHY zX)i*UZ?bL@v%TL&Z^H#Z_OA`c$`7B%sr2oDYf7$Jn(fWi`(ayX+n5i_dI5x_6%- zT`FJ7LEJ+LU9sWtX>M91mr;q+DW9Ammnon8lLOcC2~~Mxy5mdku#m$pl1CH(WB-p5 zvxOQcb76>#4{wxV3YZau2T3Ppa)4zXsM#l3j`)#VvcF_VtJunBnT)6gXw(RRDK(y9yu%X5fyQm3e)2x ze+jsV)xs!3$WBSJ0t5m}mt_MHGJ-{LOwrj#_~6t3g1bsH;p#H5ZF<>u#ej2sOoexV zfiwAcF=aqNyA*Q(t)Q!XA}8TSoRSx`r%D-}nRHf^ZkE5nudiQIQ(`Px)4Qu)jC#WH zm{Gbz6ARmcn{R$&vV>$dV?og*pk#EXw=tEA6mwNc;2~TTTP=99ESw~j%}_<$)O;SO z`Vf9~UL%Uh0jb&_qo9>ADOGx%Wyl(YWHG%$-1t{g4?i3k$_*QzqlGWYvL5f&-2cGJ z`(5H4u16O>XZ$H!1d{}$-L6?xzGnm`_~**7N&^GIN{Q)}*PY zbaWacnFR_{Hta|#;?l`DSh|^9E!!PULaboGd*AuH5MBiTRdTgIDLsb#Oj|L(d7;GEtaq_?aJ!Ci&foSmPB3*^EZT+B;Z5O~f z(&HR#A2cYDNhR00cW>v)&Z@mtD_7!=&Xp?3m&+wbM^m{;+o&}+7k{L3nbS#zL;4um zy*Z=Iub?SKx=!fRJ%h$BW|8ShKmO5ag6I8rsp ztX!^cO|~~fV^;hQq%#80#u2yna20hHvt3Br>Y7Z(g0hnJYetI?uN(Pz&!cb&WUt~xdZFB>+gk-7P zFO+#q)uqObrj9<35b-0`4j`?|1&YU_zOo^=HW0Ab1Xm+??TnHS?n4GSpG^9^38BVh zEm?2akSb|6@m{;lhdgjOS5j45w9c?#=5bALy1P5cZ))D!v@@bTgL2-qFkAN_FI0U%58GifYBwp{zl1IlZH zzOirM4TC&7J^9Q+T3HF_^#M(N!rTNIV3($%r@E6WUY$LZop5QIKtI%Cctz7w*-?py zk9#F6s4Spl1(lT*Z3bXe{di3LSZ-iToV%Knj%wyU`rI2^(?h*I!f!rQN#A^g+pxYW zZ-wES_wEGG+R5MTs~Xub$Y1_P<;}kQ3YG(#fAT`G>N|JpDoe}-`RuDZw>Ne6^Uokc zvg7?lqKZkeOY?^i=RfyGU%a=uOMqs}^_tn@F!$0+Fbl-f^dDE~2G@7u>}9TUn+bHE z;vVun_TE;Ut;6uR@ zhBc7_k)|j>oyRkvPLRnmUf5w0xS$pvoKl>NfI1G8U`Rm89bISga0(^_9;|O4uo7l4 z2iWo=bR!egzk_^SRt>^4fNi6s3#vTK+dHG7wqGn$<&_Q9kY9ABYFj4Ib6=1H7&kzD zKjt>0Sqxo7B-cl6M3fyQpqNlv3giK>N-#M5=2F(GLG4C@7?ymTG@urr5uZ^}*@oPJ z$`@E&9nFob8-yDEPO^>;=9!R+hg>XY%^oP>6TC(7RZzmmQqDhHx$<)*eA>!9n3RAL zK7C_x*Iu=BXErwkXh5d9_QIFWXLoK1m5OgpkGcA(GD=RAdvoO;a%6O#ggDGGsI^YeS8r z+E`r^Pc;Z%xXJFcy70Y_Eo2W>MFahv(M|EK$>FGW%ZIVfuBJ|>-p}~ckCYrA+VL|- zrw_4cFrshW5lN-PNsoSuIpvO+-8OyQTB=35y!vYA?b3a!gU(Uwkgd}dD5-LSClo5J z7N^J~F7{EpRPNt(tL8jJkW75pg(RMBc2^WH>W~ z*6w9kQD8Sgo|Z<%`-ihl91Le8go=DNE6#=+JjW`~cc`nLUobqcVX1x1 zYU?uX&9yJz{WIfp`*$2{7h>xEQh9;W_ix@@szA_4^ zjh*+aDNnASjZI-q2qNQLmkZFWGl_+Ka33gM>N<1YzGKes*_(c>5$efedQRYpjlL0{wPDu7C zJI7t(Qhejo&6%VzgB;Vvz!!Ab**AzALQW~uH_pu=@{Vol7s)-M%aVgDc~5i&v&*9B zNdENUmvl&(k2^zjkqPk(24cbo!<=S!j}yw{L7Z6f3vlX`E=$T&R0S_o2c*ln6p_`c z!oobEXy%Y+q;Tu1Jp8h1YvBk#G_y#JUsmPempxmzjtE0%iZu9T&j^0mQ@AycFFG@% z#xHyDHGDsdgZPdTL9m9=BDM77G-^O&!aB&42t`GjcM#RfG-WqlF2@~1q}ii%tx0Bl zLFsH8fW3KmPFwWwhnMRV#f%{DwTA1`yZRp$(xg4w?L zmYd&KBnJ`5;*n@SovsjXeEO+8)_N&xiu^RRH{Z-A0S&3#3P^i1r&rJR#+b7c7=Q#< zuHu$c@1Hs^l-5q4E{Dwe@iR$T)8tcWFPN9uTe)!Bo8~186u)&&rL;z9-^VR{o{~hl z@adn**R2<`rYT|iDck~Z6x3**z~SO??Wdp0SE{GIDI8}fxB?f)orq9Fd$^n*k7<{lYTYj z%E@O~H1q`@>rCO%T;hktvve%5S7YRowd&zr{ zLJ3ta=bT0ais@0wQvBp%qWhbTfHWPF5=el5*>Y`srluiej#>Dv4_X{HyUn3DH+MKXjhnig+gpX2*6xN)##pQ|7!_XJ zvw#1fq1zOx<)vk_xth@I@EL4SYlyBx@J zAT%-~E1#7HSEGKPH*V@`!5Fv}&J4LOk#hz@3Nc9FB z&t<{!zy}p|25c_pO_1fXILs~_Z$3~H^VKKM-g#DfRI47 zRCC~U)qh?o-UiM<1i=#Qw078?kIIaWNdC&nFF<7&bTT`Gz`(0X9wrssk)O(z>pb0nRj|hbG!09S z;f61#+r|Hvd+??M*{Yb`XGXuH(;RdrD+Wrp*Y-G4 zwE??zmZa7-SW{JPM!1!2sm^9pX{U?J>`k=+hlMRsr^I3|r(3#m>8!1RCr=lPPrk1D z{vj^!ITeN*Pa4>5y4ZD^(wz_|uVNy}H(DsPxI+)B zottf&%sso!L#177wbE6mx%Q3CC5iRgThxic=Kl6gTIm+lbv@0#<`zRsusPh!zoqsy z_#5gC^^Fa|2L2v(TORkaI#!z8n66lzF4|nVL;EtY>kq2Cb_{LaoiVo zv#FuqKB#?PZC~F|QnOKeNc;|WrP|l*r$aY32b=jnsDllSbZCEruYr$9mvQNyB$Q3G zFRLrp7L~3xYj+_G4K_E@H~hFJTuc~Ofp7R4JajR*ZMeBpi60S}_5#c)!p~gK)Cu6ddK*gdF@)aS58Wa2i= z^vX;+55pYw5Gg9Lt)81Lw~Ef9%O8+0Up~tDmk%%lJIi!116;rgpBXn$hfo&+M(+Gm zD&AA2yzX3ZhivF3oqLW9>lugr0P9WXrb%glBXNO;_{EMx&C_zPiHsB?v^y<=6Hj6) zOlm?wRwU$=kI);+BZaGWepa2@YH4Nw3zI1{fiIAfn6uD~#P9=0)u`Gpmf7|gvtA-D zE*2lxp=1w$2l-`4=F8X`nfSXK^Vs3C2Aokz_wNAvSOfUY9;i#%#UDyjbS;*yKx>ou z196Hjl?b$26ClFG9@2Mzs7pio1lzH>%^KHB__4mp6sSl4+z+HF@*(sher&OH2U?n# zPdghl(%*CcsXC`tQtooksrKZZ z%b&XN6dlb28y;Gwy7=vA>3-zf(f{nFSLdG}3(>>He7=(w0jj}kDCFl)lD~qNi2=Q= zym$><+`Fitgl!+o1t-uQv)cT}N~QW*xEpsWUN0sRWr9!21w`r@K)IOBBP(Rdlhm7# z_|02y{U!lN4-_DVG`bIc$mUvaxWOuYT`SXEh*wP;8&gx>TzF_28XC~f8h?7*wx{FT zWH6pc8qyZ}(u1}oOKcBnE#4F;0G`BU%95A%>+Zh$I)~Qgx7%z6bIO)($QVLoe64<)OS3<$rlEC0iVOWy+9WWMPmsAO4m@{YP5m61XqY2 z46n&m>8QpRTQ(;LLPPqQeoj7%9b66`VS7Ocv}UaBNJ;tH`h1TbEw*;6!Gg?lkUtpN+p%Lndw6m2x~i4#rFx4$L0lpjS8%%y zx9sTc$M+hltR_!chGzs|?M5Ng5^YH|f#NY?M^u=p;gIE-4lJHxD;Q=$Ho?Rc3byen z>=OWq7`%#XB#Vqtc!v@G^i4=og`sTRR+Vh9@pnIwbyB)?t_o^?PKTimIv)xCqvyr1 z&1O!>m#&92eWxOUN#1e>ktIU_flR{L6^?WUR}kl5o9ttfD@pFDkZpx{3+y^*6R30v zxQ|O1`viyk{CD4$E`n9?Kd~Yz?J=$`e%F-D@KIFBqDbj7Ca9hfPiG2i;vCz5c_2t& z~7tnY(LB=2kPeF3iN5OfZ*>3cRet{J@mV4P39=b%zCiB_IL1%=nXyP+$ z(_c8YG*@zRD~U^0)`_9Qrgls5M`$OI(~-!uK#WHEJL&6kLQ*d+5U>5}_xHCN57I2o zi~;oW#_xO9EV-ae4PIcOp!I>?bSok)@Lw==_jI-Q!e?x9gCF1%*+rltFOCc(ksuxJ`m4rD9Dp#_-ih?ZO3jb0 zciwsx$e`hijj1!rIrx#7#QycpU;H>FR7#J5B2N82l?Y#(cMH^x$WcW)_0Jsj3M@RZ zj8*=qcL0eh_~n#bP^=u3M3t8Wjg-?a{sELv&7`n_n3wDkDQw8u6$=l<YnuTdw*g0@SYPl zFu&kSjlA?H*aPC(WcuvDlk9kEfy+zO&Vx)>Sc-sH9i`e59Q;)Be* zl?G=r-54tTj|WI+re(Zzat0Am05iuL(KCrbn$5IH_T|6yi-@7-un7Z;LRbO96O!}5 zfO{wtVlI&>ri3Pg8pe&?UIGcMlH5;bFSb+~+hf*bSHw9+e)Q zC@!Gx#dsIs4d2M^SWARP=(=TL1#9FISHcNoEPjx7AQnGVrU!tD2mJ17_l~uZC$-K- z#zzqaP(yIOuwwrsF)aXK@HZV%SIo~pcPr=WtLiL^Y3)HfR03RKSIozw*&0pKS{Hx> zAWDwL6Fy=28jgX)Wf(bsgW6(ogT!)UvFd)M3Y$SYvnnj2)*RW}$W~`Tf?{#*=#dP+ z=i=Pl#mupR(r{^ed7quXpK=k*xsumd#kn!PuI@{;_ii?zo1P+I8R3ezVul860o+J- z-k4fEW96!1DtpKkPZ+3O6%m*Gnv-&(%Wn7bRW4PH%L;7?i5PHXSzHF~6mvBl*4COR zf7S1~Zj-57oh|WPS4_1j(bn5^A5=BR16>XA zSqF6u@fvHL0bCO8wq`I}RES7c&r-kqGG6{b%oFy6o#-9X-f_nrKLvV^eg>tcGwgZ! zK^=BuX=Q>mlVc*|50D^B$PFHq)ZC`DgCJ>!N1%)sW#E94$I`}JhW`M!07aHUo-|{U zcXpRdX^pB5Yc70;`E9%;m^j;+q^7^CdvgeUP!!4V(q-z}dK9uvn$6xWC;>sZy@F0H z!`V?}IuEabA``_WCgl1ka3Mx2*7Eg^rZd%spm+=GF$JiHtpP%@rp2DBtuoxDWF8Y- zYoa+F=hLwmQmxP8V3{;3@|=q_^|6MulSd^QVw`pMdb5)^JDteFE~00TI+9XjeOA-r zY&OSuR27Y*4|Sh| z*U1^C=Y{|cF$o)F25Xm_Pcr5hbAaV#2N8>+n?}iCT&&3vP~I8i@x5SbRA-zV-Y?r} zD$|koFlL(MX?0Jux^>G=1;R&aNXzaXW0~%pb2GWN2AUivLAl#d>kK&}Zb7_BdQf%w zOqPOGJfI2NHuE#Jc$4@bI+8;1m_Zs@#O05m)|sED0@sUt$8dHIE3OcEqEgKfXA)1 zn;R+{3>2(`tA+?(GM_^B=i;B*(}FwVpzqq`@3z!~6(*j|rQMbU%4BABgT-!k;}!d+ z$`6$Xun%25K_8P66IS*cT?wX%CgO)K5uYc1Qzf+{sX}o<1}#G14=_4TBXD{KSHh@` zrH-@Q=3l51p?EZ51Vch1?y`r3&9|x)OSo42J!g7L)#0v9R~w~+>grl^wOcUVW6SF= z1AC5iheB4FNkiYE>FxDz_OxlGM_;6%?@rKhTI;Hf4VI=>;inoY>lUuTZ?&0?Rrb!7 zkkEZ%Bw)Tp^-JkrxFQqD2`hDkT!w3c>q=IHAL=Taz&VX#FTk8~AvWxB@hR~-uFhhq ztFu7Mly5qz8o5LL322@fz4}lQc0z$-l*td%*Z88YA^8_TrGPnRD5^K$Yn!dfX1`#5 ze@|o2J*xVUy{d}UH}4XwxZ^KM=;Kh?k~LnJxxOf}X4A%B|3oMLV3^xlzc00I>z+OF zV?~4O2On{)T3t|JUC~k~Es_=i-b8p21>hm#2%=8Xo#(Y_>A2utq!~2@E6&C$Q&~vZ zC4H=_47S&G8(~Dd+grPW!Y=V+RbQy8d!td>qelAtPr%ys@7Oy0Oj}n*oxHtr*As$|Bw+%Z5 zU$X-aFGwEW6^&fk`X}0V8@6iNswoe-r0|BjvJ4MyTXb*(U*GNQk8BBdH+8nQzVJL& zGeU{I@QALpzNso~iWECb>rH$526o2}q~F}|>JvMi5u4xTq%thA(Et7yZ++Ax1e+~Q z4LW6sODm^oZExh4yWw3x&Ezsg58FU7;!IEZgkmhrFBn1`Xm$O^YjS zhsM{f3jxJpZVy^#Vd1dk&I0@o4Uq7I7+@YwkdizCClQ3CKCjMtIUpfj>i48s#gBu! zpljWbQM6E{L6NDnsC!3M;tlH7=6G{Vcvs}%QP|q+jM5wGh5=AW5`Evgi7;1u8_ocg zo2W!h-Y~5WEOYf$nmY49@G7Se71eHJ4&^LjMn4>hL<&1WX~yc4$R#qc2*S#Ym+?0u zURovI#BDCw7uek2-ye9or2onv>7X@rY$7YlybqQYWpp`k6lFJWdNvk4JcP-t^4W;{ zKey>Lyp)B@D9a*eK^87szK|^(39*A)vtwyrPEuPIDX-jUko20hE!ACK{#oaN%H4%A zSK1$o?%sN^``MVbGt}DIX%Mf{Y%1?6bMra2rDbc@YEMpKAlu@8gu#kRB&~AOkpa$| zAnV9e%7mvRJu{LLyv_G$>Qzo|%#h(5w3N6h@FM77#iXY_{t2x6=r$5ZggqAm+eH zXN{3F6i;%7!eJ%mNIA$kgL%W)lnTdVK+Fmh-2_H1EFn+D~o5nB$TC4;T};w)@V!fkgU*7{|FQ;VM|wEhDN(v$M4U zw4?P7S3~6jom8nVt}H1@2*Z+k4KOvO_O9YBMsdG-OXrr&b|GKX?1G+Uf1;|t*myIN z@u2M`7lgr{Mu>V1?=;?P0Ytv^HzlKaZ~%`drD;U2f%w^?n9AH}_SkJ}*61D{TzYW* zi@S%>k{3?<)2?TA-QJp3lkq8ac~w<8Kw?t1_#^3u;saJ~udlPS*RWI5UDs|3j;A5B zAF0THQm8k3O^F(<_!DU;QQsLzw16{J97?o%yX({XT~9vJlWokpe4N3l934j_c*mc` z@LZsLN0&JVA=-b*J71cdV{SyOEY;d@3v0y^q{KAvN&S;3d z{9}=xXER>@qGN7V(jT%oT~DsjNu}c5T)op)S7-Q;E0v>G}+9l+NV~PKN(*WEgEtTI}UH%x35WS+2ri&H4yi2 z3-9<`{%E2Pm&Ie$+{bA)6m2xsTeKz5adGt8#ADibU*C1?kl|+!AD%bw;m2=ZvTNbH z_wzpS39j`(>#_rZHS3#~t{GUfq50sNLDc~kAKVB;>EaAAC1D(C@N?u2xpiBLSx*B6 z4C6m{(L&xNoX{k^30I6?`lSa zUiKS&73 z(b^`=L#1ny5f^Yj#Dpwe74W$<&&S<7|J765QbV16f|xJm ztE$S1tOc|@zHn}y0ysaiSmy=xlhttjN7A>4SMYIbYapI#ZtdFKH2}#TLX`tPzO4LpYF7uGXJA&i62GvEoY-J$a}q8Hw9WFn?q3aP$`CxF0GMl53P zUf3Ns^|K7K+S0tj}dpLA%;LDN4f)AWB0H2wd^X!>`; zKYDx;3wt8^g#8qa}? zPpDU~qV^ROP34NI_TBfChLkz)ktmk~IP=w#3x$YDWy2W?#Dg)aA%n_3Qwo4Hlzh)u z3G5^n(FKT8cc}XlrUoHLQ*Sqw@PBxht0>#Br(zlPR>#5dh!S+1KUlG2FXt6Q0ksl;DSe+q<9ddh2oXT0awN{ka1$Ln9tj7c9=T5?fw=ju zhjQ+<3TB3ulQDJABt~}~F&-Jse(^I5xU3raknME-B znf4ST7XE$W)Vu#`{MS=+W+Vh_W5SWbcT#CTI2tWi{!aQ9i<-X5AkRWEkES!@kSHRo zcm@9ed&w!30#%HER0WnUB{Lv&sT)VZrv)P*qflO9Q3wd-X#t&q@CDEJ{6Whs*$Thj z)j7EEnZH;5m_tO7DOL*=Og`m=PLEuCUGl z%M|HA6SU{*+3$f>gZ`g=_0_XyU(F`L+p+-|$4;?CgPal*q)Vr;Vm`{BRE=UEDWUQB zN3tU>f5e<~LHhdr+^%6DEdw!kIDePU6+@-kFq_?Vl&^W6bE%(Lx#N&qc;5B&!RH_e zK8^xeq`qbTJ+o56y@{8XylITG2;y&)c>p!Vy&O6ZArl<%L~c5zv!tES(F=F9w6=#k zqn)lMv$nOawN;f!rlT#)Gpu)+Z8kP$QxS$u0)-JCf*}8(z`E@_a;6i)chdOBcAKs7S*+WDGIGjk7mN*&D*a6C;%Qg)U?4D_q6c8LkUg zk%6M01 zhvd|xyC=&-I;?m@*_|sbi`l>iu!TqIR~Wx$%s@IGT{llJFiI^-C2cO2N~Urp-pIH- z7+bM)qefO!pEmg;CN4`K5%up7tX=kzvBm10H~{I&0vM(l*GJCmh3k9^!d9`ic(QF% zq;fZ7pO*jLY;23!TwtGewV?i&a3vfeK33^YuFvyUZY*=j;WvSWpBV^3pO*?nB z@=2vb(IThgz)E%cvc(a~AbC1j0$EjDmU_I`3gU%XYE#q$1@mYN#7aAdIx!r4Jq zO6W3oOe!Vxpp@XfcqzeRb-V1sYxk?#6Yc3PBWT`(T(UFP73!7?7$$F}v�`Jl%i( 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⪼z6bT&M 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 literal 0 HcmV?d00001 diff --git a/multimedia/.config/mpv/fonts/uosc_textures.ttf b/multimedia/.config/mpv/fonts/uosc_textures.ttf new file mode 100644 index 0000000000000000000000000000000000000000..e89f1d8cb87262b02db089894e92a52e71f8bf8c GIT binary patch literal 38228 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 x7J+&k#Mt Playlist # script-binding uosc/chapters #! Utils > Chapters # script-binding uosc/open-config-directory #! Utils > Open config directory + diff --git a/multimedia/.config/mpv/scripts/battery.lua b/multimedia/.config/mpv/scripts/battery.lua index f14be30..0477821 100644 --- a/multimedia/.config/mpv/scripts/battery.lua +++ b/multimedia/.config/mpv/scripts/battery.lua @@ -1,5 +1,8 @@ -- If the laptop is on battery, the profile 'lq' will be loaded; otherwise 'hq' is used --- +local mp = require 'mp' + +local SHOULD_ADJUST = false + local lqprofile = "lowquality" local hqprofile = "highquality" @@ -12,15 +15,19 @@ local function powerstate() end local function adjust() + if not SHOULD_ADJUST then return end + local state = powerstate() -- this actually overrides automatically applied profiles -- like 'protocol.http' if state == 0 then - mp.msg.info("Running on battery, setting low-quality options.") mp.set_property("profile", lqprofile) + mp.msg.info("[quality] running battery, setting low-quality options.") + mp.osd_message("[quality] LQ") else - mp.msg.info("Not running on battery, setting high-quality options.") mp.set_property("profile", hqprofile) + mp.msg.info("[quality] running ac, setting high-quality options.") + mp.osd_message("[quality] HQ") end end mp.add_hook("on_load", 1, adjust) diff --git a/multimedia/.config/mpv/scripts/copy_videotime.lua b/multimedia/.config/mpv/scripts/copy_videotime.lua new file mode 100644 index 0000000..9331a56 --- /dev/null +++ b/multimedia/.config/mpv/scripts/copy_videotime.lua @@ -0,0 +1,80 @@ +local mp = require 'mp' +require 'mp.msg' + +-- Copy the current time of the video to clipboard. + +WINDOWS = 2 +UNIX = 3 +KEY_BIND = "y" + +local function platform_type() + local utils = require 'mp.utils' + local workdir = utils.to_string(mp.get_property_native("working-directory")) + if string.find(workdir, "\\") then + return WINDOWS + else + return UNIX + end +end + +local function command_exists(cmd) + local pipe = io.popen("type " .. cmd .. " > /dev/null 2> /dev/null; printf \"$?\"", "r") + if not pipe then return end + local exists = pipe:read() == "0" + pipe:close() + return exists +end + +local function get_clipboard_cmd() + if command_exists("xclip") then + return "xclip -silent -in -selection clipboard" + elseif command_exists("wl-copy") then + return "wl-copy" + elseif command_exists("pbcopy") then + return "pbcopy" + else + mp.msg.error("No supported clipboard command found") + return false + end +end + +local function divmod(a, b) + return a / b, a % b +end + +local function set_clipboard(text) + if platform == WINDOWS then + mp.commandv("run", "powershell", "set-clipboard", text) + return true + elseif (platform == UNIX and clipboard_cmd) then + local pipe = io.popen(clipboard_cmd, "w") + if not pipe then return end + pipe:write(text) + pipe:close() + return true + else + mp.msg.error("Set_clipboard error") + return false + end +end + +local function copyTime() + local time_pos = mp.get_property_number("time-pos") + local minutes, remainder = divmod(time_pos, 60) + local hours, minutes = divmod(minutes, 60) + local seconds = math.floor(remainder) + local milliseconds = math.floor((remainder - seconds) * 1000) + local time = string.format("%02d:%02d:%02d.%03d", hours, minutes, seconds, milliseconds) + if set_clipboard(time) then + mp.osd_message(string.format("[copytime] %s", time)) + else + mp.osd_message("[copytime] failed") + end +end + + +platform = platform_type() +if platform == UNIX then + clipboard_cmd = get_clipboard_cmd() +end +mp.add_key_binding(KEY_BIND, "copyTime", copyTime) diff --git a/multimedia/.config/mpv/scripts/gallery-dl.lua b/multimedia/.config/mpv/scripts/gallery-dl.lua index d49c561..3cc21db 100644 --- a/multimedia/.config/mpv/scripts/gallery-dl.lua +++ b/multimedia/.config/mpv/scripts/gallery-dl.lua @@ -7,23 +7,24 @@ -- e.g. -- `mpv gallery-dl://https://imgur.com/....` +local mp = require 'mp' local utils = require 'mp.utils' local msg = require 'mp.msg' local function exec(args) - local ret = utils.subprocess({args = args}) + local ret = utils.subprocess({ args = args }) return ret.status, ret.stdout, ret end mp.add_hook("on_load", 15, function() - local url = mp.get_property("stream-open-filename", "") - if (url:find("gdl://") ~= 1) then - msg.debug("not a gdl:// url: " .. url) + local fn = mp.get_property("stream-open-filename", "") + if (fn:find("gdl://") ~= 1) then + msg.debug("not a gdl:// url: " .. fn) return end - local url = string.gsub(url,"gdl://","") + local url = string.gsub(url, "gdl://", "") - local es, urls, result = exec({"gallery-dl", "-g", url}) + local es, urls, result = exec({ "gallery-dl", "-g", url }) if (es < 0) or (urls == nil) or (urls == "") then msg.error("failed to get album list.") end diff --git a/multimedia/.config/mpv/scripts/sponsorblock_minimal.lua b/multimedia/.config/mpv/scripts/sponsorblock_minimal.lua index 1940bc8..907a191 100644 --- a/multimedia/.config/mpv/scripts/sponsorblock_minimal.lua +++ b/multimedia/.config/mpv/scripts/sponsorblock_minimal.lua @@ -5,6 +5,8 @@ -- -- original from https://codeberg.org/jouni/mpv_sponsorblock_minimal -- adapted for local playback skipping and some refactoring by me +local mp = require 'mp' + local options = { API = "https://sponsor.ajay.app/api/skipSegments", @@ -31,10 +33,9 @@ local function getranges() Ranges[k] = v end end - return end -local function skip_ads(name, pos) +local function skip_ads(_, pos) if pos ~= nil then for k, v in pairs(Ranges) do if tonumber(k) <= pos and tonumber(v) > pos then @@ -51,7 +52,6 @@ local function skip_ads(name, pos) end end end - return end local function file_loaded() diff --git a/multimedia/.config/mpv/scripts/thumbfast.lua b/multimedia/.config/mpv/scripts/thumbfast.lua new file mode 100644 index 0000000..e01ef4f --- /dev/null +++ b/multimedia/.config/mpv/scripts/thumbfast.lua @@ -0,0 +1,975 @@ +-- thumbfast.lua +-- +-- High-performance on-the-fly thumbnailer +-- +-- Built for easy integration in third-party UIs. + +local options = { + -- Socket path (leave empty for auto) + socket = "", + + -- Thumbnail path (leave empty for auto) + thumbnail = "", + + -- Maximum thumbnail size in pixels (scaled down to fit) + -- Values are scaled when hidpi is enabled + max_height = 200, + max_width = 200, + + -- Apply tone-mapping, no to disable + tone_mapping = "auto", + + -- Overlay id + overlay_id = 42, + + -- Spawn thumbnailer on file load for faster initial thumbnails + spawn_first = false, + + -- Close thumbnailer process after an inactivity period in seconds, 0 to disable + quit_after_inactivity = 0, + + -- Enable on network playback + network = false, + + -- Enable on audio playback + audio = false, + + -- Enable hardware decoding + hwdec = false, + + -- Windows only: use native Windows API to write to pipe (requires LuaJIT) + direct_io = false, + + -- Custom path to the mpv executable + mpv_path = "mpv" +} + +local mp = require "mp" +mp.utils = require "mp.utils" +mp.options = require "mp.options" +mp.options.read_options(options, "thumbfast") + +local properties = {} +local pre_0_30_0 = mp.command_native_async == nil +local pre_0_33_0 = true + +function subprocess(args, async, callback) + callback = callback or function() + end + + if not pre_0_30_0 then + if async then + return mp.command_native_async({ name = "subprocess", playback_only = true, args = args }, callback) + else + return mp.command_native({ name = "subprocess", playback_only = false, capture_stdout = true, args = args }) + end + else + if async then + return mp.utils.subprocess_detached({ args = args }, callback) + else + return mp.utils.subprocess({ args = args }) + end + end +end + +local winapi = {} +if options.direct_io then + local ffi_loaded, ffi = pcall(require, "ffi") + if ffi_loaded then + winapi = { + ffi = ffi, + C = ffi.C, + bit = require("bit"), + socket_wc = "", + + -- WinAPI constants + CP_UTF8 = 65001, + GENERIC_WRITE = 0x40000000, + OPEN_EXISTING = 3, + FILE_FLAG_WRITE_THROUGH = 0x80000000, + FILE_FLAG_NO_BUFFERING = 0x20000000, + PIPE_NOWAIT = ffi.new("unsigned long[1]", 0x00000001), + + INVALID_HANDLE_VALUE = ffi.cast("void*", -1), + + -- don't care about how many bytes WriteFile wrote, so allocate something to store the result once + _lpNumberOfBytesWritten = ffi.new("unsigned long[1]"), + } + -- cache flags used in run() to avoid bor() call + winapi._createfile_pipe_flags = winapi.bit.bor(winapi.FILE_FLAG_WRITE_THROUGH, winapi.FILE_FLAG_NO_BUFFERING) + + ffi.cdef [[ + void* __stdcall CreateFileW(const wchar_t *lpFileName, unsigned long dwDesiredAccess, unsigned long dwShareMode, void *lpSecurityAttributes, unsigned long dwCreationDisposition, unsigned long dwFlagsAndAttributes, void *hTemplateFile); + bool __stdcall WriteFile(void *hFile, const void *lpBuffer, unsigned long nNumberOfBytesToWrite, unsigned long *lpNumberOfBytesWritten, void *lpOverlapped); + bool __stdcall CloseHandle(void *hObject); + bool __stdcall SetNamedPipeHandleState(void *hNamedPipe, unsigned long *lpMode, unsigned long *lpMaxCollectionCount, unsigned long *lpCollectDataTimeout); + int __stdcall MultiByteToWideChar(unsigned int CodePage, unsigned long dwFlags, const char *lpMultiByteStr, int cbMultiByte, wchar_t *lpWideCharStr, int cchWideChar); + ]] + + winapi.MultiByteToWideChar = function(MultiByteStr) + if MultiByteStr then + local utf16_len = winapi.C.MultiByteToWideChar(winapi.CP_UTF8, 0, MultiByteStr, -1, nil, 0) + if utf16_len > 0 then + local utf16_str = winapi.ffi.new("wchar_t[?]", utf16_len) + if winapi.C.MultiByteToWideChar(winapi.CP_UTF8, 0, MultiByteStr, -1, utf16_str, utf16_len) > 0 then + return utf16_str + end + end + end + return "" + end + else + options.direct_io = false + end +end + +local file = nil +local file_bytes = 0 +local spawned = false +local disabled = false +local force_disabled = false +local spawn_waiting = false +local spawn_working = false +local script_written = false + +local dirty = false + +local x = nil +local y = nil +local last_x = x +local last_y = y + +local last_seek_time = nil + +local effective_w = options.max_width +local effective_h = options.max_height +local real_w = nil +local real_h = nil +local last_real_w = nil +local last_real_h = nil + +local script_name = nil + +local show_thumbnail = false + +local filters_reset = { ["lavfi-crop"] = true, ["crop"] = true } +local filters_runtime = { ["hflip"] = true, ["vflip"] = true } +local filters_all = { ["hflip"] = true, ["vflip"] = true, ["lavfi-crop"] = true, ["crop"] = true } + +local tone_mappings = { + ["none"] = true, + ["clip"] = true, + ["linear"] = true, + ["gamma"] = true, + ["reinhard"] = true, + ["hable"] = true, + ["mobius"] = true +} +local last_tone_mapping = nil + +local last_vf_reset = "" +local last_vf_runtime = "" + +local last_rotate = 0 + +local par = "" +local last_par = "" + +local last_has_vid = 0 +local has_vid = 0 + +local file_timer = nil +local file_check_period = 1 / 60 + +local allow_fast_seek = true + +local client_script = [=[ +#!/usr/bin/env bash +MPV_IPC_FD=0; MPV_IPC_PATH="%s" +trap "kill 0" EXIT +while [[ $# -ne 0 ]]; do case $1 in --mpv-ipc-fd=*) MPV_IPC_FD=${1/--mpv-ipc-fd=/} ;; esac; shift; done +if echo "print-text thumbfast" >&"$MPV_IPC_FD"; then echo -n > "$MPV_IPC_PATH"; tail -f "$MPV_IPC_PATH" >&"$MPV_IPC_FD" & while read -r -u "$MPV_IPC_FD" 2>/dev/null; do :; done; fi +]=] + +local function get_os() + local raw_os_name = "" + + if jit and jit.os and jit.arch then + raw_os_name = jit.os + else + if package.config:sub(1, 1) == "\\" then + -- Windows + local env_OS = os.getenv("OS") + if env_OS then + raw_os_name = env_OS + end + else + raw_os_name = subprocess({ "uname", "-s" }).stdout + end + end + + raw_os_name = (raw_os_name):lower() + + local os_patterns = { + ["windows"] = "windows", + ["linux"] = "linux", + + ["osx"] = "darwin", + ["mac"] = "darwin", + ["darwin"] = "darwin", + + ["^mingw"] = "windows", + ["^cygwin"] = "windows", + + ["bsd$"] = "darwin", + ["sunos"] = "darwin" + } + + -- Default to linux + local str_os_name = "linux" + + for pattern, name in pairs(os_patterns) do + if raw_os_name:match(pattern) then + str_os_name = name + break + end + end + + return str_os_name +end + +local os_name = mp.get_property("platform") or get_os() + +local path_separator = os_name == "windows" and "\\" or "/" + +if options.socket == "" then + if os_name == "windows" then + options.socket = "thumbfast" + else + options.socket = "/tmp/thumbfast" + end +end + +if options.thumbnail == "" then + if os_name == "windows" then + options.thumbnail = os.getenv("TEMP") .. "\\thumbfast.out" + else + options.thumbnail = "/tmp/thumbfast.out" + end +end + +local unique = mp.utils.getpid() + +options.socket = options.socket .. unique +options.thumbnail = options.thumbnail .. unique + +if options.direct_io then + if os_name == "windows" then + winapi.socket_wc = winapi.MultiByteToWideChar("\\\\.\\pipe\\" .. options.socket) + end + + if winapi.socket_wc == "" then + options.direct_io = false + end +end + +local mpv_path = options.mpv_path + +if mpv_path == "mpv" and os_name == "darwin" and unique then + -- TODO: look into ~~osxbundle/ + mpv_path = string.gsub(subprocess({ "ps", "-o", "comm=", "-p", tostring(unique) }).stdout, "[\n\r]", "") + if mpv_path ~= "mpv" then + mpv_path = string.gsub(mpv_path, "/mpv%-bundle$", "/mpv") + local mpv_bin = mp.utils.file_info("/usr/local/mpv") + if mpv_bin and mpv_bin.is_file then + mpv_path = "/usr/local/mpv" + else + local mpv_app = mp.utils.file_info("/Applications/mpv.app/Contents/MacOS/mpv") + if mpv_app and mpv_app.is_file then + mp.msg.warn( + "symlink mpv to fix Dock icons: `sudo ln -s /Applications/mpv.app/Contents/MacOS/mpv /usr/local/mpv`") + else + mp.msg.warn( + "drag to your Applications folder and symlink mpv to fix Dock icons: `sudo ln -s /Applications/mpv.app/Contents/MacOS/mpv /usr/local/mpv`") + end + end + end +end + +local function vo_tone_mapping() + local passes = mp.get_property_native("vo-passes") + if passes and passes["fresh"] then + for _, v in pairs(passes["fresh"]) do + for k2, v2 in pairs(v) do + if k2 == "desc" and v2 then + local tone_mapping = string.match(v2, "([0-9a-z.-]+) tone map") + if tone_mapping then + return tone_mapping + end + end + end + end + end +end + +local function vf_string(filters, full) + local vf = "" + local vf_table = properties["vf"] + + if vf_table and #vf_table > 0 then + for i = #vf_table, 1, -1 do + if filters[vf_table[i].name] then + local args = "" + for key, value in pairs(vf_table[i].params) do + if args ~= "" then + args = args .. ":" + end + args = args .. key .. "=" .. value + end + vf = vf .. vf_table[i].name .. "=" .. args .. "," + end + end + end + + if (full and options.tone_mapping ~= "no") or options.tone_mapping == "auto" then + if properties["video-params"] and properties["video-params"]["primaries"] == "bt.2020" then + local tone_mapping = options.tone_mapping + if tone_mapping == "auto" then + tone_mapping = last_tone_mapping or properties["tone-mapping"] + if tone_mapping == "auto" and properties["current-vo"] == "gpu-next" then + tone_mapping = vo_tone_mapping() + end + end + if not tone_mappings[tone_mapping] then + tone_mapping = "hable" + end + last_tone_mapping = tone_mapping + vf = vf .. "zscale=transfer=linear,format=gbrpf32le,tonemap=" .. tone_mapping .. ",zscale=transfer=bt709," + end + end + + if full then + vf = vf .. + "scale=w=" .. + effective_w .. ":h=" .. effective_h .. par .. + ",pad=w=" .. effective_w .. ":h=" .. effective_h .. ":x=-1:y=-1,format=bgra" + end + + return vf +end + +local function calc_dimensions() + local width = properties["video-out-params"] and properties["video-out-params"]["dw"] + local height = properties["video-out-params"] and properties["video-out-params"]["dh"] + if not width or not height then return end + + local scale = properties["display-hidpi-scale"] or 1 + + if width / height > options.max_width / options.max_height then + effective_w = math.floor(options.max_width * scale + 0.5) + effective_h = math.floor(height / width * effective_w + 0.5) + else + effective_h = math.floor(options.max_height * scale + 0.5) + effective_w = math.floor(width / height * effective_h + 0.5) + end + + local v_par = properties["video-out-params"] and properties["video-out-params"]["par"] or 1 + if v_par == 1 then + par = ":force_original_aspect_ratio=decrease" + else + par = "" + end +end + +local info_timer = nil + +local function info(w, h) + local rotate = properties["video-params"] and properties["video-params"]["rotate"] + local image = properties["current-tracks"] and properties["current-tracks"]["video"] and + properties["current-tracks"]["video"]["image"] + local albumart = image and properties["current-tracks"]["video"]["albumart"] + + disabled = (w or 0) == 0 or (h or 0) == 0 or + has_vid == 0 or + (properties["demuxer-via-network"] and not options.network) or + (albumart and not options.audio) or + (image and not albumart) or + force_disabled + + if info_timer then + info_timer:kill() + info_timer = nil + elseif has_vid == 0 or (rotate == nil and not disabled) then + info_timer = mp.add_timeout(0.05, function() info(w, h) end) + end + + local json, _ = mp.utils.format_json({ + width = w, + height = h, + disabled = disabled, + available = true, + socket = options.socket, + thumbnail = options.thumbnail, + overlay_id = options.overlay_id + }) + if pre_0_30_0 then + mp.command_native({ "script-message", "thumbfast-info", json }) + else + mp.command_native_async({ "script-message", "thumbfast-info", json }, function() + end) + end +end + +local function remove_thumbnail_files() + if file then + file:close() + file = nil + file_bytes = 0 + end + os.remove(options.thumbnail) + os.remove(options.thumbnail .. ".bgra") +end + +local activity_timer + +local function spawn(time) + if disabled then return end + + local path = properties["path"] + if path == nil then return end + + if options.quit_after_inactivity > 0 then + if show_thumbnail or activity_timer:is_enabled() then + activity_timer:kill() + end + activity_timer:resume() + end + + local open_filename = properties["stream-open-filename"] + local ytdl = open_filename and properties["demuxer-via-network"] and path ~= open_filename + if ytdl then + path = open_filename + end + + remove_thumbnail_files() + + local vid = properties["vid"] + has_vid = vid or 0 + + local args = { + mpv_path, "--no-config", "--msg-level=all=no", "--idle", "--pause", "--keep-open=always", "--really-quiet", + "--no-terminal", + "--load-scripts=no", "--osc=no", "--ytdl=no", "--load-stats-overlay=no", "--load-osd-console=no", + "--load-auto-profiles=no", + "--edition=" .. (properties["edition"] or "auto"), "--vid=" .. (vid or "auto"), "--no-sub", "--no-audio", + "--start=" .. time, allow_fast_seek and "--hr-seek=no" or "--hr-seek=yes", + "--ytdl-format=worst", "--demuxer-readahead-secs=0", "--demuxer-max-bytes=128KiB", + "--vd-lavc-skiploopfilter=all", "--vd-lavc-software-fallback=1", "--vd-lavc-fast", "--vd-lavc-threads=2", + "--hwdec=" .. (options.hwdec and "auto" or "no"), + "--vf=" .. vf_string(filters_all, true), + "--sws-scaler=fast-bilinear", + "--video-rotate=" .. last_rotate, + "--ovc=rawvideo", "--of=image2", "--ofopts=update=1", "--o=" .. options.thumbnail + } + + if not pre_0_30_0 then + table.insert(args, "--sws-allow-zimg=no") + end + + if os_name == "darwin" and properties["macos-app-activation-policy"] then + table.insert(args, "--macos-app-activation-policy=accessory") + end + + if os_name == "windows" or pre_0_33_0 then + table.insert(args, "--input-ipc-server=" .. options.socket) + elseif not script_written then + local client_script_path = options.socket .. ".run" + local script = io.open(client_script_path, "w+") + if script == nil then + mp.msg.error("client script write failed") + return + else + script_written = true + script:write(string.format(client_script, options.socket)) + script:close() + subprocess({ "chmod", "+x", client_script_path }, true) + table.insert(args, "--scripts=" .. client_script_path) + end + else + local client_script_path = options.socket .. ".run" + table.insert(args, "--scripts=" .. client_script_path) + end + + table.insert(args, "--") + table.insert(args, path) + + spawned = true + spawn_waiting = true + + subprocess(args, true, + function(success, result) + if spawn_waiting and (success == false or (result.status ~= 0 and result.status ~= -2)) then + spawned = false + spawn_waiting = false + options.tone_mapping = "no" + mp.msg.error("mpv subprocess create failed") + if not spawn_working then -- notify users of required configuration + if options.mpv_path == "mpv" then + if properties["current-vo"] == "libmpv" then + if options.mpv_path == mpv_path then -- attempt to locate ImPlay + mpv_path = "ImPlay" + spawn(time) + else -- ImPlay not in path + if os_name ~= "darwin" then + force_disabled = true + info(real_w or effective_w, real_h or effective_h) + end + mp.commandv("show-text", "thumbfast: ERROR! cannot create mpv subprocess", 5000) + mp.commandv("script-message-to", "implay", "show-message", "thumbfast initial setup", + "Set mpv_path=PATH_TO_ImPlay in thumbfast config:\n" .. + string.gsub(mp.command_native({ "expand-path", "~~/script-opts/thumbfast.conf" }), + "[/\\]", path_separator) .. "\nand restart ImPlay") + end + else + mp.commandv("show-text", "thumbfast: ERROR! cannot create mpv subprocess", 5000) + if os_name == "windows" then + mp.commandv("script-message-to", "mpvnet", "show-text", + "thumbfast: ERROR! install standalone mpv, see README", 5000, 20) + mp.commandv("script-message", "mpv.net", "show-text", + "thumbfast: ERROR! install standalone mpv, see README", 5000, 20) + end + end + else + mp.commandv("show-text", "thumbfast: ERROR! cannot create mpv subprocess", 5000) + -- found ImPlay but not defined in config + mp.commandv("script-message-to", "implay", "show-message", "thumbfast", + "Set mpv_path=PATH_TO_ImPlay in thumbfast config:\n" .. + string.gsub(mp.command_native({ "expand-path", "~~/script-opts/thumbfast.conf" }), "[/\\]", + path_separator) .. "\nand restart ImPlay") + end + end + elseif success == true and (result.status == 0 or result.status == -2) then + if not spawn_working and properties["current-vo"] == "libmpv" and options.mpv_path ~= mpv_path then + mp.commandv("script-message-to", "implay", "show-message", "thumbfast initial setup", + "Set mpv_path=ImPlay in thumbfast config:\n" .. + string.gsub(mp.command_native({ "expand-path", "~~/script-opts/thumbfast.conf" }), "[/\\]", + path_separator) .. "\nand restart ImPlay") + end + spawn_working = true + spawn_waiting = false + end + end + ) +end + +local function run(command) + if not spawned then return end + + if options.direct_io then + local hPipe = winapi.C.CreateFileW(winapi.socket_wc, winapi.GENERIC_WRITE, 0, nil, winapi.OPEN_EXISTING, + winapi._createfile_pipe_flags, nil) + if hPipe ~= winapi.INVALID_HANDLE_VALUE then + local buf = command .. "\n" + winapi.C.SetNamedPipeHandleState(hPipe, winapi.PIPE_NOWAIT, nil, nil) + winapi.C.WriteFile(hPipe, buf, #buf + 1, winapi._lpNumberOfBytesWritten, nil) + winapi.C.CloseHandle(hPipe) + end + + return + end + + local command_n = command .. "\n" + + if os_name == "windows" then + if file and file_bytes + #command_n >= 4096 then + file:close() + file = nil + file_bytes = 0 + end + if not file then + file = io.open("\\\\.\\pipe\\" .. options.socket, "r+b") + end + elseif pre_0_33_0 then + subprocess({ "/usr/bin/env", "sh", "-c", "echo '" .. command .. "' | socat - " .. options.socket }) + return + elseif not file then + file = io.open(options.socket, "r+") + end + if file then + file_bytes = file:seek("end") + file:write(command_n) + file:flush() + end +end + +local function draw(w, h, script) + if not w or not show_thumbnail then return end + if x ~= nil then + if pre_0_30_0 then + mp.command_native({ "overlay-add", options.overlay_id, x, y, options.thumbnail .. ".bgra", 0, "bgra", w, h, + (4 * w) }) + else + mp.command_native_async( + { "overlay-add", options.overlay_id, x, y, options.thumbnail .. ".bgra", 0, "bgra", w, h, (4 * w) }, + function() + end) + end + elseif script then + local json, _ = mp.utils.format_json({ + width = w, + height = h, + x = x, + y = y, + socket = options.socket, + thumbnail = options.thumbnail, + overlay_id = options.overlay_id + }) + mp.commandv("script-message-to", script, "thumbfast-render", json) + end +end + +local function real_res(req_w, req_h, filesize) + local count = filesize / 4 + local diff = (req_w * req_h) - count + + if (properties["video-params"] and properties["video-params"]["rotate"] or 0) % 180 == 90 then + req_w, req_h = req_h, req_w + end + + if diff == 0 then + return req_w, req_h + else + local threshold = 5 -- throw out results that change too much + local long_side, short_side = req_w, req_h + if req_h > req_w then + long_side, short_side = req_h, req_w + end + for a = short_side, short_side - threshold, -1 do + if count % a == 0 then + local b = count / a + if long_side - b < threshold then + if req_h < req_w then return b, a else return a, b end + end + end + end + return nil + end +end + +local function move_file(from, to) + if os_name == "windows" then + os.remove(to) + end + -- move the file because it can get overwritten while overlay-add is reading it, and crash the player + os.rename(from, to) +end + +local function seek(fast) + if last_seek_time then + run("async seek " .. last_seek_time .. (fast and " absolute+keyframes" or " absolute+exact")) + end +end + +local seek_period = 3 / 60 +local seek_period_counter = 0 +local seek_timer +seek_timer = mp.add_periodic_timer(seek_period, function() + if seek_period_counter == 0 then + seek(allow_fast_seek) + seek_period_counter = 1 + else + if seek_period_counter == 2 then + if allow_fast_seek then + seek_timer:kill() + seek() + end + else + seek_period_counter = seek_period_counter + 1 + end + end +end) +seek_timer:kill() + +local function request_seek() + if seek_timer:is_enabled() then + seek_period_counter = 0 + else + seek_timer:resume() + seek(allow_fast_seek) + seek_period_counter = 1 + end +end + +local function check_new_thumb() + -- the slave might start writing to the file after checking existance and + -- validity but before actually moving the file, so move to a temporary + -- location before validity check to make sure everything stays consistant + -- and valid thumbnails don't get overwritten by invalid ones + local tmp = options.thumbnail .. ".tmp" + move_file(options.thumbnail, tmp) + local finfo = mp.utils.file_info(tmp) + if not finfo then return false end + spawn_waiting = false + local w, h = real_res(effective_w, effective_h, finfo.size) + if w then -- only accept valid thumbnails + move_file(tmp, options.thumbnail .. ".bgra") + + real_w, real_h = w, h + if real_w and (real_w ~= last_real_w or real_h ~= last_real_h) then + last_real_w, last_real_h = real_w, real_h + info(real_w, real_h) + end + if not show_thumbnail then + file_timer:kill() + end + return true + end + + return false +end + +file_timer = mp.add_periodic_timer(file_check_period, function() + if check_new_thumb() then + draw(real_w, real_h, script_name) + end +end) +file_timer:kill() + +local function clear() + file_timer:kill() + seek_timer:kill() + if options.quit_after_inactivity > 0 then + if show_thumbnail or activity_timer:is_enabled() then + activity_timer:kill() + end + activity_timer:resume() + end + last_seek_time = nil + show_thumbnail = false + last_x = nil + last_y = nil + if script_name then return end + if pre_0_30_0 then + mp.command_native({ "overlay-remove", options.overlay_id }) + else + mp.command_native_async({ "overlay-remove", options.overlay_id }, function() + end) + end +end + +local function quit() + activity_timer:kill() + if show_thumbnail then + activity_timer:resume() + return + end + run("quit") + spawned = false + real_w, real_h = nil, nil + clear() +end + +activity_timer = mp.add_timeout(options.quit_after_inactivity, quit) +activity_timer:kill() + +local function thumb(time, r_x, r_y, script) + if disabled then return end + + time = tonumber(time) + if time == nil then return end + + if r_x == "" or r_y == "" then + x, y = nil, nil + else + x, y = math.floor(r_x + 0.5), math.floor(r_y + 0.5) + end + + script_name = script + if last_x ~= x or last_y ~= y or not show_thumbnail then + show_thumbnail = true + last_x = x + last_y = y + draw(real_w, real_h, script) + end + + if options.quit_after_inactivity > 0 then + if show_thumbnail or activity_timer:is_enabled() then + activity_timer:kill() + end + activity_timer:resume() + end + + if time == last_seek_time then return end + last_seek_time = time + if not spawned then spawn(time) end + request_seek() + if not file_timer:is_enabled() then file_timer:resume() end +end + +local function watch_changes() + if not dirty or not properties["video-out-params"] then return end + dirty = false + + local old_w = effective_w + local old_h = effective_h + + calc_dimensions() + + local vf_reset = vf_string(filters_reset) + local rotate = properties["video-rotate"] or 0 + + local resized = old_w ~= effective_w or + old_h ~= effective_h or + last_vf_reset ~= vf_reset or + (last_rotate % 180) ~= (rotate % 180) or + par ~= last_par + + if resized then + last_rotate = rotate + info(effective_w, effective_h) + elseif last_has_vid ~= has_vid and has_vid ~= 0 then + info(effective_w, effective_h) + end + + if spawned then + if resized then + -- mpv doesn't allow us to change output size + local seek_time = last_seek_time + run("quit") + clear() + spawned = false + spawn(seek_time or mp.get_property_number("time-pos", 0)) + file_timer:resume() + else + if rotate ~= last_rotate then + run("set video-rotate " .. rotate) + end + local vf_runtime = vf_string(filters_runtime) + if vf_runtime ~= last_vf_runtime then + run("vf set " .. vf_string(filters_all, true)) + last_vf_runtime = vf_runtime + end + end + else + last_vf_runtime = vf_string(filters_runtime) + end + + last_vf_reset = vf_reset + last_rotate = rotate + last_par = par + last_has_vid = has_vid + + if not spawned and not disabled and options.spawn_first and resized then + spawn(mp.get_property_number("time-pos", 0)) + file_timer:resume() + end +end + +local function update_property(name, value) + properties[name] = value +end + +local function update_property_dirty(name, value) + properties[name] = value + dirty = true + if name == "tone-mapping" then + last_tone_mapping = nil + end +end + +local function update_tracklist(_, value) + -- current-tracks shim + for _, track in ipairs(value) do + if track.type == "video" and track.selected then + properties["current-tracks/video/image"] = track.image + properties["current-tracks/video/albumart"] = track.albumart + return + end + end +end + +local function sync_changes(prop, val) + update_property(prop, val) + if val == nil then return end + + if type(val) == "boolean" then + if prop == "vid" then + has_vid = 0 + last_has_vid = 0 + info(effective_w, effective_h) + clear() + return + end + val = val and "yes" or "no" + end + + if prop == "vid" then + has_vid = 1 + end + + if not spawned then return end + + run("set " .. prop .. " " .. val) + dirty = true +end + +local function file_load() + clear() + spawned = false + real_w, real_h = nil, nil + last_real_w, last_real_h = nil, nil + last_tone_mapping = nil + last_seek_time = nil + if info_timer then + info_timer:kill() + info_timer = nil + end + + calc_dimensions() + info(effective_w, effective_h) +end + +local function shutdown() + run("quit") + remove_thumbnail_files() + if os_name ~= "windows" then + os.remove(options.socket) + os.remove(options.socket .. ".run") + end +end + +local function on_duration(_, val) + allow_fast_seek = (val or 30) >= 30 +end + +mp.observe_property("current-tracks", "native", function(name, value) + if pre_0_33_0 then + mp.unobserve_property(update_tracklist) + pre_0_33_0 = false + end + update_property(name, value) +end) + +mp.observe_property("track-list", "native", update_tracklist) +mp.observe_property("display-hidpi-scale", "native", update_property_dirty) +mp.observe_property("video-out-params", "native", update_property_dirty) +mp.observe_property("video-params", "native", update_property_dirty) +mp.observe_property("vf", "native", update_property_dirty) +mp.observe_property("tone-mapping", "native", update_property_dirty) +mp.observe_property("demuxer-via-network", "native", update_property) +mp.observe_property("stream-open-filename", "native", update_property) +mp.observe_property("macos-app-activation-policy", "native", update_property) +mp.observe_property("current-vo", "native", update_property) +mp.observe_property("video-rotate", "native", update_property) +mp.observe_property("path", "native", update_property) +mp.observe_property("vid", "native", sync_changes) +mp.observe_property("edition", "native", sync_changes) +mp.observe_property("duration", "native", on_duration) + +mp.register_script_message("thumb", thumb) +mp.register_script_message("clear", clear) + +mp.register_event("file-loaded", file_load) +mp.register_event("shutdown", shutdown) + +mp.register_idle(watch_changes) diff --git a/multimedia/.config/mpv/scripts/uosc.lua b/multimedia/.config/mpv/scripts/uosc.lua index cdc3919..f8eeba9 100644 --- a/multimedia/.config/mpv/scripts/uosc.lua +++ b/multimedia/.config/mpv/scripts/uosc.lua @@ -1,3655 +1,1261 @@ ---[[ +--[[ uosc 4.7.0 - 2023-Apr-15 | https://github.com/tomasklaen/uosc ]] +local uosc_version = '4.7.0' -uosc 2.9.0 - 2020-May-11 | https://github.com/darsain/uosc +assdraw = require('mp.assdraw') +opt = require('mp.options') +utils = require('mp.utils') +msg = require('mp.msg') +osd = mp.create_osd_overlay('ass-events') +INFINITY = 1e309 +QUARTER_PI_SIN = math.sin(math.pi / 4) -Minimalist cursor proximity based UI for MPV player. +-- Enables relative requires from `scripts` directory +package.path = package.path .. ';' .. mp.find_config_file('scripts') .. '/?.lua' -uosc replaces the default osc UI, so that has to be disabled first. -Place these options into your `mpv.conf` file: +require('uosc_shared/lib/std') -``` -# required so that the 2 UIs don't fight each other -osc=no -# uosc provides its own seeking/volume indicators, so you also don't need this -osd-bar=no -# uosc will draw its own window controls if you disable window border -border=no -``` +--[[ OPTIONS ]] -Options go in `script-opts/uosc.conf`. Defaults: +defaults = { + timeline_style = 'line', + timeline_line_width = 2, + timeline_line_width_fullscreen = 3, + timeline_line_width_minimized_scale = 10, + timeline_size_min = 2, + timeline_size_max = 40, + timeline_size_min_fullscreen = 0, + timeline_size_max_fullscreen = 60, + timeline_start_hidden = false, + timeline_persistency = 'paused', + timeline_opacity = 0.9, + timeline_border = 1, + timeline_step = 5, + timeline_chapters_opacity = 0.8, + timeline_cache = true, -``` -# timeline size when fully retracted, 0 will hide it completely -timeline_size_min=2 -# timeline size when fully expanded, in pixels, 0 to disable -timeline_size_max=40 -# same as ^ but when in fullscreen -timeline_size_min_fullscreen=0 -timeline_size_max_fullscreen=60 -# same thing as calling toggle-progress command once on startup -timeline_start_hidden=no -# timeline opacity -timeline_opacity=0.8 -# top (and bottom in no-border mode) border of background color to help visually -# separate elapsed bar from a video of similar color or desktop background -timeline_border=1 -# when scrolling above timeline, wheel will seek by this amount of seconds -timeline_step=5 -# display seekable buffered ranges for streaming videos, syntax `color:opacity`, -# color is an BBGGRR hex code, set to `none` to disable -timeline_cached_ranges=345433:0.5 -# floating number font scale adjustment -timeline_font_scale=1 -# briefly show timeline on external changes (e.g. seeking with a hotkey) -timeline_flash=yes + controls = 'menu,gap,subtitles,audio,video,editions,stream-quality,gap,space,speed,space,shuffle,loop-playlist,loop-file,gap,prev,items,next,gap,fullscreen', + controls_size = 32, + controls_size_fullscreen = 40, + controls_margin = 8, + controls_spacing = 2, + controls_persistency = '', -# timeline chapters style: none, dots, lines, lines-top, lines-bottom -chapters=dots -chapters_opacity=0.3 + volume = 'right', + volume_size = 40, + volume_size_fullscreen = 52, + volume_persistency = '', + volume_opacity = 0.9, + volume_border = 1, + volume_step = 1, -# where to display volume controls: none, left, right -volume=right -volume_size=40 -volume_size_fullscreen=60 -volume_opacity=0.8 -volume_border=1 -volume_step=1 -volume_font_scale=1 -volume_flash=yes + speed_persistency = '', + speed_opacity = 0.6, + speed_step = 0.1, + speed_step_is_factor = false, -# playback speed widget: mouse drag or wheel to change, click to reset -speed=no -speed_size=46 -speed_size_fullscreen=68 -speed_opacity=1 -speed_step=0.1 -speed_font_scale=1 -speed_flash=yes + menu_item_height = 36, + menu_item_height_fullscreen = 50, + menu_min_width = 260, + menu_min_width_fullscreen = 360, + menu_opacity = 1, + menu_parent_opacity = 0.4, -# controls all menus, such as context menu, subtitle loader/selector, etc -menu_item_height=36 -menu_item_height_fullscreen=50 -menu_wasd_navigation=no -menu_hjkl_navigation=no -menu_opacity=0.8 -menu_font_scale=1 + top_bar = 'no-border', + top_bar_size = 40, + top_bar_size_fullscreen = 46, + top_bar_persistency = '', + top_bar_controls = true, + top_bar_title = 'yes', + top_bar_alt_title = '', + top_bar_alt_title_place = 'below', + top_bar_title_opacity = 0.8, -# top bar with window controls and media title shown only in no-border mode -top_bar_size=40 -top_bar_size_fullscreen=46 -top_bar_controls=yes -top_bar_title=yes + window_border_size = 1, + window_border_opacity = 0.8, -# pause video on clicks shorter than this number of milliseconds, 0 to disable -pause_on_click_shorter_than=0 -# for how long in milliseconds to show elements they're it's being flashed -flash_duration=400 -# distances in pixels below which elements are fully faded in/out -proximity_in=40 -proximity_out=120 -# BBGGRR - BLUE GREEN RED hex color codes -color_foreground=ffffff -color_foreground_text=000000 -color_background=000000 -color_background_text=ffffff -# use bold font weight throughout the whole UI -font_bold=no -# hide UI when mpv autohides the cursor -autohide=no -# can be: none, flash, static -pause_indicator=flash -# load first file when calling next on a last file in a directory and vice versa -directory_navigation_loops=no -# file types to look for when navigating media files -media_types=3gp,avi,bmp,flac,flv,gif,h264,h265,jpeg,jpg,m4a,m4v,mid,midi,mkv,mov,mp3,mp4,mp4a,mp4v,mpeg,mpg,oga,ogg,ogm,ogv,opus,png,rmvb,svg,tif,tiff,wav,weba,webm,webp,wma,wmv -# file types to look for when loading external subtitles -subtitle_types=aqt,gsub,jss,sub,ttxt,pjs,psb,rt,smi,slt,ssf,srt,ssa,ass,usf,idx,vt -# used to approximate text width -# if you are using some wide font and see a lot of right side clipping in menus, -# try bumping this up -font_height_to_letter_width_ratio=0.5 + autoload = false, + autoload_types = 'video,audio,image', + shuffle = false, -# `chapter_ranges` lets you transform chapter indicators into range indicators. -# -# Chapter range definition syntax: -# ``` -# start_patternend_pattern -# ``` -# -# Multiple start and end patterns can be defined by separating them with `|`: -# ``` -# p1|pNp1|pN -# ``` -# -# Multiple chapter ranges can be defined by separating them with comma: -# -# chapter_ranges=range1,rangeN -# -# One of `start_pattern`s can be a custom keyword `{bof}` that will match -# beginning of file when it makes sense. -# -# One of `end_pattern`s can be a custom keyword `{eof}` that will match end of -# file when it makes sense. -# -# Patterns are lua patterns (http://lua-users.org/wiki/PatternsTutorial). -# They only need to occur in a title, not match it completely. -# Matching is case insensitive. -# -# `color` is a `bbggrr` hexadecimal color code. -# `opacity` is a float number from 0 to 1. -# -# Examples: -# -# Display skippable youtube video sponsor blocks from https://github.com/po5/mpv_sponsorblock -# ``` -# chapter_ranges=sponsor start<3535a5:0.5>sponsor end -# ``` -# -# Display anime openings and endings as ranges: -# ``` -# chapter_ranges=^op| op$|opening<968638:0.5>.*, ^ed| ed$|^end|ending$<968638:0.5>.*|{eof} -# ``` -chapter_ranges=^op| op$|opening<968638:0.5>.*, ^ed| ed$|^end|ending$<968638:0.5>.*|{eof}, sponsor start<3535a5:.5>sponsor end -``` - -Available keybindings (place into `input.conf`): - -``` -Key script-binding uosc/peek-timeline -Key script-binding uosc/toggle-progress -Key script-binding uosc/menu -Key script-binding uosc/load-subtitles -Key script-binding uosc/subtitles -Key script-binding uosc/audio -Key script-binding uosc/video -Key script-binding uosc/playlist -Key script-binding uosc/chapters -Key script-binding uosc/open-file -Key script-binding uosc/next -Key script-binding uosc/prev -Key script-binding uosc/first -Key script-binding uosc/last -Key script-binding uosc/next-file -Key script-binding uosc/prev-file -Key script-binding uosc/first-file -Key script-binding uosc/last-file -Key script-binding uosc/delete-file-next -Key script-binding uosc/delete-file-quit -Key script-binding uosc/show-in-directory -Key script-binding uosc/open-config-directory -``` -]] if mp.get_property('osc') == 'yes' then - mp.msg.info('Disabled because original osc is enabled!') - return -end - -local assdraw = require('mp.assdraw') -local opt = require('mp.options') -local utils = require('mp.utils') -local msg = require('mp.msg') -local osd = mp.create_osd_overlay('ass-events') -local infinity = 1e309 - --- OPTIONS/CONFIG/STATE -local options = { - timeline_size_min = 2, - timeline_size_max = 40, - timeline_size_min_fullscreen = 0, - timeline_size_max_fullscreen = 60, - timeline_start_hidden = false, - timeline_opacity = 0.8, - timeline_border = 1, - timeline_step = 5, - timeline_cached_ranges = '345433:0.5', - timeline_font_scale = 1, - timeline_flash = true, - - chapters = 'dots', - chapters_opacity = 0.3, - - volume = 'right', - volume_size = 40, - volume_size_fullscreen = 60, - volume_opacity = 0.8, - volume_border = 1, - volume_step = 1, - volume_font_scale = 1, - volume_flash = true, - - speed = false, - speed_size = 46, - speed_size_fullscreen = 68, - speed_opacity = 1, - speed_step = 0.1, - speed_font_scale = 1, - speed_flash = true, - - menu_item_height = 36, - menu_item_height_fullscreen = 50, - menu_wasd_navigation = false, - menu_hjkl_navigation = false, - menu_opacity = 0.8, - menu_font_scale = 1, - - top_bar_size = 40, - top_bar_size_fullscreen = 46, - top_bar_controls = true, - top_bar_title = true, - - pause_on_click_shorter_than = 0, - flash_duration = 400, - proximity_in = 40, - proximity_out = 120, - color_foreground = 'ffffff', - color_foreground_text = '000000', - color_background = '000000', - color_background_text = 'ffffff', - font_bold = false, - autohide = false, - pause_indicator = 'flash', - directory_navigation_loops = false, - media_types = '3gp,avi,bmp,flac,flv,gif,h264,h265,jpeg,jpg,m4a,m4v,mid,midi,mkv,mov,mp3,mp4,mp4a,mp4v,mpeg,mpg,oga,ogg,ogm,ogv,opus,png,rmvb,svg,tif,tiff,wav,weba,webm,webp,wma,wmv', - subtitle_types = 'aqt,gsub,jss,sub,ttxt,pjs,psb,rt,smi,slt,ssf,srt,ssa,ass,usf,idx,vt', - font_height_to_letter_width_ratio = 0.5, - chapter_ranges = '^op| op$|opening<968638:0.5>.*, ^ed| ed$|^end|ending$<968638:0.5>.*|{eof}, sponsor start<3535a5:.5>sponsor end' + ui_scale = 1, + font_scale = 1, + text_border = 1.2, + text_width_estimation = true, + pause_on_click_shorter_than = 0, -- deprecated by below + click_threshold = 0, + click_command = 'cycle pause; script-binding uosc/flash-pause-indicator', + flash_duration = 1000, + proximity_in = 40, + proximity_out = 120, + foreground = 'ffffff', + foreground_text = '000000', + background = '000000', + background_text = 'ffffff', + total_time = false, -- deprecated by below + destination_time = 'playtime-remaining', + time_precision = 0, + font_bold = false, + autohide = false, + buffered_time_threshold = 60, + pause_indicator = 'flash', + curtain_opacity = 0.5, + stream_quality_options = '4320,2160,1440,1080,720,480,360,240,144', + video_types= '3g2,3gp,asf,avi,f4v,flv,h264,h265,m2ts,m4v,mkv,mov,mp4,mp4v,mpeg,mpg,ogm,ogv,rm,rmvb,ts,vob,webm,wmv,y4m', + audio_types= 'aac,ac3,aiff,ape,au,dsf,dts,flac,m4a,mid,midi,mka,mp3,mp4a,oga,ogg,opus,spx,tak,tta,wav,weba,wma,wv', + image_types= 'apng,avif,bmp,gif,j2k,jp2,jfif,jpeg,jpg,jxl,mj2,png,svg,tga,tif,tiff,webp', + subtitle_types = 'aqt,ass,gsub,idx,jss,lrc,mks,pgs,pjs,psb,rt,slt,smi,sub,sup,srt,ssa,ssf,ttxt,txt,usf,vt,vtt', + default_directory = '~/', + use_trash = false, + adjust_osd_margins = true, + chapter_ranges = 'openings:30abf964,endings:30abf964,ads:c54e4e80', + chapter_range_patterns = 'openings:オープニング;endings:エンディング', } +options = table_shallow_copy(defaults) opt.read_options(options, 'uosc') -local config = { - render_delay = 0.03, -- sets max rendering frequency - font = mp.get_property('options/osd-font'), - menu_parent_opacity = 0.4, - menu_min_width = 260 -} -local bold_tag = options.font_bold and '\\b1' or '' -local display = {width = 1280, height = 720, aspect = 1.77778} -local cursor = { - hidden = true, -- true when autohidden or outside of the player window - x = 0, - y = 0 -} -local state = { - os = (function() - if os.getenv('windir') ~= nil then return 'windows' end - local homedir = os.getenv('HOME') - if homedir ~= nil and string.sub(homedir, 1, 6) == '/Users' then - return 'macos' - end - return 'linux' - end)(), - cwd = mp.get_property('working-directory'), - media_title = '', - duration = nil, - position = nil, - pause = false, - chapters = nil, - chapter_ranges = nil, - fullscreen = mp.get_property_native('fullscreen'), - maximized = mp.get_property_native('window-maximized'), - render_timer = nil, - render_last_time = 0, - volume = nil, - volume_max = nil, - mute = nil, - cursor_autohide_timer = mp.add_timeout( - mp.get_property_native('cursor-autohide') / 1000, function() - if not options.autohide then return end - handle_mouse_leave() - end), - mouse_bindings_enabled = false, - cached_ranges = nil -} -local forced_key_bindings -- defined at the bottom next to events - --- HELPERS - -function round(number) - local modulus = number % 1 - return modulus < 0.5 and math.floor(number) or math.ceil(number) +-- Normalize values +options.proximity_out = math.max(options.proximity_out, options.proximity_in + 1) +if options.chapter_ranges:sub(1, 4) == '^op|' then options.chapter_ranges = defaults.chapter_ranges end +if options.pause_on_click_shorter_than > 0 and options.click_threshold == 0 then + msg.warn('`pause_on_click_shorter_than` is deprecated. Use `click_threshold` and `click_command` instead.') + options.click_threshold = options.pause_on_click_shorter_than end - -function call_me_maybe(fn, value1, value2, value3) - if fn then fn(value1, value2, value3) end +if options.total_time and options.destination_time == 'playtime-remaining' then + msg.warn('`total_time` is deprecated. Use `destination_time` instead.') + options.destination_time = 'total' +elseif not itable_index_of({'total', 'playtime-remaining', 'time-remaining'}, options.destination_time) then + options.destination_time = 'playtime-remaining' end +-- Ensure required environment configuration +if options.autoload then mp.commandv('set', 'keep-open-pause', 'no') end +-- Color shorthands +fg, bg = serialize_rgba(options.foreground).color, serialize_rgba(options.background).color +fgt, bgt = serialize_rgba(options.foreground_text).color, serialize_rgba(options.background_text).color -function split(str, pattern) - local list = {} - local full_pattern = '(.-)' .. pattern - local last_end = 1 - local start_index, end_index, capture = str:find(full_pattern, 1) - while start_index do - list[#list + 1] = capture - last_end = end_index + 1 - start_index, end_index, capture = str:find(full_pattern, last_end) - end - if last_end <= (#str + 1) then - capture = str:sub(last_end) - list[#list + 1] = capture - end - return list -end +--[[ CONFIG ]] -function itable_find(haystack, needle) - local is_needle = type(needle) == 'function' and needle or - function(index, value) return value == needle end - for index, value in ipairs(haystack) do - if is_needle(index, value) then return index, value end - end -end - -function itable_filter(haystack, needle) - local is_needle = type(needle) == 'function' and needle or - function(index, value) return value == needle end - local filtered = {} - for index, value in ipairs(haystack) do - if is_needle(index, value) then filtered[#filtered + 1] = value end - end - return filtered -end - -function itable_remove(haystack, needle) - local should_remove = type(needle) == 'function' and needle or - function(value) return value == needle end - local new_table = {} - for _, value in ipairs(haystack) do - if not should_remove(value) then - new_table[#new_table + 1] = value - end - end - return new_table -end - -function itable_slice(haystack, start_pos, end_pos) - start_pos = start_pos and start_pos or 1 - end_pos = end_pos and end_pos or #haystack - - if end_pos < 0 then end_pos = #haystack + end_pos + 1 end - if start_pos < 0 then start_pos = #haystack + start_pos + 1 end - - local new_table = {} - for index, value in ipairs(haystack) do - if index >= start_pos and index <= end_pos then - new_table[#new_table + 1] = value - end - end - return new_table -end - -function table_copy(table) - local new_table = {} - for key, value in pairs(table) do new_table[key] = value end - return new_table -end - --- Sorting comparator close to (but not exactly) how file explorers sort files -local word_order_comparator = (function() - local symbol_order - local default_order - - if state.os == 'win' then - symbol_order = { - ['!'] = 1, - ['#'] = 2, - ['$'] = 3, - ['%'] = 4, - ['&'] = 5, - ['('] = 6, - [')'] = 6, - [','] = 7, - ['.'] = 8, - ["'"] = 9, - ['-'] = 10, - [';'] = 11, - ['@'] = 12, - ['['] = 13, - [']'] = 13, - ['^'] = 14, - ['_'] = 15, - ['`'] = 16, - ['{'] = 17, - ['}'] = 17, - ['~'] = 18, - ['+'] = 19, - ['='] = 20 - } - default_order = 21 - else - symbol_order = { - ['`'] = 1, - ['^'] = 2, - ['~'] = 3, - ['='] = 4, - ['_'] = 5, - ['-'] = 6, - [','] = 7, - [';'] = 8, - ['!'] = 9, - ["'"] = 10, - ['('] = 11, - [')'] = 11, - ['['] = 12, - [']'] = 12, - ['{'] = 13, - ['}'] = 14, - ['@'] = 15, - ['$'] = 16, - ['*'] = 17, - ['&'] = 18, - ['%'] = 19, - ['+'] = 20, - ['.'] = 22, - ['#'] = 23 - } - default_order = 21 - end - - return function(a, b) - a = a:lower() - b = b:lower() - for i = 1, math.max(#a, #b) do - local ai = a:sub(i, i) - local bi = b:sub(i, i) - if ai == nil and bi then return true end - if bi == nil and ai then return false end - local a_order = symbol_order[ai] or default_order - local b_order = symbol_order[bi] or default_order - if a_order == b_order then - return a < b - else - return a_order < b_order - end - end - end -end)() - --- Creates in-between frames to animate value from `from` to `to` numbers. --- Returns function that terminates animation. --- `to` can be a function that returns target value, useful for movable targets. --- `speed` is an optional float between 1-instant and 0-infinite duration --- `callback` is called either on animation end, or when animation is canceled -function tween(from, to, setter, speed, callback) - if type(speed) ~= 'number' then - callback = speed - speed = 0.3 - end - local timeout - local getTo = type(to) == 'function' and to or function() return to end - local cutoff = math.abs(getTo() - from) * 0.01 - function tick() - from = from + ((getTo() - from) * speed) - local is_end = math.abs(getTo() - from) <= cutoff - setter(is_end and getTo() or from) - request_render() - if is_end then - call_me_maybe(callback) - else - timeout:resume() - end - end - timeout = mp.add_timeout(0.016, tick) - tick() - return function() - timeout:kill() - call_me_maybe(callback) - end -end - --- Kills ongoing animation if one is already running on this element. --- Killed animation will not get its `on_end` called. -function tween_element(element, from, to, setter, speed, callback) - if type(speed) ~= 'number' then - callback = speed - speed = 0.3 - end - - tween_element_stop(element) - - element.stop_current_animation = tween(from, to, function(value) - setter(element, value) - end, speed, function() - element.stop_current_animation = nil - call_me_maybe(callback, element) - end) -end - --- Stopped animation will not get its on_end called. -function tween_element_is_tweening(element) - return element and element.stop_current_animation -end - --- Stopped animation will not get its on_end called. -function tween_element_stop(element) - call_me_maybe(element and element.stop_current_animation) -end - --- Helper to automatically use an element property setter -function tween_element_property(element, prop, from, to, speed, callback) - tween_element(element, from, to, - function(_, value) element[prop] = value end, speed, callback) -end - -function get_point_to_rectangle_proximity(point, rect) - local dx = math.max(rect.ax - point.x, 0, point.x - rect.bx + 1) - local dy = math.max(rect.ay - point.y, 0, point.y - rect.by + 1) - return math.sqrt(dx * dx + dy * dy); -end - -function text_width_estimate(letters, font_size) - return letters and letters * font_size * - options.font_height_to_letter_width_ratio or 0 -end - -function opacity_to_alpha(opacity) return 255 - math.ceil(255 * opacity) end - -function ass_opacity(opacity, fraction) - fraction = fraction ~= nil and fraction or 1 - if type(opacity) == 'number' then - return string.format('{\\alpha&H%X&}', - opacity_to_alpha(opacity * fraction)) - else - return string.format('{\\1a&H%X&\\2a&H%X&\\3a&H%X&\\4a&H%X&}', - opacity_to_alpha((opacity[1] or 0) * fraction), - opacity_to_alpha((opacity[2] or 0) * fraction), - opacity_to_alpha((opacity[3] or 0) * fraction), - opacity_to_alpha((opacity[4] or 0) * fraction)) - end -end - --- Ensures path is absolute and normalizes slashes to the current platform -function normalize_path(path) - if not path or is_protocol(path) then return path end - - -- Ensure path is absolute - if not (path:match('^/') or path:match('^%a+:') or path:match('^\\\\')) then - path = utils.join_path(state.cwd, path) - end - - -- Use proper slashes - if state.os == 'windows' then - return path:gsub('/', '\\') - else - return path:gsub('\\', '/') - end -end - --- Check if path is a protocol, such as `http://...` -function is_protocol(path) return path:match('^%a[%a%d-_]+://') end - -function get_extension(path) - local parts = split(path, '%.') - return parts and #parts > 1 and parts[#parts] or nil -end - --- Serializes path into its semantic parts -function serialize_path(path) - if not path or is_protocol(path) then return end - path = normalize_path(path) - local parts = split(path, '[\\/]+') - if parts[#parts] == '' then table.remove(parts, #parts) end -- remove trailing separator - local basename = parts and parts[#parts] or path - local dirname = #parts > 1 and - table.concat(itable_slice(parts, 1, #parts - 1), - state.os == 'windows' and '\\' or '/') or - nil - local dot_split = split(basename, '%.') - return { - path = path:sub(-1) == ':' and state.os == 'windows' and path .. '\\' or - path, - is_root = dirname == nil, - dirname = dirname, - basename = basename, - filename = #dot_split > 1 and - table.concat(itable_slice(dot_split, 1, #dot_split - 1), '.') or - basename, - extension = #dot_split > 1 and dot_split[#dot_split] or nil - } -end - -function get_files_in_directory(directory, allowed_types) - local files, error = utils.readdir(directory, 'files') - - if not files then - msg.error('Retrieving files failed: ' .. (error or '')) - return - end - - -- Filter only requested file types - if allowed_types then - files = itable_filter(files, function(_, file) - local extension = get_extension(file) - return extension and itable_find(allowed_types, extension:lower()) - end) - end - - table.sort(files, word_order_comparator) - - return files -end - -function get_adjacent_file(file_path, direction, allowed_types) - local current_file = serialize_path(file_path) - local files = get_files_in_directory(current_file.dirname, allowed_types) - - if not files then return end - - for index, file in ipairs(files) do - if current_file.basename == file then - if direction == 'forward' then - if files[index + 1] then - return utils.join_path(current_file.dirname, - files[index + 1]) - end - if options.directory_navigation_loops and files[1] then - return utils.join_path(current_file.dirname, files[1]) - end - else - if files[index - 1] then - return utils.join_path(current_file.dirname, - files[index - 1]) - end - if options.directory_navigation_loops and files[#files] then - return utils.join_path(current_file.dirname, files[#files]) - end - end - - -- This is the only file in directory - return nil - end - end -end - --- Ensures chapters are in chronological order -function get_normalized_chapters() - local chapters = mp.get_property_native('chapter-list') - - if not chapters then return end - - -- Copy table - chapters = itable_slice(chapters) - - -- Ensure chronological order of chapters - table.sort(chapters, function(a, b) return a.time < b.time end) - - return chapters -end - --- Element ---[[ -Signature: -{ - -- enables capturing button groups for this element - captures = {mouse_buttons = true, wheel = true}, - -- element rectangle coordinates - ax = 0, ay = 0, bx = 0, by = 0, - -- cursor<>element relative proximity as a 0-1 floating number - -- where 0 = completely away, and 1 = touching/hovering - -- so it's easy to work with and throw into equations - proximity = 0, - -- raw cursor<>element proximity in pixels - proximity_raw = infinity, - -- called when element is created - ?init = function(this), - -- called manually when disposing of element - ?destroy = function(this), - -- triggered when event happens and cursor is above element - ?on_{event_name} = function(this), - -- triggered when any event happens anywhere on a page - ?on_global_{event_name} = function(this), - -- object - ?render = function(this_element), -} -]] -local Element = { - captures = nil, - ax = 0, - ay = 0, - bx = 0, - by = 0, - proximity = 0, - proximity_raw = infinity -} -Element.__index = Element - -function Element.new(props) - local element = setmetatable(props, Element) - element._eventListeners = {} - - -- Flash timer - element._flash_out_timer = mp.add_timeout(options.flash_duration / 1000, - function() - local getTo = function() return element.proximity end - element:tween_property('forced_proximity', 1, getTo, - function() element.forced_proximity = nil end) - end) - element._flash_out_timer:kill() - - element:init() - - return element -end - -function Element:init() end -function Element:destroy() end - --- Call method if it exists -function Element:maybe(name, ...) - if self[name] then return self[name](self, ...) end -end - --- Tween helpers -function Element:tween(...) tween_element(self, ...) end -function Element:tween_property(...) tween_element_property(self, ...) end -function Element:tween_stop() tween_element_stop(self) end -function Element:is_tweening() tween_element_is_tweening(self) end - --- Event listeners -function Element:on(name, handler) - if self._eventListeners[name] == nil then self._eventListeners[name] = {} end - local preexistingIndex = itable_find(self._eventListeners[name], handler) - if preexistingIndex then - return - else - self._eventListeners[name][#self._eventListeners[name] + 1] = handler - end -end -function Element:off(name, handler) - if self._eventListeners[name] == nil then return end - local index = itable_find(self._eventListeners, handler) - if index then table.remove(self._eventListeners, index) end -end -function Element:trigger(name, ...) - self:maybe('on_' .. name, ...) - if self._eventListeners[name] == nil then return end - for _, handler in ipairs(self._eventListeners[name]) do handler(...) end -end - --- Briefly flashes the element for `options.flash_duration` milliseconds. --- Useful to visualize changes of volume and timeline when changed via hotkeys. --- Implemented by briefly adding animated `forced_proximity` property to the element. -function Element:flash() - if options.flash_duration > 0 and - (self.proximity < 1 or self._flash_out_timer:is_enabled()) then - self:tween_stop() - self.forced_proximity = 1 - self._flash_out_timer:kill() - self._flash_out_timer:resume() - end -end - --- ELEMENTS - -local Elements = {itable = {}} -Elements.__index = Elements -local elements = setmetatable({}, Elements) - -function Elements:add(name, element) - local insert_index = #Elements.itable + 1 - - -- Replace if element already exists - if self:has(name) then - insert_index = itable_find(Elements.itable, function(_, element) - return element.name == name - end) - end - - element.name = name - Elements.itable[insert_index] = element - self[name] = element - - request_render() -end - -function Elements:remove(name, props) - Elements.itable = itable_remove(Elements.itable, self[name]) - self[name] = nil - request_render() -end - -function Elements:has(name) return self[name] ~= nil end -function Elements:ipairs() return ipairs(Elements.itable) end -function Elements:pairs(elements) return pairs(self) end - --- MENU ---[[ -Usage: -``` -local items = { - {title = 'Foo title', hint = 'Ctrl+F', value = 'foo'}, - {title = 'Bar title', hint = 'Ctrl+B', value = 'bar'}, - { - title = 'Submenu', - items = { - {title = 'Sub item 1', value = 'sub1'}, - {title = 'Sub item 2', value = 'sub2'} - } +function create_default_menu() + return { + {title = 'Subtitles', value = 'script-binding uosc/subtitles'}, + {title = 'Audio tracks', value = 'script-binding uosc/audio'}, + {title = 'Stream quality', value = 'script-binding uosc/stream-quality'}, + {title = 'Playlist', value = 'script-binding uosc/items'}, + {title = 'Chapters', value = 'script-binding uosc/chapters'}, + {title = 'Navigation', items = { + {title = 'Next', hint = 'playlist or file', value = 'script-binding uosc/next'}, + {title = 'Prev', hint = 'playlist or file', value = 'script-binding uosc/prev'}, + {title = 'Delete file & Next', value = 'script-binding uosc/delete-file-next'}, + {title = 'Delete file & Prev', value = 'script-binding uosc/delete-file-prev'}, + {title = 'Delete file & Quit', value = 'script-binding uosc/delete-file-quit'}, + {title = 'Open file', value = 'script-binding uosc/open-file'}, + },}, + {title = 'Utils', items = { + {title = 'Aspect ratio', items = { + {title = 'Default', value = 'set video-aspect-override "-1"'}, + {title = '16:9', value = 'set video-aspect-override "16:9"'}, + {title = '4:3', value = 'set video-aspect-override "4:3"'}, + {title = '2.35:1', value = 'set video-aspect-override "2.35:1"'}, + },}, + {title = 'Audio devices', value = 'script-binding uosc/audio-device'}, + {title = 'Editions', value = 'script-binding uosc/editions'}, + {title = 'Screenshot', value = 'async screenshot'}, + {title = 'Show in directory', value = 'script-binding uosc/show-in-directory'}, + {title = 'Open config folder', value = 'script-binding uosc/open-config-directory'}, + },}, + {title = 'Quit', value = 'quit'}, } +end + +config = { + version = uosc_version, + -- sets max rendering frequency in case the + -- native rendering frequency could not be detected + render_delay = 1 / 60, + font = mp.get_property('options/osd-font'), + osd_margin_x = mp.get_property('osd-margin-x'), + osd_margin_y = mp.get_property('osd-margin-y'), + osd_alignment_x = mp.get_property('osd-align-x'), + osd_alignment_y = mp.get_property('osd-align-y'), + types = { + video = split(options.video_types, ' *, *'), + audio = split(options.audio_types, ' *, *'), + image = split(options.image_types, ' *, *'), + subtitle = split(options.subtitle_types, ' *, *'), + media = split(options.video_types .. ',' .. options.audio_types .. ',' .. options.image_types, ' *, *'), + autoload = (function() + ---@type string[] + local option_values = {} + for _, name in ipairs(split(options.autoload_types, ' *, *')) do + local value = options[name .. '_types'] + if type(value) == 'string' then option_values[#option_values + 1] = value end + end + return split(table.concat(option_values, ','), ' *, *') + end)(), + }, + stream_quality_options = split(options.stream_quality_options, ' *, *'), + menu_items = (function() + local input_conf_property = mp.get_property_native('input-conf') + local input_conf_path = mp.command_native({ + 'expand-path', input_conf_property == '' and '~~/input.conf' or input_conf_property, + }) + local input_conf_meta, meta_error = utils.file_info(input_conf_path) + + -- File doesn't exist + if not input_conf_meta or not input_conf_meta.is_file then return create_default_menu() end + + local main_menu = {items = {}, items_by_command = {}} + local by_id = {} + + for line in io.lines(input_conf_path) do + local key, command, comment = string.match(line, '%s*([%S]+)%s+(.-)%s+#%s*(.-)%s*$') + local title = '' + if comment then + local comments = split(comment, '#') + local titles = itable_filter(comments, function(v, i) return v:match('^!') or v:match('^menu:') end) + if titles and #titles > 0 then + title = titles[1]:match('^!%s*(.*)%s*') or titles[1]:match('^menu:%s*(.*)%s*') + end + end + if title ~= '' then + local is_dummy = key:sub(1, 1) == '#' + local submenu_id = '' + local target_menu = main_menu + local title_parts = split(title or '', ' *> *') + + for index, title_part in ipairs(#title_parts > 0 and title_parts or {''}) do + if index < #title_parts then + submenu_id = submenu_id .. title_part + + if not by_id[submenu_id] then + local items = {} + by_id[submenu_id] = {items = items, items_by_command = {}} + target_menu.items[#target_menu.items + 1] = {title = title_part, items = items} + end + + target_menu = by_id[submenu_id] + else + if command == 'ignore' then break end + -- If command is already in menu, just append the key to it + if target_menu.items_by_command[command] then + local hint = target_menu.items_by_command[command].hint + target_menu.items_by_command[command].hint = hint and hint .. ', ' .. key or key + else + local item = { + title = title_part, + hint = not is_dummy and key or nil, + value = command, + } + target_menu.items_by_command[command] = item + target_menu.items[#target_menu.items + 1] = item + end + end + end + end + end + + if #main_menu.items > 0 then + return main_menu.items + else + -- Default context menu + return create_default_menu() + end + end)(), + chapter_ranges = (function() + ---@type table Alternative patterns. + local alt_patterns = {} + if options.chapter_range_patterns and options.chapter_range_patterns ~= '' then + for _, definition in ipairs(split(options.chapter_range_patterns, ';+ *')) do + local name_patterns = split(definition, ' *:') + local name, patterns = name_patterns[1], name_patterns[2] + if name and patterns then alt_patterns[name] = split(patterns, ',') end + end + end + + ---@type table + local ranges = {} + if options.chapter_ranges and options.chapter_ranges ~= '' then + for _, definition in ipairs(split(options.chapter_ranges, ' *,+ *')) do + local name_color = split(definition, ' *:+ *') + local name, color = name_color[1], name_color[2] + if name and color + and name:match('^[a-zA-Z0-9_]+$') and color:match('^[a-fA-F0-9]+$') + and (#color == 6 or #color == 8) then + local range = serialize_rgba(name_color[2]) + range.patterns = alt_patterns[name] + ranges[name_color[1]] = range + end + end + end + return ranges + end)(), } - -function open_item(value) - value -- value from `item.value` +-- Adds `{element}_persistency` property with table of flags when the element should be visible (`{paused = true}`) +for _, name in ipairs({'timeline', 'controls', 'volume', 'top_bar', 'speed'}) do + local option_name = name .. '_persistency' + local value, flags = options[option_name], {} + if type(value) == 'string' then + for _, state in ipairs(split(value, ' *, *')) do flags[state] = true end + end + config[option_name] = flags end -menu:open(items, open_item) -``` -]] -local Menu = {} -Menu.__index = Menu -local menu = setmetatable({key_bindings = {}, is_closing = false}, Menu) +--[[ STATE ]] -function Menu:is_open(menu_type) - return elements.menu ~= nil and - (not menu_type or elements.menu.type == menu_type) -end +display = {width = 1280, height = 720, scale_x = 1, scale_y = 1, initialized = false} +cursor = { + x = 0, + y = 0, + hidden = true, + hover_raw = false, + -- Event handlers that are only fired on cursor, bound during render loop. Guidelines: + -- - element activations (clicks) go to `on_primary_down` handler + -- - `on_primary_up` is only for clearing dragging/swiping, and prevents autohide when bound + on_primary_down = nil, + on_primary_up = nil, + on_wheel_down = nil, + on_wheel_up = nil, + -- Called at the beginning of each render + reset_handlers = function() + cursor.on_primary_down, cursor.on_primary_up = nil, nil + cursor.on_wheel_down, cursor.on_wheel_up = nil, nil + end, + -- Enables pointer key group captures needed by handlers (called at the end of each render) + mbtn_left_enabled = nil, + wheel_enabled = nil, + decide_keybinds = function() + local enable_mbtn_left = (cursor.on_primary_down or cursor.on_primary_up) ~= nil + local enable_wheel = (cursor.on_wheel_down or cursor.on_wheel_up) ~= nil + if enable_mbtn_left ~= cursor.mbtn_left_enabled then + mp[(enable_mbtn_left and 'enable' or 'disable') .. '_key_bindings']('mbtn_left') + cursor.mbtn_left_enabled = enable_mbtn_left + end + if enable_wheel ~= cursor.wheel_enabled then + mp[(enable_wheel and 'enable' or 'disable') .. '_key_bindings']('wheel') + cursor.wheel_enabled = enable_wheel + end + end, + -- Cursor auto-hiding after period of inactivity + autohide = function() + if not cursor.on_primary_up and not Menu:is_open() then handle_mouse_leave() end + end, + autohide_timer = (function() + local timer = mp.add_timeout(mp.get_property_native('cursor-autohide') / 1000, function() cursor.autohide() end) + timer:kill() + return timer + end)(), + queue_autohide = function() + if options.autohide and not cursor.on_primary_up then + cursor.autohide_timer:kill() + cursor.autohide_timer:resume() + end + end +} +state = { + platform = (function() + local platform = mp.get_property_native('platform') + if platform then + if itable_index_of({'windows', 'darwin'}, platform) then return platform end + else + if os.getenv('windir') ~= nil then return 'windows' end + local homedir = os.getenv('HOME') + if homedir ~= nil and string.sub(homedir, 1, 6) == '/Users' then return 'darwin' end + end + return 'linux' + end)(), + cwd = mp.get_property('working-directory'), + path = nil, -- current file path or URL + title = nil, + alt_title = nil, + time = nil, -- current media playback time + speed = 1, + duration = nil, -- current media duration + time_human = nil, -- current playback time in human format + destination_time_human = nil, -- depends on options.destination_time + pause = mp.get_property_native('pause'), + chapters = {}, + current_chapter = nil, + chapter_ranges = {}, + border = mp.get_property_native('border'), + fullscreen = mp.get_property_native('fullscreen'), + maximized = mp.get_property_native('window-maximized'), + fullormaxed = mp.get_property_native('fullscreen') or mp.get_property_native('window-maximized'), + render_timer = nil, + render_last_time = 0, + volume = nil, + volume_max = nil, + mute = nil, + is_idle = false, + is_video = false, + is_audio = false, -- true if file is audio only (mp3, etc) + is_image = false, + is_stream = false, + has_audio = false, + has_sub = false, + has_chapter = false, + has_playlist = false, + shuffle = options.shuffle, + mouse_bindings_enabled = false, + uncached_ranges = nil, + cache = nil, + cache_buffering = 100, + cache_underrun = false, + core_idle = false, + eof_reached = false, + render_delay = config.render_delay, + first_real_mouse_move_received = false, + playlist_count = 0, + playlist_pos = 0, + margin_top = 0, + margin_bottom = 0, + margin_left = 0, + margin_right = 0, + hidpi_scale = 1, +} +thumbnail = {width = 0, height = 0, disabled = false} +external = {} -- Properties set by external scripts +key_binding_overwrites = {} -- Table of key_binding:mpv_command +Elements = require('uosc_shared/elements/Elements') +Menu = require('uosc_shared/elements/Menu') -function Menu:open(items, open_item, opts) - opts = opts or {} +-- State dependent utilities +require('uosc_shared/lib/utils') +require('uosc_shared/lib/text') +require('uosc_shared/lib/ass') +require('uosc_shared/lib/menus') - if menu:is_open() then - if not opts.parent_menu then - menu:close(true, function() - menu:open(items, open_item, opts) - end) - return - end - else - menu:enable_key_bindings() - elements.curtain:fadein() - end - - elements:add('menu', Element.new({ - captures = {mouse_buttons = true}, - type = nil, -- menu type such as `menu`, `chapters`, ... - title = nil, - width = nil, - height = nil, - offset_x = 0, -- used to animated from/to left when submenu - item_height = nil, - item_spacing = 1, - item_content_spacing = nil, - font_size = nil, - scroll_step = nil, - scroll_height = nil, - scroll_y = 0, - opacity = 0, - relative_parent_opacity = 0.4, - items = items, - active_item = nil, - selected_item = nil, - open_item = open_item, - parent_menu = nil, - init = function(this) - -- Already initialized - if this.width ~= nil then return end - - -- Apply options - for key, value in pairs(opts) do this[key] = value end - this.selected_item = this.active_item - - -- Set initial dimensions - this:on_display_resize() - - -- Scroll to active item - this:scroll_to_item(this.active_item) - - -- Transition in animation - menu.transition = {to = 'child', target = this} - local start_offset = this.parent_menu and - (this.parent_menu.width + this.width) / 2 or - 0 - - tween_element(menu.transition.target, 0, 1, function(_, pos) - this:set_offset_x(round(start_offset * (1 - pos))) - this.opacity = pos - this:set_parent_opacity(1 - - ((1 - config.menu_parent_opacity) * - pos)) - end, function() - menu.transition = nil - update_proximities() - end) - end, - destroy = function(this) request_render() end, - on_display_resize = function(this) - this.item_height = (state.fullscreen or state.maximized) and - options.menu_item_height_fullscreen or - options.menu_item_height - this.font_size = round(this.item_height * 0.48 * - options.menu_font_scale) - this.item_content_spacing = round( - (this.item_height - this.font_size) * - 0.6) - this.scroll_step = this.item_height + this.item_spacing - - -- Estimate width of a widest item - local estimated_max_width = 0 - for _, item in ipairs(items) do - local item_text_length = - ((item.title and item.title:len() or 0) + - (item.hint and item.hint:len() or 0)) - local spacings_in_item = item.hint and 3 or 2 - local estimated_width = text_width_estimate(item_text_length, - this.font_size) + - (this.item_content_spacing * - spacings_in_item) - if estimated_width > estimated_max_width then - estimated_max_width = estimated_width - end - end - - -- Also check menu title - local menu_title_length = this.title and this.title:len() or 0 - local estimated_menu_title_width = - text_width_estimate(menu_title_length, this.font_size) - if estimated_menu_title_width > estimated_max_width then - estimated_max_width = estimated_menu_title_width - end - - -- Coordinates and sizes are of the scrollable area to make - -- consuming values in rendering easier. Title drawn above this, so - -- we need to account for that in max_height and ay position. - this.width = round(math.min(math.max(estimated_max_width, - config.menu_min_width), - display.width * 0.9)) - local title_height = this.title and this.scroll_step or 0 - local max_height = round(display.height * 0.9) - title_height - this.height = math.min(round(this.scroll_step * #items) - - this.item_spacing, max_height) - this.scroll_height = math.max( - (this.scroll_step * #this.items) - - this.height - this.item_spacing, 0) - this.ax = round((display.width - this.width) / 2) + this.offset_x - this.ay = round((display.height - this.height) / 2 + - (title_height / 2)) - this.bx = round(this.ax + this.width) - this.by = round(this.ay + this.height) - - if this.parent_menu then - this.parent_menu:on_display_resize() - end - end, - set_items = function(this, items, props) - this.items = items - this.selected_item = nil - this.active_item = nil - if props then - for key, value in pairs(props) do - this[key] = value - end - end - this:on_display_resize() - request_render() - end, - set_offset_x = function(this, offset) - local delta = offset - this.offset_x - this.offset_x = offset - this.ax = this.ax + delta - this.bx = this.bx + delta - if this.parent_menu then - this.parent_menu:set_offset_x( - offset - ((this.width + this.parent_menu.width) / 2) - - this.item_spacing) - else - update_proximities() - end - end, - fadeout = function(this, callback) - this:tween(1, 0, function(this, pos) - this.opacity = pos - this:set_parent_opacity(pos * config.menu_parent_opacity) - end, callback) - end, - set_parent_opacity = function(this, opacity) - if this.parent_menu then - this.parent_menu.opacity = opacity - this.parent_menu:set_parent_opacity( - opacity * config.menu_parent_opacity) - end - end, - get_item_index_below_cursor = function(this) - return math.ceil((cursor.y - this.ay + this.scroll_y) / - this.scroll_step) - end, - get_first_visible_index = function(this) - return round(this.scroll_y / this.scroll_step) + 1 - end, - get_last_visible_index = function(this) - return round((this.scroll_y + this.height) / this.scroll_step) - end, - get_centermost_visible_index = function(this) - return round((this.scroll_y + (this.height / 2)) / this.scroll_step) - end, - scroll_to = function(this, pos) - this.scroll_y = math.max(math.min(pos, this.scroll_height), 0) - request_render() - end, - scroll_to_item = function(this, index) - if (index and index >= 1 and index <= #this.items) then - this:scroll_to(round((this.scroll_step * (index - 1)) - - ((this.height - this.scroll_step) / 2))) - end - end, - select_index = function(this, index) - this.selected_item = - (index and index >= 1 and index <= #this.items) and index or nil - request_render() - end, - select_value = function(this, value) - this:select_index(itable_find(this.items, function(_, item) - return item.value == value - end)) - end, - activate_index = function(this, index) - this.active_item = - (index and index >= 1 and index <= #this.items) and index or nil - request_render() - end, - activate_value = function(this, value) - this:activate_index(itable_find(this.items, function(_, item) - return item.value == value - end)) - end, - delete_index = function(this, index) - if (index and index >= 1 and index <= #this.items) then - local previous_active_value = - this.active_index and this.items[this.active_index].value or - nil - table.remove(this.items, index) - this:on_display_resize() - if previous_active_value then - this:activate_value(previous_active_value) - end - this:scroll_to_item(this.selected_item) - end - end, - delete_value = function(this, value) - this:delete_index(itable_find(this.items, function(_, item) - return item.value == value - end)) - end, - prev = function(this) - local default_anchor = this.scroll_height > this.scroll_step and - this:get_centermost_visible_index() or - this:get_last_visible_index() - local current_index = this.selected_item or default_anchor + 1 - this.selected_item = math.max(current_index - 1, 1) - this:scroll_to_item(this.selected_item) - end, - next = function(this) - local default_anchor = this.scroll_height > this.scroll_step and - this:get_centermost_visible_index() or - this:get_first_visible_index() - local current_index = this.selected_item or default_anchor - 1 - this.selected_item = math.min(current_index + 1, #this.items) - this:scroll_to_item(this.selected_item) - end, - back = function(this) - if menu.transition then - local transition_target = menu.transition.target - local transition_target_type = menu.transition.target - tween_element_stop(transition_target) - if transition_target_type == 'parent' then - elements:add('menu', transition_target) - end - menu.transition = nil - transition_target:back() - return - else - menu.transition = {to = 'parent', target = this.parent_menu} - end - - if menu.transition.target == nil then - menu:close() - return - end - - local target = menu.transition.target - local to_offset = -target.offset_x + this.offset_x - - tween_element(target, 0, 1, function(_, pos) - this:set_offset_x(round(to_offset * pos)) - this.opacity = 1 - pos - this:set_parent_opacity(config.menu_parent_opacity + - ((1 - config.menu_parent_opacity) * - pos)) - end, function() - menu.transition = nil - elements:add('menu', target) - update_proximities() - end) - end, - open_selected_item = function(this) - -- If there is a transition active and this method got called, it - -- means we are animating from this menu to parent menu, and all - -- calls to this method should be relayed to the parent menu. - if menu.transition and menu.transition.to == 'parent' then - local target = menu.transition.target - tween_element_stop(target) - menu.transition = nil - target:open_selected_item() - return - end - - if this.selected_item then - local item = this.items[this.selected_item] - -- Is submenu - if item.items then - local opts = table_copy(opts) - opts.parent_menu = this - menu:open(item.items, this.open_item, opts) - else - menu:close(true) - this.open_item(item.value) - end - end - end, - close = function(this) menu:close() end, - on_global_mbtn_left_down = function(this) - if this.proximity_raw == 0 then - this.selected_item = this:get_item_index_below_cursor() - this:open_selected_item() - else - -- check if this is clicking on any parent menus - local parent_menu = this.parent_menu - repeat - if parent_menu then - if get_point_to_rectangle_proximity(cursor, parent_menu) == - 0 then - this:back() - return - end - parent_menu = parent_menu.parent_menu - end - until parent_menu == nil - - menu:close() - end - end, - on_global_mouse_move = function(this) - if this.proximity_raw == 0 then - this.selected_item = this:get_item_index_below_cursor() - else - if this.selected_item then - this.selected_item = nil - end - end - request_render() - end, - on_wheel_up = function(this) - this.selected_item = nil - this:scroll_to(this.scroll_y - this.scroll_step) - -- Selects item below cursor - this:on_global_mouse_move() - request_render() - end, - on_wheel_down = function(this) - this.selected_item = nil - this:scroll_to(this.scroll_y + this.scroll_step) - -- Selects item below cursor - this:on_global_mouse_move() - request_render() - end, - on_pgup = function(this) - this.selected_item = nil - this:scroll_to(this.scroll_y - this.height) - end, - on_pgdwn = function(this) - this.selected_item = nil - this:scroll_to(this.scroll_y + this.height) - end, - on_home = function(this) - this.selected_item = nil - this:scroll_to(0) - end, - on_end = function(this) - this.selected_item = nil - this:scroll_to(this.scroll_height) - end, - render = render_menu - })) - - elements.menu:maybe('on_open') -end - -function Menu:add_key_binding(key, name, fn, flags) - menu.key_bindings[#menu.key_bindings + 1] = name - mp.add_forced_key_binding(key, name, fn, flags) -end - -function Menu:enable_key_bindings() - menu.key_bindings = {} - -- The `mp.set_key_bindings()` method would be easier here, but that - -- doesn't support 'repeatable' flag, so we are stuck with this monster. - menu:add_key_binding('up', 'menu-prev', self:create_action('prev'), - 'repeatable') - menu:add_key_binding('down', 'menu-next', self:create_action('next'), - 'repeatable') - menu:add_key_binding('left', 'menu-back', self:create_action('back')) - menu:add_key_binding('right', 'menu-select', - self:create_action('open_selected_item')) - - if options.menu_wasd_navigation then - menu:add_key_binding('w', 'menu-prev-alt', self:create_action('prev'), - 'repeatable') - menu:add_key_binding('a', 'menu-back-alt', self:create_action('back')) - menu:add_key_binding('s', 'menu-next-alt', self:create_action('next'), - 'repeatable') - menu:add_key_binding('d', 'menu-select-alt', - self:create_action('open_selected_item')) - end - - if options.menu_hjkl_navigation then - menu:add_key_binding('h', 'menu-back-alt2', self:create_action('back')) - menu:add_key_binding('j', 'menu-next-alt2', self:create_action('next'), - 'repeatable') - menu:add_key_binding('k', 'menu-prev-alt2', self:create_action('prev'), - 'repeatable') - menu:add_key_binding('l', 'menu-select-alt2', - self:create_action('open_selected_item')) - end - - menu:add_key_binding('mbtn_back', 'menu-back-alt3', - self:create_action('back')) - menu:add_key_binding('bs', 'menu-back-alt4', self:create_action('back')) - menu:add_key_binding('enter', 'menu-select-alt3', - self:create_action('open_selected_item')) - menu:add_key_binding('kp_enter', 'menu-select-alt4', - self:create_action('open_selected_item')) - menu:add_key_binding('esc', 'menu-close', self:create_action('close')) - menu:add_key_binding('pgup', 'menu-page-up', self:create_action('on_pgup')) - menu:add_key_binding('pgdwn', 'menu-page-down', - self:create_action('on_pgdwn')) - menu:add_key_binding('home', 'menu-home', self:create_action('on_home')) - menu:add_key_binding('end', 'menu-end', self:create_action('on_end')) -end - -function Menu:disable_key_bindings() - for _, name in ipairs(menu.key_bindings) do mp.remove_key_binding(name) end - menu.key_bindings = {} -end - -function Menu:create_action(name) - return function(...) - if elements.menu then elements.menu:maybe(name, ...) end - end -end - -function Menu:close(immediate, callback) - if type(immediate) ~= 'boolean' then callback = immediate end - - if elements:has('menu') and not menu.is_closing then - function close() - elements.menu:maybe('on_close') - elements.menu:destroy() - elements:remove('menu') - menu.is_closing = false - update_proximities() - menu:disable_key_bindings() - call_me_maybe(callback) - end - - menu.is_closing = true - elements.curtain:fadeout() - - if immediate then - close() - else - elements.menu:fadeout(close) - end - end -end - --- ICONS ---[[ -ASS \shadN shadows are drawn also below the element, which when there is an -opacity in play, blends icon colors into ugly greys. The mess below is an -attempt to fix it by rendering shadows for icons with clipping. - -Add icons by adding functions to render them to `icons` table. - -Signature: function(pos_x, pos_y, size) => string - -Function has to return ass path coordinates to draw the icon centered at pox_x -and pos_y of passed size. -]] -local icons = {} -function icon(name, icon_x, icon_y, icon_size, shad_x, shad_y, shad_size, - backdrop, opacity, clip) - local ass = assdraw.ass_new() - local icon_path = icons[name](icon_x, icon_y, icon_size) - local icon_color = options['color_' .. backdrop .. '_text'] - local shad_color = options['color_' .. backdrop] - local use_border = (shad_x + shad_y) == 0 - local icon_border = use_border and shad_size or 0 - - -- clip can't clip out shadows, a very annoying limitation I can't work - -- around without going back to ugly default ass shadows, but atm I actually - -- don't need clipping of icons with shadows, so I'm choosing to ignore this - if not clip then clip = '' end - - if not use_border then - ass:new_event() - ass:append('{\\blur0\\bord0\\shad0\\1c&H' .. shad_color .. '\\iclip(' .. - ass.scale .. ', ' .. icon_path .. ')}') - ass:append(ass_opacity(opacity)) - ass:pos(shad_x + shad_size, shad_y + shad_size) - ass:draw_start() - ass:append(icon_path) - ass:draw_stop() - end - - ass:new_event() - ass:append( - '{\\blur0\\bord' .. icon_border .. '\\shad0\\1c&H' .. icon_color .. - '\\3c&H' .. shad_color .. clip .. '}') - ass:append(ass_opacity(opacity)) - ass:pos(0, 0) - ass:draw_start() - ass:append(icon_path) - ass:draw_stop() - - return ass.text -end - -function icons._volume(muted, pos_x, pos_y, size) - local ass = assdraw.ass_new() - local scale = size / 200 - function x(number) return pos_x + (number * scale) end - function y(number) return pos_y + (number * scale) end - ass:move_to(x(-85), y(-35)) - ass:line_to(x(-50), y(-35)) - ass:line_to(x(-5), y(-75)) - ass:line_to(x(-5), y(75)) - ass:line_to(x(-50), y(35)) - ass:line_to(x(-85), y(35)) - if muted then - ass:move_to(x(76), y(-35)) - ass:line_to(x(50), y(-9)) - ass:line_to(x(24), y(-35)) - ass:line_to(x(15), y(-26)) - ass:line_to(x(41), y(0)) - ass:line_to(x(15), y(26)) - ass:line_to(x(24), y(35)) - ass:line_to(x(50), y(9)) - ass:line_to(x(76), y(35)) - ass:line_to(x(85), y(26)) - ass:line_to(x(59), y(0)) - ass:line_to(x(85), y(-26)) - else - ass:move_to(x(20), y(-30)) - ass:line_to(x(20), y(30)) - ass:line_to(x(35), y(30)) - ass:line_to(x(35), y(-30)) - - ass:move_to(x(55), y(-60)) - ass:line_to(x(55), y(60)) - ass:line_to(x(70), y(60)) - ass:line_to(x(70), y(-60)) - end - return ass.text -end -function icons.volume(pos_x, pos_y, size) - return icons._volume(false, pos_x, pos_y, size) -end -function icons.volume_muted(pos_x, pos_y, size) - return icons._volume(true, pos_x, pos_y, size) -end - -function icons.arrow_right(pos_x, pos_y, size) - local ass = assdraw.ass_new() - local scale = size / 200 - function x(number) return pos_x + (number * scale) end - function y(number) return pos_y + (number * scale) end - ass:move_to(x(-22), y(-80)) - ass:line_to(x(-45), y(-57)) - ass:line_to(x(12), y(0)) - ass:line_to(x(-45), y(57)) - ass:line_to(x(-22), y(80)) - ass:line_to(x(58), y(0)) - return ass.text -end - --- STATE UPDATES +--[[ STATE UPDATERS ]] function update_display_dimensions() - local o = mp.get_property_native('osd-dimensions') - display.width = o.w - display.height = o.h - display.aspect = o.aspect + local scale = (state.hidpi_scale or 1) * options.ui_scale + local real_width, real_height = mp.get_osd_size() + if real_width <= 0 then return end + local scaled_width, scaled_height = round(real_width / scale), round(real_height / scale) + display.width, display.height = scaled_width, scaled_height + display.scale_x, display.scale_y = real_width / scaled_width, real_height / scaled_height + display.initialized = true - -- Tell elements about this - for _, element in elements:ipairs() do - if element.on_display_resize ~= nil then - element.on_display_resize(element) - end - end + -- Tell elements about this + Elements:trigger('display') + + -- Some elements probably changed their rectangles as a reaction to `display` + Elements:update_proximities() + request_render() end -function update_element_cursor_proximity(element) - if cursor.hidden then - element.proximity_raw = infinity - element.proximity = 0 - else - local range = options.proximity_out - options.proximity_in - element.proximity_raw = - get_point_to_rectangle_proximity(cursor, element) - element.proximity = menu:is_open() and 0 or 1 - - (math.min( - math.max( - element.proximity_raw - - options.proximity_in, 0), range) / - range) - end +function update_fullormaxed() + state.fullormaxed = state.fullscreen or state.maximized + update_display_dimensions() + Elements:trigger('prop_fullormaxed', state.fullormaxed) + update_cursor_position(INFINITY, INFINITY) end -function update_proximities() - local capture_mouse_buttons = false - local capture_wheel = false - local menu_only = menu:is_open() - local mouse_left_elements = {} - local mouse_entered_elements = {} - - -- Calculates proximities and opacities for defined elements - for _, element in elements:ipairs() do - local previous_proximity_raw = element.proximity_raw - - -- If menu is open, all other elements have to be disabled - if menu_only then - if element.name == 'menu' then - capture_mouse_buttons = true - capture_wheel = true - update_element_cursor_proximity(element) - else - element.proximity_raw = infinity - element.proximity = 0 - end - else - update_element_cursor_proximity(element) - end - - if element.proximity_raw == 0 then - -- Mouse is over element - if element.captures and element.captures.mouse_buttons then - capture_mouse_buttons = true - end - if element.captures and element.captures.wheel then - capture_wheel = true - end - - -- Mouse entered element area - if previous_proximity_raw ~= 0 then - mouse_entered_elements[#mouse_entered_elements + 1] = element - end - else - -- Mouse left element area - if previous_proximity_raw == 0 then - mouse_left_elements[#mouse_left_elements + 1] = element - end - end - end - - -- Enable key group captures elements request. - if capture_mouse_buttons then - forced_key_bindings.mouse_buttons:enable() - else - forced_key_bindings.mouse_buttons:disable() - end - if capture_wheel then - forced_key_bindings.wheel:enable() - else - forced_key_bindings.wheel:disable() - end - - -- Trigger `mouse_leave` and `mouse_enter` events - for _, element in ipairs(mouse_left_elements) do - element:trigger('mouse_leave') - end - for _, element in ipairs(mouse_entered_elements) do - element:trigger('mouse_enter') - end +function update_human_times() + if state.time then + state.time_human = format_time(state.time, state.duration) + if state.duration then + local speed = state.speed or 1 + if options.destination_time == 'playtime-remaining' then + state.destination_time_human = format_time((state.time - state.duration) / speed, state.duration) + elseif options.destination_time == 'total' then + state.destination_time_human = format_time(state.duration, state.duration) + else + state.destination_time_human = format_time(state.time - state.duration, state.duration) + end + else + state.destination_time_human = nil + end + else + state.time_human = nil + end end --- ELEMENT RENDERERS +-- Notifies other scripts such as console about where the unoccupied parts of the screen are. +function update_margins() + if display.height == 0 then return end -function render_timeline(this) - if this.size_max == 0 or state.duration == nil or state.position == nil then - return - end + local function is_persistent(element) return element and element.enabled and element:is_persistent() end + local timeline, top_bar, controls, volume = Elements.timeline, Elements.top_bar, Elements.controls, Elements.volume + -- margins are normalized to window size + local left, right, top, bottom = 0, 0, 0, 0 - local size_min = this:get_effective_size_min() - local size = this:get_effective_size() + if is_persistent(controls) then bottom = (display.height - controls.ay) / display.height + elseif is_persistent(timeline) then bottom = (display.height - timeline.ay) / display.height end - if size < 1 then return end + if is_persistent(top_bar) then top = top_bar.title_by / display.height end - local ass = assdraw.ass_new() + if is_persistent(volume) then + if options.volume == 'left' then left = volume.bx / display.width + elseif options.volume == 'right' then right = volume.ax / display.width end + end - -- Text opacity rapidly drops to 0 just before it starts overflowing, or before it reaches timeline.size_min - local hide_text_below = math.max(this.font_size * 0.7, size_min * 2) - local hide_text_ramp = hide_text_below / 2 - local text_opacity = math.max(math.min(size - hide_text_below, - hide_text_ramp), 0) / hide_text_ramp + if top == state.margin_top and bottom == state.margin_bottom and + left == state.margin_left and right == state.margin_right then return end - local spacing = math.max(math.floor((this.size_max - this.font_size) / 2.5), - 4) - local progress = state.position / state.duration + state.margin_top = top + state.margin_bottom = bottom + state.margin_left = left + state.margin_right = right - -- Background bar coordinates - local bax = 0 - local bay = display.height - size - this.bottom_border - this.top_border - local bbx = display.width - local bby = display.height + utils.shared_script_property_set('osc-margins', string.format('%f,%f,%f,%f', 0, 0, top, bottom)) + mp.set_property_native('user-data/osc/margins', { l = left, r = right, t = top, b = bottom }) - -- Foreground bar coordinates - local fax = bax - local fay = bay + this.top_border - local fbx = bbx * progress - local fby = bby - this.bottom_border - local foreground_size = bby - bay - local foreground_coordinates = fax .. ',' .. fay .. ',' .. fbx .. ',' .. fby -- for clipping - - -- Background - ass:new_event() - ass:append( - '{\\blur0\\bord0\\1c&H' .. options.color_background .. '\\iclip(' .. - foreground_coordinates .. ')}') - ass:append(ass_opacity(math.max(options.timeline_opacity - 0.1, 0))) - ass:pos(0, 0) - ass:draw_start() - ass:rect_cw(bax, bay, bbx, bby) - ass:draw_stop() - - -- Foreground - ass:new_event() - ass:append('{\\blur0\\bord0\\1c&H' .. options.color_foreground .. '}') - ass:append(ass_opacity(options.timeline_opacity)) - ass:pos(0, 0) - ass:draw_start() - ass:rect_cw(fax, fay, fbx, fby) - ass:draw_stop() - - -- Seekable ranges - if options.timeline_cached_ranges and state.cached_ranges then - local range_height = math.max(foreground_size / 8, size_min) - local range_ay = fby - range_height - for _, range in ipairs(state.cached_ranges) do - ass:new_event() - ass:append('{\\blur0\\bord0\\1c&H' .. - options.timeline_cached_ranges.color .. '}') - ass:append(ass_opacity(options.timeline_cached_ranges.opacity)) - ass:pos(0, 0) - ass:draw_start() - ass:rect_cw(bbx * (range['start'] / state.duration), range_ay, - bbx * (range['end'] / state.duration), - range_ay + range_height) - ass:draw_stop() - end - end - - -- Custom ranges - if state.chapter_ranges ~= nil then - for i, chapter_range in ipairs(state.chapter_ranges) do - for i, range in ipairs(chapter_range.ranges) do - local rax = display.width * - (range['start'].time / state.duration) - local rbx = display.width * (range['end'].time / state.duration) - ass:new_event() - ass:append('{\\blur0\\bord0\\1c&H' .. chapter_range.color .. '}') - ass:append(ass_opacity(chapter_range.opacity)) - ass:pos(0, 0) - ass:draw_start() - -- for 1px chapter size, use the whole size of the bar including padding - if size <= 1 then - ass:rect_cw(rax, bay, rbx, bby) - else - ass:rect_cw(rax, fay, rbx, fby) - end - ass:draw_stop() - end - end - end - - -- Chapters - if options.chapters ~= 'none' and state.chapters ~= nil and #state.chapters > - 0 then - local half_size = size / 2 - local dots = false - local chapter_size, chapter_y - if options.chapters == 'dots' then - dots = true - chapter_size = math.min(6, (foreground_size / 2) + 2) - chapter_y = math.min(fay + chapter_size, fay + half_size) - elseif options.chapters == 'lines' then - chapter_size = size - chapter_y = fay + (chapter_size / 2) - elseif options.chapters == 'lines-top' then - chapter_size = math.min(this.size_max / 3.5, size) - chapter_y = fay + (chapter_size / 2) - elseif options.chapters == 'lines-bottom' then - chapter_size = math.min(this.size_max / 3.5, size) - chapter_y = fay + size - (chapter_size / 2) - end - - if chapter_size ~= nil then - -- for 1px chapter size, use the whole size of the bar including padding - chapter_size = size <= 1 and foreground_size or chapter_size - local chapter_half_size = chapter_size / 2 - - for i, chapter in ipairs(state.chapters) do - local chapter_x = display.width * - (chapter.time / state.duration) - local color = chapter_x > fbx and options.color_foreground or - options.color_background - - ass:new_event() - ass:append('{\\blur0\\bord0\\1c&H' .. color .. '}') - ass:append(ass_opacity(options.chapters_opacity)) - ass:pos(0, 0) - ass:draw_start() - - if dots then - local bezier_stretch = chapter_size * 0.67 - ass:move_to(chapter_x - chapter_half_size, chapter_y) - ass:bezier_curve(chapter_x - chapter_half_size, - chapter_y - bezier_stretch, - chapter_x + chapter_half_size, - chapter_y - bezier_stretch, - chapter_x + chapter_half_size, chapter_y) - ass:bezier_curve(chapter_x + chapter_half_size, - chapter_y + bezier_stretch, - chapter_x - chapter_half_size, - chapter_y + bezier_stretch, - chapter_x - chapter_half_size, chapter_y) - else - ass:rect_cw(chapter_x, chapter_y - chapter_half_size, - chapter_x + 1, chapter_y + chapter_half_size) - end - - ass:draw_stop() - end - end - end - - if text_opacity > 0 then - -- Elapsed time - if state.elapsed_seconds then - ass:new_event() - ass:append('{\\blur0\\bord0\\shad0\\1c&H' .. - options.color_foreground_text .. '\\fn' .. - config.font .. '\\fs' .. this.font_size .. bold_tag .. - '\\clip(' .. foreground_coordinates .. ')') - ass:append(ass_opacity(math.min(options.timeline_opacity + 0.1, 1), - text_opacity)) - ass:pos(spacing, fay + (size / 2)) - ass:an(4) - ass:append(state.elapsed_time) - ass:new_event() - ass:append('{\\blur0\\bord0\\shad1\\1c&H' .. - options.color_background_text .. '\\4c&H' .. - options.color_background .. '\\fn' .. config.font .. - '\\fs' .. this.font_size .. bold_tag .. '\\iclip(' .. - foreground_coordinates .. ')') - ass:append(ass_opacity(math.min(options.timeline_opacity + 0.1, 1), - text_opacity)) - ass:pos(spacing, fay + (size / 2)) - ass:an(4) - ass:append(state.elapsed_time) - end - - -- Remaining time - if state.remaining_seconds then - ass:new_event() - ass:append('{\\blur0\\bord0\\shad0\\1c&H' .. - options.color_foreground_text .. '\\fn' .. - config.font .. '\\fs' .. this.font_size .. bold_tag .. - '\\clip(' .. foreground_coordinates .. ')') - ass:append(ass_opacity(math.min(options.timeline_opacity + 0.1, 1), - text_opacity)) - ass:pos(display.width - spacing, fay + (size / 2)) - ass:an(6) - ass:append('-' .. state.remaining_time) - ass:new_event() - ass:append('{\\blur0\\bord0\\shad1\\1c&H' .. - options.color_background_text .. '\\4c&H' .. - options.color_background .. '\\fn' .. config.font .. - '\\fs' .. this.font_size .. bold_tag .. '\\iclip(' .. - foreground_coordinates .. ')') - ass:append(ass_opacity(math.min(options.timeline_opacity + 0.1, 1), - text_opacity)) - ass:pos(display.width - spacing, fay + (size / 2)) - ass:an(6) - ass:append('-' .. state.remaining_time) - end - end - - if (this.proximity_raw == 0 or this.pressed) and - not (elements.speed and elements.speed.dragging) then - -- Hovered time - local hovered_seconds = state.duration * (cursor.x / display.width) - local box_half_width_guesstimate = (this.font_size * 4.2) / 2 - ass:new_event() - ass:append('{\\blur0\\bord1\\shad0\\1c&H' .. - options.color_background_text .. '\\3c&H' .. - options.color_background .. '\\fn' .. config.font .. - '\\fs' .. this.font_size .. bold_tag .. '') - ass:append(ass_opacity(math.min(options.timeline_opacity + 0.1, 1))) - ass:pos(math.min(math.max(cursor.x, box_half_width_guesstimate), - display.width - box_half_width_guesstimate), fay) - ass:an(2) - ass:append(mp.format_time(hovered_seconds)) - - -- Cursor line - ass:new_event() - ass:append('{\\blur0\\bord0\\xshad-1\\yshad0\\1c&H' .. - options.color_foreground .. '\\4c&H' .. - options.color_background .. '}') - ass:append(ass_opacity(0.2)) - ass:pos(0, 0) - ass:draw_start() - ass:rect_cw(cursor.x, fay, cursor.x + 1, fby) - ass:draw_stop() - end - - return ass + if not options.adjust_osd_margins then return end + local osd_margin_y, osd_margin_x, osd_factor_x = 0, 0, display.width / display.height * 720 + if config.osd_alignment_y == 'bottom' then osd_margin_y = round(bottom * 720) + elseif config.osd_alignment_y == 'top' then osd_margin_y = round(top * 720) end + if config.osd_alignment_x == 'left' then osd_margin_x = round(left * osd_factor_x) + elseif config.osd_alignment_x == 'right' then osd_margin_x = round(right * osd_factor_x) end + mp.set_property_native('osd-margin-y', osd_margin_y + config.osd_margin_y) + mp.set_property_native('osd-margin-x', osd_margin_x + config.osd_margin_x) +end +function create_state_setter(name, callback) + return function(_, value) + set_state(name, value) + if callback then callback() end + request_render() + end end -function render_top_bar(this) - local opacity = this:get_effective_proximity() - - if not this.enabled or opacity == 0 then return end - - local ass = assdraw.ass_new() - - if options.top_bar_controls then - -- Close button - local close = elements.window_controls_close - if close.proximity_raw == 0 then - -- Background on hover - ass:new_event() - ass:append('{\\blur0\\bord0\\1c&H2311e8}') - ass:append(ass_opacity(this.button_opacity, opacity)) - ass:pos(0, 0) - ass:draw_start() - ass:rect_cw(close.ax, close.ay, close.bx, close.by) - ass:draw_stop() - end - ass:new_event() - ass:append('{\\blur0\\bord1\\shad1\\3c&HFFFFFF\\4c&H000000}') - ass:append(ass_opacity(this.button_opacity, opacity)) - ass:pos(close.ax + (this.button_width / 2), (this.size / 2)) - ass:draw_start() - ass:move_to(-this.icon_size, this.icon_size) - ass:line_to(this.icon_size, -this.icon_size) - ass:move_to(-this.icon_size, -this.icon_size) - ass:line_to(this.icon_size, this.icon_size) - ass:draw_stop() - - -- Maximize button - local maximize = elements.window_controls_maximize - if maximize.proximity_raw == 0 then - -- Background on hover - ass:new_event() - ass:append('{\\blur0\\bord0\\1c&H222222}') - ass:append(ass_opacity(this.button_opacity, opacity)) - ass:pos(0, 0) - ass:draw_start() - ass:rect_cw(maximize.ax, maximize.ay, maximize.bx, maximize.by) - ass:draw_stop() - end - ass:new_event() - ass:append('{\\blur0\\bord2\\shad0\\1c\\3c&H000000}') - ass:append(ass_opacity({[3] = this.button_opacity}, opacity)) - ass:pos(maximize.ax + (this.button_width / 2), (this.size / 2)) - ass:draw_start() - ass:rect_cw(-this.icon_size + 1, -this.icon_size + 1, - this.icon_size + 1, this.icon_size + 1) - ass:draw_stop() - ass:new_event() - ass:append('{\\blur0\\bord2\\shad0\\1c\\3c&HFFFFFF}') - ass:append(ass_opacity({[3] = this.button_opacity}, opacity)) - ass:pos(maximize.ax + (this.button_width / 2), (this.size / 2)) - ass:draw_start() - ass:rect_cw(-this.icon_size, -this.icon_size, this.icon_size, - this.icon_size) - ass:draw_stop() - - -- Minimize button - local minimize = elements.window_controls_minimize - if minimize.proximity_raw == 0 then - -- Background on hover - ass:new_event() - ass:append('{\\blur0\\bord0\\1c&H222222}') - ass:append(ass_opacity(this.button_opacity, opacity)) - ass:pos(0, 0) - ass:draw_start() - ass:rect_cw(minimize.ax, minimize.ay, minimize.bx, minimize.by) - ass:draw_stop() - end - ass:new_event() - ass:append('{\\blur0\\bord1\\shad1\\3c&HFFFFFF\\4c&H000000}') - ass:append(ass_opacity(this.button_opacity, opacity)) - ass:append('{\\1a&HFF&}') - ass:pos(minimize.ax + (this.button_width / 2), (this.size / 2)) - ass:draw_start() - ass:move_to(-this.icon_size, 0) - ass:line_to(this.icon_size, 0) - ass:draw_stop() - end - - -- Window title - if options.top_bar_title and state.media_title then - local clip_coordinates = - '0,0,' .. (this.title_bx - this.spacing) .. ',' .. this.size - - ass:new_event() - ass:append('{\\q2\\blur0\\bord1\\shad0\\1c&HFFFFFF\\3c&H000000\\fn' .. - config.font .. '\\fs' .. this.font_size .. bold_tag .. - '\\clip(' .. clip_coordinates .. ')') - ass:append(ass_opacity(1, opacity)) - ass:pos(0 + this.spacing, this.size / 2) - ass:an(4) - ass:append(state.media_title) - end - - return ass +function set_state(name, value) + state[name] = value + Elements:trigger('prop_' .. name, value) end -function render_volume(this) - local slider = elements.volume_slider - local opacity = this:get_effective_proximity() - - if this.width == 0 or opacity == 0 then return end - - local ass = assdraw.ass_new() - - if slider.height > 0 then - -- Background bar coordinates - local bax = slider.ax - local bay = slider.ay - local bbx = slider.bx - local bby = slider.by - - -- Foreground bar coordinates - local height_without_border = slider.height - - (options.volume_border * 2) - local fax = slider.ax + options.volume_border - local fay = slider.ay + - (height_without_border * - (1 - math.min(state.volume / state.volume_max, 1))) + - options.volume_border - local fbx = slider.bx - options.volume_border - local fby = slider.by - options.volume_border - - -- Path to draw a foreground bar with a 100% volume indicator, already - -- clipped by volume level. Can't just clip it with rectangle, as it itself - -- also needs to be used as a path to clip the background bar and volume - -- number. - local fpath = assdraw.ass_new() - fpath:move_to(fbx, fby) - fpath:line_to(fax, fby) - local nudge_bottom_y = slider.nudge_y + slider.nudge_size - if fay <= nudge_bottom_y and slider.draw_nudge then - fpath:line_to(fax, math.min(nudge_bottom_y)) - if fay <= slider.nudge_y then - fpath:line_to((fax + slider.nudge_size), slider.nudge_y) - local nudge_top_y = slider.nudge_y - slider.nudge_size - if fay <= nudge_top_y then - fpath:line_to(fax, nudge_top_y) - fpath:line_to(fax, fay) - fpath:line_to(fbx, fay) - fpath:line_to(fbx, nudge_top_y) - else - local triangle_side = fay - nudge_top_y - fpath:line_to((fax + triangle_side), fay) - fpath:line_to((fbx - triangle_side), fay) - end - fpath:line_to((fbx - slider.nudge_size), slider.nudge_y) - else - local triangle_side = nudge_bottom_y - fay - fpath:line_to((fax + triangle_side), fay) - fpath:line_to((fbx - triangle_side), fay) - end - fpath:line_to(fbx, nudge_bottom_y) - else - fpath:line_to(fax, fay) - fpath:line_to(fbx, fay) - end - fpath:line_to(fbx, fby) - - -- Background - ass:new_event() - ass:append('{\\blur0\\bord0\\1c&H' .. options.color_background .. - '\\iclip(' .. fpath.scale .. ', ' .. fpath.text .. ')}') - ass:append(ass_opacity(math.max(options.volume_opacity - 0.1, 0), - opacity)) - ass:pos(0, 0) - ass:draw_start() - ass:move_to(bax, bay) - ass:line_to(bbx, bay) - local half_border = options.volume_border / 2 - if slider.draw_nudge then - ass:line_to(bbx, math.max(slider.nudge_y - slider.nudge_size + - half_border, bay)) - ass:line_to(bbx - slider.nudge_size + half_border, slider.nudge_y) - ass:line_to(bbx, slider.nudge_y + slider.nudge_size - half_border) - end - ass:line_to(bbx, bby) - ass:line_to(bax, bby) - if slider.draw_nudge then - ass:line_to(bax, slider.nudge_y + slider.nudge_size - half_border) - ass:line_to(bax + slider.nudge_size - half_border, slider.nudge_y) - ass:line_to(bax, math.max(slider.nudge_y - slider.nudge_size + - half_border, bay)) - end - ass:line_to(bax, bay) - ass:draw_stop() - - -- Foreground - ass:new_event() - ass:append('{\\blur0\\bord0\\1c&H' .. options.color_foreground .. '}') - ass:append(ass_opacity(options.volume_opacity, opacity)) - ass:pos(0, 0) - ass:draw_start() - ass:append(fpath.text) - ass:draw_stop() - - -- Current volume value - local volume_string = tostring(round(state.volume * 10) / 10) - local font_size = round(((this.width * 0.6) - - (#volume_string * (this.width / 20))) * - options.volume_font_scale) - if fay < slider.by - slider.spacing then - ass:new_event() - ass:append('{\\blur0\\bord0\\shad0\\1c&H' .. - options.color_foreground_text .. '\\fn' .. - config.font .. '\\fs' .. font_size .. bold_tag .. - '\\clip(' .. fpath.scale .. ', ' .. fpath.text .. - ')}') - ass:append(ass_opacity(math.min(options.volume_opacity + 0.1, 1), - opacity)) - ass:pos(slider.ax + (slider.width / 2), slider.by - slider.spacing) - ass:an(2) - ass:append(volume_string) - end - if fay > slider.by - slider.spacing - font_size then - ass:new_event() - ass:append('{\\blur0\\bord0\\shad1\\1c&H' .. - options.color_background_text .. '\\4c&H' .. - options.color_background .. '\\fn' .. config.font .. - '\\fs' .. font_size .. bold_tag .. '\\iclip(' .. - fpath.scale .. ', ' .. fpath.text .. ')}') - ass:append(ass_opacity(math.min(options.volume_opacity + 0.1, 1), - opacity)) - ass:pos(slider.ax + (slider.width / 2), slider.by - slider.spacing) - ass:an(2) - ass:append(volume_string) - end - end - - -- Mute button - local mute = elements.volume_mute - local icon_name = state.mute and 'volume_muted' or 'volume' - ass:new_event() - ass:append(icon(icon_name, mute.ax + (mute.width / 2), - mute.ay + (mute.height / 2), mute.width * 0.7, -- x, y, size - 0, 0, options.volume_border, -- shadow_x, shadow_y, shadow_size - 'background', options.volume_opacity * opacity -- backdrop, opacity - )) - return ass -end - -function render_speed(this) - if not this.dragging and (elements.curtain.opacity > 0) then return end - - local timeline = elements.timeline - local proximity = timeline:get_effective_proximity() - local opacity = this.forced_proximity and this.forced_proximity or - (this.dragging and 1 or proximity) - - if opacity == 0 then return end - - local ass = assdraw.ass_new() - - -- Coordinates - local ax = this.ax - local ay = this.ay + timeline.size_max - timeline:get_effective_size() - - timeline.top_border - timeline.bottom_border - local bx = this.bx - local by = ay + this.height - local half_width = (this.width / 2) - local half_x = ax + half_width - - -- Notches - local speed_at_center = state.speed - if this.dragging then - speed_at_center = this.dragging.start_speed + - ((-this.dragging.distance / this.step_distance) * - options.speed_step) - speed_at_center = math.min(math.max(speed_at_center, 0.01), 100) - end - local nearest_notch_speed = round(speed_at_center / this.notch_every) * - this.notch_every - local nearest_notch_x = half_x + - (((nearest_notch_speed - speed_at_center) / - this.notch_every) * this.notch_spacing) - local guide_size = math.floor(this.height / 7.5) - local notch_by = by - guide_size - local notch_ay_big = ay + round(this.font_size * 1.1) - local notch_ay_medium = notch_ay_big + ((notch_by - notch_ay_big) * 0.2) - local notch_ay_small = notch_ay_big + ((notch_by - notch_ay_big) * 0.4) - local from_to_index = math.floor(this.notches / 2) - - for i = -from_to_index, from_to_index do - local notch_speed = nearest_notch_speed + (i * this.notch_every) - - if notch_speed < 0 or notch_speed > 100 then goto continue end - - local notch_x = nearest_notch_x + (i * this.notch_spacing) - local notch_thickness = 1 - local notch_ay = notch_ay_small - if (notch_speed % (this.notch_every * 10)) < 0.00000001 then - notch_ay = notch_ay_big - notch_thickness = 1 - elseif (notch_speed % (this.notch_every * 5)) < 0.00000001 then - notch_ay = notch_ay_medium - end - - ass:new_event() - ass:append('{\\blur0\\bord1\\shad0\\1c&HFFFFFF\\3c&H000000}') - ass:append(ass_opacity(math.min(1.2 - - (math.abs( - (notch_x - ax - half_width) / - half_width)), 1), opacity)) - ass:pos(0, 0) - ass:draw_start() - ass:move_to(notch_x - notch_thickness, notch_ay) - ass:line_to(notch_x + notch_thickness, notch_ay) - ass:line_to(notch_x + notch_thickness, notch_by) - ass:line_to(notch_x - notch_thickness, notch_by) - ass:draw_stop() - - ::continue:: - end - - -- Center guide - ass:new_event() - ass:append('{\\blur0\\bord1\\shad0\\1c&HFFFFFF\\3c&H000000}') - ass:append(ass_opacity(options.speed_opacity, opacity)) - ass:pos(0, 0) - ass:draw_start() - ass:move_to(half_x, by - 2 - guide_size) - ass:line_to(half_x + guide_size, by - 2) - ass:line_to(half_x - guide_size, by - 2) - ass:draw_stop() - - -- Speed value - local speed_text = (round(state.speed * 100) / 100) .. 'x' - ass:new_event() - ass:append( - '{\\blur0\\bord1\\shad0\\1c&H' .. options.color_background_text .. - '\\3c&H' .. options.color_background .. '\\fn' .. config.font .. - '\\fs' .. this.font_size .. bold_tag .. '}') - ass:append(ass_opacity(options.speed_opacity, opacity)) - ass:pos(half_x, ay) - ass:an(8) - ass:append(speed_text) - - return ass -end - -function render_menu(this) - local ass = assdraw.ass_new() - - if this.parent_menu then ass:merge(this.parent_menu:render()) end - - -- Menu title - if this.title then - -- Background - ass:new_event() - ass:append('{\\blur0\\bord0\\1c&H' .. options.color_background .. '}') - ass:append(ass_opacity(options.menu_opacity, this.opacity)) - ass:pos(0, 0) - ass:draw_start() - ass:rect_cw(this.ax, this.ay - this.item_height, this.bx, this.ay - 1) - ass:draw_stop() - - -- Title - ass:new_event() - ass:append('{\\blur0\\bord0\\shad1\\b1\\1c&H' .. - options.color_background_text .. '\\4c&H' .. - options.color_background .. '\\fn' .. config.font .. - '\\fs' .. this.font_size .. '\\q2\\clip(' .. this.ax .. - ',' .. this.ay - this.item_height .. ',' .. this.bx .. - ',' .. this.ay .. ')}') - ass:append(ass_opacity(options.menu_opacity, this.opacity)) - ass:pos(display.width / 2, this.ay - (this.item_height * 0.5)) - ass:an(5) - ass:append(this.title) - end - - local scroll_area_clip = '\\clip(' .. this.ax .. ',' .. this.ay .. ',' .. - this.bx .. ',' .. this.by .. ')' - - for index, item in ipairs(this.items) do - local item_ay = this.ay - this.scroll_y + - (this.item_height * (index - 1) + this.item_spacing * - (index - 1)) - local item_by = item_ay + this.item_height - local item_clip = '' - - -- Clip items overflowing scroll area - if item_ay <= this.ay or item_by >= this.by then - item_clip = scroll_area_clip - end - - if item_by < this.ay or item_ay > this.by then goto continue end - - local is_active = this.active_item == index - local font_color, background_color, ass_shadow, ass_shadow_color - local icon_size = this.font_size - - if is_active then - font_color, background_color = options.color_foreground_text, - options.color_foreground - ass_shadow, ass_shadow_color = '\\shad0', '' - else - font_color, background_color = options.color_background_text, - options.color_background - ass_shadow, ass_shadow_color = '\\shad1', - '\\4c&H' .. background_color - end - - local has_submenu = item.items ~= nil - local hint_width = 0 - if item.hint then - hint_width = text_width_estimate(item.hint:len(), this.font_size) + - this.item_content_spacing - elseif has_submenu then - hint_width = icon_size + this.item_content_spacing - end - - -- Background - ass:new_event() - ass:append('{\\blur0\\bord0\\1c&H' .. background_color .. item_clip .. - '}') - ass:append(ass_opacity(options.menu_opacity, this.opacity)) - ass:pos(0, 0) - ass:draw_start() - ass:rect_cw(this.ax, item_ay, this.bx, item_by) - ass:draw_stop() - - -- Selected highlight - if this.selected_item == index then - ass:new_event() - ass:append('{\\blur0\\bord0\\1c&H' .. options.color_foreground .. - item_clip .. '}') - ass:append(ass_opacity(0.1, this.opacity)) - ass:pos(0, 0) - ass:draw_start() - ass:rect_cw(this.ax, item_ay, this.bx, item_by) - ass:draw_stop() - end - - -- Title - if item.title then - item.ass_save_title = item.ass_save_title or - item.title:gsub("([{}])", "\\%1") - local title_clip_x = (this.bx - hint_width - - this.item_content_spacing) - local title_clip = '\\clip(' .. this.ax .. ',' .. - math.max(item_ay, this.ay) .. ',' .. - title_clip_x .. ',' .. - math.min(item_by, this.by) .. ')' - ass:new_event() - ass:append( - '{\\blur0\\bord0\\shad1\\1c&H' .. font_color .. '\\4c&H' .. - background_color .. '\\fn' .. config.font .. '\\fs' .. - this.font_size .. bold_tag .. title_clip .. '\\q2}') - ass:append(ass_opacity(options.menu_opacity, this.opacity)) - ass:pos(this.ax + this.item_content_spacing, - item_ay + (this.item_height / 2)) - ass:an(4) - ass:append(item.ass_save_title) - end - - -- Hint - if item.hint then - item.ass_save_hint = item.ass_save_hint or - item.hint:gsub("([{}])", "\\%1") - ass:new_event() - ass:append( - '{\\blur0\\bord0' .. ass_shadow .. '\\1c&H' .. font_color .. '' .. - ass_shadow_color .. '\\fn' .. config.font .. '\\fs' .. - (this.font_size - 1) .. bold_tag .. item_clip .. '}') - ass:append(ass_opacity(options.menu_opacity * - (has_submenu and 1 or 0.5), this.opacity)) - ass:pos(this.bx - this.item_content_spacing, - item_ay + (this.item_height / 2)) - ass:an(6) - ass:append(item.ass_save_hint) - elseif has_submenu then - ass:new_event() - ass:append(icon('arrow_right', - this.bx - this.item_content_spacing - - (icon_size / 2), -- x - item_ay + (this.item_height / 2), -- y - icon_size, -- size - 0, 0, 1, -- shadow_x, shadow_y, shadow_size - is_active and 'foreground' or 'background', this.opacity, -- backdrop, opacity - item_clip)) - end - - ::continue:: - end - - -- Scrollbar - if this.scroll_height > 0 then - local groove_height = this.height - 2 - local thumb_height = math.max((this.height / - (this.scroll_height + this.height)) * - groove_height, 40) - local thumb_y = this.ay + 1 + - ((this.scroll_y / this.scroll_height) * - (groove_height - thumb_height)) - ass:new_event() - ass:append('{\\blur0\\bord0\\1c&H' .. options.color_foreground .. '}') - ass:append(ass_opacity(options.menu_opacity, this.opacity * 0.8)) - ass:pos(0, 0) - ass:draw_start() - ass:rect_cw(this.bx - 3, thumb_y, this.bx - 1, thumb_y + thumb_height) - ass:draw_stop() - end - - return ass -end - --- MAIN RENDERING - --- Request that render() is called. --- The render is then either executed immediately, or rate-limited if it was --- called a small time ago. -function request_render() - if state.render_timer == nil then - state.render_timer = mp.add_timeout(0, render) - end - - if not state.render_timer:is_enabled() then - local now = mp.get_time() - local timeout = config.render_delay - (now - state.render_last_time) - if timeout < 0 then timeout = 0 end - state.render_timer.timeout = timeout - state.render_timer:resume() - end -end - -function render() - state.render_last_time = mp.get_time() - - -- Actual rendering - local ass = assdraw.ass_new() - - for _, element in elements.ipairs() do - local result = element:maybe('render') - if result then - ass:new_event() - ass:merge(result) - end - end - - -- submit - if osd.res_x == display.width and osd.res_y == display.height and osd.data == - ass.text then return end - - osd.res_x = display.width - osd.res_y = display.height - osd.data = ass.text - osd.z = 2000 - osd:update() -end - --- STATIC ELEMENTS - -if itable_find({'flash', 'static'}, options.pause_indicator) then - elements:add('pause_indicator', Element.new( - { - base_icon_opacity = options.pause_indicator == 'flash' and 1 or 0.8, - paused = false, - is_flash = options.pause_indicator == 'flash', - is_static = options.pause_indicator == 'static', - opacity = 0, - init = function(this) - local initial_call = true - mp.observe_property('pause', 'bool', function(_, paused) - if initial_call then - initial_call = false - return - end - - this.paused = paused - - if options.pause_indicator == 'flash' then - this.opacity = 1 - this:tween_property('opacity', 1, 0, 0.15) - else - this.opacity = paused and 1 or 0 - request_render() - end - - end) - end, - render = function(this) - if this.opacity == 0 then return end - - local ass = assdraw.ass_new() - - -- Background fadeout - if this.is_static then - ass:new_event() - ass:append('{\\blur0\\bord0\\1c&H' .. - options.color_background .. '}') - ass:append(ass_opacity(0.3, this.opacity)) - ass:pos(0, 0) - ass:draw_start() - ass:rect_cw(0, 0, display.width, display.height) - ass:draw_stop() - end - - -- Icon - local size = round((math.min(display.width, display.height) * - (this.is_static and 0.20 or 0.15)) / 2) - - size = size + size * (1 - this.opacity) - - if this.paused then - ass:new_event() - ass:append('{\\blur0\\bord1\\1c&H' .. - options.color_foreground .. '\\3c&H' .. - options.color_background .. '}') - ass:append(ass_opacity(this.base_icon_opacity, this.opacity)) - ass:pos(display.width / 2, display.height / 2) - ass:draw_start() - ass:rect_cw(-size, -size, -size / 3, size) - ass:draw_stop() - - ass:new_event() - ass:append('{\\blur0\\bord1\\1c&H' .. - options.color_foreground .. '\\3c&H' .. - options.color_background .. '}') - ass:append(ass_opacity(this.base_icon_opacity, this.opacity)) - ass:pos(display.width / 2, display.height / 2) - ass:draw_start() - ass:rect_cw(size / 3, -size, size, size) - ass:draw_stop() - elseif this.is_flash then - ass:new_event() - ass:append('{\\blur0\\bord1\\1c&H' .. - options.color_foreground .. '\\3c&H' .. - options.color_background .. '}') - ass:append(ass_opacity(this.base_icon_opacity, this.opacity)) - ass:pos(display.width / 2, display.height / 2) - ass:draw_start() - ass:move_to(-size * 0.6, -size) - ass:line_to(size, 0) - ass:line_to(-size * 0.6, size) - ass:draw_stop() - end - - return ass - end - })) -end -elements:add('timeline', Element.new({ - captures = {mouse_buttons = true, wheel = true}, - pressed = false, - size_max = 0, - size_min = 0, -- set in `on_display_resize` handler based on `state.fullscreen` - size_min_override = options.timeline_start_hidden and 0 or nil, -- used for toggle-progress command - font_size = 0, -- calculated in on_display_resize - top_border = options.timeline_border, - bottom_border = 0, -- set dynamically in `border` property observer - init = function(this) - -- Toggle 1px bottom border for timeline in no-border mode - mp.observe_property('border', 'bool', function(_, border) - this.bottom_border = not border and options.timeline_border or 0 - request_render() - end) - - -- Flash on external changes - if options.timeline_flash then - mp.register_event('seek', function() - local position = mp.get_property_native('playback-time') - if position and state.position then - local seek_length = math.abs(position - state.position) - -- Don't flash on video looping (seek to 0) or tiny seeks (frame-step) - if position > 0.5 and seek_length > 0.5 then - this:flash() - end - end - end) - end - end, - get_effective_proximity = function(this) - if (elements.volume_slider and elements.volume_slider.pressed) then - return 0 - end - if this.pressed then return 1 end - return this.forced_proximity and this.forced_proximity or this.proximity - end, - get_effective_size_min = function(this) - return this.size_min_override or this.size_min - end, - get_effective_size = function(this) - if elements.speed and elements.speed.dragging then - return this.size_max - end - local size_min = this:get_effective_size_min() - return size_min + - math.ceil((this.size_max - size_min) * - this:get_effective_proximity()) - end, - on_display_resize = function(this) - if state.fullscreen or state.maximized then - this.size_min = options.timeline_size_min_fullscreen - this.size_max = options.timeline_size_max_fullscreen - else - this.size_min = options.timeline_size_min - this.size_max = options.timeline_size_max - end - this.font_size = math.floor(math.min((this.size_max + 60) * 0.2, - this.size_max * 0.96) * - options.timeline_font_scale) - this.ax = 0 - this.ay = display.height - this.size_max - this.top_border - - this.bottom_border - this.bx = display.width - this.by = display.height - end, - set_from_cursor = function(this) - mp.commandv('seek', ((cursor.x / display.width) * 100), - 'absolute-percent+exact') - end, - on_mbtn_left_down = function(this) - this.pressed = true - this:set_from_cursor() - end, - on_global_mbtn_left_up = function(this) this.pressed = false end, - on_global_mouse_leave = function(this) this.pressed = false end, - on_global_mouse_move = function(this) - if this.pressed then this:set_from_cursor() end - end, - on_wheel_up = function(this) - if options.timeline_step > 0 then - mp.commandv('seek', -options.timeline_step) - end - end, - on_wheel_down = function(this) - if options.timeline_step > 0 then - mp.commandv('seek', options.timeline_step) - end - end, - render = render_timeline -})) -if options.top_bar_controls or options.top_bar_title then - elements:add('top_bar', Element.new({ - button_opacity = 0.8, - enabled = false, - init = function(this) - mp.observe_property('border', 'bool', function(_, border) - this.enabled = not border - end) - end, - get_effective_proximity = function(this) - if (elements.volume_slider and elements.volume_slider.pressed) or - elements.curtain.opacity > 0 then return 0 end - return this.forced_proximity and this.forced_proximity or - this.proximity - end, - on_display_resize = function(this) - this.size = (state.fullscreen or state.maximized) and - options.top_bar_size_fullscreen or - options.top_bar_size - this.icon_size = round(this.size / 8) - this.spacing = math.ceil(this.size * 0.25) - this.font_size = math.floor(this.size - (this.spacing * 2)) - this.button_width = round(this.size * 1.15) - this.title_bx = display.width - - (options.top_bar_controls and - (this.button_width * 3) or 0) - this.ax = options.top_bar_title and 0 or this.title_bx - this.ay = 0 - this.bx = display.width - this.by = this.size - end, - render = render_top_bar - })) -end -if options.top_bar_controls then - elements:add('window_controls_minimize', Element.new( - { - captures = {mouse_buttons = true}, - on_display_resize = function(this) - this.ax = display.width - (elements.top_bar.button_width * 3) - this.ay = 0 - this.bx = this.ax + elements.top_bar.button_width - this.by = elements.top_bar.size - end, - on_mbtn_left_down = function() - mp.commandv('cycle', 'window-minimized') - end - })) - elements:add('window_controls_maximize', Element.new( - { - captures = {mouse_buttons = true}, - on_display_resize = function(this) - this.ax = display.width - (elements.top_bar.button_width * 2) - this.ay = 0 - this.bx = this.ax + elements.top_bar.button_width - this.by = elements.top_bar.size - end, - on_mbtn_left_down = function() - mp.commandv('cycle', 'window-maximized') - end - })) - elements:add('window_controls_close', Element.new( - { - captures = {mouse_buttons = true}, - on_display_resize = function(this) - this.ax = display.width - elements.top_bar.button_width - this.ay = 0 - this.bx = this.ax + elements.top_bar.button_width - this.by = elements.top_bar.size - end, - on_mbtn_left_down = function() mp.commandv('quit') end - })) -end -if itable_find({'left', 'right'}, options.volume) then - elements:add('volume', Element.new({ - width = nil, -- set in `on_display_resize` handler based on `state.fullscreen` - height = nil, -- set in `on_display_resize` handler based on `state.fullscreen` - margin = nil, -- set in `on_display_resize` handler based on `state.fullscreen` - init = function(this) - -- FLash on external changes - if options.volume_flash then - local is_initial_volume_call = true - mp.observe_property('volume', 'number', function(_, value) - if not is_initial_volume_call then - this:flash() - end - is_initial_volume_call = false - end) - local is_initial_mute_call = true - mp.observe_property('mute', 'bool', function(_, value) - if not is_initial_mute_call then - this:flash() - end - is_initial_mute_call = false - end) - end - end, - get_effective_proximity = function(this) - if elements.volume_slider.pressed then return 1 end - if elements.timeline.proximity_raw == 0 or elements.curtain.opacity > - 0 then return 0 end - return this.forced_proximity and this.forced_proximity or - this.proximity - end, - on_display_resize = function(this) - this.width = (state.fullscreen or state.maximized) and - options.volume_size_fullscreen or - options.volume_size - this.height = round(math.min(this.width * 8, (elements.timeline.ay - - elements.top_bar.size) * 0.8)) - -- Don't bother rendering this if too small - if this.height < (this.width * 2) then this.height = 0 end - this.margin = this.width / 2 - this.ax = round(options.volume == 'left' and this.margin or - display.width - this.margin - this.width) - this.ay = round((display.height - this.height) / 2) - this.bx = round(this.ax + this.width) - this.by = round(this.ay + this.height) - end, - render = render_volume - })) - elements:add('volume_mute', Element.new( - { - captures = {mouse_buttons = true}, - width = 0, - height = 0, - on_display_resize = function(this) - this.width = elements.volume.width - this.height = this.width - this.ax = elements.volume.ax - this.ay = elements.volume.by - this.height - this.bx = elements.volume.bx - this.by = elements.volume.by - end, - on_mbtn_left_down = function(this) - mp.commandv('cycle', 'mute') - end - })) - elements:add('volume_slider', Element.new( - { - captures = {mouse_buttons = true, wheel = true}, - pressed = false, - width = 0, - height = 0, - nudge_y = 0, -- vertical position where volume overflows 100 - nudge_size = nil, -- set on resize - font_size = nil, - spacing = nil, - on_display_resize = function(this) - this.ax = elements.volume.ax - this.ay = elements.volume.ay - this.bx = elements.volume.bx - this.by = elements.volume_mute.ay - this.width = this.bx - this.ax - this.height = this.by - this.ay - this.nudge_y = this.by - - round(this.height * (100 / state.volume_max)) - this.nudge_size = round(elements.volume.width * 0.18) - this.draw_nudge = this.ay < this.nudge_y - this.spacing = round(this.width * 0.2) - end, - set_from_cursor = function(this) - local volume_fraction = (this.by - cursor.y - - options.volume_border) / - (this.height - options.volume_border) - local new_volume = math.min(math.max(volume_fraction, 0), 1) * - state.volume_max - new_volume = round(new_volume / options.volume_step) * - options.volume_step - if state.volume ~= new_volume then - mp.commandv('set', 'volume', - math.min(new_volume, state.volume_max)) - end - end, - on_mbtn_left_down = function(this) - this.pressed = true - this:set_from_cursor() - end, - on_global_mbtn_left_up = function(this) - this.pressed = false - end, - on_global_mouse_leave = function(this) - this.pressed = false - end, - on_global_mouse_move = function(this) - if this.pressed then this:set_from_cursor() end - end, - on_wheel_up = function(this) - local current_rounded_volume = - round(state.volume / options.volume_step) * - options.volume_step - mp.commandv('set', 'volume', math.min( - current_rounded_volume + options.volume_step, - state.volume_max)) - end, - on_wheel_down = function(this) - local current_rounded_volume = - round(state.volume / options.volume_step) * - options.volume_step - mp.commandv('set', 'volume', math.min( - current_rounded_volume - options.volume_step, - state.volume_max)) - end - })) -end -if options.speed then - elements:add('speed', Element.new({ - captures = {mouse_buttons = true, wheel = true}, - dragging = nil, - width = 0, - height = 0, - notches = 10, - notch_every = 0.1, - step_distance = nil, - font_size = nil, - init = function(this) - -- Fade out/in on timeline mouse enter/leave - elements.timeline:on('mouse_enter', function() - if not this.dragging then this:fadeout() end - end) - elements.timeline:on('mouse_leave', function() - if not this.dragging then this:fadein() end - end) - - -- Flash on external changes - if options.speed_flash then - local initial_call = true - mp.observe_property('speed', 'number', function() - if not initial_call and not this.dragging then - this:flash() - end - initial_call = false - end) - end - end, - fadeout = function(this) - this:tween_property('forced_proximity', 1, 0, - function(this) - this.forced_proximity = 0 - end) - end, - fadein = function(this) - local get_current_proximity = - function() return this.proximity end - this:tween_property('forced_proximity', 0, get_current_proximity, - function(this) - this.forced_proximity = nil - end) - end, - on_display_resize = function(this) - this.height = (state.fullscreen or state.maximized) and - options.speed_size_fullscreen or - options.speed_size - this.width = round(this.height * 3.6) - this.notch_spacing = this.width / this.notches - this.step_distance = this.notch_spacing * - (options.speed_step / this.notch_every) - this.ax = (display.width - this.width) / 2 - this.by = display.height - elements.timeline.size_max - this.ay = this.by - this.height - this.bx = this.ax + this.width - this.font_size = - round(this.height * 0.48 * options.speed_font_scale) - end, - set_from_cursor = function(this) - local volume_fraction = - (this.by - cursor.y - options.volume_border) / - (this.height - options.volume_border) - local new_volume = math.min(math.max(volume_fraction, 0), 1) * - state.volume_max - new_volume = round(new_volume / options.volume_step) * - options.volume_step - if state.volume ~= new_volume then - mp.commandv('set', 'volume', new_volume) - end - end, - on_mbtn_left_down = function(this) - this:tween_stop() -- Stop and cleanup possible ongoing animations - this.dragging = { - start_time = mp.get_time(), - start_x = cursor.x, - distance = 0, - start_speed = state.speed - } - end, - on_global_mouse_move = function(this) - if not this.dragging then return end - - this.dragging.distance = cursor.x - this.dragging.start_x - local steps_dragged = round(-this.dragging.distance / - this.step_distance) - local new_speed = this.dragging.start_speed + - (steps_dragged * options.speed_step) - mp.set_property_native('speed', round(new_speed * 100) / 100) - end, - on_mbtn_left_up = function(this) - -- Reset speed on short clicks - if this.dragging and math.abs(this.dragging.distance) < 6 and - mp.get_time() - this.dragging.start_time < 0.15 then - mp.set_property_native('speed', 1) - end - end, - on_global_mbtn_left_up = function(this) - if this.dragging and elements.timeline.proximity_raw == 0 then - this:fadeout() - end - this.dragging = nil - request_render() - end, - on_global_mouse_leave = function(this) - this.dragging = nil - request_render() - end, - on_wheel_up = function(this) - mp.set_property_native('speed', state.speed - options.speed_step) - end, - on_wheel_down = function(this) - mp.set_property_native('speed', state.speed + options.speed_step) - end, - render = render_speed - })) -end -elements:add('curtain', Element.new({ - opacity = 0, - fadeout = function(this) this:tween_property('opacity', this.opacity, 0); end, - fadein = function(this) this:tween_property('opacity', this.opacity, 1); end, - render = function(this) - if this.opacity > 0 then - local ass = assdraw.ass_new() - ass:new_event() - ass:append('{\\blur0\\bord0\\1c&H' .. options.color_background .. - '}') - ass:append(ass_opacity(0.4, this.opacity)) - ass:pos(0, 0) - ass:draw_start() - ass:rect_cw(0, 0, display.width, display.height) - ass:draw_stop() - return ass - end - end -})) - --- CHAPTERS SERIALIZATION - --- Parse `chapter_ranges` option into workable data structure -for _, definition in ipairs(split(options.chapter_ranges, ' *,+ *')) do - local start_patterns, color, opacity, end_patterns = - string.match(definition, '([^<]+)<(%x%x%x%x%x%x):(%d?%.?%d*)>([^>]+)') - - -- Invalid definition - if start_patterns == nil then goto continue end - - start_patterns = start_patterns:lower() - end_patterns = end_patterns:lower() - local uses_bof = start_patterns:find('{bof}') ~= nil - local uses_eof = end_patterns:find('{eof}') ~= nil - local chapter_range = { - start_patterns = split(start_patterns, '|'), - end_patterns = split(end_patterns, '|'), - color = color, - opacity = tonumber(opacity), - ranges = {} - } - - -- Filter out special keywords so we don't use them when matching titles - if uses_bof then - chapter_range.start_patterns = itable_remove( - chapter_range.start_patterns, '{bof}') - end - if uses_eof and chapter_range.end_patterns then - chapter_range.end_patterns = itable_remove(chapter_range.end_patterns, - '{eof}') - end - - chapter_range['serialize'] = function(chapters) - chapter_range.ranges = {} - local current_range = nil - -- bof and eof should be used only once per timeline - -- eof is only used when last range is missing end - local bof_used = false - - function start_range(chapter) - -- If there is already a range started, should we append or overwrite? - -- I chose overwrite here. - current_range = {['start'] = chapter} - end - - function end_range(chapter) - current_range['end'] = chapter - chapter_range.ranges[#chapter_range.ranges + 1] = current_range - -- Mark both chapter objects - current_range['start']._uosc_used_as_range_point = true - current_range['end']._uosc_used_as_range_point = true - -- Clear for next range - current_range = nil - end - - for _, chapter in ipairs(chapters) do - if type(chapter.title) == 'string' then - local lowercase_title = chapter.title:lower() - local is_end = false - local is_start = false - - -- Is ending check and handling - if chapter_range.end_patterns then - for _, end_pattern in ipairs(chapter_range.end_patterns) do - is_end = is_end or lowercase_title:find(end_pattern) ~= - nil - end - - if is_end then - if current_range == nil and uses_bof and not bof_used then - bof_used = true - start_range({time = 0}) - end - if current_range ~= nil then - end_range(chapter) - else - is_end = false - end - end - end - - -- Is start check and handling - for _, start_pattern in ipairs(chapter_range.start_patterns) do - is_start = - is_start or lowercase_title:find(start_pattern) ~= nil - end - - if is_start then start_range(chapter) end - end - end - - -- If there is an unfinished range and range type accepts eof, use it - if current_range ~= nil and uses_eof then - end_range({time = state.duration or infinity}) - end - end - - state.chapter_ranges = state.chapter_ranges or {} - state.chapter_ranges[#state.chapter_ranges + 1] = chapter_range - - ::continue:: -end - -function parse_chapters() - -- Sometimes state.duration is not initialized yet for some reason - state.duration = mp.get_property_native('duration') - - local chapters = get_normalized_chapters() - - if not chapters or not state.duration then return end - - -- Reset custom ranges - for _, chapter_range in ipairs(state.chapter_ranges or {}) do - chapter_range.serialize(chapters) - end - - -- Filter out chapters that were used as ranges - state.chapters = itable_remove(chapters, function(chapter) - return chapter._uosc_used_as_range_point == true - end) - - request_render() -end - --- CONTEXT MENU SERIALIZATION - -state.context_menu_items = (function() - local input_conf_path = mp.command_native({'expand-path', '~~/input.conf'}) - local input_conf_meta, meta_error = utils.file_info(input_conf_path) - - -- File doesn't exist - if not input_conf_meta or not input_conf_meta.is_file then return end - - local items = {} - local items_by_command = {} - local submenus_by_id = {} - - for line in io.lines(input_conf_path) do - local key, command, title = string.match(line, - ' *([%S]+) +(.*) #! *(.*)') - if key then - local is_dummy = key:sub(1, 1) == '#' - local submenu_id = '' - local target_menu = items - local title_parts = split(title or '', ' *> *') - - for index, title_part in ipairs( - #title_parts > 0 and title_parts or - {''}) do - if index < #title_parts then - submenu_id = submenu_id .. title_part - - if not submenus_by_id[submenu_id] then - submenus_by_id[submenu_id] = - {title = title_part, items = {}} - target_menu[#target_menu + 1] = - submenus_by_id[submenu_id] - end - - target_menu = submenus_by_id[submenu_id].items - else - -- If command is already in menu, just append the key to it - if items_by_command[command] then - items_by_command[command].hint = - items_by_command[command].hint .. ', ' .. key - else - items_by_command[command] = - { - title = title_part, - hint = not is_dummy and key or nil, - value = command - } - target_menu[#target_menu + 1] = - items_by_command[command] - end - end - end - end - end - - if #items > 0 then return items end -end)() - --- EVENT HANDLERS - -function create_state_setter(name) - return function(_, value) - state[name] = value - dispatch_event_to_elements('prop_' .. name, value) - request_render() - end -end - -function dispatch_event_to_elements(name, ...) - for _, element in pairs(elements) do - if element.proximity_raw == 0 then - element:maybe('on_' .. name, ...) - end - element:maybe('on_global_' .. name, ...) - end -end - -function create_event_to_elements_dispatcher(name, ...) - return function(...) dispatch_event_to_elements(name, ...) end +function update_cursor_position(x, y) + local old_x, old_y = cursor.x, cursor.y + + -- mpv reports initial mouse position on linux as (0, 0), which always + -- displays the top bar, so we hardcode cursor position as infinity until + -- we receive a first real mouse move event with coordinates other than 0,0. + if not state.first_real_mouse_move_received then + if x > 0 and y > 0 then state.first_real_mouse_move_received = true + else x, y = INFINITY, INFINITY end + end + + -- add 0.5 to be in the middle of the pixel + cursor.x, cursor.y = (x + 0.5) / display.scale_x, (y + 0.5) / display.scale_y + + if old_x ~= cursor.x or old_y ~= cursor.y then + Elements:update_proximities() + + if cursor.x == INFINITY or cursor.y == INFINITY then + cursor.hidden = true + Elements:trigger('global_mouse_leave') + elseif cursor.hidden then + cursor.hidden = false + Elements:trigger('global_mouse_enter') + end + + Elements:proximity_trigger('mouse_move') + cursor.queue_autohide() + end + + request_render() end function handle_mouse_leave() - -- Slowly fadeout elements that are currently visible - for _, element_name in ipairs({'timeline', 'volume', 'top_bar'}) do - local element = elements[element_name] - if element and element.proximity > 0 then - element:tween_property('forced_proximity', - element:get_effective_proximity(), 0, - function() - element.forced_proximity = nil - end) - end - end + -- Slowly fadeout elements that are currently visible + for _, element_name in ipairs({'timeline', 'volume', 'top_bar'}) do + local element = Elements[element_name] + if element and element.proximity > 0 then + element:tween_property('forced_visibility', element:get_visibility(), 0, function() + element.forced_visibility = nil + end) + end + end - cursor.hidden = true - update_proximities() - dispatch_event_to_elements('mouse_leave') + update_cursor_position(INFINITY, INFINITY) end -function handle_mouse_enter() - cursor.hidden = false - cursor.x, cursor.y = mp.get_mouse_pos() - tween_element_stop(state) - dispatch_event_to_elements('mouse_enter') +function handle_file_end() + local resume = false + if not state.loop_file then + if state.has_playlist then resume = state.shuffle and navigate_playlist(1) + else resume = options.autoload and navigate_directory(1) end + end + -- Resume only when navigation happened + if resume then mp.command('set pause no') end +end +local file_end_timer = mp.add_timeout(1, handle_file_end) +file_end_timer:kill() + +function load_file_index_in_current_directory(index) + if not state.path or is_protocol(state.path) then return end + + local serialized = serialize_path(state.path) + if serialized and serialized.dirname then + local files = read_directory(serialized.dirname, config.types.autoload) + + if not files then return end + sort_filenames(files) + if index < 0 then index = #files + index + 1 end + + if files[index] then + mp.commandv('loadfile', join_path(serialized.dirname, files[index])) + end + end end -function handle_mouse_move() - -- Handle case when we are in cursor hidden state but not left the actual - -- window (i.e. when autohide simulates mouse_leave). - if cursor.hidden then - handle_mouse_enter() - return - end - - cursor.x, cursor.y = mp.get_mouse_pos() - update_proximities() - dispatch_event_to_elements('mouse_move') - request_render() - - -- Restart timer that hides UI when mouse is autohidden - if options.autohide then - state.cursor_autohide_timer:kill() - state.cursor_autohide_timer:resume() - end +function update_render_delay(name, fps) + if fps then state.render_delay = 1 / fps end end -function navigate_directory(direction) - local path = mp.get_property_native("path") - - if not path or is_protocol(path) then return end - - local next_file = get_adjacent_file(path, direction, options.media_types) - - if next_file then - mp.commandv("loadfile", - utils.join_path(serialize_path(path).dirname, next_file)) - end +function observe_display_fps(name, fps) + if fps then + mp.unobserve_property(update_render_delay) + mp.unobserve_property(observe_display_fps) + mp.observe_property('display-fps', 'native', update_render_delay) + end end -function load_file_in_current_directory(index) - local path = mp.get_property_native("path") - - if not path or is_protocol(path) then return end - - local dirname = serialize_path(path).dirname - local files = get_files_in_directory(dirname, options.media_types) - - if not files then return end - if index < 0 then index = #files + index + 1 end - - if files[index] then - mp.commandv("loadfile", utils.join_path(dirname, files[index])) - end +function select_current_chapter() + local current_chapter + if state.time and state.chapters then + _, current_chapter = itable_find(state.chapters, function(c) return state.time >= c.time end, true) + end + set_state('current_chapter', current_chapter) end --- MENUS +--[[ STATE HOOKS ]] -function create_select_tracklist_type_menu_opener(menu_title, track_type, - track_prop) - return function() - if menu:is_open(track_type) then - menu:close() - return - end - - local items = {} - local active_item = nil - - for index, track in ipairs(mp.get_property_native('track-list')) do - if track.type == track_type then - if track.selected then active_item = track.id end - - items[#items + 1] = { - title = (track.title and track.title or 'Track ' .. track.id), - hint = track.lang and track.lang:upper() or nil, - value = track.id - } - end - end - - -- Add option to disable a subtitle track. This works for all tracks, - -- but why would anyone want to disable audio or video? Better to not - -- let people mistakenly select what is unwanted 99.999% of the time. - -- If I'm mistaken and there is an active need for this, feel free to - -- open an issue. - if track_type == 'sub' then - active_item = active_item and active_item + 1 or 1 - table.insert(items, 1, {hint = 'disabled', value = nil}) - end - - menu:open(items, function(id) - mp.commandv('set', track_prop, id and id or 'no') - - -- If subtitle track was selected, assume user also wants to see it - if id and track_type == 'sub' then - mp.commandv('set', 'sub-visibility', 'yes') - end - - menu:close() - end, {type = track_type, title = menu_title, active_item = active_item}) - end +-- Click detection +if options.click_threshold > 0 then + -- Executes custom command for clicks shorter than `options.click_threshold` + -- while filtering out double clicks. + local click_time = options.click_threshold / 1000 + local doubleclick_time = mp.get_property_native('input-doubleclick-time') / 1000 + local last_down, last_up = 0, 0 + local click_timer = mp.add_timeout(math.max(click_time, doubleclick_time), function() + local delta = last_up - last_down + if delta > 0 and delta < click_time and delta > 0.02 then mp.command(options.click_command) end + end) + click_timer:kill() + mp.set_key_bindings({{'mbtn_left', + function() last_up = mp.get_time() end, + function() + last_down = mp.get_time() + if click_timer:is_enabled() then click_timer:kill() else click_timer:resume() end + end, + },}, 'mouse_movement', 'force') + mp.enable_key_bindings('mouse_movement', 'allow-vo-dragging+allow-hide-cursor') end --- `menu_options`: --- **allowed_types** - table with file extensions to display --- **active_path** - full path of a file to preselect --- Rest of the options are passed to `menu:open()` -function open_file_navigation_menu(directory, handle_select, menu_options) - directory = serialize_path(directory) - local directories, error = utils.readdir(directory.path, 'dirs') - local files, error = get_files_in_directory(directory.path, - menu_options.allowed_types) - - if not files or not directories then - msg.error('Retrieving files from ' .. directory .. ' failed: ' .. - (error or '')) - return - end - - -- Files are already sorted - table.sort(directories, word_order_comparator) - - -- Pre-populate items with parent directory selector if not at root - local items = not directory.dirname and {} or - { - {title = '..', hint = 'parent dir', value = directory.dirname} - } - - for _, dir in ipairs(directories) do - local serialized = serialize_path(utils.join_path(directory.path, dir)) - items[#items + 1] = { - title = serialized.basename, - value = serialized.path, - hint = '/' - } - end - - menu_options.active_item = nil - - for _, file in ipairs(files) do - local serialized = serialize_path(utils.join_path(directory.path, file)) - local item_index = #items + 1 - - items[item_index] = { - title = serialized.basename, - value = serialized.path - } - - if menu_options.active_path == serialized.path then - menu_options.active_item = item_index - end - end - - menu_options.title = directory.basename .. '/' - - menu:open(items, function(path) - local meta, error = utils.file_info(path) - - if not meta then - msg.error('Retrieving file info for ' .. path .. ' failed: ' .. - (error or '')) - return - end - - if meta.is_dir then - open_file_navigation_menu(path, handle_select, menu_options) - else - handle_select(path) - menu:close() - end - end, menu_options) +function handle_mouse_pos(_, mouse) + if not mouse then return end + if cursor.hover_raw and not mouse.hover then + handle_mouse_leave() + else + update_cursor_position(mouse.x, mouse.y) + end + cursor.hover_raw = mouse.hover end +mp.observe_property('mouse-pos', 'native', handle_mouse_pos) +mp.observe_property('osc', 'bool', function(name, value) if value == true then mp.set_property('osc', 'no') end end) +mp.register_event('file-loaded', function() + set_state('path', normalize_path(mp.get_property_native('path'))) + Elements:flash({'top_bar'}) +end) +mp.register_event('end-file', function(event) + set_state('path', nil) + if event.reason == 'eof' then + file_end_timer:kill() + handle_file_end() + end +end) +-- Top bar titles +do + local function update_state_with_template(prop, template) + -- escape ASS, and strip newlines and trailing slashes and trim whitespace + local tmp = mp.command_native({'expand-text', template}):gsub('\\n', ' '):gsub('[\\%s]+$', ''):gsub('^%s+', '') + set_state(prop, ass_escape(tmp)) + end --- VALUE SERIALIZATION/NORMALIZATION + local function add_template_listener(template, callback) + local props = get_expansion_props(template) + for prop, _ in pairs(props) do + mp.observe_property(prop, 'native', callback) + end + if not next(props) then callback() end + end -options.proximity_out = - math.max(options.proximity_out, options.proximity_in + 1) -options.chapters = itable_find({'dots', 'lines', 'lines-top', 'lines-bottom'}, - options.chapters) and options.chapters or 'none' -options.media_types = split(options.media_types, ' *, *') -options.subtitle_types = split(options.subtitle_types, ' *, *') -options.timeline_cached_ranges = (function() - if options.timeline_cached_ranges == '' or options.timeline_cached_ranges == - 'no' then return nil end - local parts = split(options.timeline_cached_ranges, ':') - return parts[1] and {color = parts[1], opacity = tonumber(parts[2])} or nil -end)() + local function remove_template_listener(callback) mp.unobserve_property(callback) end --- HOOKS -mp.register_event('file-loaded', parse_chapters) -mp.observe_property('chapter-list', 'native', parse_chapters) -mp.observe_property('duration', 'number', create_state_setter('duration')) -mp.observe_property('media-title', 'string', create_state_setter('media_title')) -mp.observe_property('fullscreen', 'bool', create_state_setter('fullscreen')) -mp.observe_property('window-maximized', 'bool', create_state_setter('maximized')) -mp.observe_property('idle-active', 'bool', create_state_setter('idle')) -mp.observe_property('speed', 'number', create_state_setter('speed')) -mp.observe_property('pause', 'bool', create_state_setter('pause')) + -- Main title + if #options.top_bar_title > 0 and options.top_bar_title ~= 'no' then + if options.top_bar_title == 'yes' then + local template = nil + local function update_title() update_state_with_template('title', template) end + mp.observe_property('title', 'string', function(_, title) + remove_template_listener(update_title) + template = title + if template then + if template:sub(-6) == ' - mpv' then template = template:sub(1, -7) end + add_template_listener(template, update_title) + end + end) + elseif type(options.top_bar_title) == 'string' then + add_template_listener(options.top_bar_title, function() + update_state_with_template('title', options.top_bar_title) + end) + end + end + + -- Alt title + if #options.top_bar_alt_title > 0 and options.top_bar_alt_title ~= 'no' then + add_template_listener(options.top_bar_alt_title, function() + update_state_with_template('alt_title', options.top_bar_alt_title) + end) + end +end +mp.observe_property('playback-time', 'number', create_state_setter('time', function() + -- Create a file-end event that triggers right before file ends + file_end_timer:kill() + if state.duration and state.time and not state.pause then + local remaining = (state.duration - state.time) / state.speed + if remaining < 5 then + local timeout = remaining - 0.02 + if timeout > 0 then + file_end_timer.timeout = timeout + file_end_timer:resume() + else handle_file_end() end + end + end + + update_human_times() + select_current_chapter() +end)) +mp.observe_property('duration', 'number', create_state_setter('duration', update_human_times)) +mp.observe_property('speed', 'number', create_state_setter('speed', update_human_times)) +mp.observe_property('track-list', 'native', function(name, value) + -- checks the file dispositions + local types = {sub = 0, image = 0, audio = 0, video = 0} + for _, track in ipairs(value) do + if track.type == 'video' then + if track.image or track.albumart then types.image = types.image + 1 + else types.video = types.video + 1 end + elseif types[track.type] then types[track.type] = types[track.type] + 1 end + end + set_state('is_audio', types.video == 0 and types.audio > 0) + set_state('is_image', types.image > 0 and types.video == 0 and types.audio == 0) + set_state('has_audio', types.audio > 0) + set_state('has_many_audio', types.audio > 1) + set_state('has_sub', types.sub > 0) + set_state('has_many_sub', types.sub > 1) + set_state('is_video', types.video > 0) + set_state('has_many_video', types.video > 1) + Elements:trigger('dispositions') +end) +mp.observe_property('editions', 'number', function(_, editions) + if editions then set_state('has_many_edition', editions > 1) end + Elements:trigger('dispositions') +end) +mp.observe_property('chapter-list', 'native', function(_, chapters) + local chapters, chapter_ranges = serialize_chapters(chapters), {} + if chapters then chapters, chapter_ranges = serialize_chapter_ranges(chapters) end + set_state('chapters', chapters) + set_state('chapter_ranges', chapter_ranges) + set_state('has_chapter', #chapters > 0) + select_current_chapter() + Elements:trigger('dispositions') +end) +mp.observe_property('border', 'bool', create_state_setter('border')) +mp.observe_property('loop-file', 'native', create_state_setter('loop_file')) +mp.observe_property('ab-loop-a', 'number', create_state_setter('ab_loop_a')) +mp.observe_property('ab-loop-b', 'number', create_state_setter('ab_loop_b')) +mp.observe_property('playlist-pos-1', 'number', create_state_setter('playlist_pos')) +mp.observe_property('playlist-count', 'number', function(_, value) + set_state('playlist_count', value) + set_state('has_playlist', value > 1) + Elements:trigger('dispositions') +end) +mp.observe_property('fullscreen', 'bool', create_state_setter('fullscreen', update_fullormaxed)) +mp.observe_property('window-maximized', 'bool', create_state_setter('maximized', update_fullormaxed)) +mp.observe_property('idle-active', 'bool', function(_, idle) + set_state('is_idle', idle) + Elements:trigger('dispositions') +end) +mp.observe_property('pause', 'bool', create_state_setter('pause', function() file_end_timer:kill() end)) mp.observe_property('volume', 'number', create_state_setter('volume')) mp.observe_property('volume-max', 'number', create_state_setter('volume_max')) mp.observe_property('mute', 'bool', create_state_setter('mute')) -mp.observe_property('playback-time', 'number', function(name, val) - -- Ignore the initial call with nil value - if val == nil then return end - - state.position = val - state.elapsed_seconds = val - state.elapsed_time = state.elapsed_seconds and - mp.format_time(state.elapsed_seconds) or nil - state.remaining_seconds = mp.get_property_native('playtime-remaining') - state.remaining_time = state.remaining_seconds and - mp.format_time(state.remaining_seconds) or nil - - request_render() -end) mp.observe_property('osd-dimensions', 'native', function(name, val) - update_display_dimensions() - request_render() + update_display_dimensions() + request_render() end) +mp.observe_property('display-hidpi-scale', 'native', create_state_setter('hidpi_scale', update_display_dimensions)) +mp.observe_property('cache', 'string', create_state_setter('cache')) +mp.observe_property('cache-buffering-state', 'number', create_state_setter('cache_buffering')) +mp.observe_property('demuxer-via-network', 'native', create_state_setter('is_stream', function() + Elements:trigger('dispositions') +end)) mp.observe_property('demuxer-cache-state', 'native', function(prop, cache_state) - if cache_state == nil then - state.cached_ranges = nil - return - end - local cache_ranges = cache_state['seekable-ranges'] - state.cached_ranges = #cache_ranges > 0 and cache_ranges or nil + local cached_ranges, bof, eof, uncached_ranges = nil, nil, nil, nil + if cache_state then + cached_ranges, bof, eof = cache_state['seekable-ranges'], cache_state['bof-cached'], cache_state['eof-cached'] + set_state('cache_underrun', cache_state['underrun']) + else cached_ranges = {} end + + if not (state.duration and (#cached_ranges > 0 or state.cache == 'yes' or + (state.cache == 'auto' and state.is_stream))) then + if state.uncached_ranges then set_state('uncached_ranges', nil) end + return + end + + -- Normalize + local ranges = {} + for _, range in ipairs(cached_ranges) do + ranges[#ranges + 1] = { + math.max(range['start'] or 0, 0), + math.min(range['end'] or state.duration, state.duration), + } + end + table.sort(ranges, function(a, b) return a[1] < b[1] end) + if bof then ranges[1][1] = 0 end + if eof then ranges[#ranges][2] = state.duration end + -- Invert cached ranges into uncached ranges, as that's what we're rendering + local inverted_ranges = {{0, state.duration}} + for _, cached in pairs(ranges) do + inverted_ranges[#inverted_ranges][2] = cached[1] + inverted_ranges[#inverted_ranges + 1] = {cached[2], state.duration} + end + uncached_ranges = {} + local last_range = nil + for _, range in ipairs(inverted_ranges) do + if last_range and last_range[2] + 0.5 > range[1] then -- fuse ranges + last_range[2] = range[2] + else + if range[2] - range[1] > 0.5 then -- skip short ranges + uncached_ranges[#uncached_ranges + 1] = range + last_range = range + end + end + end + + set_state('uncached_ranges', uncached_ranges) end) +mp.observe_property('display-fps', 'native', observe_display_fps) +mp.observe_property('estimated-display-fps', 'native', update_render_delay) +mp.observe_property('eof-reached', 'native', create_state_setter('eof_reached')) +mp.observe_property('core-idle', 'native', create_state_setter('core_idle')) --- CONTROLS +--[[ KEY BINDS ]] --- Mouse movement key binds -local base_keybinds = { - {'mouse_move', handle_mouse_move}, {'mouse_leave', handle_mouse_leave}, - {'mouse_enter', handle_mouse_enter} -} -if options.pause_on_click_shorter_than > 0 then - -- Cycles pause when click is shorter than `options.pause_on_click_shorter_than` - -- while filtering out double clicks. - local duration_seconds = options.pause_on_click_shorter_than / 1000 - local last_down_event; - local click_timer = mp.add_timeout(duration_seconds, - function() mp.command('cycle pause') end); - click_timer:kill() - base_keybinds[#base_keybinds + 1] = { - 'mbtn_left', function() - if mp.get_time() - last_down_event < duration_seconds then - click_timer:resume() - end - end, function() - if click_timer:is_enabled() then - click_timer:kill() - last_down_event = 0 - else - last_down_event = mp.get_time() - end - end - } +-- Pointer related binding groups +function make_cursor_handler(event, cb) + return function(...) + call_maybe(cursor[event], ...) + call_maybe(cb, ...) + cursor.queue_autohide() -- refresh cursor autohide timer + end end -mp.set_key_bindings(base_keybinds, 'mouse_movement', 'force') -mp.enable_key_bindings('mouse_movement', 'allow-vo-dragging+allow-hide-cursor') +mp.set_key_bindings({ + { + 'mbtn_left', + make_cursor_handler('on_primary_up'), + make_cursor_handler('on_primary_down', function(...) + handle_mouse_pos(nil, mp.get_property_native('mouse-pos')) + end), + }, + {'mbtn_left_dbl', 'ignore'}, +}, 'mbtn_left', 'force') +mp.set_key_bindings({ + {'wheel_up', make_cursor_handler('on_wheel_up')}, + {'wheel_down', make_cursor_handler('on_wheel_down')}, +}, 'wheel', 'force') --- Context based key bind groups +-- Adds a key binding that respects rerouting set by `key_binding_overwrites` table. +---@param name string +---@param callback fun(event: table) +---@param flags nil|string +function bind_command(name, callback, flags) + mp.add_key_binding(nil, name, function(...) + if key_binding_overwrites[name] then mp.command(key_binding_overwrites[name]) + else callback(...) end + end, flags) +end -forced_key_bindings = (function() - mp.set_key_bindings({ - { - 'mbtn_left', create_event_to_elements_dispatcher('mbtn_left_up'), - create_event_to_elements_dispatcher('mbtn_left_down') - }, {'mbtn_left_dbl', 'ignore'} - }, 'mouse_buttons', 'force') - mp.set_key_bindings({ - {'wheel_up', create_event_to_elements_dispatcher('wheel_up')}, - {'wheel_down', create_event_to_elements_dispatcher('wheel_down')} - }, 'wheel', 'force') - - local groups = {} - for _, group in ipairs({'mouse_buttons', 'wheel'}) do - groups[group] = { - is_enabled = false, - enable = function(this) - if this.is_enabled then return end - this.is_enabled = true - mp.enable_key_bindings(group) - end, - disable = function(this) - if not this.is_enabled then return end - this.is_enabled = false - mp.disable_key_bindings(group) - end - } - end - return groups -end)() - --- KEY BINDABLE FEATURES - -mp.add_key_binding(nil, 'peek-timeline', function() - if elements.timeline.proximity > 0.5 then - elements.timeline:tween_property('proximity', - elements.timeline.proximity, 0) - else - elements.timeline:tween_property('proximity', - elements.timeline.proximity, 1) - end +bind_command('toggle-ui', function() Elements:toggle({'timeline', 'controls', 'volume', 'top_bar'}) end) +bind_command('flash-ui', function() Elements:flash({'timeline', 'controls', 'volume', 'top_bar'}) end) +bind_command('flash-timeline', function() Elements:flash({'timeline'}) end) +bind_command('flash-top-bar', function() Elements:flash({'top_bar'}) end) +bind_command('flash-volume', function() Elements:flash({'volume'}) end) +bind_command('flash-speed', function() Elements:flash({'speed'}) end) +bind_command('flash-pause-indicator', function() Elements:flash({'pause_indicator'}) end) +bind_command('toggle-progress', function() + local timeline = Elements.timeline + if timeline.size_min_override then + timeline:tween_property('size_min_override', timeline.size_min_override, timeline.size_min, function() + timeline.size_min_override = nil + end) + else + timeline:tween_property('size_min_override', timeline.size_min, 0) + end end) -mp.add_key_binding(nil, 'toggle-progress', function() - local timeline = elements.timeline - if timeline.size_min_override then - timeline:tween_property('size_min_override', timeline.size_min_override, - timeline.size_min, - function() - timeline.size_min_override = nil - end) - else - timeline:tween_property('size_min_override', timeline.size_min, 0) - end +bind_command('toggle-title', function() Elements.top_bar:toggle_title() end) +bind_command('decide-pause-indicator', function() Elements.pause_indicator:decide() end) +bind_command('menu', function() toggle_menu_with_items() end) +bind_command('menu-blurred', function() toggle_menu_with_items({mouse_nav = true}) end) +local track_loaders = { + {name = 'subtitles', prop = 'sub', allowed_types = itable_join(config.types.video, config.types.subtitle)}, + {name = 'audio', prop = 'audio', allowed_types = itable_join(config.types.video, config.types.audio)}, + {name = 'video', prop = 'video', allowed_types = config.types.video}, +} +for _, loader in ipairs(track_loaders) do + local menu_type = 'load-' .. loader.name + bind_command(menu_type, function() + if Menu:is_open(menu_type) then Menu:close() return end + + local path = state.path + if path then + if is_protocol(path) then + path = false + else + local serialized_path = serialize_path(path) + path = serialized_path ~= nil and serialized_path.dirname or false + end + end + if not path then + path = get_default_directory() + end + open_file_navigation_menu( + path, + function(path) mp.commandv(loader.prop .. '-add', path) end, + {type = menu_type, title = 'Load ' .. loader.name, allowed_types = loader.allowed_types} + ) + end) +end +bind_command('subtitles', create_select_tracklist_type_menu_opener( + 'Subtitles', 'sub', 'sid', 'script-binding uosc/load-subtitles' +)) +bind_command('audio', create_select_tracklist_type_menu_opener( + 'Audio', 'audio', 'aid', 'script-binding uosc/load-audio' +)) +bind_command('video', create_select_tracklist_type_menu_opener( + 'Video', 'video', 'vid', 'script-binding uosc/load-video' +)) +bind_command('playlist', create_self_updating_menu_opener({ + title = 'Playlist', + type = 'playlist', + list_prop = 'playlist', + serializer = function(playlist) + local items = {} + for index, item in ipairs(playlist) do + local is_url = item.filename:find('://') + local item_title = type(item.title) == 'string' and #item.title > 0 and item.title or false + items[index] = { + title = item_title or (is_url and item.filename or serialize_path(item.filename).basename), + hint = tostring(index), + active = item.current, + value = index, + } + end + return items + end, + on_select = function(index) mp.commandv('set', 'playlist-pos-1', tostring(index)) end, + on_move_item = function(from, to) + mp.commandv('playlist-move', tostring(math.max(from, to) - 1), tostring(math.min(from, to) - 1)) + end, + on_delete_item = function(index) mp.commandv('playlist-remove', tostring(index - 1)) end, +})) +bind_command('chapters', create_self_updating_menu_opener({ + title = 'Chapters', + type = 'chapters', + list_prop = 'chapter-list', + active_prop = 'chapter', + serializer = function(chapters, current_chapter) + local items = {} + chapters = normalize_chapters(chapters) + for index, chapter in ipairs(chapters) do + items[index] = { + title = chapter.title or '', + hint = format_time(chapter.time, state.duration), + value = index, + active = index - 1 == current_chapter, + } + end + return items + end, + on_select = function(index) mp.commandv('set', 'chapter', tostring(index - 1)) end, +})) +bind_command('editions', create_self_updating_menu_opener({ + title = 'Editions', + type = 'editions', + list_prop = 'edition-list', + active_prop = 'current-edition', + serializer = function(editions, current_id) + local items = {} + for _, edition in ipairs(editions or {}) do + items[#items + 1] = { + title = edition.title or 'Edition', + hint = tostring(edition.id + 1), + value = edition.id, + active = edition.id == current_id, + } + end + return items + end, + on_select = function(id) mp.commandv('set', 'edition', id) end, +})) +bind_command('show-in-directory', function() + -- Ignore URLs + if not state.path or is_protocol(state.path) then return end + + if state.platform == 'windows' then + utils.subprocess_detached({args = {'explorer', '/select,', state.path}, cancellable = false}) + elseif state.platform == 'macos' then + utils.subprocess_detached({args = {'open', '-R', state.path}, cancellable = false}) + elseif state.platform == 'linux' then + local result = utils.subprocess({args = {'nautilus', state.path}, cancellable = false}) + + -- Fallback opens the folder with xdg-open instead + if result.status ~= 0 then + utils.subprocess({args = {'xdg-open', serialize_path(state.path).dirname}, cancellable = false}) + end + end end) -mp.add_key_binding(nil, 'menu', function() - if menu:is_open('menu') then - menu:close() - elseif state.context_menu_items then - menu:open(state.context_menu_items, - function(command) mp.command(command) end, {type = 'menu'}) - end +bind_command('stream-quality', function() + if Menu:is_open('stream-quality') then Menu:close() return end + + local ytdl_format = mp.get_property_native('ytdl-format') + local items = {} + + for _, height in ipairs(config.stream_quality_options) do + local format = 'bestvideo[height<=?' .. height .. ']+bestaudio/best[height<=?' .. height .. ']' + items[#items + 1] = {title = height .. 'p', value = format, active = format == ytdl_format} + end + + Menu:open({type = 'stream-quality', title = 'Stream quality', items = items}, function(format) + mp.set_property('ytdl-format', format) + + -- Reload the video to apply new format + -- This is taken from https://github.com/jgreco/mpv-youtube-quality + -- which is in turn taken from https://github.com/4e6/mpv-reload/ + -- Dunno if playlist_pos shenanigans below are necessary. + local playlist_pos = mp.get_property_number('playlist-pos') + local duration = mp.get_property_native('duration') + local time_pos = mp.get_property('time-pos') + + mp.set_property_number('playlist-pos', playlist_pos) + + -- Tries to determine live stream vs. pre-recorded VOD. VOD has non-zero + -- duration property. When reloading VOD, to keep the current time position + -- we should provide offset from the start. Stream doesn't have fixed start. + -- Decent choice would be to reload stream from it's current 'live' position. + -- That's the reason we don't pass the offset when reloading streams. + if duration and duration > 0 then + local function seeker() + mp.commandv('seek', time_pos, 'absolute') + mp.unregister_event(seeker) + end + mp.register_event('file-loaded', seeker) + end + end) end) -mp.add_key_binding(nil, 'load-subtitles', function() - if menu:is_open('load-subtitles') then - menu:close() - return - end +bind_command('open-file', function() + if Menu:is_open('open-file') then Menu:close() return end - local path = mp.get_property_native('path') - if path and not is_protocol(path) then - open_file_navigation_menu(serialize_path(path).dirname, function(path) - mp.commandv('sub-add', path) - end, {type = 'load-subtitles', allowed_types = options.subtitle_types}) - end + local directory + local active_file + + if state.path == nil or is_protocol(state.path) then + local serialized = serialize_path(get_default_directory()) + if serialized then + directory = serialized.path + active_file = nil + end + else + local serialized = serialize_path(state.path) + if serialized then + directory = serialized.dirname + active_file = serialized.path + end + end + + if not directory then + msg.error('Couldn\'t serialize path "' .. state.path .. '".') + return + end + + -- Update active file in directory navigation menu + local function handle_file_loaded() + if Menu:is_open('open-file') then + Elements.menu:activate_one_value(normalize_path(mp.get_property_native('path'))) + end + end + + open_file_navigation_menu( + directory, + function(path) mp.commandv('loadfile', path) end, + { + type = 'open-file', + allowed_types = config.types.media, + active_path = active_file, + on_open = function() mp.register_event('file-loaded', handle_file_loaded) end, + on_close = function() mp.unregister_event(handle_file_loaded) end, + } + ) end) -mp.add_key_binding(nil, 'subtitles', create_select_tracklist_type_menu_opener( - 'Subtitles', 'sub', 'sid')) -mp.add_key_binding(nil, 'audio', create_select_tracklist_type_menu_opener( - 'Audio', 'audio', 'aid')) -mp.add_key_binding(nil, 'video', create_select_tracklist_type_menu_opener( - 'Video', 'video', 'vid')) -mp.add_key_binding(nil, 'playlist', function() - if menu:is_open('playlist') then - menu:close() - return - end - - function serialize_playlist() - local pos = mp.get_property_number('playlist-pos-1', 0) - local items = {} - local active_item - for index, item in ipairs(mp.get_property_native('playlist')) do - local is_url = item.filename:find('://') - items[index] = { - title = is_url and item.filename or - serialize_path(item.filename).basename, - hint = tostring(index), - value = index - } - - if index == pos then active_item = index end - end - return items, active_item - end - - -- Update active index and playlist content on playlist changes - function handle_playlist_change() - if menu:is_open('playlist') then - local items, active_item = serialize_playlist() - elements.menu:set_items(items, { - active_item = active_item, - selected_item = active_item - }) - end - end - - local items, active_item = serialize_playlist() - - menu:open(items, function(index) - mp.commandv('set', 'playlist-pos-1', tostring(index)) - end, { - type = 'playlist', - title = 'Playlist', - active_item = active_item, - on_open = function() - mp.observe_property('playlist', 'native', handle_playlist_change) - mp.observe_property('playlist-pos-1', 'native', - handle_playlist_change) - end, - on_close = function() - mp.unobserve_property(handle_playlist_change) - end - }) +bind_command('shuffle', function() set_state('shuffle', not state.shuffle) end) +bind_command('items', function() + if state.has_playlist then + mp.command('script-binding uosc/playlist') + else + mp.command('script-binding uosc/open-file') + end end) -mp.add_key_binding(nil, 'chapters', function() - if menu:is_open('chapters') then - menu:close() - return - end - - local items = {} - local chapters = get_normalized_chapters() - - for index, chapter in ipairs(chapters) do - items[#items + 1] = { - title = chapter.title or '', - hint = mp.format_time(chapter.time), - value = chapter.time - } - end - - -- Select first chapter from the end with time lower - -- than current playing position (with 100ms leeway). - function get_selected_chapter_index() - local position = mp.get_property_native('playback-time') - if not position then return nil end - for index = #items, 1, -1 do - if position - 0.1 > items[index].value then return index end - end - end - - -- Update selected chapter in chapter navigation menu - function seek_handler() - if menu:is_open('chapters') then - elements.menu:activate_index(get_selected_chapter_index()) - end - end - - menu:open(items, function(time) - mp.commandv('seek', tostring(time), 'absolute') - end, { - type = 'chapters', - title = 'Chapters', - active_item = get_selected_chapter_index(), - on_open = function() mp.register_event('seek', seek_handler) end, - on_close = function() mp.unregister_event(seek_handler) end - }) +bind_command('next', function() navigate_item(1) end) +bind_command('prev', function() navigate_item(-1) end) +bind_command('next-file', function() navigate_directory(1) end) +bind_command('prev-file', function() navigate_directory(-1) end) +bind_command('first', function() + if state.has_playlist then + mp.commandv('set', 'playlist-pos-1', '1') + else + load_file_index_in_current_directory(1) + end end) -mp.add_key_binding(nil, 'show-in-directory', function() - local path = mp.get_property_native('path') - - -- Ignore URLs - if not path or is_protocol(path) then return end - - path = normalize_path(path) - - if state.os == 'windows' then - utils.subprocess_detached({ - args = {'explorer', '/select,', path}, - cancellable = false - }) - elseif state.os == 'macos' then - utils.subprocess_detached({ - args = {'open', '-R', path}, - cancellable = false - }) - elseif state.os == 'linux' then - local result = utils.subprocess({ - args = {'nautilus', path}, - cancellable = false - }) - - -- Fallback opens the folder with xdg-open instead - if result.status ~= 0 then - utils.subprocess({ - args = {'xdg-open', serialize_path(path).dirname}, - cancellable = false - }) - end - end +bind_command('last', function() + if state.has_playlist then + mp.commandv('set', 'playlist-pos-1', tostring(state.playlist_count)) + else + load_file_index_in_current_directory(-1) + end end) -mp.add_key_binding(nil, 'open-file', function() - if menu:is_open('open-file') then - menu:close() - return - end +bind_command('first-file', function() load_file_index_in_current_directory(1) end) +bind_command('last-file', function() load_file_index_in_current_directory(-1) end) +bind_command('delete-file-next', function() + local next_file = nil + local is_local_file = state.path and not is_protocol(state.path) - local path = mp.get_property_native('path') - local directory - local active_file + if is_local_file then + if Menu:is_open('open-file') then Elements.menu:delete_value(state.path) end + end - if path == nil or is_protocol(path) then - local path = serialize_path(mp.command_native({'expand-path', '~/'})) - directory = path.path - active_file = nil - else - local path = serialize_path(path) - directory = path.dirname - active_file = path.path - end + if state.has_playlist then + mp.commandv('playlist-remove', 'current') + else + if is_local_file then + local paths, current_index = get_adjacent_files(state.path, config.types.autoload) + if paths and current_index then + local index, path = decide_navigation_in_list(paths, current_index, 1) + if path then next_file = path end + end + end - -- Update selected file in directory navigation menu - function handle_file_loaded() - if menu:is_open('open-file') then - local path = normalize_path(mp.get_property_native('path')) - elements.menu:activate_value(path) - elements.menu:select_value(path) - end - end + if next_file then mp.commandv('loadfile', next_file) + else mp.commandv('stop') end + end - open_file_navigation_menu(directory, - function(path) mp.commandv('loadfile', path) end, - { - type = 'open-file', - allowed_types = options.media_types, - active_path = active_file, - on_open = function() - mp.register_event('file-loaded', handle_file_loaded) - end, - on_close = function() mp.unregister_event(handle_file_loaded) end - }) + if is_local_file then delete_file(state.path) end end) -mp.add_key_binding(nil, 'next', function() - if mp.get_property_native('playlist-count') > 1 then - mp.command('playlist-next') - else - navigate_directory('forward') - end +bind_command('delete-file-quit', function() + mp.command('stop') + if state.path and not is_protocol(state.path) then delete_file(state.path) end + mp.command('quit') end) -mp.add_key_binding(nil, 'prev', function() - if mp.get_property_native('playlist-count') > 1 then - mp.command('playlist-prev') - else - navigate_directory('backward') - end +bind_command('audio-device', create_self_updating_menu_opener({ + title = 'Audio devices', + type = 'audio-device-list', + list_prop = 'audio-device-list', + active_prop = 'audio-device', + serializer = function(audio_device_list, current_device) + current_device = current_device or 'auto' + local ao = mp.get_property('current-ao') or '' + local items = {} + for _, device in ipairs(audio_device_list) do + if device.name == 'auto' or string.match(device.name, '^' .. ao) then + local hint = string.match(device.name, ao .. '/(.+)') + if not hint then hint = device.name end + items[#items + 1] = { + title = device.description, + hint = hint, + active = device.name == current_device, + value = device.name, + } + end + end + return items + end, + on_select = function(name) mp.commandv('set', 'audio-device', name) end, +})) +bind_command('open-config-directory', function() + local config_path = mp.command_native({'expand-path', '~~/mpv.conf'}) + local config = serialize_path(normalize_path(config_path)) + + if config then + local args + + if state.platform == 'windows' then + args = {'explorer', '/select,', config.path} + elseif state.platform == 'macos' then + args = {'open', '-R', config.path} + elseif state.platform == 'linux' then + args = {'xdg-open', config.dirname} + end + + utils.subprocess_detached({args = args, cancellable = false}) + else + msg.error('Couldn\'t serialize config path "' .. config_path .. '".') + end end) -mp.add_key_binding(nil, 'next-file', - function() navigate_directory('forward') end) -mp.add_key_binding(nil, 'prev-file', - function() navigate_directory('backward') end) -mp.add_key_binding(nil, 'first', function() - if mp.get_property_native('playlist-count') > 1 then - mp.commandv('set', 'playlist-pos-1', '1') - else - load_file_in_current_directory(1) - end + +--[[ MESSAGE HANDLERS ]] + +mp.register_script_message('show-submenu', function(id) toggle_menu_with_items({submenu = id}) end) +mp.register_script_message('show-submenu-blurred', function(id) + toggle_menu_with_items({submenu = id, mouse_nav = true}) end) -mp.add_key_binding(nil, 'last', function() - local playlist_count = mp.get_property_native('playlist-count') - if playlist_count > 1 then - mp.commandv('set', 'playlist-pos-1', tostring(playlist_count)) - else - load_file_in_current_directory(-1) - end +mp.register_script_message('get-version', function(script) + mp.commandv('script-message-to', script, 'uosc-version', config.version) end) -mp.add_key_binding(nil, 'first-file', - function() load_file_in_current_directory(1) end) -mp.add_key_binding(nil, 'last-file', - function() load_file_in_current_directory(-1) end) -mp.add_key_binding(nil, 'delete-file-next', function() - local path = mp.get_property_native('path') - - if not path or is_protocol(path) then return end - - path = normalize_path(path) - local playlist_count = mp.get_property_native('playlist-count') - - if playlist_count > 1 then - mp.commandv('playlist-remove', 'current') - else - local next_file = - get_adjacent_file(path, 'forward', options.media_types) - - if menu:is_open('open-file') then - elements.menu:delete_value(path) - end - - if next_file then - mp.commandv('loadfile', next_file) - else - mp.commandv('stop') - end - end - - os.remove(path) +mp.register_script_message('open-menu', function(json, submenu_id) + local data = utils.parse_json(json) + if type(data) ~= 'table' or type(data.items) ~= 'table' then + msg.error('open-menu: received json didn\'t produce a table with menu configuration') + else + if data.type and Menu:is_open(data.type) then Menu:close() + else open_command_menu(data, {submenu = submenu_id, on_close = data.on_close}) end + end end) -mp.add_key_binding(nil, 'delete-file-quit', function() - local path = mp.get_property_native('path') - if not path or is_protocol(path) then return end - os.remove(normalize_path(path)) - mp.command('quit') +mp.register_script_message('update-menu', function(json) + local data = utils.parse_json(json) + if type(data) ~= 'table' or type(data.items) ~= 'table' then + msg.error('update-menu: received json didn\'t produce a table with menu configuration') + else + local menu = data.type and Menu:is_open(data.type) + if menu then menu:update(data) + else open_command_menu(data) end + end end) -mp.add_key_binding(nil, 'open-config-directory', function() - local config = serialize_path(mp.command_native( - {'expand-path', '~~/mpv.conf'})) - local args - - if state.os == 'windows' then - args = {'explorer', '/select,', config.path} - elseif state.os == 'macos' then - args = {'open', '-R', config.path} - elseif state.os == 'linux' then - args = {'xdg-open', config.dirname} - end - - utils.subprocess_detached({args = args, cancellable = false}) +mp.register_script_message('thumbfast-info', function(json) + local data = utils.parse_json(json) + if type(data) ~= 'table' or not data.width or not data.height then + thumbnail.disabled = true + msg.error('thumbfast-info: received json didn\'t produce a table with thumbnail information') + else + thumbnail = data + request_render() + end end) +mp.register_script_message('set', function(name, value) + external[name] = value + Elements:trigger('external_prop_' .. name, value) +end) +mp.register_script_message('toggle-elements', function(elements) Elements:toggle(split(elements, ' *, *')) end) +mp.register_script_message('set-min-visibility', function(visibility, elements) + local fraction = tonumber(visibility) + local ids = split(elements and elements ~= '' and elements or 'timeline,controls,volume,top_bar', ' *, *') + if fraction then Elements:set_min_visibility(clamp(0, fraction, 1), ids) end +end) +mp.register_script_message('flash-elements', function(elements) Elements:flash(split(elements, ' *, *')) end) +mp.register_script_message('overwrite-binding', function(name, command) key_binding_overwrites[name] = command end) + +--[[ ELEMENTS ]] + +require('uosc_shared/elements/WindowBorder'):new() +require('uosc_shared/elements/BufferingIndicator'):new() +require('uosc_shared/elements/PauseIndicator'):new() +require('uosc_shared/elements/TopBar'):new() +require('uosc_shared/elements/Timeline'):new() +if options.controls and options.controls ~= 'never' then require('uosc_shared/elements/Controls'):new() end +if itable_index_of({'left', 'right'}, options.volume) then require('uosc_shared/elements/Volume'):new() end +require('uosc_shared/elements/Curtain'):new() diff --git a/multimedia/.config/mpv/scripts/uosc_shared/elements/BufferingIndicator.lua b/multimedia/.config/mpv/scripts/uosc_shared/elements/BufferingIndicator.lua new file mode 100644 index 0000000..e2aa071 --- /dev/null +++ b/multimedia/.config/mpv/scripts/uosc_shared/elements/BufferingIndicator.lua @@ -0,0 +1,37 @@ +local Element = require('uosc_shared/elements/Element') + +---@class BufferingIndicator : Element +local BufferingIndicator = class(Element) + +function BufferingIndicator:new() return Class.new(self) --[[@as BufferingIndicator]] end +function BufferingIndicator:init() + Element.init(self, 'buffer_indicator') + self.ignores_menu = true + self.enabled = false +end + +function BufferingIndicator:decide_enabled() + local cache = state.cache_underrun or state.cache_buffering and state.cache_buffering < 100 + local player = state.core_idle and not state.eof_reached + if self.enabled then + if not player or (state.pause and not cache) then self.enabled = false end + elseif player and cache and state.uncached_ranges then self.enabled = true end +end + +function BufferingIndicator:on_prop_pause() self:decide_enabled() end +function BufferingIndicator:on_prop_core_idle() self:decide_enabled() end +function BufferingIndicator:on_prop_eof_reached() self:decide_enabled() end +function BufferingIndicator:on_prop_uncached_ranges() self:decide_enabled() end +function BufferingIndicator:on_prop_cache_buffering() self:decide_enabled() end +function BufferingIndicator:on_prop_cache_underrun() self:decide_enabled() end + +function BufferingIndicator:render() + local ass = assdraw.ass_new() + ass:rect(0, 0, display.width, display.height, {color = bg, opacity = 0.3}) + local size = round(30 + math.min(display.width, display.height) / 10) + local opacity = (Elements.menu and not Elements.menu.is_closing) and 0.3 or 0.8 + ass:spinner(display.width / 2, display.height / 2, size, {color = fg, opacity = opacity}) + return ass +end + +return BufferingIndicator diff --git a/multimedia/.config/mpv/scripts/uosc_shared/elements/Button.lua b/multimedia/.config/mpv/scripts/uosc_shared/elements/Button.lua new file mode 100644 index 0000000..e57d614 --- /dev/null +++ b/multimedia/.config/mpv/scripts/uosc_shared/elements/Button.lua @@ -0,0 +1,90 @@ +local Element = require('uosc_shared/elements/Element') + +---@alias ButtonProps {icon: string; on_click: function; anchor_id?: string; active?: boolean; badge?: string|number; foreground?: string; background?: string; tooltip?: string} + +---@class Button : Element +local Button = class(Element) + +---@param id string +---@param props ButtonProps +function Button:new(id, props) return Class.new(self, id, props) --[[@as Button]] end +---@param id string +---@param props ButtonProps +function Button:init(id, props) + self.icon = props.icon + self.active = props.active + self.tooltip = props.tooltip + self.badge = props.badge + self.foreground = props.foreground or fg + self.background = props.background or bg + ---@type fun() + self.on_click = props.on_click + Element.init(self, id, props) +end + +function Button:on_coordinates() self.font_size = round((self.by - self.ay) * 0.7) end +function Button:handle_cursor_down() + -- We delay the callback to next tick, otherwise we are risking race + -- conditions as we are in the middle of event dispatching. + -- For example, handler might add a menu to the end of the element stack, and that + -- than picks up this click event we are in right now, and instantly closes itself. + mp.add_timeout(0.01, self.on_click) +end + +function Button:render() + local visibility = self:get_visibility() + if visibility <= 0 then return end + if self.proximity_raw == 0 then + cursor.on_primary_down = function() self:handle_cursor_down() end + end + + local ass = assdraw.ass_new() + local is_hover = self.proximity_raw == 0 + local is_hover_or_active = is_hover or self.active + local foreground = self.active and self.background or self.foreground + local background = self.active and self.foreground or self.background + + -- Background + if is_hover_or_active then + ass:rect(self.ax, self.ay, self.bx, self.by, { + color = self.active and background or foreground, radius = 2, + opacity = visibility * (self.active and 1 or 0.3), + }) + end + + -- Tooltip on hover + if is_hover and self.tooltip then ass:tooltip(self, self.tooltip) end + + -- Badge + local icon_clip + if self.badge then + local badge_font_size = self.font_size * 0.6 + local badge_opts = {size = badge_font_size, color = background, opacity = visibility} + local badge_width = text_width(self.badge, badge_opts) + local width, height = math.ceil(badge_width + (badge_font_size / 7) * 2), math.ceil(badge_font_size * 0.93) + local bx, by = self.bx - 1, self.by - 1 + ass:rect(bx - width, by - height, bx, by, { + color = foreground, radius = 2, opacity = visibility, + border = self.active and 0 or 1, border_color = background, + }) + ass:txt(bx - width / 2, by - height / 2, 5, self.badge, badge_opts) + + local clip_border = math.max(self.font_size / 20, 1) + local clip_path = assdraw.ass_new() + clip_path:round_rect_cw( + math.floor((bx - width) - clip_border), math.floor((by - height) - clip_border), bx, by, 3 + ) + icon_clip = '\\iclip(' .. clip_path.scale .. ', ' .. clip_path.text .. ')' + end + + -- Icon + local x, y = round(self.ax + (self.bx - self.ax) / 2), round(self.ay + (self.by - self.ay) / 2) + ass:icon(x, y, self.font_size, self.icon, { + color = foreground, border = self.active and 0 or options.text_border, border_color = background, + opacity = visibility, clip = icon_clip, + }) + + return ass +end + +return Button diff --git a/multimedia/.config/mpv/scripts/uosc_shared/elements/Controls.lua b/multimedia/.config/mpv/scripts/uosc_shared/elements/Controls.lua new file mode 100644 index 0000000..9a6be72 --- /dev/null +++ b/multimedia/.config/mpv/scripts/uosc_shared/elements/Controls.lua @@ -0,0 +1,329 @@ +local Element = require('uosc_shared/elements/Element') +local Button = require('uosc_shared/elements/Button') +local CycleButton = require('uosc_shared/elements/CycleButton') +local Speed = require('uosc_shared/elements/Speed') + +-- `scale` - `options.controls_size` scale factor. +-- `ratio` - Width/height ratio of a static or dynamic element. +-- `ratio_min` Min ratio for 'dynamic' sized element. +---@alias ControlItem {element?: Element; kind: string; sizing: 'space' | 'static' | 'dynamic'; scale: number; ratio?: number; ratio_min?: number; hide: boolean; dispositions?: table} + +---@class Controls : Element +local Controls = class(Element) + +function Controls:new() return Class.new(self) --[[@as Controls]] end +function Controls:init() + Element.init(self, 'controls') + ---@type ControlItem[] All control elements serialized from `options.controls`. + self.controls = {} + ---@type ControlItem[] Only controls that match current dispositions. + self.layout = {} + + -- Serialize control elements + local shorthands = { + menu = 'command:menu:script-binding uosc/menu-blurred?Menu', + subtitles = 'command:subtitles:script-binding uosc/subtitles#sub>0?Subtitles', + audio = 'command:graphic_eq:script-binding uosc/audio#audio>1?Audio', + ['audio-device'] = 'command:speaker:script-binding uosc/audio-device?Audio device', + video = 'command:theaters:script-binding uosc/video#video>1?Video', + playlist = 'command:list_alt:script-binding uosc/playlist?Playlist', + chapters = 'command:bookmark:script-binding uosc/chapters#chapters>0?Chapters', + ['editions'] = 'command:bookmarks:script-binding uosc/editions#editions>1?Editions', + ['stream-quality'] = 'command:high_quality:script-binding uosc/stream-quality?Stream quality', + ['open-file'] = 'command:file_open:script-binding uosc/open-file?Open file', + ['items'] = 'command:list_alt:script-binding uosc/items?Playlist/Files', + prev = 'command:arrow_back_ios:script-binding uosc/prev?Previous', + next = 'command:arrow_forward_ios:script-binding uosc/next?Next', + first = 'command:first_page:script-binding uosc/first?First', + last = 'command:last_page:script-binding uosc/last?Last', + ['loop-playlist'] = 'cycle:repeat:loop-playlist:no/inf!?Loop playlist', + ['loop-file'] = 'cycle:repeat_one:loop-file:no/inf!?Loop file', + shuffle = 'toggle:shuffle:shuffle?Shuffle', + fullscreen = 'cycle:crop_free:fullscreen:no/yes=fullscreen_exit!?Fullscreen', + } + + -- Parse out disposition/config pairs + local items = {} + local in_disposition = false + local current_item = nil + for c in options.controls:gmatch('.') do + if not current_item then current_item = {disposition = '', config = ''} end + if c == '<' and #current_item.config == 0 then in_disposition = true + elseif c == '>' and #current_item.config == 0 then in_disposition = false + elseif c == ',' and not in_disposition then + items[#items + 1] = current_item + current_item = nil + else + local prop = in_disposition and 'disposition' or 'config' + current_item[prop] = current_item[prop] .. c + end + end + items[#items + 1] = current_item + + -- Create controls + self.controls = {} + for i, item in ipairs(items) do + local config = shorthands[item.config] and shorthands[item.config] or item.config + local config_tooltip = split(config, ' *%? *') + local tooltip = config_tooltip[2] + config = shorthands[config_tooltip[1]] + and split(shorthands[config_tooltip[1]], ' *%? *')[1] or config_tooltip[1] + local config_badge = split(config, ' *# *') + config = config_badge[1] + local badge = config_badge[2] + local parts = split(config, ' *: *') + local kind, params = parts[1], itable_slice(parts, 2) + + -- Serialize dispositions + local dispositions = {} + for _, definition in ipairs(split(item.disposition, ' *, *')) do + if #definition > 0 then + local value = definition:sub(1, 1) ~= '!' + local name = not value and definition:sub(2) or definition + local prop = name:sub(1, 4) == 'has_' and name or 'is_' .. name + dispositions[prop] = value + end + end + + -- Convert toggles into cycles + if kind == 'toggle' then + kind = 'cycle' + params[#params + 1] = 'no/yes!' + end + + -- Create a control element + local control = {dispositions = dispositions, kind = kind} + + if kind == 'space' then + control.sizing = 'space' + elseif kind == 'gap' then + table_assign(control, {sizing = 'dynamic', scale = 1, ratio = params[1] or 0.3, ratio_min = 0}) + elseif kind == 'command' then + if #params ~= 2 then + mp.error(string.format( + 'command button needs 2 parameters, %d received: %s', #params, table.concat(params, '/') + )) + else + local element = Button:new('control_' .. i, { + icon = params[1], + anchor_id = 'controls', + on_click = function() mp.command(params[2]) end, + tooltip = tooltip, + count_prop = 'sub', + }) + table_assign(control, {element = element, sizing = 'static', scale = 1, ratio = 1}) + if badge then self:register_badge_updater(badge, element) end + end + elseif kind == 'cycle' then + if #params ~= 3 then + mp.error(string.format( + 'cycle button needs 3 parameters, %d received: %s', + #params, table.concat(params, '/') + )) + else + local state_configs = split(params[3], ' */ *') + local states = {} + + for _, state_config in ipairs(state_configs) do + local active = false + if state_config:sub(-1) == '!' then + active = true + state_config = state_config:sub(1, -2) + end + local state_params = split(state_config, ' *= *') + local value, icon = state_params[1], state_params[2] or params[1] + states[#states + 1] = {value = value, icon = icon, active = active} + end + + local element = CycleButton:new('control_' .. i, { + prop = params[2], anchor_id = 'controls', states = states, tooltip = tooltip, + }) + table_assign(control, {element = element, sizing = 'static', scale = 1, ratio = 1}) + if badge then self:register_badge_updater(badge, element) end + end + elseif kind == 'speed' then + if not Elements.speed then + local element = Speed:new({anchor_id = 'controls'}) + table_assign(control, { + element = element, sizing = 'dynamic', scale = params[1] or 1.3, ratio = 3.5, ratio_min = 2, + }) + else + msg.error('there can only be 1 speed slider') + end + else + msg.error('unknown element kind "' .. kind .. '"') + break + end + + self.controls[#self.controls + 1] = control + end + + self:reflow() +end + +function Controls:reflow() + -- Populate the layout only with items that match current disposition + self.layout = {} + for _, control in ipairs(self.controls) do + local matches = true + for prop, value in pairs(control.dispositions) do + if state[prop] ~= value then + matches = false + break + end + end + if control.element then control.element.enabled = matches end + if matches then self.layout[#self.layout + 1] = control end + end + + self:update_dimensions() + Elements:trigger('controls_reflow') +end + +---@param badge string +---@param element Element An element that supports `badge` property. +function Controls:register_badge_updater(badge, element) + local prop_and_limit = split(badge, ' *> *') + local prop, limit = prop_and_limit[1], tonumber(prop_and_limit[2] or -1) + local observable_name, serializer, is_external_prop = prop, nil, false + + if itable_index_of({'sub', 'audio', 'video'}, prop) then + observable_name = 'track-list' + serializer = function(value) + local count = 0 + for _, track in ipairs(value) do if track.type == prop then count = count + 1 end end + return count + end + else + local parts = split(prop, '@') + -- Support both new `prop@owner` and old `@prop` syntaxes + if #parts > 1 then prop, is_external_prop = parts[1] ~= '' and parts[1] or parts[2], true end + serializer = function(value) return value and (type(value) == 'table' and #value or tostring(value)) or nil end + end + + local function handler(_, value) + local new_value = serializer(value) --[[@as nil|string|integer]] + local value_number = tonumber(new_value) + if value_number then new_value = value_number > limit and value_number or nil end + element.badge = new_value + request_render() + end + + if is_external_prop then element['on_external_prop_' .. prop] = function(_, value) handler(prop, value) end + else mp.observe_property(observable_name, 'native', handler) end +end + +function Controls:get_visibility() + return (Elements.speed and Elements.speed.dragging) and 1 or Elements.timeline:get_is_hovered() + and -1 or Element.get_visibility(self) +end + +function Controls:update_dimensions() + local window_border = Elements.window_border.size + local size = state.fullormaxed and options.controls_size_fullscreen or options.controls_size + local spacing = options.controls_spacing + local margin = options.controls_margin + + -- Disable when not enough space + local available_space = display.height - Elements.window_border.size * 2 + if Elements.top_bar.enabled then available_space = available_space - Elements.top_bar.size end + if Elements.timeline.enabled then available_space = available_space - Elements.timeline.size_max end + self.enabled = available_space > size + 10 + + -- Reset hide/enabled flags + for c, control in ipairs(self.layout) do + control.hide = false + if control.element then control.element.enabled = self.enabled end + end + + if not self.enabled then return end + + -- Container + self.bx = display.width - window_border - margin + self.by = (Elements.timeline.enabled and Elements.timeline.ay or display.height - window_border) - margin + self.ax, self.ay = window_border + margin, self.by - size + + -- Controls + local available_width = self.bx - self.ax + local statics_width = (#self.layout - 1) * spacing + local min_content_width = statics_width + local max_dynamics_width, dynamic_units, spaces = 0, 0, 0 + + -- Calculate statics_width, min_content_width, and count spaces + for c, control in ipairs(self.layout) do + if control.sizing == 'space' then + spaces = spaces + 1 + elseif control.sizing == 'static' then + local width = size * control.scale * control.ratio + statics_width = statics_width + width + min_content_width = min_content_width + width + elseif control.sizing == 'dynamic' then + min_content_width = min_content_width + size * control.scale * control.ratio_min + max_dynamics_width = max_dynamics_width + size * control.scale * control.ratio + dynamic_units = dynamic_units + control.scale * control.ratio + end + end + + -- Hide & disable elements in the middle until we fit into available width + if min_content_width > available_width then + local i = math.ceil(#self.layout / 2 + 0.1) + for a = 0, #self.layout - 1, 1 do + i = i + (a * (a % 2 == 0 and 1 or -1)) + local control = self.layout[i] + + if control.kind ~= 'gap' and control.kind ~= 'space' then + control.hide = true + if control.element then control.element.enabled = false end + if control.sizing == 'static' then + local width = size * control.scale * control.ratio + min_content_width = min_content_width - width - spacing + statics_width = statics_width - width - spacing + elseif control.sizing == 'dynamic' then + min_content_width = min_content_width - size * control.scale * control.ratio_min - spacing + max_dynamics_width = max_dynamics_width - size * control.scale * control.ratio + dynamic_units = dynamic_units - control.scale * control.ratio + end + + if min_content_width < available_width then break end + end + end + end + + -- Lay out the elements + local current_x = self.ax + local width_for_dynamics = available_width - statics_width + local space_width = (width_for_dynamics - max_dynamics_width) / spaces + + for c, control in ipairs(self.layout) do + if not control.hide then + local sizing, element, scale, ratio = control.sizing, control.element, control.scale, control.ratio + local width, height = 0, 0 + + if sizing == 'space' then + if space_width > 0 then width = space_width end + elseif sizing == 'static' then + height = size * scale + width = height * ratio + elseif sizing == 'dynamic' then + height = size * scale + width = max_dynamics_width < width_for_dynamics + and height * ratio or width_for_dynamics * ((scale * ratio) / dynamic_units) + end + + local bx = current_x + width + if element then element:set_coordinates(round(current_x), round(self.by - height), bx, self.by) end + current_x = bx + spacing + end + end + + Elements:update_proximities() + request_render() +end + +function Controls:on_dispositions() self:reflow() end +function Controls:on_display() self:update_dimensions() end +function Controls:on_prop_border() self:update_dimensions() end +function Controls:on_prop_fullormaxed() self:update_dimensions() end +function Controls:on_timeline_enabled() self:update_dimensions() end + +return Controls diff --git a/multimedia/.config/mpv/scripts/uosc_shared/elements/Curtain.lua b/multimedia/.config/mpv/scripts/uosc_shared/elements/Curtain.lua new file mode 100644 index 0000000..99b9f14 --- /dev/null +++ b/multimedia/.config/mpv/scripts/uosc_shared/elements/Curtain.lua @@ -0,0 +1,35 @@ +local Element = require('uosc_shared/elements/Element') + +---@class Curtain : Element +local Curtain = class(Element) + +function Curtain:new() return Class.new(self) --[[@as Curtain]] end +function Curtain:init() + Element.init(self, 'curtain', {ignores_menu = true}) + self.opacity = 0 + ---@type string[] + self.dependents = {} +end + +---@param id string +function Curtain:register(id) + self.dependents[#self.dependents + 1] = id + if #self.dependents == 1 then self:tween_property('opacity', self.opacity, 1) end +end + +---@param id string +function Curtain:unregister(id) + self.dependents = itable_filter(self.dependents, function(item) return item ~= id end) + if #self.dependents == 0 then self:tween_property('opacity', self.opacity, 0) end +end + +function Curtain:render() + if self.opacity == 0 or options.curtain_opacity == 0 then return end + local ass = assdraw.ass_new() + ass:rect(0, 0, display.width, display.height, { + color = '000000', opacity = options.curtain_opacity * self.opacity, + }) + return ass +end + +return Curtain diff --git a/multimedia/.config/mpv/scripts/uosc_shared/elements/CycleButton.lua b/multimedia/.config/mpv/scripts/uosc_shared/elements/CycleButton.lua new file mode 100644 index 0000000..7f1c02f --- /dev/null +++ b/multimedia/.config/mpv/scripts/uosc_shared/elements/CycleButton.lua @@ -0,0 +1,64 @@ +local Button = require('uosc_shared/elements/Button') + +---@alias CycleState {value: any; icon: string; active?: boolean} +---@alias CycleButtonProps {prop: string; states: CycleState[]; anchor_id?: string; tooltip?: string} + +---@class CycleButton : Button +local CycleButton = class(Button) + +---@param id string +---@param props CycleButtonProps +function CycleButton:new(id, props) return Class.new(self, id, props) --[[@as CycleButton]] end +---@param id string +---@param props CycleButtonProps +function CycleButton:init(id, props) + local is_state_prop = itable_index_of({'shuffle'}, props.prop) + self.prop = props.prop + self.states = props.states + + Button.init(self, id, props) + + self.icon = self.states[1].icon + self.active = self.states[1].active + self.current_state_index = 1 + self.on_click = function() + local new_state = self.states[self.current_state_index + 1] or self.states[1] + local new_value = new_state.value + if self.owner then + mp.commandv('script-message-to', self.owner, 'set', self.prop, new_value) + elseif is_state_prop then + if itable_index_of({'yes', 'no'}, new_value) then new_value = new_value == 'yes' end + set_state(self.prop, new_value) + else + mp.set_property(self.prop, new_value) + end + end + + self.handle_change = function(name, value) + if is_state_prop and type(value) == 'boolean' then value = value and 'yes' or 'no' end + local index = itable_find(self.states, function(state) return state.value == value end) + self.current_state_index = index or 1 + self.icon = self.states[self.current_state_index].icon + self.active = self.states[self.current_state_index].active + request_render() + end + + local prop_parts = split(self.prop, '@') + if #prop_parts == 2 then -- External prop with a script owner + self.prop, self.owner = prop_parts[1], prop_parts[2] + self['on_external_prop_' .. self.prop] = function(_, value) self.handle_change(self.prop, value) end + self.handle_change(self.prop, external[self.prop]) + elseif is_state_prop then -- uosc's state props + self['on_prop_' .. self.prop] = function(self, value) self.handle_change(self.prop, value) end + self.handle_change(self.prop, state[self.prop]) + else + mp.observe_property(self.prop, 'string', self.handle_change) + end +end + +function CycleButton:destroy() + Button.destroy(self) + mp.unobserve_property(self.handle_change) +end + +return CycleButton diff --git a/multimedia/.config/mpv/scripts/uosc_shared/elements/Element.lua b/multimedia/.config/mpv/scripts/uosc_shared/elements/Element.lua new file mode 100644 index 0000000..1bcbe08 --- /dev/null +++ b/multimedia/.config/mpv/scripts/uosc_shared/elements/Element.lua @@ -0,0 +1,154 @@ +---@alias ElementProps {enabled?: boolean; ax?: number; ay?: number; bx?: number; by?: number; ignores_menu?: boolean; anchor_id?: string;} + +-- Base class all elements inherit from. +---@class Element : Class +local Element = class() + +---@param id string +---@param props? ElementProps +function Element:init(id, props) + self.id = id + -- `false` means element won't be rendered, or receive events + self.enabled = true + -- Element coordinates + self.ax, self.ay, self.bx, self.by = 0, 0, 0, 0 + -- Relative proximity from `0` - mouse outside `proximity_max` range, to `1` - mouse within `proximity_min` range. + self.proximity = 0 + -- Raw proximity in pixels. + self.proximity_raw = INFINITY + ---@type number `0-1` factor to force min visibility. Used for toggling element's permanent visibility. + self.min_visibility = 0 + ---@type number `0-1` factor to force a visibility value. Used for flashing, fading out, and other animations + self.forced_visibility = nil + ---@type boolean Render this element even when menu is open. + self.ignores_menu = false + ---@type nil|string ID of an element from which this one should inherit visibility. + self.anchor_id = nil + + if props then table_assign(self, props) end + + -- Flash timer + self._flash_out_timer = mp.add_timeout(options.flash_duration / 1000, function() + local function getTo() return self.proximity end + local function onTweenEnd() self.forced_visibility = nil end + if self.enabled then self:tween_property('forced_visibility', 1, getTo, onTweenEnd) + else onTweenEnd() end + end) + self._flash_out_timer:kill() + + Elements:add(self) +end + +function Element:destroy() + self.destroyed = true + Elements:remove(self) +end + +function Element:reset_proximity() self.proximity, self.proximity_raw = 0, INFINITY end + +---@param ax number +---@param ay number +---@param bx number +---@param by number +function Element:set_coordinates(ax, ay, bx, by) + self.ax, self.ay, self.bx, self.by = ax, ay, bx, by + Elements:update_proximities() + self:maybe('on_coordinates') +end + +function Element:update_proximity() + if cursor.hidden then + self:reset_proximity() + else + local range = options.proximity_out - options.proximity_in + self.proximity_raw = get_point_to_rectangle_proximity(cursor, self) + self.proximity = 1 - (clamp(0, self.proximity_raw - options.proximity_in, range) / range) + end +end + +function Element:is_persistent() + local persist = config[self.id .. '_persistency'] + return persist and ( + (persist.audio and state.is_audio) + or (persist.paused and state.pause and (not Elements.timeline.pressed or Elements.timeline.pressed.pause)) + or (persist.video and state.is_video) + or (persist.image and state.is_image) + or (persist.idle and state.is_idle) + ) +end + +-- Decide elements visibility based on proximity and various other factors +function Element:get_visibility() + -- Hide when menu is open, unless this is a menu + ---@diagnostic disable-next-line: undefined-global + if not self.ignores_menu and Menu and Menu:is_open() then return 0 end + + -- Persistency + if self:is_persistent() then return 1 end + + -- Forced visibility + if self.forced_visibility then return math.max(self.forced_visibility, self.min_visibility) end + + -- Anchor inheritance + -- If anchor returns -1, it means all attached elements should force hide. + local anchor = self.anchor_id and Elements[self.anchor_id] + local anchor_visibility = anchor and anchor:get_visibility() or 0 + + return anchor_visibility == -1 and 0 or math.max(self.proximity, anchor_visibility, self.min_visibility) +end + +-- Call method if it exists +function Element:maybe(name, ...) + if self[name] then return self[name](self, ...) end +end + +-- Attach a tweening animation to this element +---@param from number +---@param to number|fun():number +---@param setter fun(value: number) +---@param factor_or_callback? number|fun() +---@param callback? fun() Called either on animation end, or when animation is killed. +function Element:tween(from, to, setter, factor_or_callback, callback) + self:tween_stop() + self._kill_tween = self.enabled and tween( + from, to, setter, factor_or_callback, + function() + self._kill_tween = nil + if callback then callback() end + end + ) +end + +function Element:is_tweening() return self and self._kill_tween end +function Element:tween_stop() self:maybe('_kill_tween') end + +-- Animate an element property between 2 values. +---@param prop string +---@param from number +---@param to number|fun():number +---@param factor_or_callback? number|fun() +---@param callback? fun() Called either on animation end, or when animation is killed. +function Element:tween_property(prop, from, to, factor_or_callback, callback) + self:tween(from, to, function(value) self[prop] = value end, factor_or_callback, callback) +end + +---@param name string +function Element:trigger(name, ...) + local result = self:maybe('on_' .. name, ...) + request_render() + return result +end + +-- Briefly flashes the element for `options.flash_duration` milliseconds. +-- Useful to visualize changes of volume and timeline when changed via hotkeys. +function Element:flash() + if self.enabled and options.flash_duration > 0 and (self.proximity < 1 or self._flash_out_timer:is_enabled()) then + self:tween_stop() + self.forced_visibility = 1 + request_render() + self._flash_out_timer:kill() + self._flash_out_timer:resume() + end +end + +return Element diff --git a/multimedia/.config/mpv/scripts/uosc_shared/elements/Elements.lua b/multimedia/.config/mpv/scripts/uosc_shared/elements/Elements.lua new file mode 100644 index 0000000..489819a --- /dev/null +++ b/multimedia/.config/mpv/scripts/uosc_shared/elements/Elements.lua @@ -0,0 +1,125 @@ +local Elements = {itable = {}} + +---@param element Element +function Elements:add(element) + if not element.id then + msg.error('attempt to add element without "id" property') + return + end + + if self:has(element.id) then Elements:remove(element.id) end + + self.itable[#self.itable + 1] = element + self[element.id] = element + + request_render() +end + +function Elements:remove(idOrElement) + if not idOrElement then return end + local id = type(idOrElement) == 'table' and idOrElement.id or idOrElement + local element = Elements[id] + if element then + if not element.destroyed then element:destroy() end + element.enabled = false + self.itable = itable_remove(self.itable, self[id]) + self[id] = nil + request_render() + end +end + +function Elements:update_proximities() + local menu_only = Elements.menu ~= nil + local mouse_leave_elements = {} + local mouse_enter_elements = {} + + -- Calculates proximities and opacities for defined elements + for _, element in self:ipairs() do + if element.enabled then + local previous_proximity_raw = element.proximity_raw + + -- If menu is open, all other elements have to be disabled + if menu_only then + if element.ignores_menu then element:update_proximity() + else element:reset_proximity() end + else + element:update_proximity() + end + + if element.proximity_raw == 0 then + -- Mouse entered element area + if previous_proximity_raw ~= 0 then + mouse_enter_elements[#mouse_enter_elements + 1] = element + end + else + -- Mouse left element area + if previous_proximity_raw == 0 then + mouse_leave_elements[#mouse_leave_elements + 1] = element + end + end + end + end + + -- Trigger `mouse_leave` and `mouse_enter` events + for _, element in ipairs(mouse_leave_elements) do element:trigger('mouse_leave') end + for _, element in ipairs(mouse_enter_elements) do element:trigger('mouse_enter') end +end + +-- Toggles passed elements' min visibilities between 0 and 1. +---@param ids string[] IDs of elements to peek. +function Elements:toggle(ids) + local has_invisible = itable_find(ids, function(id) return Elements[id] and Elements[id]:get_visibility() ~= 1 end) + self:set_min_visibility(has_invisible and 1 or 0, ids) + -- Reset proximities when toggling off. Has to happen after `set_min_visibility`, + -- as that is using proximity as a tween starting point. + if not has_invisible then + for _, id in ipairs(ids) do + if Elements[id] then Elements[id]:reset_proximity() end + end + end +end + +-- Set (animate) elements' min visibilities to passed value. +---@param visibility number 0-1 floating point. +---@param ids string[] IDs of elements to peek. +function Elements:set_min_visibility(visibility, ids) + for _, id in ipairs(ids) do + local element = Elements[id] + if element then + local from = math.max(0, element:get_visibility()) + element:tween_property('min_visibility', from, visibility) + end + end +end + +-- Flash passed elements. +---@param ids string[] IDs of elements to peek. +function Elements:flash(ids) + local elements = itable_filter(self.itable, function(element) return itable_index_of(ids, element.id) ~= nil end) + for _, element in ipairs(elements) do element:flash() end +end + +---@param name string Event name. +function Elements:trigger(name, ...) + for _, element in self:ipairs() do element:trigger(name, ...) end +end + +-- Trigger two events, `name` and `global_name`, depending on element-cursor proximity. +-- Disabled elements don't receive these events. +---@param name string Event name. +function Elements:proximity_trigger(name, ...) + for i = #self.itable, 1, -1 do + local element = self.itable[i] + if element.enabled then + if element.proximity_raw == 0 then + if element:trigger(name, ...) == 'stop_propagation' then break end + end + if element:trigger('global_' .. name, ...) == 'stop_propagation' then break end + end + end +end + +function Elements:has(id) return self[id] ~= nil end +function Elements:ipairs() return ipairs(self.itable) end + +return Elements diff --git a/multimedia/.config/mpv/scripts/uosc_shared/elements/Menu.lua b/multimedia/.config/mpv/scripts/uosc_shared/elements/Menu.lua new file mode 100644 index 0000000..1830647 --- /dev/null +++ b/multimedia/.config/mpv/scripts/uosc_shared/elements/Menu.lua @@ -0,0 +1,854 @@ +local Element = require('uosc_shared/elements/Element') + +-- Menu data structure accepted by `Menu:open(menu)`. +---@alias MenuData {type?: string; title?: string; hint?: string; keep_open?: boolean; separator?: boolean; items?: MenuDataItem[]; selected_index?: integer;} +---@alias MenuDataItem MenuDataValue|MenuData +---@alias MenuDataValue {title?: string; hint?: string; icon?: string; value: any; bold?: boolean; italic?: boolean; muted?: boolean; active?: boolean; keep_open?: boolean; separator?: boolean;} +---@alias MenuOptions {mouse_nav?: boolean; on_open?: fun(); on_close?: fun(); on_back?: fun(); on_move_item?: fun(from_index: integer, to_index: integer, submenu_path: integer[]); on_delete_item?: fun(index: integer, submenu_path: integer[])} + +-- Internal data structure created from `Menu`. +---@alias MenuStack {id?: string; type?: string; title?: string; hint?: string; selected_index?: number; keep_open?: boolean; separator?: boolean; items: MenuStackItem[]; parent_menu?: MenuStack; submenu_path: integer[]; active?: boolean; width: number; height: number; top: number; scroll_y: number; scroll_height: number; title_width: number; hint_width: number; max_width: number; is_root?: boolean; fling?: Fling} +---@alias MenuStackItem MenuStackValue|MenuStack +---@alias MenuStackValue {title?: string; hint?: string; icon?: string; value: any; active?: boolean; bold?: boolean; italic?: boolean; muted?: boolean; keep_open?: boolean; separator?: boolean; title_width: number; hint_width: number} +---@alias Fling {y: number, distance: number, time: number, easing: fun(x: number), duration: number, update_cursor?: boolean} + +---@alias Modifiers {shift?: boolean, ctrl?: boolean, alt?: boolean} +---@alias MenuCallbackMeta {modifiers: Modifiers} +---@alias MenuCallback fun(value: any, meta: MenuCallbackMeta) + +---@class Menu : Element +local Menu = class(Element) + +---@param data MenuData +---@param callback MenuCallback +---@param opts? MenuOptions +function Menu:open(data, callback, opts) + local open_menu = self:is_open() + if open_menu then + open_menu.is_being_replaced = true + open_menu:close(true) + end + return Menu:new(data, callback, opts) +end + +---@param menu_type? string +---@return Menu|nil +function Menu:is_open(menu_type) + return Elements.menu and (not menu_type or Elements.menu.type == menu_type) and Elements.menu or nil +end + +---@param immediate? boolean Close immediately without fadeout animation. +---@param callback? fun() Called after the animation (if any) ends and element is removed and destroyed. +---@overload fun(callback: fun()) +function Menu:close(immediate, callback) + if type(immediate) ~= 'boolean' then callback = immediate end + + local menu = self == Menu and Elements.menu or self + + if menu and not menu.destroyed then + if menu.is_closing then + menu:tween_stop() + return + end + + local function close() + Elements:remove('menu') + menu.is_closing, menu.stack, menu.current, menu.all, menu.by_id = false, nil, nil, {}, {} + menu:disable_key_bindings() + Elements:update_proximities() + cursor.queue_autohide() + if callback then callback() end + request_render() + end + + menu.is_closing = true + + if immediate then close() + else menu:fadeout(close) end + end +end + +---@param data MenuData +---@param callback MenuCallback +---@param opts? MenuOptions +---@return Menu +function Menu:new(data, callback, opts) return Class.new(self, data, callback, opts) --[[@as Menu]] end +---@param data MenuData +---@param callback MenuCallback +---@param opts? MenuOptions +function Menu:init(data, callback, opts) + Element.init(self, 'menu', {ignores_menu = true}) + + -----@type fun() + self.callback = callback + self.opts = opts or {} + self.offset_x = 0 -- Used for submenu transition animation. + self.mouse_nav = self.opts.mouse_nav -- Stops pre-selecting items + ---@type Modifiers|nil + self.modifiers = nil + self.item_height = nil + self.item_spacing = 1 + self.item_padding = nil + self.font_size = nil + self.font_size_hint = nil + self.scroll_step = nil -- Item height + item spacing. + self.scroll_height = nil -- Items + spacings - container height. + self.opacity = 0 -- Used to fade in/out. + self.type = data.type + ---@type MenuStack Root MenuStack. + self.root = nil + ---@type MenuStack Current MenuStack. + self.current = nil + ---@type MenuStack[] All menus in a flat array. + self.all = nil + ---@type table Map of submenus by their ids, such as `'Tools > Aspect ratio'`. + self.by_id = {} + self.key_bindings = {} + self.is_being_replaced = false + self.is_closing, self.is_closed = false, false + ---@type {y: integer, time: number}[] + self.drag_data = nil + self.is_dragging = false + + self:update(data) + + if self.mouse_nav then + if self.current then self.current.selected_index = nil end + else + for _, menu in ipairs(self.all) do self:scroll_to_index(menu.selected_index, menu) end + end + + self:tween_property('opacity', 0, 1) + self:enable_key_bindings() + Elements.curtain:register('menu') + if self.opts.on_open then self.opts.on_open() end +end + +function Menu:destroy() + Element.destroy(self) + self:disable_key_bindings() + self.is_closed = true + if not self.is_being_replaced then Elements.curtain:unregister('menu') end + if self.opts.on_close then self.opts.on_close() end +end + +---@param data MenuData +function Menu:update(data) + self.type = data.type + + local new_root = {is_root = true, submenu_path = {}} + local new_all = {} + local new_by_id = {} + local menus_to_serialize = {{new_root, data}} + local old_current_id = self.current and self.current.id + + table_assign(new_root, data, {'type', 'title', 'hint', 'keep_open'}) + + local i = 0 + while i < #menus_to_serialize do + i = i + 1 + local menu, menu_data = menus_to_serialize[i][1], menus_to_serialize[i][2] + local parent_id = menu.parent_menu and not menu.parent_menu.is_root and menu.parent_menu.id + if not menu.is_root then + menu.id = (parent_id and parent_id .. ' > ' or '') .. (menu_data.title or i) + end + menu.icon = 'chevron_right' + + -- Update items + local first_active_index = nil + menu.items = {} + + for i, item_data in ipairs(menu_data.items or {}) do + if item_data.active and not first_active_index then first_active_index = i end + + local item = {} + table_assign(item, item_data, { + 'title', 'icon', 'hint', 'active', 'bold', 'italic', 'muted', 'value', 'keep_open', 'separator', + }) + if item.keep_open == nil then item.keep_open = menu.keep_open end + + -- Submenu + if item_data.items then + item.parent_menu = menu + item.submenu_path = itable_join(menu.submenu_path, {i}) + menus_to_serialize[#menus_to_serialize + 1] = {item, item_data} + end + + menu.items[i] = item + end + + if menu.is_root then menu.selected_index = menu_data.selected_index or first_active_index end + + -- Retain old state + local old_menu = self.by_id[menu.is_root and '__root__' or menu.id] + if old_menu then table_assign(menu, old_menu, {'selected_index', 'scroll_y', 'fling'}) end + + new_all[#new_all + 1] = menu + new_by_id[menu.is_root and '__root__' or menu.id] = menu + end + + self.root, self.all, self.by_id = new_root, new_all, new_by_id + self.current = self.by_id[old_current_id] or self.root + + self:update_content_dimensions() + self:reset_navigation() +end + +---@param items MenuDataItem[] +function Menu:update_items(items) + local data = table_shallow_copy(self.root) + data.items = items + self:update(data) +end + +function Menu:update_content_dimensions() + self.item_height = state.fullormaxed and options.menu_item_height_fullscreen or options.menu_item_height + self.font_size = round(self.item_height * 0.48 * options.font_scale) + self.font_size_hint = self.font_size - 1 + self.item_padding = round((self.item_height - self.font_size) * 0.6) + self.scroll_step = self.item_height + self.item_spacing + + local title_opts = {size = self.font_size, italic = false, bold = false} + local hint_opts = {size = self.font_size_hint} + + for _, menu in ipairs(self.all) do + title_opts.bold, title_opts.italic = true, false + local max_width = text_width(menu.title, title_opts) + 2 * self.item_padding + + -- Estimate width of a widest item + for _, item in ipairs(menu.items) do + local icon_width = item.icon and self.font_size or 0 + item.title_width = text_width(item.title, title_opts) + item.hint_width = text_width(item.hint, hint_opts) + local spacings_in_item = 1 + (item.title_width > 0 and 1 or 0) + + (item.hint_width > 0 and 1 or 0) + (icon_width > 0 and 1 or 0) + local estimated_width = item.title_width + item.hint_width + icon_width + + (self.item_padding * spacings_in_item) + if estimated_width > max_width then max_width = estimated_width end + end + + menu.max_width = max_width + end + + self:update_dimensions() +end + +function Menu:update_dimensions() + -- Coordinates and sizes are of the scrollable area to make + -- consuming values in rendering and collisions easier. Title is rendered + -- above it, so we need to account for that in max_height and ay position. + local min_width = state.fullormaxed and options.menu_min_width_fullscreen or options.menu_min_width + + for _, menu in ipairs(self.all) do + menu.width = round(clamp(min_width, menu.max_width, display.width * 0.9)) + local title_height = (menu.is_root and menu.title) and self.scroll_step or 0 + local max_height = round((display.height - title_height) * 0.9) + local content_height = self.scroll_step * #menu.items + menu.height = math.min(content_height - self.item_spacing, max_height) + menu.top = round(math.max((display.height - menu.height) / 2, title_height * 1.5)) + menu.scroll_height = math.max(content_height - menu.height - self.item_spacing, 0) + menu.scroll_y = menu.scroll_y or 0 + self:scroll_to(menu.scroll_y, menu) -- clamps scroll_y to scroll limits + end + + self:update_coordinates() +end + +-- Updates element coordinates to match currently open (sub)menu. +function Menu:update_coordinates() + local ax = round((display.width - self.current.width) / 2) + self.offset_x + self:set_coordinates(ax, self.current.top, ax + self.current.width, self.current.top + self.current.height) +end + +function Menu:reset_navigation() + local menu = self.current + + -- Reset indexes and scroll + self:scroll_to(menu.scroll_y) -- clamps scroll_y to scroll limits + if self.mouse_nav then + self:select_item_below_cursor() + else + self:select_index((menu.items and #menu.items > 0) and clamp(1, menu.selected_index or 1, #menu.items) or nil) + end + + -- Walk up the parent menu chain and activate items that lead to current menu + local parent = menu.parent_menu + while parent do + parent.selected_index = itable_index_of(parent.items, menu) + menu, parent = parent, parent.parent_menu + end + + request_render() +end + +function Menu:set_offset_x(offset) + local delta = offset - self.offset_x + self.offset_x = offset + self:set_coordinates(self.ax + delta, self.ay, self.bx + delta, self.by) +end + +function Menu:fadeout(callback) self:tween_property('opacity', 1, 0, callback) end + +function Menu:get_item_index_below_cursor() + local menu = self.current + if #menu.items < 1 or self.proximity_raw > 0 then return nil end + return math.max(1, math.min(math.ceil((cursor.y - self.ay + menu.scroll_y) / self.scroll_step), #menu.items)) +end + +function Menu:get_first_active_index(menu) + menu = menu or self.current + for index, item in ipairs(self.current.items) do + if item.active then return index end + end +end + +---@param pos? number +---@param menu? MenuStack +function Menu:set_scroll_to(pos, menu) + menu = menu or self.current + menu.scroll_y = clamp(0, pos or 0, menu.scroll_height) + request_render() +end + +---@param delta? number +---@param menu? MenuStack +function Menu:set_scroll_by(delta, menu) + menu = menu or self.current + self:set_scroll_to(menu.scroll_y + delta, menu) +end + +---@param pos? number +---@param menu? MenuStack +---@param fling_options? table +function Menu:scroll_to(pos, menu, fling_options) + menu = menu or self.current + menu.fling = { + y = menu.scroll_y, distance = clamp(-menu.scroll_y, pos - menu.scroll_y, menu.scroll_height - menu.scroll_y), + time = mp.get_time(), duration = 0.1, easing = ease_out_sext, + } + if fling_options then table_assign(menu.fling, fling_options) end + request_render() +end + +---@param delta? number +---@param menu? MenuStack +---@param fling_options? Fling +function Menu:scroll_by(delta, menu, fling_options) + menu = menu or self.current + self:scroll_to((menu.fling and (menu.fling.y + menu.fling.distance) or menu.scroll_y) + delta, menu, fling_options) +end + +---@param index? integer +---@param menu? MenuStack +---@param immediate? boolean +function Menu:scroll_to_index(index, menu, immediate) + menu = menu or self.current + if (index and index >= 1 and index <= #menu.items) then + local position = round((self.scroll_step * (index - 1)) - ((menu.height - self.scroll_step) / 2)) + if immediate then self:set_scroll_to(position, menu) + else self:scroll_to(position, menu) end + end +end + +---@param index? integer +---@param menu? MenuStack +function Menu:select_index(index, menu) + menu = menu or self.current + menu.selected_index = (index and index >= 1 and index <= #menu.items) and index or nil + request_render() +end + +---@param value? any +---@param menu? MenuStack +function Menu:select_value(value, menu) + menu = menu or self.current + local index = itable_find(menu.items, function(item) return item.value == value end) + self:select_index(index) +end + +---@param menu? MenuStack +function Menu:deactivate_items(menu) + menu = menu or self.current + for _, item in ipairs(menu.items) do item.active = false end + request_render() +end + +---@param index? integer +---@param menu? MenuStack +function Menu:activate_index(index, menu) + menu = menu or self.current + if index and index >= 1 and index <= #menu.items then menu.items[index].active = true end + request_render() +end + +---@param index? integer +---@param menu? MenuStack +function Menu:activate_one_index(index, menu) + self:deactivate_items(menu) + self:activate_index(index, menu) +end + +---@param value? any +---@param menu? MenuStack +function Menu:activate_value(value, menu) + menu = menu or self.current + local index = itable_find(menu.items, function(item) return item.value == value end) + self:activate_index(index, menu) +end + +---@param value? any +---@param menu? MenuStack +function Menu:activate_one_value(value, menu) + menu = menu or self.current + local index = itable_find(menu.items, function(item) return item.value == value end) + self:activate_one_index(index, menu) +end + +---@param menu MenuStack One of menus in `self.all`. +function Menu:activate_menu(menu) + if itable_index_of(self.all, menu) then + self.current = menu + self:update_coordinates() + self:reset_navigation() + request_render() + else + msg.error('Attempt to open a menu not in `self.all` list.') + end +end + +---@param id string +function Menu:activate_submenu(id) + local submenu = self.by_id[id] + if submenu then self:activate_menu(submenu) + else msg.error(string.format('Requested submenu id "%s" doesn\'t exist', id)) end +end + +---@param index? integer +---@param menu? MenuStack +function Menu:delete_index(index, menu) + menu = menu or self.current + if (index and index >= 1 and index <= #menu.items) then + table.remove(menu.items, index) + self:update_content_dimensions() + self:scroll_to_index(menu.selected_index, menu) + end +end + +---@param value? any +---@param menu? MenuStack +function Menu:delete_value(value, menu) + menu = menu or self.current + local index = itable_find(menu.items, function(item) return item.value == value end) + self:delete_index(index) +end + +---@param menu? MenuStack +function Menu:prev(menu) + menu = menu or self.current + menu.selected_index = math.max(menu.selected_index and menu.selected_index - 1 or #menu.items, 1) + self:scroll_to_index(menu.selected_index, menu, true) +end + +---@param menu? MenuStack +function Menu:next(menu) + menu = menu or self.current + menu.selected_index = math.min(menu.selected_index and menu.selected_index + 1 or 1, #menu.items) + self:scroll_to_index(menu.selected_index, menu, true) +end + +function Menu:back() + if self.opts.on_back then + self.opts.on_back() + if self.is_closed then return end + end + + local menu = self.current + local parent = menu.parent_menu + + if parent then + menu.selected_index = nil + self:activate_menu(parent) + self:tween(self.offset_x - menu.width / 2, 0, function(offset) self:set_offset_x(offset) end) + self.opacity = 1 -- in case tween above canceled fade in animation + else + self:close() + end +end + +---@param opts? {keep_open?: boolean, preselect_submenu_item?: boolean} +function Menu:open_selected_item(opts) + opts = opts or {} + local menu = self.current + if menu.selected_index then + local item = menu.items[menu.selected_index] + -- Is submenu + if item.items then + if opts.preselect_submenu_item then + item.selected_index = #item.items > 0 and 1 or nil + end + self:activate_menu(item) + self:tween(self.offset_x + menu.width / 2, 0, function(offset) self:set_offset_x(offset) end) + self.opacity = 1 -- in case tween above canceled fade in animation + else + self.callback(item.value, {modifiers = self.modifiers or {}}) + if not item.keep_open and not opts.keep_open then self:close() end + end + end +end + +function Menu:open_selected_item_soft() self:open_selected_item({keep_open = true}) end +function Menu:open_selected_item_preselect() self:open_selected_item({preselect_submenu_item = true}) end +function Menu:select_item_below_cursor() self.current.selected_index = self:get_item_index_below_cursor() end + +---@param index integer +function Menu:move_selected_item_to(index) + local from, callback = self.current.selected_index, self.opts.on_move_item + if callback and from and from ~= index and index >= 1 and index <= #self.current.items then + callback(from, index, self.current.submenu_path) + self.current.selected_index = index + request_render() + end +end + +function Menu:move_selected_item_up() + if self.current.selected_index then self:move_selected_item_to(self.current.selected_index - 1) end +end + +function Menu:move_selected_item_down() + if self.current.selected_index then self:move_selected_item_to(self.current.selected_index + 1) end +end + +function Menu:delete_selected_item() + local index, callback = self.current.selected_index, self.opts.on_delete_item + if callback and index then callback(index, self.current.submenu_path) end +end + +function Menu:on_display() self:update_dimensions() end +function Menu:on_prop_fullormaxed() self:update_content_dimensions() end + +function Menu:handle_cursor_down() + if self.proximity_raw == 0 then + self.drag_data = {{y = cursor.y, time = mp.get_time()}} + self.current.fling = nil + else + if cursor.x < self.ax and self.current.parent_menu then self:back() + else self:close() end + end +end + +function Menu:fling_distance() + local first, last = self.drag_data[1], self.drag_data[#self.drag_data] + if mp.get_time() - last.time > 0.05 then return 0 end + for i = #self.drag_data - 1, 1, -1 do + local drag = self.drag_data[i] + if last.time - drag.time > 0.03 then return ((drag.y - last.y) / ((last.time - drag.time) / 0.03)) * 10 end + end + return #self.drag_data < 2 and 0 or ((first.y - last.y) / ((first.time - last.time) / 0.03)) * 10 +end + +function Menu:handle_cursor_up() + if self.proximity_raw == 0 and self.drag_data and not self.is_dragging then + self:select_item_below_cursor() + self:open_selected_item({preselect_submenu_item = false, keep_open = self.modifiers and self.modifiers.shift}) + end + if self.is_dragging then + local distance = self:fling_distance() + if math.abs(distance) > 50 then + self.current.fling = { + y = self.current.scroll_y, distance = distance, time = self.drag_data[#self.drag_data].time, + easing = ease_out_quart, duration = 0.5, update_cursor = true, + } + end + end + self.is_dragging = false + self.drag_data = nil +end + + +function Menu:on_global_mouse_move() + self.mouse_nav = true + if self.drag_data then + self.is_dragging = self.is_dragging or math.abs(cursor.y - self.drag_data[1].y) >= 10 + local distance = self.drag_data[#self.drag_data].y - cursor.y + if distance ~= 0 then self:set_scroll_by(distance) end + self.drag_data[#self.drag_data + 1] = {y = cursor.y, time = mp.get_time()} + end + if self.proximity_raw == 0 or self.is_dragging then self:select_item_below_cursor() + else self.current.selected_index = nil end + request_render() +end + +function Menu:handle_wheel_up() self:scroll_by(self.scroll_step * -3, nil, {update_cursor = true}) end +function Menu:handle_wheel_down() self:scroll_by(self.scroll_step * 3, nil, {update_cursor = true}) end + +function Menu:on_pgup() + local menu = self.current + local items_per_page = round((menu.height / self.scroll_step) * 0.4) + local paged_index = (menu.selected_index and menu.selected_index or #menu.items) - items_per_page + menu.selected_index = clamp(1, paged_index, #menu.items) + if menu.selected_index > 0 then self:scroll_to_index(menu.selected_index) end +end + +function Menu:on_pgdwn() + local menu = self.current + local items_per_page = round((menu.height / self.scroll_step) * 0.4) + local paged_index = (menu.selected_index and menu.selected_index or 1) + items_per_page + menu.selected_index = clamp(1, paged_index, #menu.items) + if menu.selected_index > 0 then self:scroll_to_index(menu.selected_index) end +end + +function Menu:on_home() + self.current.selected_index = math.min(1, #self.current.items) + if self.current.selected_index > 0 then self:scroll_to_index(self.current.selected_index) end +end + +function Menu:on_end() + self.current.selected_index = #self.current.items + if self.current.selected_index > 0 then self:scroll_to_index(self.current.selected_index) end +end + +function Menu:add_key_binding(key, name, fn, flags) + self.key_bindings[#self.key_bindings + 1] = name + mp.add_forced_key_binding(key, name, fn, flags) +end + +function Menu:enable_key_bindings() + -- The `mp.set_key_bindings()` method would be easier here, but that + -- doesn't support 'repeatable' flag, so we are stuck with this monster. + self:add_key_binding('up', 'menu-prev1', self:create_key_action('prev'), 'repeatable') + self:add_key_binding('down', 'menu-next1', self:create_key_action('next'), 'repeatable') + self:add_key_binding('ctrl+up', 'menu-move-up', self:create_key_action('move_selected_item_up'), 'repeatable') + self:add_key_binding('ctrl+down', 'menu-move-down', self:create_key_action('move_selected_item_down'), 'repeatable') + self:add_key_binding('left', 'menu-back1', self:create_key_action('back')) + self:add_key_binding('right', 'menu-select1', self:create_key_action('open_selected_item_preselect')) + self:add_key_binding('shift+right', 'menu-select-soft1', + self:create_key_action('open_selected_item_soft', {shift = true})) + self:add_key_binding('shift+mbtn_left', 'menu-select3', self:create_modified_mbtn_left_handler({shift = true})) + self:add_key_binding('ctrl+mbtn_left', 'menu-select4', self:create_modified_mbtn_left_handler({ctrl = true})) + self:add_key_binding('mbtn_back', 'menu-back-alt3', self:create_key_action('back')) + self:add_key_binding('bs', 'menu-back-alt4', self:create_key_action('back')) + self:add_key_binding('enter', 'menu-select-alt3', self:create_key_action('open_selected_item_preselect')) + self:add_key_binding('kp_enter', 'menu-select-alt4', self:create_key_action('open_selected_item_preselect')) + self:add_key_binding('ctrl+enter', 'menu-select-ctrl1', + self:create_key_action('open_selected_item_preselect', {ctrl = true})) + self:add_key_binding('ctrl+kp_enter', 'menu-select-ctrl2', + self:create_key_action('open_selected_item_preselect', {ctrl = true})) + self:add_key_binding('shift+enter', 'menu-select-alt5', + self:create_key_action('open_selected_item_soft', {shift = true})) + self:add_key_binding('shift+kp_enter', 'menu-select-alt6', + self:create_key_action('open_selected_item_soft', {shift = true})) + self:add_key_binding('esc', 'menu-close', self:create_key_action('close')) + self:add_key_binding('pgup', 'menu-page-up', self:create_key_action('on_pgup'), 'repeatable') + self:add_key_binding('pgdwn', 'menu-page-down', self:create_key_action('on_pgdwn'), 'repeatable') + self:add_key_binding('home', 'menu-home', self:create_key_action('on_home')) + self:add_key_binding('end', 'menu-end', self:create_key_action('on_end')) + self:add_key_binding('del', 'menu-delete-item', self:create_key_action('delete_selected_item')) +end + +function Menu:disable_key_bindings() + for _, name in ipairs(self.key_bindings) do mp.remove_key_binding(name) end + self.key_bindings = {} +end + +---@param modifiers Modifiers +function Menu:create_modified_mbtn_left_handler(modifiers) + return function() + self.mouse_nav = true + self.modifiers = modifiers + self:handle_cursor_down() + self:handle_cursor_up() + self.modifiers = nil + end +end + +---@param name string +---@param modifiers? Modifiers +function Menu:create_key_action(name, modifiers) + return function() + self.mouse_nav = false + self.modifiers = modifiers + self:maybe(name) + self.modifiers = nil + end +end + +function Menu:render() + local update_cursor = false + for _, menu in ipairs(self.all) do + if menu.fling then + update_cursor = update_cursor or menu.fling.update_cursor or false + local time_delta = state.render_last_time - menu.fling.time + local progress = menu.fling.easing(math.min(time_delta / menu.fling.duration, 1)) + self:set_scroll_to(round(menu.fling.y + menu.fling.distance * progress), menu) + if progress < 1 then request_render() else menu.fling = nil end + end + end + if update_cursor then self:select_item_below_cursor() end + + cursor.on_primary_down = function() self:handle_cursor_down() end + cursor.on_primary_up = function() self:handle_cursor_up() end + if self.proximity_raw == 0 then + cursor.on_wheel_down = function() self:handle_wheel_down() end + cursor.on_wheel_up = function() self:handle_wheel_up() end + end + + local ass = assdraw.ass_new() + local opacity = options.menu_opacity * self.opacity + local spacing = self.item_padding + local icon_size = self.font_size + + function draw_menu(menu, x, y, opacity) + local ax, ay, bx, by = x, y, x + menu.width, y + menu.height + local draw_title = menu.is_root and menu.title + local scroll_clip = '\\clip(0,' .. ay .. ',' .. display.width .. ',' .. by .. ')' + local start_index = math.floor(menu.scroll_y / self.scroll_step) + 1 + local end_index = math.ceil((menu.scroll_y + menu.height) / self.scroll_step) + local selected_index = menu.selected_index or -1 + -- remove menu_opacity to start off with full opacity, but still decay for parent menus + local text_opacity = opacity / options.menu_opacity + + -- Background + ass:rect(ax, ay - (draw_title and self.item_height or 0) - 2, bx, by + 2, { + color = bg, opacity = opacity, radius = 4, + }) + + for index = start_index, end_index, 1 do + local item = menu.items[index] + local next_item = menu.items[index + 1] + local is_highlighted = selected_index == index or item.active + local next_is_active = next_item and next_item.active + local next_is_highlighted = selected_index == index + 1 or next_is_active + + if not item then break end + + local item_ay = ay - menu.scroll_y + self.scroll_step * (index - 1) + local item_by = item_ay + self.item_height + local item_center_y = item_ay + (self.item_height / 2) + local item_clip = (item_ay < ay or item_by > by) and scroll_clip or nil + local content_ax, content_bx = ax + spacing, bx - spacing + local font_color = item.active and fgt or bgt + local shadow_color = item.active and fg or bg + + -- Separator + local separator_ay = item.separator and item_by - 1 or item_by + local separator_by = item_by + (item.separator and 2 or 1) + if is_highlighted then separator_ay = item_by + 1 end + if next_is_highlighted then separator_by = item_by end + if separator_by - separator_ay > 0 and item_by < by then + ass:rect(ax + spacing / 2, separator_ay, bx - spacing / 2, separator_by, { + color = fg, opacity = opacity * (item.separator and 0.08 or 0.06), + }) + end + + -- Highlight + local highlight_opacity = 0 + (item.active and 0.8 or 0) + (selected_index == index and 0.15 or 0) + if highlight_opacity > 0 then + ass:rect(ax + 2, item_ay, bx - 2, item_by, { + radius = 2, color = fg, opacity = highlight_opacity * text_opacity, + clip = item_clip, + }) + end + + -- Icon + if item.icon then + local x, y = content_bx - (icon_size / 2), item_center_y + if item.icon == 'spinner' then + ass:spinner(x, y, icon_size * 1.5, {color = font_color, opacity = text_opacity * 0.8}) + else + ass:icon(x, y, icon_size * 1.5, item.icon, { + color = font_color, opacity = text_opacity, clip = item_clip, + shadow = 1, shadow_color = shadow_color, + }) + end + content_bx = content_bx - icon_size - spacing + end + + local title_cut_x = content_bx + if item.hint_width > 0 then + -- controls title & hint clipping proportional to the ratio of their widths + local title_content_ratio = item.title_width / (item.title_width + item.hint_width) + title_cut_x = round(content_ax + (content_bx - content_ax - spacing) * title_content_ratio + + (item.title_width > 0 and spacing / 2 or 0)) + end + + -- Hint + if item.hint then + item.ass_safe_hint = item.ass_safe_hint or ass_escape(item.hint) + local clip = '\\clip(' .. title_cut_x .. ',' .. + math.max(item_ay, ay) .. ',' .. bx .. ',' .. math.min(item_by, by) .. ')' + ass:txt(content_bx, item_center_y, 6, item.ass_safe_hint, { + size = self.font_size_hint, color = font_color, wrap = 2, opacity = 0.5 * opacity, clip = clip, + shadow = 1, shadow_color = shadow_color, + }) + end + + -- Title + if item.title then + item.ass_safe_title = item.ass_safe_title or ass_escape(item.title) + local clip = '\\clip(' .. ax .. ',' .. math.max(item_ay, ay) .. ',' + .. title_cut_x .. ',' .. math.min(item_by, by) .. ')' + ass:txt(content_ax, item_center_y, 4, item.ass_safe_title, { + size = self.font_size, color = font_color, italic = item.italic, bold = item.bold, wrap = 2, + opacity = text_opacity * (item.muted and 0.5 or 1), clip = clip, + shadow = 1, shadow_color = shadow_color, + }) + end + end + + -- Menu title + if draw_title then + local title_ay = ay - self.item_height + local title_height = self.item_height - 3 + menu.ass_safe_title = menu.ass_safe_title or ass_escape(menu.title) + + -- Background + ass:rect(ax + 2, title_ay, bx - 2, title_ay + title_height, { + color = fg, opacity = opacity * 0.8, radius = 2, + }) + ass:texture(ax + 2, title_ay, bx - 2, title_ay + title_height, 'n', { + size = 80, color = bg, opacity = opacity * 0.1, + }) + + -- Title + ass:txt(ax + menu.width / 2, title_ay + (title_height / 2), 5, menu.ass_safe_title, { + size = self.font_size, bold = true, color = bg, wrap = 2, opacity = opacity, + clip = '\\clip(' .. ax .. ',' .. title_ay .. ',' .. bx .. ',' .. ay .. ')', + }) + end + + -- Scrollbar + if menu.scroll_height > 0 then + local groove_height = menu.height - 2 + local thumb_height = math.max((menu.height / (menu.scroll_height + menu.height)) * groove_height, 40) + local thumb_y = ay + 1 + ((menu.scroll_y / menu.scroll_height) * (groove_height - thumb_height)) + ass:rect(bx - 3, thumb_y, bx - 1, thumb_y + thumb_height, {color = fg, opacity = opacity * 0.8}) + end + end + + -- Main menu + draw_menu(self.current, self.ax, self.ay, opacity) + + -- Parent menus + local parent_menu = self.current.parent_menu + local parent_offset_x = self.ax + local parent_opacity_factor = options.menu_parent_opacity + local menu_gap = 2 + + while parent_menu do + parent_offset_x = parent_offset_x - parent_menu.width - menu_gap + draw_menu(parent_menu, parent_offset_x, parent_menu.top, parent_opacity_factor * opacity) + parent_opacity_factor = parent_opacity_factor * parent_opacity_factor + parent_menu = parent_menu.parent_menu + end + + -- Selected menu + local selected_menu = self.current.items[self.current.selected_index] + + if selected_menu and selected_menu.items then + draw_menu(selected_menu, self.bx + menu_gap, selected_menu.top, options.menu_parent_opacity * opacity) + end + + return ass +end + +return Menu diff --git a/multimedia/.config/mpv/scripts/uosc_shared/elements/PauseIndicator.lua b/multimedia/.config/mpv/scripts/uosc_shared/elements/PauseIndicator.lua new file mode 100644 index 0000000..82a7e43 --- /dev/null +++ b/multimedia/.config/mpv/scripts/uosc_shared/elements/PauseIndicator.lua @@ -0,0 +1,80 @@ +local Element = require('uosc_shared/elements/Element') + +---@class PauseIndicator : Element +local PauseIndicator = class(Element) + +function PauseIndicator:new() return Class.new(self) --[[@as PauseIndicator]] end +function PauseIndicator:init() + Element.init(self, 'pause_indicator') + self.ignores_menu = true + self.base_icon_opacity = options.pause_indicator == 'flash' and 1 or 0.8 + self.paused = state.pause + self.type = options.pause_indicator + self.is_manual = options.pause_indicator == 'manual' + self.fadeout_requested = false + self.opacity = 0 + + mp.observe_property('pause', 'bool', function(_, paused) + if Elements.timeline.pressed then return end + if options.pause_indicator == 'flash' then + if self.paused == paused then return end + self:flash() + elseif options.pause_indicator == 'static' then + self:decide() + end + end) +end + +function PauseIndicator:flash() + if not self.is_manual and self.type ~= 'flash' then return end + -- can't wait for pause property event listener to set this, because when this is used inside a binding like: + -- cycle pause; script-binding uosc/flash-pause-indicator + -- the pause event is not fired fast enough, and indicator starts rendering with old icon + self.paused = mp.get_property_native('pause') + if self.is_manual then self.type = 'flash' end + self.opacity = 1 + self:tween_property('opacity', 1, 0, 0.15) +end + +-- decides whether static indicator should be visible or not +function PauseIndicator:decide() + if not self.is_manual and self.type ~= 'static' then return end + self.paused = mp.get_property_native('pause') -- see flash() for why this line is necessary + if self.is_manual then self.type = 'static' end + self.opacity = self.paused and 1 or 0 + request_render() + + -- Workaround for an mpv race condition bug during pause on windows builds, which causes osd updates to be ignored. + -- .03 was still loosing renders, .04 was fine, but to be safe I added 10ms more + mp.add_timeout(.05, function() osd:update() end) +end + +function PauseIndicator:render() + if self.opacity == 0 then return end + + local ass = assdraw.ass_new() + local is_static = self.type == 'static' + + -- Background fadeout + if is_static then + ass:rect(0, 0, display.width, display.height, {color = bg, opacity = self.opacity * 0.3}) + end + + -- Icon + local size = round(math.min(display.width, display.height) * (is_static and 0.20 or 0.15)) + size = size + size * (1 - self.opacity) + + if self.paused then + ass:icon(display.width / 2, display.height / 2, size, 'pause', + {border = 1, opacity = self.base_icon_opacity * self.opacity} + ) + else + ass:icon(display.width / 2, display.height / 2, size * 1.2, 'play_arrow', + {border = 1, opacity = self.base_icon_opacity * self.opacity} + ) + end + + return ass +end + +return PauseIndicator diff --git a/multimedia/.config/mpv/scripts/uosc_shared/elements/Speed.lua b/multimedia/.config/mpv/scripts/uosc_shared/elements/Speed.lua new file mode 100644 index 0000000..6ea5097 --- /dev/null +++ b/multimedia/.config/mpv/scripts/uosc_shared/elements/Speed.lua @@ -0,0 +1,192 @@ +local Element = require('uosc_shared/elements/Element') + +---@alias Dragging { start_time: number; start_x: number; distance: number; speed_distance: number; start_speed: number; } + +---@class Speed : Element +local Speed = class(Element) + +---@param props? ElementProps +function Speed:new(props) return Class.new(self, props) --[[@as Speed]] end +function Speed:init(props) + Element.init(self, 'speed', props) + + self.width = 0 + self.height = 0 + self.notches = 10 + self.notch_every = 0.1 + ---@type number + self.notch_spacing = nil + ---@type number + self.font_size = nil + ---@type Dragging|nil + self.dragging = nil +end + +function Speed:on_coordinates() + self.height, self.width = self.by - self.ay, self.bx - self.ax + self.notch_spacing = self.width / (self.notches + 1) + self.font_size = round(self.height * 0.48 * options.font_scale) +end + +function Speed:speed_step(speed, up) + if options.speed_step_is_factor then + if up then + return speed * options.speed_step + else + return speed * 1 / options.speed_step + end + else + if up then + return speed + options.speed_step + else + return speed - options.speed_step + end + end +end + +function Speed:handle_cursor_down() + self:tween_stop() -- Stop and cleanup possible ongoing animations + self.dragging = { + start_time = mp.get_time(), + start_x = cursor.x, + distance = 0, + speed_distance = 0, + start_speed = state.speed, + } +end + +function Speed:on_global_mouse_move() + if not self.dragging then return end + + self.dragging.distance = cursor.x - self.dragging.start_x + self.dragging.speed_distance = (-self.dragging.distance / self.notch_spacing * self.notch_every) + + local speed_current = state.speed + local speed_drag_current = self.dragging.start_speed + self.dragging.speed_distance + speed_drag_current = clamp(0.01, speed_drag_current, 100) + local drag_dir_up = speed_drag_current > speed_current + + local speed_step_next = speed_current + local speed_drag_diff = math.abs(speed_drag_current - speed_current) + while math.abs(speed_step_next - speed_current) < speed_drag_diff do + speed_step_next = self:speed_step(speed_step_next, drag_dir_up) + end + local speed_step_prev = self:speed_step(speed_step_next, not drag_dir_up) + + local speed_new = speed_step_prev + local speed_next_diff = math.abs(speed_drag_current - speed_step_next) + local speed_prev_diff = math.abs(speed_drag_current - speed_step_prev) + if speed_next_diff < speed_prev_diff then + speed_new = speed_step_next + end + + if speed_new ~= speed_current then + mp.set_property_native('speed', speed_new) + end +end + +function Speed:handle_cursor_up() + if self.proximity_raw == 0 then + -- Reset speed on short clicks + if self.dragging and math.abs(self.dragging.distance) < 6 and mp.get_time() - self.dragging.start_time < 0.15 then + mp.set_property_native('speed', 1) + end + end + self.dragging = nil + request_render() +end + +function Speed:on_global_mouse_leave() + self.dragging = nil + request_render() +end + +function Speed:handle_wheel_up() mp.set_property_native('speed', self:speed_step(state.speed, true)) end +function Speed:handle_wheel_down() mp.set_property_native('speed', self:speed_step(state.speed, false)) end + +function Speed:render() + local visibility = self:get_visibility() + local opacity = self.dragging and 1 or visibility + + if opacity <= 0 then return end + + if self.proximity_raw == 0 then + cursor.on_primary_down = function() + self:handle_cursor_down() + cursor.on_primary_up = function() self:handle_cursor_up() end + end + cursor.on_wheel_down = function() self:handle_wheel_down() end + cursor.on_wheel_up = function() self:handle_wheel_up() end + end + if self.dragging then + cursor.on_primary_up = function() self:handle_cursor_up() end + end + + local ass = assdraw.ass_new() + + -- Background + ass:rect(self.ax, self.ay, self.bx, self.by, {color = bg, radius = 2, opacity = opacity * options.speed_opacity}) + + -- Coordinates + local ax, ay = self.ax, self.ay + local bx, by = self.bx, ay + self.height + local half_width = (self.width / 2) + local half_x = ax + half_width + + -- Notches + local speed_at_center = state.speed + if self.dragging then + speed_at_center = self.dragging.start_speed + self.dragging.speed_distance + speed_at_center = clamp(0.01, speed_at_center, 100) + end + local nearest_notch_speed = round(speed_at_center / self.notch_every) * self.notch_every + local nearest_notch_x = half_x + (((nearest_notch_speed - speed_at_center) / self.notch_every) * self.notch_spacing) + local guide_size = math.floor(self.height / 7.5) + local notch_by = by - guide_size + local notch_ay_big = ay + round(self.font_size * 1.1) + local notch_ay_medium = notch_ay_big + ((notch_by - notch_ay_big) * 0.2) + local notch_ay_small = notch_ay_big + ((notch_by - notch_ay_big) * 0.4) + local from_to_index = math.floor(self.notches / 2) + + for i = -from_to_index, from_to_index do + local notch_speed = nearest_notch_speed + (i * self.notch_every) + + if notch_speed >= 0 and notch_speed <= 100 then + local notch_x = nearest_notch_x + (i * self.notch_spacing) + local notch_thickness = 1 + local notch_ay = notch_ay_small + if (notch_speed % (self.notch_every * 10)) < 0.00000001 then + notch_ay = notch_ay_big + notch_thickness = 1.5 + elseif (notch_speed % (self.notch_every * 5)) < 0.00000001 then + notch_ay = notch_ay_medium + end + + ass:rect(notch_x - notch_thickness, notch_ay, notch_x + notch_thickness, notch_by, { + color = fg, border = 1, border_color = bg, + opacity = math.min(1.2 - (math.abs((notch_x - ax - half_width) / half_width)), 1) * opacity, + }) + end + end + + -- Center guide + ass:new_event() + ass:append('{\\rDefault\\an7\\blur0\\bord1\\shad0\\1c&H' .. fg .. '\\3c&H' .. bg .. '}') + ass:opacity(opacity) + ass:pos(0, 0) + ass:draw_start() + ass:move_to(half_x, by - 2 - guide_size) + ass:line_to(half_x + guide_size, by - 2) + ass:line_to(half_x - guide_size, by - 2) + ass:draw_stop() + + -- Speed value + local speed_text = (round(state.speed * 100) / 100) .. 'x' + ass:txt(half_x, ay + (notch_ay_big - ay) / 2, 5, speed_text, { + size = self.font_size, color = bgt, border = options.text_border, border_color = bg, opacity = opacity, + }) + + return ass +end + +return Speed diff --git a/multimedia/.config/mpv/scripts/uosc_shared/elements/Timeline.lua b/multimedia/.config/mpv/scripts/uosc_shared/elements/Timeline.lua new file mode 100644 index 0000000..8dfda9f --- /dev/null +++ b/multimedia/.config/mpv/scripts/uosc_shared/elements/Timeline.lua @@ -0,0 +1,430 @@ +local Element = require('uosc_shared/elements/Element') + +---@class Timeline : Element +local Timeline = class(Element) + +function Timeline:new() return Class.new(self) --[[@as Timeline]] end +function Timeline:init() + Element.init(self, 'timeline') + ---@type false|{pause: boolean, distance: number, last: {x: number, y: number}} + self.pressed = false + self.obstructed = false + self.size_max = 0 + self.size_min = 0 + self.size_min_override = options.timeline_start_hidden and 0 or nil + self.font_size = 0 + self.top_border = options.timeline_border + self.is_hovered = false + self.has_thumbnail = false + + -- Delayed seeking timer + self.seek_timer = mp.add_timeout(0.05, function() self:set_from_cursor() end) + self.seek_timer:kill() + + -- Release any dragging when file gets unloaded + mp.register_event('end-file', function() self.pressed = false end) +end + +function Timeline:get_visibility() + return Elements.controls and math.max(Elements.controls.proximity, Element.get_visibility(self)) + or Element.get_visibility(self) +end + +function Timeline:decide_enabled() + local previous = self.enabled + self.enabled = not self.obstructed and state.duration ~= nil and state.duration > 0 and state.time ~= nil + if self.enabled ~= previous then Elements:trigger('timeline_enabled', self.enabled) end +end + +function Timeline:get_effective_size_min() + return self.size_min_override or self.size_min +end + +function Timeline:get_effective_size() + if Elements.speed and Elements.speed.dragging then return self.size_max end + local size_min = self:get_effective_size_min() + return size_min + math.ceil((self.size_max - size_min) * self:get_visibility()) +end + +function Timeline:get_effective_line_width() + return state.fullormaxed and options.timeline_line_width_fullscreen or options.timeline_line_width +end + +function Timeline:get_is_hovered() return self.enabled and self.is_hovered end + +function Timeline:update_dimensions() + if state.fullormaxed then + self.size_min = options.timeline_size_min_fullscreen + self.size_max = options.timeline_size_max_fullscreen + else + self.size_min = options.timeline_size_min + self.size_max = options.timeline_size_max + end + self.font_size = math.floor(math.min((self.size_max + 60) * 0.2, self.size_max * 0.96) * options.font_scale) + self.ax = Elements.window_border.size + self.ay = display.height - Elements.window_border.size - self.size_max - self.top_border + self.bx = display.width - Elements.window_border.size + self.by = display.height - Elements.window_border.size + self.width = self.bx - self.ax + self.chapter_size = math.max((self.by - self.ay) / 10, 3) + self.chapter_size_hover = self.chapter_size * 2 + + -- Disable if not enough space + local available_space = display.height - Elements.window_border.size * 2 + if Elements.top_bar.enabled then available_space = available_space - Elements.top_bar.size end + self.obstructed = available_space < self.size_max + 10 + self:decide_enabled() +end + +function Timeline:get_time_at_x(x) + local line_width = (options.timeline_style == 'line' and self:get_effective_line_width() - 1 or 0) + local time_width = self.width - line_width - 1 + local fax = (time_width) * state.time / state.duration + local fbx = fax + line_width + -- time starts 0.5 pixels in + x = x - self.ax - 0.5 + if x > fbx then x = x - line_width + elseif x > fax then x = fax end + local progress = clamp(0, x / time_width, 1) + return state.duration * progress +end + +---@param fast? boolean +function Timeline:set_from_cursor(fast) + if state.time and state.duration then + mp.commandv('seek', self:get_time_at_x(cursor.x), fast and 'absolute+keyframes' or 'absolute+exact') + end +end + +function Timeline:clear_thumbnail() + mp.commandv('script-message-to', 'thumbfast', 'clear') + self.has_thumbnail = false +end + +function Timeline:handle_cursor_down() + self.pressed = {pause = state.pause, distance = 0, last = {x = cursor.x, y = cursor.y}} + mp.set_property_native('pause', true) + self:set_from_cursor() + cursor.on_primary_up = function() self:handle_cursor_up() end +end +function Timeline:on_prop_duration() self:decide_enabled() end +function Timeline:on_prop_time() self:decide_enabled() end +function Timeline:on_prop_border() self:update_dimensions() end +function Timeline:on_prop_fullormaxed() self:update_dimensions() end +function Timeline:on_display() self:update_dimensions() end +function Timeline:handle_cursor_up() + self.seek_timer:kill() + if self.pressed then + mp.set_property_native('pause', self.pressed.pause) + self.pressed = false + end +end +function Timeline:on_global_mouse_leave() + self.pressed = false +end + +function Timeline:on_global_mouse_move() + if self.pressed then + self.pressed.distance = self.pressed.distance + get_point_to_point_proximity(self.pressed.last, cursor) + self.pressed.last.x, self.pressed.last.y = cursor.x, cursor.y + if self.width / state.duration < 10 then + self:set_from_cursor(true) + self.seek_timer:kill() + self.seek_timer:resume() + else self:set_from_cursor() end + end +end +function Timeline:handle_wheel_up() mp.commandv('seek', options.timeline_step) end +function Timeline:handle_wheel_down() mp.commandv('seek', -options.timeline_step) end + +function Timeline:render() + if self.size_max == 0 then return end + + local size_min = self:get_effective_size_min() + local size = self:get_effective_size() + local visibility = self:get_visibility() + self.is_hovered = false + + if size < 1 then + if self.has_thumbnail then self:clear_thumbnail() end + return + end + + if self.proximity_raw == 0 then + self.is_hovered = true + cursor.on_primary_down = function() self:handle_cursor_down() end + cursor.on_wheel_down = function() self:handle_wheel_down() end + cursor.on_wheel_up = function() self:handle_wheel_up() end + end + + if self.pressed then + cursor.on_primary_up = function() self:handle_cursor_up() end + end + + local ass = assdraw.ass_new() + + -- Text opacity rapidly drops to 0 just before it starts overflowing, or before it reaches timeline.size_min + local hide_text_below = math.max(self.font_size * 0.8, size_min * 2) + local hide_text_ramp = hide_text_below / 2 + local text_opacity = clamp(0, size - hide_text_below, hide_text_ramp) / hide_text_ramp + + local spacing = math.max(math.floor((self.size_max - self.font_size) / 2.5), 4) + local progress = state.time / state.duration + local is_line = options.timeline_style == 'line' + + -- Foreground & Background bar coordinates + local bax, bay, bbx, bby = self.ax, self.by - size - self.top_border, self.bx, self.by + local fax, fay, fbx, fby = 0, bay + self.top_border, 0, bby + local fcy = fay + (size / 2) + + local line_width = 0 + + if is_line then + local minimized_fraction = 1 - math.min((size - size_min) / ((self.size_max - size_min) / 8), 1) + local line_width_max = self:get_effective_line_width() + local max_min_width_delta = size_min > 0 + and line_width_max - line_width_max * options.timeline_line_width_minimized_scale + or 0 + line_width = line_width_max - (max_min_width_delta * minimized_fraction) + fax = bax + (self.width - line_width) * progress + fbx = fax + line_width + line_width = line_width - 1 + else + fax, fbx = bax, bax + self.width * progress + end + + local foreground_size = fby - fay + local foreground_coordinates = round(fax) .. ',' .. fay .. ',' .. round(fbx) .. ',' .. fby -- for clipping + + -- time starts 0.5 pixels in + local time_ax = bax + 0.5 + local time_width = self.width - line_width - 1 + + -- time to x: calculates x coordinate so that it never lies inside of the line + local function t2x(time) + local x = time_ax + time_width * time / state.duration + return time <= state.time and x or x + line_width + end + + -- Background + ass:new_event() + ass:pos(0, 0) + ass:append('{\\rDefault\\an7\\blur0\\bord0\\1c&H' .. bg .. '}') + ass:opacity(options.timeline_opacity) + ass:draw_start() + ass:rect_cw(bax, bay, fax, bby) --left of progress + ass:rect_cw(fbx, bay, bbx, bby) --right of progress + ass:rect_cw(fax, bay, fbx, fay) --above progress + ass:draw_stop() + + -- Progress + ass:rect(fax, fay, fbx, fby, {opacity = options.timeline_opacity}) + + -- Uncached ranges + local buffered_playtime = nil + if state.uncached_ranges then + local opts = {size = 80, anchor_y = fby} + local texture_char = visibility > 0 and 'b' or 'a' + local offset = opts.size / (visibility > 0 and 24 or 28) + for _, range in ipairs(state.uncached_ranges) do + if not buffered_playtime and (range[1] > state.time or range[2] > state.time) then + buffered_playtime = (range[1] - state.time) / (state.speed or 1) + end + if options.timeline_cache then + local ax = range[1] < 0.5 and bax or math.floor(t2x(range[1])) + local bx = range[2] > state.duration - 0.5 and bbx or math.ceil(t2x(range[2])) + opts.color, opts.opacity, opts.anchor_x = 'ffffff', 0.4 - (0.2 * visibility), bax + ass:texture(ax, fay, bx, fby, texture_char, opts) + opts.color, opts.opacity, opts.anchor_x = '000000', 0.6 - (0.2 * visibility), bax + offset + ass:texture(ax, fay, bx, fby, texture_char, opts) + end + end + end + + -- Custom ranges + for _, chapter_range in ipairs(state.chapter_ranges) do + local rax = chapter_range.start < 0.1 and bax or t2x(chapter_range.start) + local rbx = chapter_range['end'] > state.duration - 0.1 and bbx + or t2x(math.min(chapter_range['end'], state.duration)) + ass:rect(rax, fay, rbx, fby, {color = chapter_range.color, opacity = chapter_range.opacity}) + end + + -- Chapters + local hovered_chapter = nil + if (options.timeline_chapters_opacity > 0 + and (#state.chapters > 0 or state.ab_loop_a or state.ab_loop_b) + ) then + local diamond_radius = foreground_size < 3 and foreground_size or self.chapter_size + local diamond_radius_hovered = diamond_radius * 2 + local diamond_border = options.timeline_border and math.max(options.timeline_border, 1) or 1 + + if diamond_radius > 0 then + local function draw_chapter(time, radius) + local chapter_x, chapter_y = t2x(time), fay - 1 + ass:new_event() + ass:append(string.format( + '{\\pos(0,0)\\rDefault\\an7\\blur0\\yshad0.01\\bord%f\\1c&H%s\\3c&H%s\\4c&H%s\\1a&H%X&\\3a&H00&\\4a&H00&}', + diamond_border, fg, bg, bg, opacity_to_alpha(options.timeline_opacity * options.timeline_chapters_opacity) + )) + ass:draw_start() + ass:move_to(chapter_x - radius, chapter_y) + ass:line_to(chapter_x, chapter_y - radius) + ass:line_to(chapter_x + radius, chapter_y) + ass:line_to(chapter_x, chapter_y + radius) + ass:draw_stop() + end + + if #state.chapters > 0 then + -- Find hovered chapter indicator + local closest_delta = INFINITY + + if self.proximity_raw < diamond_radius_hovered then + for i, chapter in ipairs(state.chapters) do + local chapter_x, chapter_y = t2x(chapter.time), fay - 1 + local cursor_chapter_delta = math.sqrt((cursor.x - chapter_x) ^ 2 + (cursor.y - chapter_y) ^ 2) + if cursor_chapter_delta <= diamond_radius_hovered and cursor_chapter_delta < closest_delta then + hovered_chapter, closest_delta = chapter, cursor_chapter_delta + self.is_hovered = true + cursor.on_primary_down = function() + mp.commandv('seek', hovered_chapter.time, 'absolute+exact') + end + end + end + end + + for i, chapter in ipairs(state.chapters) do + if chapter ~= hovered_chapter then draw_chapter(chapter.time, diamond_radius) end + end + + -- Render hovered chapter above others + if hovered_chapter then draw_chapter(hovered_chapter.time, diamond_radius_hovered) end + end + + -- A-B loop indicators + local has_a, has_b = state.ab_loop_a and state.ab_loop_a >= 0, state.ab_loop_b and state.ab_loop_b > 0 + local ab_radius = round(math.min(math.max(8, foreground_size * 0.25), foreground_size)) + + ---@param time number + ---@param kind 'a'|'b' + local function draw_ab_indicator(time, kind) + local x = t2x(time) + ass:new_event() + ass:append(string.format( + '{\\pos(0,0)\\rDefault\\an7\\blur0\\yshad0.01\\bord%f\\1c&H%s\\3c&H%s\\4c&H%s\\1a&H%X&\\3a&H00&\\4a&H00&}', + diamond_border, fg, bg, bg, opacity_to_alpha(options.timeline_opacity * options.timeline_chapters_opacity) + )) + ass:draw_start() + ass:move_to(x, fby - ab_radius) + if kind == 'b' then ass:line_to(x + 3, fby - ab_radius) end + ass:line_to(x + (kind == 'a' and 0 or ab_radius), fby) + ass:line_to(x - (kind == 'b' and 0 or ab_radius), fby) + if kind == 'a' then ass:line_to(x - 3, fby - ab_radius) end + ass:draw_stop() + end + + if has_a then draw_ab_indicator(state.ab_loop_a, 'a') end + if has_b then draw_ab_indicator(state.ab_loop_b, 'b') end + end + end + + local function draw_timeline_text(x, y, align, text, opts) + opts.color, opts.border_color = fgt, fg + opts.clip = '\\clip(' .. foreground_coordinates .. ')' + ass:txt(x, y, align, text, opts) + opts.color, opts.border_color = bgt, bg + opts.clip = '\\iclip(' .. foreground_coordinates .. ')' + ass:txt(x, y, align, text, opts) + end + + -- Time values + if text_opacity > 0 then + local time_opts = {size = self.font_size, opacity = text_opacity, border = 2} + -- Upcoming cache time + if buffered_playtime and options.buffered_time_threshold > 0 + and buffered_playtime < options.buffered_time_threshold then + local x, align = fbx + 5, 4 + local cache_opts = {size = self.font_size * 0.8, opacity = text_opacity * 0.6, border = 1} + local human = round(math.max(buffered_playtime, 0)) .. 's' + local width = text_width(human, cache_opts) + local time_width = timestamp_width(state.time_human, time_opts) + local time_width_end = timestamp_width(state.destination_time_human, time_opts) + local min_x, max_x = bax + spacing + 5 + time_width, bbx - spacing - 5 - time_width_end + if x < min_x then x = min_x elseif x + width > max_x then x, align = max_x, 6 end + draw_timeline_text(x, fcy, align, human, cache_opts) + end + + -- Elapsed time + if state.time_human then + draw_timeline_text(bax + spacing, fcy, 4, state.time_human, time_opts) + end + + -- End time + if state.destination_time_human then + draw_timeline_text(bbx - spacing, fcy, 6, state.destination_time_human, time_opts) + end + end + + -- Hovered time and chapter + local rendered_thumbnail = false + if (self.proximity_raw == 0 or self.pressed or hovered_chapter) and + not (Elements.speed and Elements.speed.dragging) then + local cursor_x = hovered_chapter and t2x(hovered_chapter.time) or cursor.x + local hovered_seconds = hovered_chapter and hovered_chapter.time or self:get_time_at_x(cursor.x) + + -- Cursor line + -- 0.5 to switch when the pixel is half filled in + local color = ((fax - 0.5) < cursor_x and cursor_x < (fbx + 0.5)) and bg or fg + local ax, ay, bx, by = cursor_x - 0.5, fay, cursor_x + 0.5, fby + ass:rect(ax, ay, bx, by, {color = color, opacity = 0.2}) + local tooltip_anchor = {ax = ax, ay = ay, bx = bx, by = by} + + -- Timestamp + local offset = #state.chapters > 0 and 10 or 4 + local opts = {size = self.font_size, offset = offset} + local hovered_time_human = format_time(hovered_seconds, state.duration) + opts.width_overwrite = timestamp_width(hovered_time_human, opts) + ass:tooltip(tooltip_anchor, hovered_time_human, opts) + tooltip_anchor.ay = tooltip_anchor.ay - self.font_size - offset + + -- Thumbnail + if not thumbnail.disabled + and (not self.pressed or self.pressed.distance < 5) + and thumbnail.width ~= 0 + and thumbnail.height ~= 0 + then + local scale_x, scale_y = display.scale_x, display.scale_y + local border, margin_x, margin_y = math.ceil(2 * scale_x), round(10 * scale_x), round(5 * scale_y) + local thumb_x_margin, thumb_y_margin = border + margin_x, border + margin_y + local thumb_width, thumb_height = thumbnail.width, thumbnail.height + local thumb_x = round(clamp( + thumb_x_margin, cursor_x * scale_x - thumb_width / 2, + display.width * scale_x - thumb_width - thumb_x_margin + )) + local thumb_y = round(tooltip_anchor.ay * scale_y - thumb_y_margin - thumb_height) + local ax, ay = (thumb_x - border) / scale_x, (thumb_y - border) / scale_y + local bx, by = (thumb_x + thumb_width + border) / scale_x, (thumb_y + thumb_height + border) / scale_y + ass:rect(ax, ay, bx, by, {color = bg, border = 1, border_color = fg, border_opacity = 0.08, radius = 2}) + mp.commandv('script-message-to', 'thumbfast', 'thumb', hovered_seconds, thumb_x, thumb_y) + self.has_thumbnail, rendered_thumbnail = true, true + tooltip_anchor.ax, tooltip_anchor.bx, tooltip_anchor.ay = ax, bx, ay + end + + -- Chapter title + if #state.chapters > 0 then + local _, chapter = itable_find(state.chapters, function(c) return hovered_seconds >= c.time end, true) + if chapter and not chapter.is_end_only then + ass:tooltip(tooltip_anchor, chapter.title_wrapped, { + size = self.font_size, offset = 10, responsive = false, bold = true, + width_overwrite = chapter.title_wrapped_width * self.font_size, + }) + end + end + end + + -- Clear thumbnail + if not rendered_thumbnail and self.has_thumbnail then self:clear_thumbnail() end + + return ass +end + +return Timeline diff --git a/multimedia/.config/mpv/scripts/uosc_shared/elements/TopBar.lua b/multimedia/.config/mpv/scripts/uosc_shared/elements/TopBar.lua new file mode 100644 index 0000000..514def2 --- /dev/null +++ b/multimedia/.config/mpv/scripts/uosc_shared/elements/TopBar.lua @@ -0,0 +1,253 @@ +local Element = require('uosc_shared/elements/Element') + +---@alias TopBarButtonProps {icon: string; background: string; anchor_id?: string; command: string|fun()} + +---@class TopBarButton : Element +local TopBarButton = class(Element) + +---@param id string +---@param props TopBarButtonProps +function TopBarButton:new(id, props) return Class.new(self, id, props) --[[@as TopBarButton]] end +function TopBarButton:init(id, props) + Element.init(self, id, props) + self.anchor_id = 'top_bar' + self.icon = props.icon + self.background = props.background + self.command = props.command +end + +function TopBarButton:handle_cursor_down() + mp.command(type(self.command) == 'function' and self.command() or self.command) +end + +function TopBarButton:render() + local visibility = self:get_visibility() + if visibility <= 0 then return end + local ass = assdraw.ass_new() + + -- Background on hover + if self.proximity_raw == 0 then + ass:rect(self.ax, self.ay, self.bx, self.by, {color = self.background, opacity = visibility}) + cursor.on_primary_down = function() self:handle_cursor_down() end + end + + local width, height = self.bx - self.ax, self.by - self.ay + local icon_size = math.min(width, height) * 0.5 + ass:icon(self.ax + width / 2, self.ay + height / 2, icon_size, self.icon, { + opacity = visibility, border = options.text_border, + }) + + return ass +end + +--[[ TopBar ]] + +---@class TopBar : Element +local TopBar = class(Element) + +function TopBar:new() return Class.new(self) --[[@as TopBar]] end +function TopBar:init() + Element.init(self, 'top_bar') + self.size = 0 + self.icon_size, self.spacing, self.font_size, self.title_bx, self.title_by = 1, 1, 1, 1, 1 + self.show_alt_title = false + self.main_title, self.alt_title = nil, nil + + local function get_maximized_command() + return state.border + and (state.fullscreen and 'set fullscreen no;cycle window-maximized' or 'cycle window-maximized') + or 'set window-maximized no;cycle fullscreen' + end + + -- Order aligns from right to left + self.buttons = { + TopBarButton:new('tb_close', {icon = 'close', background = '2311e8', command = 'quit'}), + TopBarButton:new('tb_max', {icon = 'crop_square', background = '222222', command = get_maximized_command}), + TopBarButton:new('tb_min', {icon = 'minimize', background = '222222', command = 'cycle window-minimized'}), + } + + self:decide_titles() +end + +function TopBar:decide_enabled() + if options.top_bar == 'no-border' then + self.enabled = not state.border or state.fullscreen + else + self.enabled = options.top_bar == 'always' + end + self.enabled = self.enabled and (options.top_bar_controls or options.top_bar_title) + for _, element in ipairs(self.buttons) do + element.enabled = self.enabled and options.top_bar_controls + end +end + +function TopBar:decide_titles() + self.alt_title = state.alt_title ~= '' and state.alt_title or nil + self.main_title = state.title ~= '' and state.title or nil + + -- Fall back to alt title if main is empty + if not self.main_title then + self.main_title, self.alt_title = self.alt_title, nil + end + + -- Deduplicate the main and alt titles by checking if one completely + -- contains the other, and using only the longer one. + if self.main_title and self.alt_title and not self.show_alt_title then + local longer_title, shorter_title + if #self.main_title < #self.alt_title then + longer_title, shorter_title = self.alt_title, self.main_title + else + longer_title, shorter_title = self.main_title, self.alt_title + end + + local escaped_shorter_title = string.gsub(shorter_title --[[@as string]], "[%(%)%.%+%-%*%?%[%]%^%$%%]", "%%%1") + if string.match(longer_title --[[@as string]], escaped_shorter_title) then + self.main_title, self.alt_title = longer_title, nil + end + end +end + +function TopBar:update_dimensions() + self.size = state.fullormaxed and options.top_bar_size_fullscreen or options.top_bar_size + self.icon_size = round(self.size * 0.5) + self.spacing = math.ceil(self.size * 0.25) + self.font_size = math.floor((self.size - (self.spacing * 2)) * options.font_scale) + self.button_width = round(self.size * 1.15) + self.ay = Elements.window_border.size + self.bx = display.width - Elements.window_border.size + self.by = self.size + Elements.window_border.size + self.title_bx = self.bx - (options.top_bar_controls and (self.button_width * 3) or 0) + self.ax = options.top_bar_title and Elements.window_border.size or self.title_bx + + local button_bx = self.bx + for _, element in pairs(self.buttons) do + element.ax, element.bx = button_bx - self.button_width, button_bx + element.ay, element.by = self.ay, self.by + button_bx = button_bx - self.button_width + end +end + +function TopBar:toggle_title() + if options.top_bar_alt_title_place ~= 'toggle' then return end + self.show_alt_title = not self.show_alt_title +end + +function TopBar:on_prop_title() self:decide_titles() end +function TopBar:on_prop_alt_title() self:decide_titles() end + +function TopBar:on_prop_border() + self:decide_enabled() + self:update_dimensions() +end + +function TopBar:on_prop_fullscreen() + self:decide_enabled() + self:update_dimensions() +end + +function TopBar:on_prop_maximized() + self:decide_enabled() + self:update_dimensions() +end + +function TopBar:on_display() self:update_dimensions() end + +function TopBar:render() + local visibility = self:get_visibility() + if visibility <= 0 then return end + local ass = assdraw.ass_new() + + -- Window title + if options.top_bar_title and (state.title or state.has_playlist) then + local bg_margin = math.floor((self.size - self.font_size) / 4) + local padding = self.font_size / 2 + local title_ax = self.ax + bg_margin + local title_ay = self.ay + bg_margin + local max_bx = self.title_bx - self.spacing + + -- Playlist position + if state.has_playlist then + local text = state.playlist_pos .. '' .. state.playlist_count + local formatted_text = '{\\b1}' .. state.playlist_pos .. '{\\b0\\fs' .. self.font_size * 0.9 .. '}/' + .. state.playlist_count + local opts = {size = self.font_size, wrap = 2, color = fgt, opacity = visibility} + local bx = round(title_ax + text_width(text, opts) + padding * 2) + ass:rect(title_ax, title_ay, bx, self.by - bg_margin, {color = fg, opacity = visibility, radius = 2}) + ass:txt(title_ax + (bx - title_ax) / 2, self.ay + (self.size / 2), 5, formatted_text, opts) + title_ax = bx + bg_margin + local rect = {ax = self.ax, ay = self.ay, bx = bx, by = self.by} + + if get_point_to_rectangle_proximity(cursor, rect) == 0 then + cursor.on_primary_down = function() mp.command('script-binding uosc/playlist') end + end + end + + -- Skip rendering titles if there's not enough horizontal space + if max_bx - title_ax > self.font_size * 3 then + -- Main title + local main_title = self.show_alt_title and self.alt_title or self.main_title + if main_title then + local opts = { + size = self.font_size, wrap = 2, color = bgt, border = 1, border_color = bg, opacity = visibility, + clip = string.format('\\clip(%d, %d, %d, %d)', self.ax, self.ay, max_bx, self.by), + } + local bx = math.min(max_bx, title_ax + text_width(main_title, opts) + padding * 2) + local by = self.by - bg_margin + local rect = {ax = title_ax, ay = self.ay, bx = self.title_bx, by = self.by} + + if get_point_to_rectangle_proximity(cursor, rect) == 0 then + cursor.on_primary_down = function() self:toggle_title() end + end + + ass:rect(title_ax, title_ay, bx, by, { + color = bg, opacity = visibility * options.top_bar_title_opacity, radius = 2, + }) + ass:txt(title_ax + padding, self.ay + (self.size / 2), 4, main_title, opts) + title_ay = by + 1 + end + + -- Alt title + if self.alt_title and options.top_bar_alt_title_place == 'below' then + local font_size = self.font_size * 0.9 + local height = font_size * 1.3 + local by = title_ay + height + local opts = { + size = font_size, wrap = 2, color = bgt, border = 1, border_color = bg, opacity = visibility + } + local bx = math.min(max_bx, title_ax + text_width(self.alt_title, opts) + padding * 2) + opts.clip = string.format('\\clip(%d, %d, %d, %d)', title_ax, title_ay, bx, by) + ass:rect(title_ax, title_ay, bx, by, { + color = bg, opacity = visibility * options.top_bar_title_opacity, radius = 2, + }) + ass:txt(title_ax + padding, title_ay + height / 2, 4, self.alt_title, opts) + title_ay = by + 1 + end + + -- Subtitle: current chapter + if state.current_chapter then + local font_size = self.font_size * 0.8 + local height = font_size * 1.3 + local text = '└ ' .. state.current_chapter.index .. ': ' .. state.current_chapter.title + local by = title_ay + height + local opts = { + size = font_size, italic = true, wrap = 2, color = bgt, + border = 1, border_color = bg, opacity = visibility * 0.8, + } + local bx = math.min(max_bx, title_ax + text_width(text, opts) + padding * 2) + opts.clip = string.format('\\clip(%d, %d, %d, %d)', title_ax, title_ay, bx, by) + ass:rect(title_ax, title_ay, bx, by, { + color = bg, opacity = visibility * options.top_bar_title_opacity, radius = 2, + }) + ass:txt(title_ax + padding, title_ay + height / 2, 4, text, opts) + title_ay = by + 1 + end + end + self.title_by = title_ay - 1 + else + self.title_by = self.ay + end + + return ass +end + +return TopBar diff --git a/multimedia/.config/mpv/scripts/uosc_shared/elements/Volume.lua b/multimedia/.config/mpv/scripts/uosc_shared/elements/Volume.lua new file mode 100644 index 0000000..2f591b6 --- /dev/null +++ b/multimedia/.config/mpv/scripts/uosc_shared/elements/Volume.lua @@ -0,0 +1,252 @@ +local Element = require('uosc_shared/elements/Element') + +--[[ MuteButton ]] + +---@class MuteButton : Element +local MuteButton = class(Element) +---@param props? ElementProps +function MuteButton:new(props) return Class.new(self, 'volume_mute', props) --[[@as MuteButton]] end +function MuteButton:get_visibility() return Elements.volume:get_visibility(self) end +function MuteButton:render() + local visibility = self:get_visibility() + if visibility <= 0 then return end + if self.proximity_raw == 0 then + cursor.on_primary_down = function() mp.commandv('cycle', 'mute') end + end + local ass = assdraw.ass_new() + local icon_name = state.mute and 'volume_off' or 'volume_up' + local width = self.bx - self.ax + ass:icon(self.ax + (width / 2), self.by, width * 0.7, icon_name, + {border = options.text_border, opacity = options.volume_opacity * visibility, align = 2} + ) + return ass +end + +--[[ VolumeSlider ]] + +---@class VolumeSlider : Element +local VolumeSlider = class(Element) +---@param props? ElementProps +function VolumeSlider:new(props) return Class.new(self, props) --[[@as VolumeSlider]] end +function VolumeSlider:init(props) + Element.init(self, 'volume_slider', props) + self.pressed = false + self.nudge_y = 0 -- vertical position where volume overflows 100 + self.nudge_size = 0 + self.draw_nudge = false + self.spacing = 0 + self.radius = 1 +end + +function VolumeSlider:get_visibility() return Elements.volume:get_visibility(self) end + +function VolumeSlider:set_volume(volume) + volume = round(volume / options.volume_step) * options.volume_step + if state.volume == volume then return end + mp.commandv('set', 'volume', clamp(0, volume, state.volume_max)) +end + +function VolumeSlider:set_from_cursor() + local volume_fraction = (self.by - cursor.y - options.volume_border) / (self.by - self.ay - options.volume_border) + self:set_volume(volume_fraction * state.volume_max) +end + +function VolumeSlider:on_coordinates() + if type(state.volume_max) ~= 'number' or state.volume_max <= 0 then return end + local width = self.bx - self.ax + self.nudge_y = self.by - round((self.by - self.ay) * (100 / state.volume_max)) + self.nudge_size = round(width * 0.18) + self.draw_nudge = self.ay < self.nudge_y + self.spacing = round(width * 0.2) + self.radius = math.max(2, (self.bx - self.ax) / 10) +end +function VolumeSlider:on_global_mouse_move() + if self.pressed then self:set_from_cursor() end +end +function VolumeSlider:handle_wheel_up() self:set_volume(state.volume + options.volume_step) end +function VolumeSlider:handle_wheel_down() self:set_volume(state.volume - options.volume_step) end + +function VolumeSlider:render() + local visibility = self:get_visibility() + local ax, ay, bx, by = self.ax, self.ay, self.bx, self.by + local width, height = bx - ax, by - ay + + if width <= 0 or height <= 0 or visibility <= 0 then return end + + if self.proximity_raw == 0 then + cursor.on_primary_down = function() + self.pressed = true + self:set_from_cursor() + cursor.on_primary_up = function() self.pressed = false end + end + cursor.on_wheel_down = function() self:handle_wheel_down() end + cursor.on_wheel_up = function() self:handle_wheel_up() end + end + if self.pressed then cursor.on_primary_up = function() + self.pressed = false end + end + + local ass = assdraw.ass_new() + local nudge_y, nudge_size = self.draw_nudge and self.nudge_y or -INFINITY, self.nudge_size + local volume_y = self.ay + options.volume_border + + ((height - (options.volume_border * 2)) * (1 - math.min(state.volume / state.volume_max, 1))) + + -- Draws a rectangle with nudge at requested position + ---@param p number Padding from slider edges. + ---@param cy? number A y coordinate where to clip the path from the bottom. + function create_nudged_path(p, cy) + cy = cy or ay + p + local ax, bx, by = ax + p, bx - p, by - p + local r = math.max(1, self.radius - p) + local d, rh = r * 2, r / 2 + local nudge_size = ((QUARTER_PI_SIN * (nudge_size - p)) + p) / QUARTER_PI_SIN + local path = assdraw.ass_new() + path:move_to(bx - r, by) + path:line_to(ax + r, by) + if cy > by - d then + local subtracted_radius = (d - (cy - (by - d))) / 2 + local xbd = (r - subtracted_radius * 1.35) -- x bezier delta + path:bezier_curve(ax + xbd, by, ax + xbd, cy, ax + r, cy) + path:line_to(bx - r, cy) + path:bezier_curve(bx - xbd, cy, bx - xbd, by, bx - r, by) + else + path:bezier_curve(ax + rh, by, ax, by - rh, ax, by - r) + local nudge_bottom_y = nudge_y + nudge_size + + if cy + rh <= nudge_bottom_y then + path:line_to(ax, nudge_bottom_y) + if cy <= nudge_y then + path:line_to((ax + nudge_size), nudge_y) + local nudge_top_y = nudge_y - nudge_size + if cy <= nudge_top_y then + local r, rh = r, rh + if cy > nudge_top_y - r then + r = nudge_top_y - cy + rh = r / 2 + end + path:line_to(ax, nudge_top_y) + path:line_to(ax, cy + r) + path:bezier_curve(ax, cy + rh, ax + rh, cy, ax + r, cy) + path:line_to(bx - r, cy) + path:bezier_curve(bx - rh, cy, bx, cy + rh, bx, cy + r) + path:line_to(bx, nudge_top_y) + else + local triangle_side = cy - nudge_top_y + path:line_to((ax + triangle_side), cy) + path:line_to((bx - triangle_side), cy) + end + path:line_to((bx - nudge_size), nudge_y) + else + local triangle_side = nudge_bottom_y - cy + path:line_to((ax + triangle_side), cy) + path:line_to((bx - triangle_side), cy) + end + path:line_to(bx, nudge_bottom_y) + else + path:line_to(ax, cy + r) + path:bezier_curve(ax, cy + rh, ax + rh, cy, ax + r, cy) + path:line_to(bx - r, cy) + path:bezier_curve(bx - rh, cy, bx, cy + rh, bx, cy + r) + end + path:line_to(bx, by - r) + path:bezier_curve(bx, by - rh, bx - rh, by, bx - r, by) + end + return path + end + + -- BG & FG paths + local bg_path = create_nudged_path(0) + local fg_path = create_nudged_path(options.volume_border, volume_y) + + -- Background + ass:new_event() + ass:append('{\\rDefault\\an7\\blur0\\bord0\\1c&H' .. bg .. + '\\iclip(' .. fg_path.scale .. ', ' .. fg_path.text .. ')}') + ass:opacity(options.volume_opacity, visibility) + ass:pos(0, 0) + ass:draw_start() + ass:append(bg_path.text) + ass:draw_stop() + + -- Foreground + ass:new_event() + ass:append('{\\rDefault\\an7\\blur0\\bord0\\1c&H' .. fg .. '}') + ass:opacity(options.volume_opacity, visibility) + ass:pos(0, 0) + ass:draw_start() + ass:append(fg_path.text) + ass:draw_stop() + + -- Current volume value + local volume_string = tostring(round(state.volume * 10) / 10) + local font_size = round(((width * 0.6) - (#volume_string * (width / 20))) * options.font_scale) + if volume_y < self.by - self.spacing then + ass:txt(self.ax + (width / 2), self.by - self.spacing, 2, volume_string, { + size = font_size, color = fgt, opacity = visibility, + clip = '\\clip(' .. fg_path.scale .. ', ' .. fg_path.text .. ')', + }) + end + if volume_y > self.by - self.spacing - font_size then + ass:txt(self.ax + (width / 2), self.by - self.spacing, 2, volume_string, { + size = font_size, color = bgt, opacity = visibility, + clip = '\\iclip(' .. fg_path.scale .. ', ' .. fg_path.text .. ')', + }) + end + + -- Disabled stripes for no audio + if not state.has_audio then + local fg_100_path = create_nudged_path(options.volume_border) + local texture_opts = { + size = 200, color = 'ffffff', opacity = visibility * 0.1, anchor_x = ax, + clip = '\\clip(' .. fg_100_path.scale .. ',' .. fg_100_path.text .. ')', + } + ass:texture(ax, ay, bx, by, 'a', texture_opts) + texture_opts.color = '000000' + texture_opts.anchor_x = ax + texture_opts.size / 28 + ass:texture(ax, ay, bx, by, 'a', texture_opts) + end + + return ass +end + +--[[ Volume ]] + +---@class Volume : Element +local Volume = class(Element) + +function Volume:new() return Class.new(self) --[[@as Volume]] end +function Volume:init() + Element.init(self, 'volume') + self.mute = MuteButton:new({anchor_id = 'volume'}) + self.slider = VolumeSlider:new({anchor_id = 'volume'}) +end + +function Volume:get_visibility() + return self.slider.pressed and 1 or Elements.timeline:get_is_hovered() and -1 or Element.get_visibility(self) +end + +function Volume:update_dimensions() + local width = state.fullormaxed and options.volume_size_fullscreen or options.volume_size + local controls, timeline, top_bar = Elements.controls, Elements.timeline, Elements.top_bar + local min_y = top_bar.enabled and top_bar.by or 0 + local max_y = (controls and controls.enabled and controls.ay) or (timeline.enabled and timeline.ay) + or display.height - top_bar.size + local available_height = max_y - min_y + local max_height = available_height * 0.8 + local height = round(math.min(width * 8, max_height)) + self.enabled = height > width * 2 -- don't render if too small + local margin = (width / 2) + Elements.window_border.size + self.ax = round(options.volume == 'left' and margin or display.width - margin - width) + self.ay = min_y + round((available_height - height) / 2) + self.bx = round(self.ax + width) + self.by = round(self.ay + height) + self.mute.enabled, self.slider.enabled = self.enabled, self.enabled + self.mute:set_coordinates(self.ax, self.by - round(width * 0.8), self.bx, self.by) + self.slider:set_coordinates(self.ax, self.ay, self.bx, self.mute.ay) +end + +function Volume:on_display() self:update_dimensions() end +function Volume:on_prop_border() self:update_dimensions() end +function Volume:on_controls_reflow() self:update_dimensions() end + +return Volume diff --git a/multimedia/.config/mpv/scripts/uosc_shared/elements/WindowBorder.lua b/multimedia/.config/mpv/scripts/uosc_shared/elements/WindowBorder.lua new file mode 100644 index 0000000..c5544f5 --- /dev/null +++ b/multimedia/.config/mpv/scripts/uosc_shared/elements/WindowBorder.lua @@ -0,0 +1,33 @@ +local Element = require('uosc_shared/elements/Element') + +---@class WindowBorder : Element +local WindowBorder = class(Element) + +function WindowBorder:new() return Class.new(self) --[[@as WindowBorder]] end +function WindowBorder:init() + Element.init(self, 'window_border') + self.ignores_menu = true + self.size = 0 +end + +function WindowBorder:decide_enabled() + self.enabled = options.window_border_size > 0 and not state.fullormaxed and not state.border + self.size = self.enabled and options.window_border_size or 0 +end + +function WindowBorder:on_prop_border() self:decide_enabled() end +function WindowBorder:on_prop_fullormaxed() self:decide_enabled() end + +function WindowBorder:render() + if self.size > 0 then + local ass = assdraw.ass_new() + local clip = '\\iclip(' .. self.size .. ',' .. self.size .. ',' .. + (display.width - self.size) .. ',' .. (display.height - self.size) .. ')' + ass:rect(0, 0, display.width + 1, display.height + 1, { + color = bg, clip = clip, opacity = options.window_border_opacity, + }) + return ass + end +end + +return WindowBorder diff --git a/multimedia/.config/mpv/scripts/uosc_shared/lib/ass.lua b/multimedia/.config/mpv/scripts/uosc_shared/lib/ass.lua new file mode 100644 index 0000000..108953f --- /dev/null +++ b/multimedia/.config/mpv/scripts/uosc_shared/lib/ass.lua @@ -0,0 +1,170 @@ +--[[ ASSDRAW EXTENSIONS ]] + +local ass_mt = getmetatable(assdraw.ass_new()) + +-- Opacity. +---@param opacity number|number[] Opacity of all elements, or an array of [primary, secondary, border, shadow] opacities. +---@param fraction? number Optionally adjust the above opacity by this fraction. +function ass_mt:opacity(opacity, fraction) + fraction = fraction ~= nil and fraction or 1 + if type(opacity) == 'number' then + self.text = self.text .. string.format('{\\alpha&H%X&}', opacity_to_alpha(opacity * fraction)) + else + self.text = self.text .. string.format( + '{\\1a&H%X&\\2a&H%X&\\3a&H%X&\\4a&H%X&}', + opacity_to_alpha((opacity[1] or 0) * fraction), + opacity_to_alpha((opacity[2] or 0) * fraction), + opacity_to_alpha((opacity[3] or 0) * fraction), + opacity_to_alpha((opacity[4] or 0) * fraction) + ) + end +end + +-- Icon. +---@param x number +---@param y number +---@param size number +---@param name string +---@param opts? {color?: string; border?: number; border_color?: string; opacity?: number; clip?: string; align?: number} +function ass_mt:icon(x, y, size, name, opts) + opts = opts or {} + opts.font, opts.size, opts.bold = 'MaterialIconsRound-Regular', size, false + self:txt(x, y, opts.align or 5, name, opts) +end + +-- Text. +-- Named `txt` because `ass.text` is a value. +---@param x number +---@param y number +---@param align number +---@param value string|number +---@param opts {size: number; font?: string; color?: string; bold?: boolean; italic?: boolean; border?: number; border_color?: string; shadow?: number; shadow_color?: string; rotate?: number; wrap?: number; opacity?: number; clip?: string} +function ass_mt:txt(x, y, align, value, opts) + local border_size = opts.border or 0 + local shadow_size = opts.shadow or 0 + local tags = '\\pos(' .. x .. ',' .. y .. ')\\rDefault\\an' .. align .. '\\blur0' + -- font + tags = tags .. '\\fn' .. (opts.font or config.font) + -- font size + tags = tags .. '\\fs' .. opts.size + -- bold + if opts.bold or (opts.bold == nil and options.font_bold) then tags = tags .. '\\b1' end + -- italic + if opts.italic then tags = tags .. '\\i1' end + -- rotate + if opts.rotate then tags = tags .. '\\frz' .. opts.rotate end + -- wrap + if opts.wrap then tags = tags .. '\\q' .. opts.wrap end + -- border + tags = tags .. '\\bord' .. border_size + -- shadow + tags = tags .. '\\shad' .. shadow_size + -- colors + tags = tags .. '\\1c&H' .. (opts.color or bgt) + if border_size > 0 then tags = tags .. '\\3c&H' .. (opts.border_color or bg) end + if shadow_size > 0 then tags = tags .. '\\4c&H' .. (opts.shadow_color or bg) end + -- opacity + if opts.opacity then tags = tags .. string.format('\\alpha&H%X&', opacity_to_alpha(opts.opacity)) end + -- clip + if opts.clip then tags = tags .. opts.clip end + -- render + self:new_event() + self.text = self.text .. '{' .. tags .. '}' .. value +end + +-- Tooltip. +---@param element {ax: number; ay: number; bx: number; by: number} +---@param value string|number +---@param opts? {size?: number; offset?: number; bold?: boolean; italic?: boolean; width_overwrite?: number, responsive?: boolean} +function ass_mt:tooltip(element, value, opts) + opts = opts or {} + opts.size = opts.size or 16 + opts.border = options.text_border + opts.border_color = bg + local offset = opts.offset or opts.size / 2 + local align_top = opts.responsive == false or element.ay - offset > opts.size * 2 + local x = element.ax + (element.bx - element.ax) / 2 + local y = align_top and element.ay - offset or element.by + offset + local margin = (opts.width_overwrite or text_width(value, opts)) / 2 + 10 + self:txt(clamp(margin, x, display.width - margin), y, align_top and 2 or 8, value, opts) +end + +-- Rectangle. +---@param ax number +---@param ay number +---@param bx number +---@param by number +---@param opts? {color?: string; border?: number; border_color?: string; opacity?: number; border_opacity?: number; clip?: string, radius?: number} +function ass_mt:rect(ax, ay, bx, by, opts) + opts = opts or {} + local border_size = opts.border or 0 + local tags = '\\pos(0,0)\\rDefault\\an7\\blur0' + -- border + tags = tags .. '\\bord' .. border_size + -- colors + tags = tags .. '\\1c&H' .. (opts.color or fg) + if border_size > 0 then tags = tags .. '\\3c&H' .. (opts.border_color or bg) end + -- opacity + if opts.opacity then tags = tags .. string.format('\\alpha&H%X&', opacity_to_alpha(opts.opacity)) end + if opts.border_opacity then tags = tags .. string.format('\\3a&H%X&', opacity_to_alpha(opts.border_opacity)) end + -- clip + if opts.clip then + tags = tags .. opts.clip + end + -- draw + self:new_event() + self.text = self.text .. '{' .. tags .. '}' + self:draw_start() + if opts.radius then + self:round_rect_cw(ax, ay, bx, by, opts.radius) + else + self:rect_cw(ax, ay, bx, by) + end + self:draw_stop() +end + +-- Circle. +---@param x number +---@param y number +---@param radius number +---@param opts? {color?: string; border?: number; border_color?: string; opacity?: number; clip?: string} +function ass_mt:circle(x, y, radius, opts) + opts = opts or {} + opts.radius = radius + self:rect(x - radius, y - radius, x + radius, y + radius, opts) +end + +-- Texture. +---@param ax number +---@param ay number +---@param bx number +---@param by number +---@param char string Texture font character. +---@param opts {size?: number; color: string; opacity?: number; clip?: string; anchor_x?: number, anchor_y?: number} +function ass_mt:texture(ax, ay, bx, by, char, opts) + opts = opts or {} + local anchor_x, anchor_y = opts.anchor_x or ax, opts.anchor_y or ay + local clip = opts.clip or ('\\clip(' .. ax .. ',' .. ay .. ',' .. bx .. ',' .. by .. ')') + local tile_size, opacity = opts.size or 100, opts.opacity or 0.2 + local x, y = ax - (ax - anchor_x) % tile_size, ay - (ay - anchor_y) % tile_size + local width, height = bx - x, by - y + local line = string.rep(char, math.ceil((width / tile_size))) + local lines = '' + for i = 1, math.ceil(height / tile_size), 1 do lines = lines .. (lines == '' and '' or '\\N') .. line end + self:txt( + x, y, 7, lines, + {font = 'uosc_textures', size = tile_size, color = opts.color, bold = false, opacity = opacity, clip = clip}) +end + +-- Rotating spinner icon. +---@param x number +---@param y number +---@param size number +---@param opts? {color?: string; opacity?: number; clip?: string; border?: number; border_color?: string;} +function ass_mt:spinner(x, y, size, opts) + opts = opts or {} + opts.rotate = (state.render_last_time * 1.75 % 1) * -360 + opts.color = opts.color or fg + self:icon(x, y, size, 'autorenew', opts) + request_render() +end diff --git a/multimedia/.config/mpv/scripts/uosc_shared/lib/menus.lua b/multimedia/.config/mpv/scripts/uosc_shared/lib/menus.lua new file mode 100644 index 0000000..5b7b790 --- /dev/null +++ b/multimedia/.config/mpv/scripts/uosc_shared/lib/menus.lua @@ -0,0 +1,292 @@ +---@param data MenuData +---@param opts? {submenu?: string; mouse_nav?: boolean; on_close?: string | string[]} +function open_command_menu(data, opts) + local function run_command(command) + if type(command) == 'string' then + mp.command(command) + else + ---@diagnostic disable-next-line: deprecated + mp.commandv(unpack(command)) + end + end + ---@type MenuOptions + local menu_opts = {} + if opts then + menu_opts.mouse_nav = opts.mouse_nav + if opts.on_close then menu_opts.on_close = function() run_command(opts.on_close) end end + end + local menu = Menu:open(data, run_command, menu_opts) + if opts and opts.submenu then menu:activate_submenu(opts.submenu) end + return menu +end + +---@param opts? {submenu?: string; mouse_nav?: boolean; on_close?: string | string[]} +function toggle_menu_with_items(opts) + if Menu:is_open('menu') then Menu:close() + else open_command_menu({type = 'menu', items = config.menu_items}, opts) end +end + +---@param options {type: string; title: string; list_prop: string; active_prop?: string; serializer: fun(list: any, active: any): MenuDataItem[]; on_select: fun(value: any); on_move_item?: fun(from_index: integer, to_index: integer, submenu_path: integer[]); on_delete_item?: fun(index: integer, submenu_path: integer[])} +function create_self_updating_menu_opener(options) + return function() + if Menu:is_open(options.type) then Menu:close() return end + local list = mp.get_property_native(options.list_prop) + local active = options.active_prop and mp.get_property_native(options.active_prop) or nil + local menu + + local function update() menu:update_items(options.serializer(list, active)) end + + local ignore_initial_list = true + local function handle_list_prop_change(name, value) + if ignore_initial_list then ignore_initial_list = false + else list = value update() end + end + + local ignore_initial_active = true + local function handle_active_prop_change(name, value) + if ignore_initial_active then ignore_initial_active = false + else active = value update() end + end + + local initial_items, selected_index = options.serializer(list, active) + + -- Items and active_index are set in the handle_prop_change callback, since adding + -- a property observer triggers its handler immediately, we just let that initialize the items. + menu = Menu:open( + {type = options.type, title = options.title, items = initial_items, selected_index = selected_index}, + options.on_select, { + on_open = function() + mp.observe_property(options.list_prop, 'native', handle_list_prop_change) + if options.active_prop then + mp.observe_property(options.active_prop, 'native', handle_active_prop_change) + end + end, + on_close = function() + mp.unobserve_property(handle_list_prop_change) + mp.unobserve_property(handle_active_prop_change) + end, + on_move_item = options.on_move_item, + on_delete_item = options.on_delete_item, + }) + end +end + +function create_select_tracklist_type_menu_opener(menu_title, track_type, track_prop, load_command) + local function serialize_tracklist(tracklist) + local items = {} + + if load_command then + items[#items + 1] = { + title = 'Load', bold = true, italic = true, hint = 'open file', value = '{load}', separator = true, + } + end + + local first_item_index = #items + 1 + local active_index = nil + local disabled_item = nil + + -- Add option to disable a subtitle track. This works for all tracks, + -- but why would anyone want to disable audio or video? Better to not + -- let people mistakenly select what is unwanted 99.999% of the time. + -- If I'm mistaken and there is an active need for this, feel free to + -- open an issue. + if track_type == 'sub' then + disabled_item = {title = 'Disabled', italic = true, muted = true, hint = '—', value = nil, active = true} + items[#items + 1] = disabled_item + end + + for _, track in ipairs(tracklist) do + if track.type == track_type then + local hint_values = {} + local function h(value) hint_values[#hint_values + 1] = value end + + if track.lang then h(track.lang:upper()) end + if track['demux-h'] then + h(track['demux-w'] and (track['demux-w'] .. 'x' .. track['demux-h']) or (track['demux-h'] .. 'p')) + end + if track['demux-fps'] then h(string.format('%.5gfps', track['demux-fps'])) end + h(track.codec) + if track['audio-channels'] then h(track['audio-channels'] .. ' channels') end + if track['demux-samplerate'] then h(string.format('%.3gkHz', track['demux-samplerate'] / 1000)) end + if track.forced then h('forced') end + if track.default then h('default') end + if track.external then h('external') end + + items[#items + 1] = { + title = (track.title and track.title or 'Track ' .. track.id), + hint = table.concat(hint_values, ', '), + value = track.id, + active = track.selected, + } + + if track.selected then + if disabled_item then disabled_item.active = false end + active_index = #items + end + end + end + + return items, active_index or first_item_index + end + + local function selection_handler(value) + if value == '{load}' then + mp.command(load_command) + else + mp.commandv('set', track_prop, value and value or 'no') + + -- If subtitle track was selected, assume user also wants to see it + if value and track_type == 'sub' then + mp.commandv('set', 'sub-visibility', 'yes') + end + end + end + + return create_self_updating_menu_opener({ + title = menu_title, + type = track_type, + list_prop = 'track-list', + serializer = serialize_tracklist, + on_select = selection_handler, + }) +end + +---@alias NavigationMenuOptions {type: string, title?: string, allowed_types?: string[], active_path?: string, selected_path?: string; on_open?: fun(); on_close?: fun()} + +-- Opens a file navigation menu with items inside `directory_path`. +---@param directory_path string +---@param handle_select fun(path: string): nil +---@param opts NavigationMenuOptions +function open_file_navigation_menu(directory_path, handle_select, opts) + directory = serialize_path(normalize_path(directory_path)) + opts = opts or {} + + if not directory then + msg.error('Couldn\'t serialize path "' .. directory_path .. '.') + return + end + + local files, directories = read_directory(directory.path, opts.allowed_types) + local is_root = not directory.dirname + local path_separator = path_separator(directory.path) + + if not files or not directories then return end + + sort_filenames(directories) + sort_filenames(files) + + -- Pre-populate items with parent directory selector if not at root + -- Each item value is a serialized path table it points to. + local items = {} + + if is_root then + if state.platform == 'windows' then + items[#items + 1] = {title = '..', hint = 'Drives', value = '{drives}', separator = true} + end + else + items[#items + 1] = {title = '..', hint = 'parent dir', value = directory.dirname, separator = true} + end + + local back_path = items[#items] and items[#items].value + local selected_index = #items + 1 + + for _, dir in ipairs(directories) do + items[#items + 1] = {title = dir, value = join_path(directory.path, dir), hint = path_separator} + end + + for _, file in ipairs(files) do + items[#items + 1] = {title = file, value = join_path(directory.path, file)} + end + + for index, item in ipairs(items) do + if not item.value.is_to_parent and opts.active_path == item.value then + item.active = true + if not opts.selected_path then selected_index = index end + end + + if opts.selected_path == item.value then selected_index = index end + end + + ---@type MenuCallback + local function open_path(path, meta) + local is_drives = path == '{drives}' + local is_to_parent = is_drives or #path < #directory_path + local inheritable_options = { + type = opts.type, title = opts.title, allowed_types = opts.allowed_types, active_path = opts.active_path, + } + + if is_drives then + open_drives_menu(function(drive_path) + open_file_navigation_menu(drive_path, handle_select, inheritable_options) + end, { + type = inheritable_options.type, title = inheritable_options.title, selected_path = directory.path, + on_open = opts.on_open, on_close = opts.on_close, + }) + return + end + + local info, error = utils.file_info(path) + + if not info then + msg.error('Can\'t retrieve path info for "' .. path .. '". Error: ' .. (error or '')) + return + end + + if info.is_dir and not meta.modifiers.ctrl then + -- Preselect directory we are coming from + if is_to_parent then + inheritable_options.selected_path = directory.path + end + + open_file_navigation_menu(path, handle_select, inheritable_options) + else + handle_select(path) + end + end + + local function handle_back() + if back_path then open_path(back_path, {modifiers = {}}) end + end + + local menu_data = { + type = opts.type, title = opts.title or directory.basename .. path_separator, items = items, + selected_index = selected_index, + } + local menu_options = {on_open = opts.on_open, on_close = opts.on_close, on_back = handle_back} + + return Menu:open(menu_data, open_path, menu_options) +end + +-- Opens a file navigation menu with Windows drives as items. +---@param handle_select fun(path: string): nil +---@param opts? NavigationMenuOptions +function open_drives_menu(handle_select, opts) + opts = opts or {} + local process = mp.command_native({ + name = 'subprocess', + capture_stdout = true, + playback_only = false, + args = {'wmic', 'logicaldisk', 'get', 'name', '/value'}, + }) + local items, selected_index = {}, 1 + + if process.status == 0 then + for _, value in ipairs(split(process.stdout, '\n')) do + local drive = string.match(value, 'Name=([A-Z]:)') + if drive then + local drive_path = normalize_path(drive) + items[#items + 1] = { + title = drive, hint = 'drive', value = drive_path, active = opts.active_path == drive_path, + } + if opts.selected_path == drive_path then selected_index = #items end + end + end + else + msg.error(process.stderr) + end + + return Menu:open( + {type = opts.type, title = opts.title or 'Drives', items = items, selected_index = selected_index}, + handle_select + ) +end diff --git a/multimedia/.config/mpv/scripts/uosc_shared/lib/std.lua b/multimedia/.config/mpv/scripts/uosc_shared/lib/std.lua new file mode 100644 index 0000000..1261666 --- /dev/null +++ b/multimedia/.config/mpv/scripts/uosc_shared/lib/std.lua @@ -0,0 +1,181 @@ +--[[ Stateless utilities missing in lua standard library ]] + +---@param number number +function round(number) return math.floor(number + 0.5) end + +---@param min number +---@param value number +---@param max number +function clamp(min, value, max) return math.max(min, math.min(value, max)) end + +---@param rgba string `rrggbb` or `rrggbbaa` hex string. +function serialize_rgba(rgba) + local a = rgba:sub(7, 8) + return { + color = rgba:sub(5, 6) .. rgba:sub(3, 4) .. rgba:sub(1, 2), + opacity = clamp(0, tonumber(#a == 2 and a or 'ff', 16) / 255, 1), + } +end + +-- Trim any `char` from the end of the string. +---@param str string +---@param char string +---@return string +function trim_end(str, char) + local char, end_i = char:byte(), 0 + for i = #str, 1, -1 do + if str:byte(i) ~= char then + end_i = i + break + end + end + return str:sub(1, end_i) +end + +---@param str string +---@param pattern string +---@return string[] +function split(str, pattern) + local list = {} + local full_pattern = '(.-)' .. pattern + local last_end = 1 + local start_index, end_index, capture = str:find(full_pattern, 1) + while start_index do + list[#list + 1] = capture + last_end = end_index + 1 + start_index, end_index, capture = str:find(full_pattern, last_end) + end + if last_end <= (#str + 1) then + capture = str:sub(last_end) + list[#list + 1] = capture + end + return list +end + +-- Get index of the last appearance of `sub` in `str`. +---@param str string +---@param sub string +---@return integer|nil +function string_last_index_of(str, sub) + local sub_length = #sub + for i = #str, 1, -1 do + for j = 1, sub_length do + if str:byte(i + j - 1) ~= sub:byte(j) then break end + if j == sub_length then return i end + end + end +end + +---@param itable table +---@param value any +---@return integer|nil +function itable_index_of(itable, value) + for index, item in ipairs(itable) do + if item == value then return index end + end +end + +---@param itable table +---@param compare fun(value: any, index: number) +---@param from_end? boolean Search from the end of the table. +---@return number|nil index +---@return any|nil value +function itable_find(itable, compare, from_end) + local from, to, step = from_end and #itable or 1, from_end and 1 or #itable, from_end and -1 or 1 + for index = from, to, step do + if compare(itable[index], index) then return index, itable[index] end + end +end + +---@param itable table +---@param decider fun(value: any, index: number) +function itable_filter(itable, decider) + local filtered = {} + for index, value in ipairs(itable) do + if decider(value, index) then filtered[#filtered + 1] = value end + end + return filtered +end + +---@param itable table +---@param value any +function itable_remove(itable, value) + return itable_filter(itable, function(item) return item ~= value end) +end + +---@param itable table +---@param start_pos? integer +---@param end_pos? integer +function itable_slice(itable, start_pos, end_pos) + start_pos = start_pos and start_pos or 1 + end_pos = end_pos and end_pos or #itable + + if end_pos < 0 then end_pos = #itable + end_pos + 1 end + if start_pos < 0 then start_pos = #itable + start_pos + 1 end + + local new_table = {} + for index, value in ipairs(itable) do + if index >= start_pos and index <= end_pos then + new_table[#new_table + 1] = value + end + end + return new_table +end + +---@generic T +---@param a T[]|nil +---@param b T[]|nil +---@return T[] +function itable_join(a, b) + local result = {} + if a then for _, value in ipairs(a) do result[#result + 1] = value end end + if b then for _, value in ipairs(b) do result[#result + 1] = value end end + return result +end + +---@param target any[] +---@param source any[] +function itable_append(target, source) + for _, value in ipairs(source) do target[#target + 1] = value end + return target +end + +---@param target any[] +---@param source any[] +---@param props? string[] +function table_assign(target, source, props) + if props then + for _, name in ipairs(props) do target[name] = source[name] end + else + for prop, value in pairs(source) do target[prop] = value end + end + return target +end + +---@generic T +---@param table T +---@return T +function table_shallow_copy(table) + local result = {} + for key, value in pairs(table) do result[key] = value end + return result +end + +--[[ EASING FUNCTIONS ]] + +function ease_out_quart(x) return 1 - ((1 - x) ^ 4) end +function ease_out_sext(x) return 1 - ((1 - x) ^ 6) end + +--[[ CLASSES ]] + +---@class Class +Class = {} +function Class:new(...) + local object = setmetatable({}, {__index = self}) + object:init(...) + return object +end +function Class:init() end +function Class:destroy() end + +function class(parent) return setmetatable({}, {__index = parent or Class}) end diff --git a/multimedia/.config/mpv/scripts/uosc_shared/lib/text.lua b/multimedia/.config/mpv/scripts/uosc_shared/lib/text.lua new file mode 100644 index 0000000..d573b81 --- /dev/null +++ b/multimedia/.config/mpv/scripts/uosc_shared/lib/text.lua @@ -0,0 +1,461 @@ +-- https://en.wikipedia.org/wiki/Unicode_block +---@alias CodePointRange {[1]: integer; [2]: integer} + +---@type CodePointRange[] +local zero_width_blocks = { + {0x0000, 0x001F}, -- C0 + {0x007F, 0x009F}, -- Delete + C1 + {0x034F, 0x034F}, -- combining grapheme joiner + {0x061C, 0x061C}, -- Arabic Letter Strong + {0x200B, 0x200F}, -- {zero-width space, zero-width non-joiner, zero-width joiner, left-to-right mark, right-to-left mark} + {0x2028, 0x202E}, -- {line separator, paragraph separator, Left-to-Right Embedding, Right-to-Left Embedding, Pop Directional Format, Left-to-Right Override, Right-to-Left Override} + {0x2060, 0x2060}, -- word joiner + {0x2066, 0x2069}, -- {Left-to-Right Isolate, Right-to-Left Isolate, First Strong Isolate, Pop Directional Isolate} + {0xFEFF, 0xFEFF}, -- zero-width non-breaking space + -- Some other characters can also be combined https://en.wikipedia.org/wiki/Combining_character + {0x0300, 0x036F}, -- Combining Diacritical Marks 0 BMP Inherited + {0x1AB0, 0x1AFF}, -- Combining Diacritical Marks Extended 0 BMP Inherited + {0x1DC0, 0x1DFF}, -- Combining Diacritical Marks Supplement 0 BMP Inherited + {0x20D0, 0x20FF}, -- Combining Diacritical Marks for Symbols 0 BMP Inherited + {0xFE20, 0xFE2F}, -- Combining Half Marks 0 BMP Cyrillic (2 characters), Inherited (14 characters) + -- Egyptian Hieroglyph Format Controls and Shorthand format Controls + {0x13430, 0x1345F}, -- Egyptian Hieroglyph Format Controls 1 SMP Egyptian Hieroglyphs + {0x1BCA0, 0x1BCAF}, -- Shorthand Format Controls 1 SMP Common + -- not sure how to deal with those https://en.wikipedia.org/wiki/Spacing_Modifier_Letters + {0x02B0, 0x02FF}, -- Spacing Modifier Letters 0 BMP Bopomofo (2 characters), Latin (14 characters), Common (64 characters) +} + +-- All characters have the same width as the first one +---@type CodePointRange[] +local same_width_blocks = { + {0x3400, 0x4DBF}, -- CJK Unified Ideographs Extension A 0 BMP Han + {0x4E00, 0x9FFF}, -- CJK Unified Ideographs 0 BMP Han + {0x20000, 0x2A6DF}, -- CJK Unified Ideographs Extension B 2 SIP Han + {0x2A700, 0x2B73F}, -- CJK Unified Ideographs Extension C 2 SIP Han + {0x2B740, 0x2B81F}, -- CJK Unified Ideographs Extension D 2 SIP Han + {0x2B820, 0x2CEAF}, -- CJK Unified Ideographs Extension E 2 SIP Han + {0x2CEB0, 0x2EBEF}, -- CJK Unified Ideographs Extension F 2 SIP Han + {0x2F800, 0x2FA1F}, -- CJK Compatibility Ideographs Supplement 2 SIP Han + {0x30000, 0x3134F}, -- CJK Unified Ideographs Extension G 3 TIP Han + {0x31350, 0x323AF}, -- CJK Unified Ideographs Extension H 3 TIP Han +} + +local width_length_ratio = 0.5 + +---@type integer, integer +local osd_width, osd_height = 100, 100 + +---Get byte count of utf-8 character at index i in str +---@param str string +---@param i integer? +---@return integer +local function utf8_char_bytes(str, i) + local char_byte = str:byte(i) + if char_byte < 0xC0 then return 1 + elseif char_byte < 0xE0 then return 2 + elseif char_byte < 0xF0 then return 3 + elseif char_byte < 0xF8 then return 4 + else return 1 end +end + +---Creates an iterator for an utf-8 encoded string +---Iterates over utf-8 characters instead of bytes +---@param str string +---@return fun(): integer?, string? +local function utf8_iter(str) + local byte_start = 1 + return function() + local start = byte_start + if #str < start then return nil end + local byte_count = utf8_char_bytes(str, start) + byte_start = start + byte_count + return start, str:sub(start, start + byte_count - 1) + end +end + +---Extract Unicode code point from utf-8 character at index i in str +---@param str string +---@param i integer +---@return integer +local function utf8_to_unicode(str, i) + local byte_count = utf8_char_bytes(str, i) + local char_byte = str:byte(i) + local unicode = char_byte + if byte_count ~= 1 then + local shift = 2 ^ (8 - byte_count) + char_byte = char_byte - math.floor(0xFF / shift) * shift + unicode = char_byte * (2 ^ 6) ^ (byte_count - 1) + end + for j = 2, byte_count do + char_byte = str:byte(i + j - 1) - 0x80 + unicode = unicode + char_byte * (2 ^ 6) ^ (byte_count - j) + end + return round(unicode) +end + +---Convert Unicode code point to utf-8 string +---@param unicode integer +---@return string? +local function unicode_to_utf8(unicode) + if unicode < 0x80 then return string.char(unicode) + else + local byte_count + if unicode < 0x800 then byte_count = 2 + elseif unicode < 0x10000 then byte_count = 3 + elseif unicode < 0x110000 then byte_count = 4 + else return end -- too big + + local res = {} + local shift = 2 ^ 6 + local after_shift = unicode + for _ = byte_count, 2, -1 do + local before_shift = after_shift + after_shift = math.floor(before_shift / shift) + table.insert(res, 1, before_shift - after_shift * shift + 0x80) + end + shift = 2 ^ (8 - byte_count) + table.insert(res, 1, after_shift + math.floor(0xFF / shift) * shift) + ---@diagnostic disable-next-line: deprecated + return string.char(unpack(res)) + end +end + +---Update osd resolution if valid +---@param width integer +---@param height integer +local function update_osd_resolution(width, height) + if width > 0 and height > 0 then osd_width, osd_height = width, height end +end + +mp.observe_property('osd-dimensions', 'native', function (_, dim) + if dim then update_osd_resolution(dim.w, dim.h) end +end) + +local measure_bounds +do + local text_osd = mp.create_osd_overlay("ass-events") + text_osd.compute_bounds, text_osd.hidden = true, true + + ---@param ass_text string + ---@return integer, integer, integer, integer + measure_bounds = function(ass_text) + update_osd_resolution(mp.get_osd_size()) + text_osd.res_x, text_osd.res_y = osd_width, osd_height + text_osd.data = ass_text + local res = text_osd:update() + return res.x0, res.y0, res.x1, res.y1 + end +end + +local normalized_text_width +do + ---@type {wrap: integer; bold: boolean; italic: boolean, rotate: number; size: number} + local bounds_opts = {wrap = 2, bold = false, italic = false, rotate = 0, size = 0} + + ---Measure text width and normalize to a font size of 1 + ---text has to be ass safe + ---@param text string + ---@param size number + ---@param bold boolean + ---@param italic boolean + ---@param horizontal boolean + ---@return number, integer + normalized_text_width = function(text, size, bold, italic, horizontal) + bounds_opts.bold, bounds_opts.italic, bounds_opts.rotate = bold, italic, horizontal and 0 or -90 + local x1, y1 = nil, nil + size = size / 0.8 + -- prevent endless loop + local repetitions_left = 5 + repeat + size = size * 0.8 + bounds_opts.size = size + local ass = assdraw.ass_new() + ass:txt(0, 0, horizontal and 7 or 1, text, bounds_opts) + _, _, x1, y1 = measure_bounds(ass.text) + repetitions_left = repetitions_left - 1 + -- make sure nothing got clipped + until (x1 and x1 < osd_width and y1 < osd_height) or repetitions_left == 0 + local width = (repetitions_left == 0 and not x1) and 0 or (horizontal and x1 or y1) + return width / size, horizontal and osd_width or osd_height + end +end + +---Estimates character length based on utf8 byte count +---1 character length is roughly the size of a latin character +---@param char string +---@return number +local function char_length(char) + return #char > 2 and 2 or 1 +end + +---Estimates string length based on utf8 byte count +---Note: Making a string in the iterator with the character is a waste here, +---but as this function is only used when measuring whole string widths it's fine +---@param text string +---@return number +local function text_length(text) + if not text or text == '' then return 0 end + local text_length = 0 + for _, char in utf8_iter(tostring(text)) do text_length = text_length + char_length(char) end + return text_length +end + +---Finds the best orientation of text on screen and returns the estimated max size +---and if the text should be drawn horizontally +---@param text string +---@return number, boolean +local function fit_on_screen(text) + local estimated_width = text_length(text) * width_length_ratio + if osd_width >= osd_height then + -- Fill the screen as much as we can, bigger is more accurate. + return math.min(osd_width / estimated_width, osd_height), true + else + return math.min(osd_height / estimated_width, osd_width), false + end +end + +---Gets next stage from cache +---@param cache {[any]: table} +---@param value any +local function get_cache_stage(cache, value) + local stage = cache[value] + if not stage then + stage = {} + cache[value] = stage + end + return stage +end + +---Is measured resolution sufficient +---@param px integer +---@return boolean +local function no_remeasure_required(px) + return px >= 800 or (px * 1.1 >= osd_width and px * 1.1 >= osd_height) +end + +local character_width +do + ---@type {[boolean]: {[string]: {[1]: number, [2]: integer}}} + local char_width_cache = {} + + ---Get measured width of character + ---@param char string + ---@param bold boolean + ---@return number, integer + character_width = function(char, bold) + ---@type {[string]: {[1]: number, [2]: integer}} + local char_widths = get_cache_stage(char_width_cache, bold) + local width_px = char_widths[char] + if width_px and no_remeasure_required(width_px[2]) then return width_px[1], width_px[2] end + + local unicode = utf8_to_unicode(char, 1) + for _, block in ipairs(zero_width_blocks) do + if unicode >= block[1] and unicode <= block[2] then + char_widths[char] = {0, INFINITY} + return 0, INFINITY + end + end + + local measured_char = nil + for _, block in ipairs(same_width_blocks) do + if unicode >= block[1] and unicode <= block[2] then + measured_char = unicode_to_utf8(block[1]) + width_px = char_widths[measured_char] + if width_px and no_remeasure_required(width_px[2]) then + char_widths[char] = width_px + return width_px[1], width_px[2] + end + break + end + end + + if not measured_char then measured_char = char end + -- half as many repetitions for wide characters + local char_count = 10 / char_length(char) + local max_size, horizontal = fit_on_screen(measured_char:rep(char_count)) + local size = math.min(max_size * 0.9, 50) + char_count = math.min(math.floor(char_count * max_size / size * 0.8), 100) + local enclosing_char, enclosing_width, next_char_count = '|', 0, char_count + if measured_char == enclosing_char then enclosing_char = '' + else enclosing_width = 2 * character_width(enclosing_char, bold) end + local width_ratio, width, px = nil, nil, nil + repeat + char_count = next_char_count + local str = enclosing_char .. measured_char:rep(char_count) .. enclosing_char + width, px = normalized_text_width(str, size, bold, false, horizontal) + width = width - enclosing_width + width_ratio = width * size / (horizontal and osd_width or osd_height) + next_char_count = math.min(math.floor(char_count / width_ratio * 0.9), 100) + until width_ratio < 0.05 or width_ratio > 0.5 or char_count == next_char_count + width = width / char_count + + width_px = {width, px} + if char ~= measured_char then char_widths[measured_char] = width_px end + char_widths[char] = width_px + return width, px + end +end + +---Calculate text width from individual measured characters +---@param text string|number +---@param bold boolean +---@return number, integer +local function character_based_width(text, bold) + local max_width = 0 + local min_px = INFINITY + for line in tostring(text):gmatch("([^\n]*)\n?") do + local total_width = 0 + for _, char in utf8_iter(line) do + local width, px = character_width(char, bold) + total_width = total_width + width + if px < min_px then min_px = px end + end + if total_width > max_width then max_width = total_width end + end + return max_width, min_px +end + +---Measure width of whole text +---@param text string|number +---@param bold boolean +---@param italic boolean +---@return number, integer +local function whole_text_width(text, bold, italic) + text = tostring(text) + local size, horizontal = fit_on_screen(text) + return normalized_text_width(ass_escape(text), size * 0.9, bold, italic, horizontal) +end + +---Scale normalized width to real width based on font size and italic +---@param opts {size: number; italic?: boolean} +---@return number, number +local function opts_factor_offset(opts) + return opts.size, opts.italic and opts.size * 0.2 or 0 +end + +---Scale normalized width to real width based on font size and italic +---@param opts {size: number; italic?: boolean} +---@return number +local function normalized_to_real(width, opts) + local factor, offset = opts_factor_offset(opts) + return factor * width + offset +end + +do + ---@type {[boolean]: {[boolean]: {[string|number]: {[1]: number, [2]: integer}}}} | {[boolean]: {[string|number]: {[1]: number, [2]: integer}}} + local width_cache = {} + + ---Calculate width of text with the given opts + ---@param text string|number + ---@return number + ---@param opts {size: number; bold?: boolean; italic?: boolean} + function text_width(text, opts) + if not text or text == '' then return 0 end + + ---@type boolean, boolean + local bold, italic = opts.bold or options.font_bold, opts.italic or false + + if options.text_width_estimation then + ---@type {[string|number]: {[1]: number, [2]: integer}} + local text_width = get_cache_stage(width_cache, bold) + local width_px = text_width[text] + if width_px and no_remeasure_required(width_px[2]) then return normalized_to_real(width_px[1], opts) end + + local width, px = character_based_width(text, bold) + width_cache[bold][text] = {width, px} + return normalized_to_real(width, opts) + else + ---@type {[string|number]: {[1]: number, [2]: integer}} + local text_width = get_cache_stage(get_cache_stage(width_cache, bold), italic) + local width_px = text_width[text] + if width_px and no_remeasure_required(width_px[2]) then return width_px[1] * opts.size end + + local width, px = whole_text_width(text, bold, italic) + width_cache[bold][italic][text] = {width, px} + return width * opts.size + end + end +end + +do + ---@type {[string]: string} + local cache = {} + + ---Get width of formatted timestamp as if all the digits were replaced with 0 + ---@param timestamp string + ---@param opts {size: number; bold?: boolean; italic?: boolean} + ---@return number + function timestamp_width(timestamp, opts) + local substitute = cache[#timestamp] + if not substitute then + substitute = timestamp:gsub('%d', '0') + cache[#timestamp] = substitute + end + return text_width(substitute, opts) + end +end + +---Wrap the text at the closest opportunity to target_line_length +---@param text string +---@param opts {size: number; bold?: boolean; italic?: boolean} +---@param target_line_length number +---@return string +function wrap_text(text, opts, target_line_length) + local target_line_width = target_line_length * width_length_ratio * opts.size + local bold, scale_factor, scale_offset = opts.bold or false, opts_factor_offset(opts) + local wrap_at_chars = {' ', ' ', '-', '–'} + local remove_when_wrap = {' ', ' '} + local lines = {} + for text_line in text:gmatch("([^\n]*)\n?") do + local line_width = scale_offset + local line_start = 1 + local before_end = nil + local before_width = scale_offset + local before_line_start = 0 + local before_removed_width = 0 + for char_start, char in utf8_iter(text_line) do + local char_end = char_start + #char - 1 + local can_wrap = false + for _, c in ipairs(wrap_at_chars) do + if char == c then + can_wrap = true + break + end + end + local char_width = character_width(char, bold) * scale_factor + line_width = line_width + char_width + if can_wrap or (char_end == #text_line) then + local remove = false + for _, c in ipairs(remove_when_wrap) do + if char == c then + remove = true + break + end + end + local line_width_after_remove = line_width - (remove and char_width or 0) + if line_width_after_remove < target_line_width then + before_end = remove and char_start - 1 or char_end + before_width = line_width_after_remove + before_line_start = char_end + 1 + before_removed_width = remove and char_width or 0 + else + if (target_line_width - before_width) < + (line_width_after_remove - target_line_width) then + lines[#lines + 1] = text_line:sub(line_start, before_end) + line_start = before_line_start + line_width = line_width - before_width - before_removed_width + scale_offset + else + lines[#lines + 1] = text_line:sub(line_start, remove and char_start - 1 or char_end) + line_start = char_end + 1 + line_width = scale_offset + end + before_end = line_start + before_width = scale_offset + end + end + end + if #text_line >= line_start then lines[#lines + 1] = text_line:sub(line_start) + elseif text_line == '' then lines[#lines + 1] = '' end + end + return table.concat(lines, '\n') +end diff --git a/multimedia/.config/mpv/scripts/uosc_shared/lib/utils.lua b/multimedia/.config/mpv/scripts/uosc_shared/lib/utils.lua new file mode 100644 index 0000000..f64485c --- /dev/null +++ b/multimedia/.config/mpv/scripts/uosc_shared/lib/utils.lua @@ -0,0 +1,609 @@ +--[[ UI specific utilities that might or might not depend on its state or options ]] + +-- Sorting comparator close to (but not exactly) how file explorers sort files. +sort_filenames = (function() + local symbol_order + local default_order + + if state.platform == 'windows' then + symbol_order = { + ['!'] = 1, ['#'] = 2, ['$'] = 3, ['%'] = 4, ['&'] = 5, ['('] = 6, [')'] = 6, [','] = 7, + ['.'] = 8, ["'"] = 9, ['-'] = 10, [';'] = 11, ['@'] = 12, ['['] = 13, [']'] = 13, ['^'] = 14, + ['_'] = 15, ['`'] = 16, ['{'] = 17, ['}'] = 17, ['~'] = 18, ['+'] = 19, ['='] = 20, + } + default_order = 21 + else + symbol_order = { + ['`'] = 1, ['^'] = 2, ['~'] = 3, ['='] = 4, ['_'] = 5, ['-'] = 6, [','] = 7, [';'] = 8, + ['!'] = 9, ["'"] = 10, ['('] = 11, [')'] = 11, ['['] = 12, [']'] = 12, ['{'] = 13, ['}'] = 14, + ['@'] = 15, ['$'] = 16, ['*'] = 17, ['&'] = 18, ['%'] = 19, ['+'] = 20, ['.'] = 22, ['#'] = 23, + } + default_order = 21 + end + + -- Alphanumeric sorting for humans in Lua + -- http://notebook.kulchenko.com/algorithms/alphanumeric-natural-sorting-for-humans-in-lua + local function pad_number(n, d) + return #d > 0 and ("%03d%s%.12f"):format(#n, n, tonumber(d) / (10 ^ #d)) + or ("%03d%s"):format(#n, n) + end + + --- In place sorting of filenames + ---@param filenames string[] + return function(filenames) + local tuples = {} + for i, filename in ipairs(filenames) do + local first_char = filename:sub(1, 1) + local order = symbol_order[first_char] or default_order + local formatted = filename:lower():gsub('0*(%d+)%.?(%d*)', pad_number) + tuples[i] = {order, formatted, filename} + end + table.sort(tuples, function(a, b) + if a[1] ~= b[1] then return a[1] < b[1] end + return a[2] == b[2] and #b[3] < #a[3] or a[2] < b[2] + end) + for i, tuple in ipairs(tuples) do filenames[i] = tuple[3] end + end +end)() + +-- Creates in-between frames to animate value from `from` to `to` numbers. +---@param from number +---@param to number|fun():number +---@param setter fun(value: number) +---@param factor_or_callback? number|fun() +---@param callback? fun() Called either on animation end, or when animation is killed. +function tween(from, to, setter, factor_or_callback, callback) + local factor = factor_or_callback + if type(factor_or_callback) == 'function' then callback = factor_or_callback end + if type(factor) ~= 'number' then factor = 0.3 end + + local current, done, timeout = from, false, nil + local get_to = type(to) == 'function' and to or function() return to --[[@as number]] end + local cutoff = math.abs(get_to() - from) * 0.01 + + local function finish() + if not done then + done = true + timeout:kill() + if callback then callback() end + end + end + + local function tick() + local to = get_to() + current = current + ((to - current) * factor) + local is_end = math.abs(to - current) <= cutoff + setter(is_end and to or current) + request_render() + if is_end then finish() + else timeout:resume() end + end + + timeout = mp.add_timeout(state.render_delay, tick) + tick() + + return finish +end + +---@param point {x: number; y: number} +---@param rect {ax: number; ay: number; bx: number; by: number} +function get_point_to_rectangle_proximity(point, rect) + local dx = math.max(rect.ax - point.x, 0, point.x - rect.bx) + local dy = math.max(rect.ay - point.y, 0, point.y - rect.by) + return math.sqrt(dx * dx + dy * dy) +end + +---@param point_a {x: number; y: number} +---@param point_b {x: number; y: number} +function get_point_to_point_proximity(point_a, point_b) + local dx, dy = point_a.x - point_b.x, point_a.y - point_b.y + return math.sqrt(dx * dx + dy * dy) +end + +-- Call function with args if it exists +function call_maybe(fn, ...) + if type(fn) == 'function' then fn(...) end +end + +-- Extracts the properties used by property expansion of that string. +---@param str string +---@param res { [string] : boolean } | nil +---@return { [string] : boolean } +function get_expansion_props(str, res) + res = res or {} + for str in str:gmatch('%$(%b{})') do + local name, str = str:match('^{[?!]?=?([^:]+):?(.*)}$') + if name then + local s = name:find('==') or nil + if s then name = name:sub(0, s - 1) end + res[name] = true + if str and str ~= '' then get_expansion_props(str, res) end + end + end + return res +end + +-- Escape a string for verbatim display on the OSD. +---@param str string +function ass_escape(str) + -- There is no escape for '\' in ASS (I think?) but '\' is used verbatim if + -- it isn't followed by a recognized character, so add a zero-width + -- non-breaking space + str = str:gsub('\\', '\\\239\187\191') + str = str:gsub('{', '\\{') + str = str:gsub('}', '\\}') + -- Precede newlines with a ZWNBSP to prevent ASS's weird collapsing of + -- consecutive newlines + str = str:gsub('\n', '\239\187\191\\N') + -- Turn leading spaces into hard spaces to prevent ASS from stripping them + str = str:gsub('\\N ', '\\N\\h') + str = str:gsub('^ ', '\\h') + return str +end + +---@param seconds number +---@param max_seconds number|nil Trims unnecessary `00:` if time is not expected to reach it. +---@return string +function format_time(seconds, max_seconds) + local human = mp.format_time(seconds) + if options.time_precision > 0 then + local formatted = string.format('%.' .. options.time_precision .. 'f', math.abs(seconds) % 1) + human = human .. '.' .. string.sub(formatted, 3) + end + if max_seconds then + local trim_length = (max_seconds < 60 and 7 or (max_seconds < 3600 and 4 or 0)) + if trim_length > 0 then + local has_minus = seconds < 0 + human = string.sub(human, trim_length + (has_minus and 1 or 0)) + if has_minus then human = '-' .. human end + end + end + return human +end + +---@param opacity number 0-1 +function opacity_to_alpha(opacity) + return 255 - math.ceil(255 * opacity) +end + +path_separator = (function() + local os_separator = state.platform == 'windows' and '\\' or '/' + + -- Get appropriate path separator for the given path. + ---@param path string + ---@return string + return function(path) + return path:sub(1, 2) == '\\\\' and '\\' or os_separator + end +end)() + +-- Joins paths with the OS aware path separator or UNC separator. +---@param p1 string +---@param p2 string +---@return string +function join_path(p1, p2) + local p1, separator = trim_trailing_separator(p1) + -- Prevents joining drive letters with a redundant separator (`C:\\foo`), + -- as `trim_trailing_separator()` doesn't trim separators from drive letters. + return p1:sub(#p1) == separator and p1 .. p2 or p1 .. separator.. p2 +end + +-- Check if path is absolute. +---@param path string +---@return boolean +function is_absolute(path) + if path:sub(1, 2) == '\\\\' then return true + elseif state.platform == 'windows' then return path:find('^%a+:') ~= nil + else return path:sub(1, 1) == '/' end +end + +-- Ensure path is absolute. +---@param path string +---@return string +function ensure_absolute(path) + if is_absolute(path) then return path end + return join_path(state.cwd, path) +end + +-- Remove trailing slashes/backslashes. +---@param path string +---@return string path, string trimmed_separator_type +function trim_trailing_separator(path) + local separator = path_separator(path) + path = trim_end(path, separator) + if state.platform == 'windows' then + -- Drive letters on windows need trailing backslash + if path:sub(#path) == ':' then path = path .. '\\' end + else + if path == '' then path = '/' end + end + return path, separator +end + +-- Ensures path is absolute, remove trailing slashes/backslashes. +-- Lightweight version of normalize_path for performance critical parts. +---@param path string +---@return string +function normalize_path_lite(path) + if not path or is_protocol(path) then return path end + path = trim_trailing_separator(ensure_absolute(path)) + return path +end + +-- Ensures path is absolute, remove trailing slashes/backslashes, normalization of path separators and deduplication. +---@param path string +---@return string +function normalize_path(path) + if not path or is_protocol(path) then return path end + + path = ensure_absolute(path) + local is_unc = path:sub(1, 2) == '\\\\' + if state.platform == 'windows' or is_unc then path = path:gsub('/', '\\') end + path = trim_trailing_separator(path) + + --Deduplication of path separators + if is_unc then path = path:gsub('(.\\)\\+', '%1') + elseif state.platform == 'windows' then path = path:gsub('\\\\+', '\\') + else path = path:gsub('//+', '/') end + + return path +end + +-- Check if path is a protocol, such as `http://...`. +---@param path string +function is_protocol(path) + return type(path) == 'string' and (path:find('^%a[%a%d-_]+://') ~= nil or path:find('^%a[%a%d-_]+:\\?') ~= nil) +end + +---@param path string +---@param extensions string[] Lowercase extensions without the dot. +function has_any_extension(path, extensions) + local path_last_dot_index = string_last_index_of(path, '.') + if not path_last_dot_index then return false end + local path_extension = path:sub(path_last_dot_index + 1):lower() + for _, extension in ipairs(extensions) do + if path_extension == extension then return true end + end + return false +end + +---@return string +function get_default_directory() + return mp.command_native({'expand-path', options.default_directory}) +end + +-- Serializes path into its semantic parts. +---@param path string +---@return nil|{path: string; is_root: boolean; dirname?: string; basename: string; filename: string; extension?: string;} +function serialize_path(path) + if not path or is_protocol(path) then return end + + local normal_path = normalize_path_lite(path) + local dirname, basename = utils.split_path(normal_path) + if basename == '' then basename, dirname = dirname:sub(1, #dirname - 1), nil end + local dot_i = string_last_index_of(basename, '.') + + return { + path = normal_path, + is_root = dirname == nil, + dirname = dirname, + basename = basename, + filename = dot_i and basename:sub(1, dot_i - 1) or basename, + extension = dot_i and basename:sub(dot_i + 1) or nil, + } +end + +-- Reads items in directory and splits it into directories and files tables. +---@param path string +---@param allowed_types? string[] Filter `files` table to contain only files with these extensions. +---@return string[]|nil files +---@return string[]|nil directories +function read_directory(path, allowed_types) + local items, error = utils.readdir(path, 'all') + + if not items then + msg.error('Reading files from "' .. path .. '" failed: ' .. error) + return nil, nil + end + + local files, directories = {}, {} + + for _, item in ipairs(items) do + if item ~= '.' and item ~= '..' then + local info = utils.file_info(join_path(path, item)) + if info then + if info.is_file then + if not allowed_types or has_any_extension(item, allowed_types) then + files[#files + 1] = item + end + else directories[#directories + 1] = item end + end + end + end + + return files, directories +end + +-- Returns full absolute paths of files in the same directory as `file_path`, +-- and index of the current file in the table. +-- Returned table will always contain `file_path`, regardless of `allowed_types`. +---@param file_path string +---@param allowed_types? string[] Filter adjacent file types. Does NOT filter out the `file_path`. +function get_adjacent_files(file_path, allowed_types) + local current_meta = serialize_path(file_path) + if not current_meta then return end + local files = read_directory(current_meta.dirname) + if not files then return end + sort_filenames(files) + local current_file_index + local paths = {} + for _, file in ipairs(files) do + local is_current_file = current_meta.basename == file + if is_current_file or not allowed_types or has_any_extension(file, allowed_types) then + paths[#paths + 1] = join_path(current_meta.dirname, file) + if is_current_file then current_file_index = #paths end + end + end + if not current_file_index then return end + return paths, current_file_index +end + +-- Navigates in a list, using delta or, when `state.shuffle` is enabled, +-- randomness to determine the next item. Loops around if `loop-playlist` is enabled. +---@param list table +---@param current_index number +---@param delta number +function decide_navigation_in_list(list, current_index, delta) + if #list < 2 then return #list, list[#list] end + + if state.shuffle then + local new_index = current_index + math.randomseed(os.time()) + while current_index == new_index do new_index = math.random(#list) end + return new_index, list[new_index] + end + + local new_index = current_index + delta + if mp.get_property_native('loop-playlist') then + if new_index > #list then new_index = new_index % #list + elseif new_index < 1 then new_index = #list - new_index end + elseif new_index < 1 or new_index > #list then + return + end + + return new_index, list[new_index] +end + +---@param delta number +function navigate_directory(delta) + if not state.path or is_protocol(state.path) then return false end + local paths, current_index = get_adjacent_files(state.path, config.types.autoload) + if paths and current_index then + local _, path = decide_navigation_in_list(paths, current_index, delta) + if path then mp.commandv('loadfile', path) return true end + end + return false +end + +---@param delta number +function navigate_playlist(delta) + local playlist, pos = mp.get_property_native('playlist'), mp.get_property_native('playlist-pos-1') + if playlist and #playlist > 1 and pos then + local index = decide_navigation_in_list(playlist, pos, delta) + if index then mp.commandv('playlist-play-index', index - 1) return true end + end + return false +end + +---@param delta number +function navigate_item(delta) + if state.has_playlist then return navigate_playlist(delta) else return navigate_directory(delta) end +end + +-- Can't use `os.remove()` as it fails on paths with unicode characters. +-- Returns `result, error`, result is table of: +-- `status:number(<0=error), stdout, stderr, error_string, killed_by_us:boolean` +---@param path string +function delete_file(path) + if state.platform == 'windows' then + if options.use_trash then + local ps_code = [[ + Add-Type -AssemblyName Microsoft.VisualBasic + [Microsoft.VisualBasic.FileIO.FileSystem]::DeleteFile('__path__', 'OnlyErrorDialogs', 'SendToRecycleBin') + ]] + + local escaped_path = string.gsub(path, "'", "''") + escaped_path = string.gsub(escaped_path, "’", "’’") + escaped_path = string.gsub(escaped_path, "%%", "%%%%") + ps_code = string.gsub(ps_code, "__path__", escaped_path) + args = { 'powershell', '-NoProfile', '-Command', ps_code } + else + args = { 'cmd', '/C', 'del', path } + end + else + if options.use_trash then + --On Linux and Macos the app trash-cli/trash must be installed first. + args = { 'trash', path } + else + args = { 'rm', path } + end + end + return mp.command_native({ + name = 'subprocess', + args = args, + playback_only = false, + capture_stdout = true, + capture_stderr = true, + }) +end + +function serialize_chapter_ranges(normalized_chapters) + local ranges = {} + local simple_ranges = { + {name = 'openings', patterns = { + '^op ', '^op$', ' op$', + '^opening$', ' opening$' + }, requires_next_chapter = true}, + {name = 'intros', patterns = { + '^intro$', ' intro$', + '^avant$', '^prologue$' + }, requires_next_chapter = true}, + {name = 'endings', patterns = { + '^ed ', '^ed$', ' ed$', + '^ending ', '^ending$', ' ending$', + }}, + {name = 'outros', patterns = { + '^outro$', ' outro$', + '^closing$', '^closing ', + '^preview$', '^pv$', + }}, + } + local sponsor_ranges = {} + + -- Extend with alt patterns + for _, meta in ipairs(simple_ranges) do + local alt_patterns = config.chapter_ranges[meta.name] and config.chapter_ranges[meta.name].patterns + if alt_patterns then meta.patterns = itable_join(meta.patterns, alt_patterns) end + end + + -- Clone chapters + local chapters = {} + for i, normalized in ipairs(normalized_chapters) do chapters[i] = table_shallow_copy(normalized) end + + for i, chapter in ipairs(chapters) do + -- Simple ranges + for _, meta in ipairs(simple_ranges) do + if config.chapter_ranges[meta.name] then + local match = itable_find(meta.patterns, function(p) return chapter.lowercase_title:find(p) end) + if match then + local next_chapter = chapters[i + 1] + if next_chapter or not meta.requires_next_chapter then + ranges[#ranges + 1] = table_assign({ + start = chapter.time, + ['end'] = next_chapter and next_chapter.time or INFINITY, + }, config.chapter_ranges[meta.name]) + end + end + end + end + + -- Sponsor blocks + if config.chapter_ranges.ads then + local id = chapter.lowercase_title:match('segment start *%(([%w]%w-)%)') + if id then -- ad range from sponsorblock + for j = i + 1, #chapters, 1 do + local end_chapter = chapters[j] + local end_match = end_chapter.lowercase_title:match('segment end *%(' .. id .. '%)') + if end_match then + local range = table_assign({ + start_chapter = chapter, end_chapter = end_chapter, + start = chapter.time, ['end'] = end_chapter.time, + }, config.chapter_ranges.ads) + ranges[#ranges + 1], sponsor_ranges[#sponsor_ranges + 1] = range, range + end_chapter.is_end_only = true + break + end + end -- single chapter for ad + elseif not chapter.is_end_only and + (chapter.lowercase_title:find('%[sponsorblock%]:') or chapter.lowercase_title:find('^sponsors?')) then + local next_chapter = chapters[i + 1] + ranges[#ranges + 1] = table_assign({ + start = chapter.time, + ['end'] = next_chapter and next_chapter.time or INFINITY, + }, config.chapter_ranges.ads) + end + end + end + + -- Fix overlapping sponsor block segments + for index, range in ipairs(sponsor_ranges) do + local next_range = sponsor_ranges[index + 1] + if next_range then + local delta = next_range.start - range['end'] + if delta < 0 then + local mid_point = range['end'] + delta / 2 + range['end'], range.end_chapter.time = mid_point - 0.01, mid_point - 0.01 + next_range.start, next_range.start_chapter.time = mid_point, mid_point + end + end + end + table.sort(chapters, function(a, b) return a.time < b.time end) + + return chapters, ranges +end + +-- Ensures chapters are in chronological order +function normalize_chapters(chapters) + if not chapters then return {} end + -- Ensure chronological order + table.sort(chapters, function(a, b) return a.time < b.time end) + -- Ensure titles + for index, chapter in ipairs(chapters) do + chapter.title = chapter.title or ('Chapter ' .. index) + chapter.lowercase_title = chapter.title:lower() + end + return chapters +end + +function serialize_chapters(chapters) + chapters = normalize_chapters(chapters) + if not chapters then return end + --- timeline font size isn't accessible here, so normalize to size 1 and then scale during rendering + local opts = {size = 1, bold = true} + for index, chapter in ipairs(chapters) do + chapter.index = index + chapter.title_wrapped = wrap_text(chapter.title, opts, 25) + chapter.title_wrapped_width = text_width(chapter.title_wrapped, opts) + chapter.title_wrapped = ass_escape(chapter.title_wrapped) + end + return chapters +end + +--[[ RENDERING ]] + +function render() + if not display.initialized then return end + state.render_last_time = mp.get_time() + + cursor.reset_handlers() + + -- Actual rendering + local ass = assdraw.ass_new() + + for _, element in Elements:ipairs() do + if element.enabled then + local result = element:maybe('render') + if result then + ass:new_event() + ass:merge(result) + end + end + end + + cursor.decide_keybinds() + + -- submit + if osd.res_x == display.width and osd.res_y == display.height and osd.data == ass.text then + return + end + + osd.res_x = display.width + osd.res_y = display.height + osd.data = ass.text + osd.z = 2000 + osd:update() + + update_margins() +end + +-- Request that render() is called. +-- The render is then either executed immediately, or rate-limited if it was +-- called a small time ago. +state.render_timer = mp.add_timeout(0, render) +state.render_timer:kill() +function request_render() + if state.render_timer:is_enabled() then return end + local timeout = math.max(0, state.render_delay - (mp.get_time() - state.render_last_time)) + state.render_timer.timeout = timeout + state.render_timer:resume() +end diff --git a/multimedia/.config/mpv/scripts/uosc_shared/main.lua b/multimedia/.config/mpv/scripts/uosc_shared/main.lua new file mode 100644 index 0000000..323027f --- /dev/null +++ b/multimedia/.config/mpv/scripts/uosc_shared/main.lua @@ -0,0 +1,5 @@ +--[[ +File required for compatibility between mpv: +- 0.32 - doesn't support `dir/main.lua`, so we need `uosc.lua` in root +- 0.33 - requires `main.lua` in directories +]] From 2f4e71ad9fe01977e7ddbb1d0d69c32558658434 Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Tue, 23 May 2023 15:35:37 +0200 Subject: [PATCH 17/27] task: Make taskopen adhere to xdg Using `TASKOPENRC` we set the configuration file to be in the correct xdg configuration directory. --- office/.config/sh/env.d/taskopen-xdg.sh | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 office/.config/sh/env.d/taskopen-xdg.sh diff --git a/office/.config/sh/env.d/taskopen-xdg.sh b/office/.config/sh/env.d/taskopen-xdg.sh new file mode 100644 index 0000000..5aac3a7 --- /dev/null +++ b/office/.config/sh/env.d/taskopen-xdg.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env sh + +export TASKOPENRC="${XDG_CONFIG_HOME:-"$HOME/.config"}/task/taskopenrc" From 605e0abdbbec8c7ec2e4e1827e178d246e9b80f0 Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Tue, 23 May 2023 15:37:29 +0200 Subject: [PATCH 18/27] neomutt: Fix laggy mail list display Fixed display of longer mail directory lists which would be very laggy by simply removing calls to attachment_info in the overview. --- office/.config/neomutt/colors | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/office/.config/neomutt/colors b/office/.config/neomutt/colors index faae263..8461d30 100644 --- a/office/.config/neomutt/colors +++ b/office/.config/neomutt/colors @@ -73,7 +73,7 @@ color progress black cyan # Formatting ---------------------------------------------------------------------- set date_format = "%a %d %h %H:%M" -set index_format=" %zc %zs %zt | %-35.35L %@attachment_info@ %?M10?~(%1M) ?%-30.100s %> %?Y?%Y ? %(!%a %d %h %H:%M) " +set index_format=" %zc %zs %zt | %-35.35L %?X?📎& ? %?M10?~(%1M) ?%-30.100s %> %?Y?%Y ? %(!%a %d %h %H:%M) " set pager_format="%n %T %s%*  %{!%d %b · %H:%M} %?X? %X?%P" set status_format = " %D %?u? %u ?%?R? %R ?%?d? %d ?%?t? %t ?%?F? %F ?%?p? %p? \n \n" set compose_format="-- NeoMutt: Compose [Approx. msg size: %l Atts: %a]%>-" From 8627a51bb7da3b3b5d354349764dfd76e542e147 Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Tue, 23 May 2023 15:38:57 +0200 Subject: [PATCH 19/27] neomutt: Update macro key maps Updated key mappings to use `,` as the 'local-prefix' (or 'macro-prefix') which allows easier setup for an additional functionality layer. Also make `dd` the standard way to delete a whole sub-thread instead of single mail by single mail of a conversation. That functionality can now be achieved with `dD` instead, while `dd` removes a whole thread (not just sub-thread). --- office/.config/neomutt/maps | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/office/.config/neomutt/maps b/office/.config/neomutt/maps index da5854d..fd86c50 100644 --- a/office/.config/neomutt/maps +++ b/office/.config/neomutt/maps @@ -21,9 +21,9 @@ bind index,pager w display-toggle-weed # Thread manipulation bind pager d noop -bind index,pager dd delete-message -bind index,pager dT delete-thread -bind index,pager dt delete-subthread +bind index,pager dD delete-message +bind index,pager dd delete-subthread +bind index,pager dt delete-thread bind pager,index gt next-thread bind pager,index gT previous-thread bind pager,index za collapse-thread @@ -31,15 +31,15 @@ bind pager,index zA collapse-all bind pager,index zr reconstruct-thread bind pager,index zR entire-thread # Saner copy/move dialogs -macro index,pager C "?" "copy a message to a mailbox" -macro index,pager M "?" "move a message to a mailbox" -macro index,pager gM "?" "move thread to a mailbox" +macro index,pager ,c "?" "copy a message to a mailbox" +macro index,pager ,m "?" "move thread to a mailbox" +macro index,pager ,M "?" "move a message to a mailbox" # Email completion bindings bind editor complete-query bind editor ^T complete # Press A to add contact to Khard address book -macro index,pager A \ +macro index,pager ,a \ "khard add-email" \ "add the sender email address to khard" @@ -69,14 +69,14 @@ bind pager G bottom # compose postpone bind compose p postpone-message # markdown to html for composition -macro compose M "F pandoc -s -f markdown -t html \ny^T^Utext/html; charset=UTF-8\n" "Convert from MD to HTML" +macro compose ,m "F pandoc -s -f markdown -t html \ny^T^Utext/html; charset=UTF-8\n" "Convert from MD to HTML" # since we unbound the original g bind index,pager r noop # to avoid accidentally sending replies bind index,pager rr group-reply bind index,pager ro reply # open urls found in the e-mail -macro index,pager \CU "|urlview" "call urlview to open links" +macro index,pager \CU " unset pipe_decodeextract_url | fzf | clip" "get URLs" # Refresh far imap email macro index O "export MBSYNC_PRE=true; sync-mail" "refresh all e-mail" @@ -84,5 +84,5 @@ macro index o "export MBSYNC_PRE=true; sync-mail gma # Send mail to taskwarrior -macro index,pager T "mutt2task -c -d -t" "add mail as task to taskwarrior with custom description and tags" -macro index,pager t "mutt2task -c" "add mail as task to taskwarrior" +macro index,pager ,T "mutt2task -c -d -t" "add mail as task to taskwarrior with custom description and tags" +macro index,pager ,t "mutt2task -c" "add mail as task to taskwarrior" From 7121897385529d51d187cc4d09c645afa4e848d7 Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Tue, 23 May 2023 15:42:03 +0200 Subject: [PATCH 20/27] qutebrowser: Add URL rewriting for scribe redirects Scribe links are often not redirected correctly if belonging to medium's 'global-identity' redirections. This is a first attempt at fixing those by removing the superfluous string from the scribe URL. Generalized enough to work as a 'post-processing' function for the redirection plugin, which can be set up by pointing the 'postprocess' key of a page entry to a callable object (most likely a function). --- qutebrowser/.config/qutebrowser/redirects.py | 21 +++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/qutebrowser/.config/qutebrowser/redirects.py b/qutebrowser/.config/qutebrowser/redirects.py index 8323b9d..55c8a14 100644 --- a/qutebrowser/.config/qutebrowser/redirects.py +++ b/qutebrowser/.config/qutebrowser/redirects.py @@ -2,6 +2,17 @@ import random import re from qutebrowser.api import interceptor from qutebrowser.extensions.interceptors import RedirectException +from qutebrowser.utils import message + +def fixScribePath(url): + """ Fix external medium blog to scribe translation. + Some paths from medium will go through a 'global identity' + path which messes up the actual url path we want to go + to and puts it in queries. This puts it back on the path. + """ + new_path = f"{url.path()}{url.query()}" + url.setQuery("") + url.setPath(re.sub(r"m/global-identity-2redirectUrl=", "", new_path)) redirects = { "youtube": { @@ -132,13 +143,11 @@ redirects = { "rimgo.bcow.xyz", "rimgo.pussthecat.org", "rimgo.totaldarkness.net", - "rimgo.bus-hit.me", "rimgo.esmailelbob.xyz", "imgur.artemislena.eu", "rimgo.vern.cc", "rim.odyssey346.dev", "rimgo.privacytools.io", - "i.habedieeh.re", "rimgo.hostux.net", "ri.zzls.xyz", "rimgo.marcopisco.com", @@ -157,6 +166,7 @@ redirects = { "scribe.privacydev.net", "sc.vern.cc", ], + "postprocess": fixScribePath }, "google": { "source": ["google.com"], @@ -172,7 +182,6 @@ redirects = { "wiki.slipfox.xyz", "wikiless.esmailelbob.xyz", "wikiless.funami.tech", - "wikiless.northboot.xyz", "wikiless.org", "wikiless.tiekoetter.com", ], @@ -204,10 +213,12 @@ def rewrite(request: interceptor.Request): if matched: target = service["target"][random.randint(0, len(service["target"]) - 1)] if target is not None and url.setHost(target) is not False: + if "postprocess" in service: + service["postprocess"](url) try: request.redirect(url) - except RedirectException: - pass + except RedirectException as e: + message.error(str(e)) break From b8c59db4c23aa6397643c6fb3a5a2c20b69ad288 Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Tue, 23 May 2023 15:47:08 +0200 Subject: [PATCH 21/27] wezterm: Refactor and format --- terminal/.config/wezterm/events.lua | 4 +--- terminal/.config/wezterm/wezterm.lua | 3 ++- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/terminal/.config/wezterm/events.lua b/terminal/.config/wezterm/events.lua index c486f1c..4b64134 100644 --- a/terminal/.config/wezterm/events.lua +++ b/terminal/.config/wezterm/events.lua @@ -5,9 +5,7 @@ local act = wezterm.action local function setup() local function isViProcess(pane) - local proc = pane:get_foreground_process_name() - if (proc:find('vim') or proc:find('nvim')) then return true end - return false + return pane:get_foreground_process_name():find('n?vim') ~= nil end local function conditionalActivatePane(window, pane, pane_direction, diff --git a/terminal/.config/wezterm/wezterm.lua b/terminal/.config/wezterm/wezterm.lua index fc9fb24..6ab66c5 100644 --- a/terminal/.config/wezterm/wezterm.lua +++ b/terminal/.config/wezterm/wezterm.lua @@ -74,7 +74,8 @@ local settings = { event = { Up = { streak = 1, button = 'Left' } }, mods = 'NONE', action = wezterm.action - .CompleteSelectionOrOpenLinkAtMouseCursor 'ClipboardAndPrimarySelection' + .CompleteSelectionOrOpenLinkAtMouseCursor + 'ClipboardAndPrimarySelection' } } } From 2b40315142e891687c5d2483c73b471b7618b140 Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Tue, 23 May 2023 15:47:51 +0200 Subject: [PATCH 22/27] newsboat: Implement half-page up/down mappings Can be invoked by c-b and c-d respectively, mimicking vim. --- social/.config/newsboat/config | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/social/.config/newsboat/config b/social/.config/newsboat/config index fdb9de3..fecfbee 100644 --- a/social/.config/newsboat/config +++ b/social/.config/newsboat/config @@ -74,9 +74,9 @@ bind-key h quit searchresultslist bind-key g home bind-key G end bind-key ^F pagedown -bind-key ^B pageup -bind-key ^D pagedown bind-key ^U pageup +bind-key ^B halfpageup +bind-key ^D halfpagedown bind-key n next-unread bind-key N prev-unread bind-key ^n next-unread-feed articlelist From 760ed037ba39a72caa4b5e6c579abbd83f9d60e0 Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Tue, 23 May 2023 15:50:37 +0200 Subject: [PATCH 23/27] nvim: Update plugins --- nvim/.config/nvim/lazy-lock.json | 70 +++++++++++++++---------------- nvim/.config/nvim/lua/plugins.lua | 57 ++++++++++--------------- 2 files changed, 57 insertions(+), 70 deletions(-) diff --git a/nvim/.config/nvim/lazy-lock.json b/nvim/.config/nvim/lazy-lock.json index 43a8dbf..7020220 100644 --- a/nvim/.config/nvim/lazy-lock.json +++ b/nvim/.config/nvim/lazy-lock.json @@ -7,61 +7,61 @@ "cmp-beancount": { "branch": "main", "commit": "da154ea94d598e6649d6ad01efa0a8611eff460d" }, "cmp-buffer": { "branch": "main", "commit": "3022dbc9166796b644a841a02de8dd1cc1d311fa" }, "cmp-calc": { "branch": "main", "commit": "50792f34a628ea6eb31d2c90e8df174671e4e7a0" }, - "cmp-cmdline": { "branch": "main", "commit": "8fcc934a52af96120fe26358985c10c035984b53" }, + "cmp-cmdline": { "branch": "main", "commit": "5af1bb7d722ef8a96658f01d6eb219c4cf746b32" }, "cmp-digraphs": { "branch": "master", "commit": "5efc1f0078d7c5f3ea1c8e3aad04da3fd6e081a9" }, "cmp-latex-symbols": { "branch": "main", "commit": "165fb66afdbd016eaa1570e41672c4c557b57124" }, "cmp-nvim-lsp": { "branch": "main", "commit": "0e6b2ed705ddcff9738ec4ea838141654f12eeef" }, - "cmp-nvim-lua": { "branch": "main", "commit": "f3491638d123cfd2c8048aefaf66d246ff250ca6" }, + "cmp-nvim-lua": { "branch": "main", "commit": "f12408bdb54c39c23e67cab726264c10db33ada8" }, "cmp-pandoc-references": { "branch": "master", "commit": "2c808dff631a783ddd2c554c4c6033907589baf6" }, "cmp-path": { "branch": "main", "commit": "91ff86cd9c29299a64f968ebb45846c485725f23" }, "cmp-rg": { "branch": "master", "commit": "1cad8eb315643d0df13c37401c03d7986f891011" }, "cmp-spell": { "branch": "master", "commit": "60584cb75e5e8bba5a0c9e4c3ab0791e0698bffa" }, "cmp-tmux": { "branch": "main", "commit": "984772716f66d8ee88535a6bf3f94c4b4e1301f5" }, - "cmp-treesitter": { "branch": "master", "commit": "b40178b780d547bcf131c684bc5fd41af17d05f2" }, + "cmp-treesitter": { "branch": "master", "commit": "389eadd48c27aa6dc0e6b992644704f026802a2e" }, "cmp_luasnip": { "branch": "master", "commit": "18095520391186d634a0045dacaa346291096566" }, "completion-vcard": { "branch": "master", "commit": "2220fd517a985ececed1adcf0e5be8f2815564c7" }, "dial.nvim": { "branch": "master", "commit": "54b503f906bc9e5ab85288414840a1b86d40769f" }, - "dressing.nvim": { "branch": "master", "commit": "5f44f829481640be0f96759c965ae22a3bcaf7ce" }, - "easyread.nvim": { "branch": "main", "commit": "73df5f4dc8fd38bef079b890b2a34412844c00b1" }, - "fidget.nvim": { "branch": "main", "commit": "688b4fec4517650e29c3e63cfbb6e498b3112ba1" }, - "formatter.nvim": { "branch": "master", "commit": "ed949c13e1a942db29ababa35e8c7864ced90eb6" }, - "friendly-snippets": { "branch": "main", "commit": "25ddcd96540a2ce41d714bd7fea2e7f75fea8ead" }, + "dressing.nvim": { "branch": "master", "commit": "66e4990240f92e31b0d5e4df6deb6bb0160ae832" }, + "easyread.nvim": { "branch": "main", "commit": "0b07e315a4cd7d700c4a794bdddbec79fdc2628b" }, + "fidget.nvim": { "branch": "main", "commit": "0ba1e16d07627532b6cae915cc992ecac249fb97" }, + "formatter.nvim": { "branch": "master", "commit": "fa4f2729cc2909db599169f22d8e55632d4c8d59" }, + "friendly-snippets": { "branch": "main", "commit": "1d0dac346de7c6895ac72528df3276386c6b149b" }, "fwatch.nvim": { "branch": "main", "commit": "a691f7349dc66285cd75a1a698dd28bca45f2bf8" }, "gitsigns.nvim": { "branch": "main", "commit": "bb808fc7376ed7bac0fbe8f47b83d4bf01738167" }, - "jupyter-kernel.nvim": { "branch": "main", "commit": "997dd7303f1e9cb210e511364725cfc4b6c4aa36" }, - "lazy.nvim": { "branch": "main", "commit": "887eb75591520a01548134c4623617b639289d0b" }, + "jupyter-kernel.nvim": { "branch": "main", "commit": "5b409598033884a3d819e2a3bcd1fe340bc8d783" }, + "lazy.nvim": { "branch": "main", "commit": "aba872ec78ffe7f7367764ab0fff6f0170421fde" }, "lightspeed.nvim": { "branch": "main", "commit": "299eefa6a9e2d881f1194587c573dad619fdb96f" }, "lsp-format.nvim": { "branch": "master", "commit": "ca0df5c8544e51517209ea7b86ecc522c98d4f0a" }, - "lsp-zero.nvim": { "branch": "v2.x", "commit": "3beb377de24b81ba3c6e719b3c15cf1f50536338" }, + "lsp-zero.nvim": { "branch": "v2.x", "commit": "56a50ebe9b0f46ecfabca3f1613084c74fd45414" }, "lsp_signature.nvim": { "branch": "master", "commit": "4665921ff8e30601c7c1328625b3abc1427a6143" }, - "lualine.nvim": { "branch": "master", "commit": "e99d733e0213ceb8f548ae6551b04ae32e590c80" }, + "lualine.nvim": { "branch": "master", "commit": "05d78e9fd0cdfb4545974a5aa14b1be95a86e9c9" }, "magma-nvim-goose": { "branch": "main", "commit": "5d916c39c1852e09fcd39eab174b8e5bbdb25f8f" }, "markdown-preview.nvim": { "branch": "master", "commit": "9becceee5740b7db6914da87358a183ad11b2049" }, - "mason-lspconfig.nvim": { "branch": "main", "commit": "2b811031febe5f743e07305738181ff367e1e452" }, - "mason.nvim": { "branch": "main", "commit": "9f6fd51ce6a3381fbed5fe33169ff20b5bd8f00b" }, - "mini.nvim": { "branch": "main", "commit": "427751024313e2270ca723eb16af7b218c83a7fc" }, - "nabla.nvim": { "branch": "master", "commit": "4870fce48aa4ce3565fafb0e778378d728ad02b0" }, + "mason-lspconfig.nvim": { "branch": "main", "commit": "90a8bbf106b85b76951a34c542058ffa807de2b1" }, + "mason.nvim": { "branch": "main", "commit": "253961cfe9b0a63b2524088be294acd7522366e5" }, + "mini.nvim": { "branch": "main", "commit": "889be69623395ad183ae6f3c21c8efe006350226" }, + "nabla.nvim": { "branch": "master", "commit": "8c143ad2b3ab3b8ffbd51e238ccfcbd246452a7e" }, "neural": { "branch": "main", "commit": "155618730b87a67655bdde373ee27bfce8b07ac9" }, - "nui.nvim": { "branch": "main", "commit": "0dc148c6ec06577fcf06cbab3b7dac96d48ba6be" }, - "nvim-base16": { "branch": "master", "commit": "db9ac827d833236b2b7bbacf6ec3a92f96b88890" }, - "nvim-cmp": { "branch": "main", "commit": "777450fd0ae289463a14481673e26246b5e38bf2" }, + "nui.nvim": { "branch": "main", "commit": "698e75814cd7c56b0dd8af4936bcef2d13807f3c" }, + "nvim-base16": { "branch": "master", "commit": "4f3aa29f49b38edb6db1c52cea57e64ce3de2373" }, + "nvim-cmp": { "branch": "main", "commit": "d153771162bd9795d9f7142df5c674b61066a585" }, "nvim-colorizer.lua": { "branch": "master", "commit": "dde3084106a70b9a79d48f426f6d6fec6fd203f7" }, - "nvim-lspconfig": { "branch": "master", "commit": "0f94c5fded29c0024254259f3d8a0284bfb507ea" }, + "nvim-lspconfig": { "branch": "master", "commit": "df58d91c9351a9dc5be6cf8d54f49ab0d9a64e73" }, "nvim-notify": { "branch": "master", "commit": "bdd647f61a05c9b8a57c83b78341a0690e9c29d7" }, - "nvim-surround": { "branch": "main", "commit": "056f69ed494198ff6ea0070cfc66997cfe0a6c8b" }, - "nvim-toggleterm.lua": { "branch": "main", "commit": "a5638b2206c3930a16a24e5c184dddd572f8cd34" }, - "nvim-tree.lua": { "branch": "master", "commit": "aa9971768a08caa4f10f94ab84e48d2ceb30b1c0" }, - "nvim-treesitter": { "branch": "master", "commit": "c38646edf2bdfac157ca619697ecad9ea87fd469" }, - "nvim-treesitter-context": { "branch": "master", "commit": "88d1627285f7477883516ef60521601862dae7a1" }, + "nvim-surround": { "branch": "main", "commit": "e6047128e57c1aff1566fb9f627521d2887fc77a" }, + "nvim-toggleterm.lua": { "branch": "main", "commit": "026dff5e2b504941cf172691561a67ea362596aa" }, + "nvim-tree.lua": { "branch": "master", "commit": "89816ace70642e9d3db0dab3dc68918f8979ec31" }, + "nvim-treesitter": { "branch": "master", "commit": "cc360a9beb1b30d172438f640e2c3450358c4086" }, + "nvim-treesitter-context": { "branch": "master", "commit": "f24a86c32238867f24fbff49913db0068f8488d2" }, "nvim-treesitter-textsubjects": { "branch": "master", "commit": "b913508f503527ff540f7fe2dcf1bf1d1f259887" }, - "nvim-ts-context-commentstring": { "branch": "main", "commit": "729d83ecb990dc2b30272833c213cc6d49ed5214" }, + "nvim-ts-context-commentstring": { "branch": "main", "commit": "0bf8fbc2ca8f8cdb6efbd0a9e32740d7a991e4c3" }, "nvim-ts-rainbow2": { "branch": "master", "commit": "cee4601ff8aac73dee4afa1074814343bb5a0b80" }, - "nvim-web-devicons": { "branch": "master", "commit": "95b1e300699be8eb6b5be1758a9d4d69fe93cc7f" }, - "otter.nvim": { "branch": "main", "commit": "cfb548957aed403d9838febd7223595d47b32031" }, - "playground": { "branch": "master", "commit": "4044b53c4d4fcd7a78eae20b8627f78ce7dc6f56" }, + "nvim-web-devicons": { "branch": "master", "commit": "986875b7364095d6535e28bd4aac3a9357e91bbe" }, + "otter.nvim": { "branch": "main", "commit": "4630e71b3e94552b7b33ddbfca061d92d0b466c2" }, + "playground": { "branch": "master", "commit": "2b81a018a49f8e476341dfcb228b7b808baba68b" }, "plenary.nvim": { "branch": "master", "commit": "253d34830709d690f013daf2853a9d21ad7accab" }, "popup.nvim": { "branch": "master", "commit": "b7404d35d5d3548a82149238289fa71f7f6de4ac" }, - "quarto-nvim": { "branch": "main", "commit": "91c82b96660d0b2d830c668365719b295272432d" }, + "quarto-nvim": { "branch": "main", "commit": "43898e09b5f49dee35ff01ff0f873e7d600376be" }, "significant.nvim": { "branch": "main", "commit": "5450e9d5917dc6aa9afb0fcbe32355799b8303fb" }, "smartcolumn.nvim": { "branch": "main", "commit": "0c572e3eae48874f25b74394a486f38cadb5c958" }, "spellsitter.nvim": { "branch": "master", "commit": "4af8640d9d706447e78c13150ef7475ea2c16b30" }, @@ -69,7 +69,7 @@ "telescope-fzf-native.nvim": { "branch": "main", "commit": "580b6c48651cabb63455e97d7e131ed557b8c7e2" }, "telescope.nvim": { "branch": "master", "commit": "c1a2af0af69e80e14e6b226d3957a064cd080805" }, "twilight.nvim": { "branch": "main", "commit": "8bb7fa7b918baab1ca81b977102ddb54afa63512" }, - "vifm.vim": { "branch": "master", "commit": "6898b7fcbc36324c127ba42cabe488ab15c785f4" }, + "vifm.vim": { "branch": "master", "commit": "a8130c37d144b51d84bee19f0532abcd3583383f" }, "vim-criticmarkup": { "branch": "master", "commit": "d15dc134eb177a170c79f6377f81eb02a9d20b02" }, "vim-easy-align": { "branch": "master", "commit": "0db4ea6132110631ec678a99a82aa49a0686ae65" }, "vim-exchange": { "branch": "master", "commit": "784d63083ad7d613aa96f00021cd0dfb126a781a" }, @@ -77,9 +77,9 @@ "vim-oscyank": { "branch": "main", "commit": "ffe827a27dae98aa826e2295336c650c9a434da0" }, "vim-pandoc-syntax": { "branch": "master", "commit": "4268535e1d33117a680a91160d845cd3833dfe28" }, "vim-spellsync": { "branch": "master", "commit": "3d6dd50de9c4d953cc16638112a6ae196df41463" }, - "which-key.nvim": { "branch": "main", "commit": "2a0c2d80c0a60f041afb1b789cfedbd510e2b2b6" }, - "wrapping.nvim": { "branch": "master", "commit": "a4013c377e2ffa3be00fb67791d3605ae3115acb" }, - "zen-mode.nvim": { "branch": "main", "commit": "d907e638c879642d226d27469b53db6925f69d4c" }, + "which-key.nvim": { "branch": "main", "commit": "912ef1a9b018bbe45df1529345e42ae0ac896d63" }, + "wrapping.nvim": { "branch": "master", "commit": "c04a7163dc692d80a2907d06a3af8df1fedffec2" }, + "zen-mode.nvim": { "branch": "main", "commit": "6e6c963d70a8e47854fa656987666bfb863f9c4e" }, "zettelkasten.nvim": { "branch": "main", "commit": "0e77624689b470410f5355b613d45219c9350264" }, - "zk-nvim": { "branch": "main", "commit": "50fc25b88fb28829ec7f5e5a4d4b458fca21a550" } + "zk-nvim": { "branch": "main", "commit": "275578853dc76d282ee5b31f86cd3a4f02d91f2f" } } \ No newline at end of file diff --git a/nvim/.config/nvim/lua/plugins.lua b/nvim/.config/nvim/lua/plugins.lua index 70df9a8..4264323 100644 --- a/nvim/.config/nvim/lua/plugins.lua +++ b/nvim/.config/nvim/lua/plugins.lua @@ -14,14 +14,13 @@ return { event = "BufRead" }, { "m4xshen/smartcolumn.nvim", config = true }, -- auto-hiding colorcolumn -- files - { 'vifm/vifm.vim' }, -- integrate file manager + { 'vifm/vifm.vim' }, -- integrate file manager { - 'nvim-tree/nvim-tree.lua', -- integrate file tree + 'nvim-tree/nvim-tree.lua', -- integrate file tree config = true, dependencies = { 'nvim-tree/nvim-web-devicons', config = true }, cmd = "NvimTreeToggle" - }, - -- colors + }, -- colors { 'RRethy/nvim-base16', event = "BufWinEnter", @@ -157,12 +156,11 @@ return { vim.g.magma_image_provider = "kitty" vim.g.magma_automatically_open_output = false end - }, - { - 'echasnovski/mini.nvim', - version = '*', - config = function() require('plug._mini') end }, { + 'echasnovski/mini.nvim', + version = '*', + config = function() require('plug._mini') end +}, { "akinsho/nvim-toggleterm.lua", -- simpler, programmable and multiple terminal toggling for nvim config = function() require('plug._toggleterm') end }, @@ -221,32 +219,21 @@ return { "VonHeikemen/lsp-zero.nvim", dependencies = { { "neovim/nvim-lspconfig", branch = "master" }, - "williamboman/mason.nvim", - "williamboman/mason-lspconfig.nvim", - { - "hrsh7th/nvim-cmp", - branch = "main", - dependencies = { - "andersevenrud/cmp-tmux", - "cbarrete/completion-vcard", - "f3fora/cmp-spell", - "hrsh7th/cmp-nvim-lsp", - "hrsh7th/cmp-path", - "hrsh7th/cmp-buffer", - "hrsh7th/cmp-calc", - "hrsh7th/cmp-cmdline", - "hrsh7th/cmp-nvim-lua", - "dmitmel/cmp-digraphs", - "jc-doyle/cmp-pandoc-references", - "kdheepak/cmp-latex-symbols", - "lukas-reineke/cmp-rg", - "crispgm/cmp-beancount", - "ray-x/cmp-treesitter", - "saadparwaiz1/cmp_luasnip", - } - }, - "L3MON4D3/LuaSnip", - "rafamadriz/friendly-snippets", + "williamboman/mason.nvim", "williamboman/mason-lspconfig.nvim", { + "hrsh7th/nvim-cmp", + branch = "main", + dependencies = { + "andersevenrud/cmp-tmux", "cbarrete/completion-vcard", + "f3fora/cmp-spell", "hrsh7th/cmp-nvim-lsp", + "hrsh7th/cmp-path", "hrsh7th/cmp-buffer", + "hrsh7th/cmp-calc", "hrsh7th/cmp-cmdline", + "hrsh7th/cmp-nvim-lua", "dmitmel/cmp-digraphs", + "jc-doyle/cmp-pandoc-references", + "kdheepak/cmp-latex-symbols", "lukas-reineke/cmp-rg", + "crispgm/cmp-beancount", "ray-x/cmp-treesitter", + "saadparwaiz1/cmp_luasnip" + } + }, "L3MON4D3/LuaSnip", "rafamadriz/friendly-snippets", { "lukas-reineke/lsp-format.nvim", config = true }, { "j-hui/fidget.nvim", config = true } -- loading animations for some LSP }, From 744c08f0f50b30a707717bb45759da1bfd0d9d79 Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Tue, 23 May 2023 15:50:50 +0200 Subject: [PATCH 24/27] nvim: Update spellfile --- nvim/.config/nvim/spell/en.utf-8.add | 1 + 1 file changed, 1 insertion(+) diff --git a/nvim/.config/nvim/spell/en.utf-8.add b/nvim/.config/nvim/spell/en.utf-8.add index 41ec2f1..36a166d 100644 --- a/nvim/.config/nvim/spell/en.utf-8.add +++ b/nvim/.config/nvim/spell/en.utf-8.add @@ -180,3 +180,4 @@ reproducability positivity dataset endogeneity +outliers From 7b810f94a599e26bb4bf354c4aee2a34e764cd12 Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Tue, 23 May 2023 15:51:40 +0200 Subject: [PATCH 25/27] nvim: Format --- nvim/.config/nvim/after/ftplugin/quarto.lua | 4 ---- nvim/.config/nvim/lua/plug/_lsp.lua | 14 +++++++++++--- nvim/.config/nvim/lua/plug/_toggleterm.lua | 6 ++---- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/nvim/.config/nvim/after/ftplugin/quarto.lua b/nvim/.config/nvim/after/ftplugin/quarto.lua index cbe7b57..fe290a1 100644 --- a/nvim/.config/nvim/after/ftplugin/quarto.lua +++ b/nvim/.config/nvim/after/ftplugin/quarto.lua @@ -70,10 +70,6 @@ map('n', 'ln', 'lua vim.lsp.buf.rename()', { buffer = bufnr, desc = 'Rename element' }) map('n', 'lr', 'lua vim.lsp.buf.references()', { buffer = bufnr, desc = 'References' }) -if client and client.server_capabilities.document_formatting then - map('n', 'lf', "lua vim.lsp.buf.formatting()", - { buffer = bufnr, desc = 'Format document' }) -end map('n', 'K', 'lua vim.lsp.buf.hover()', { buffer = bufnr, desc = 'Hover definition' }) diff --git a/nvim/.config/nvim/lua/plug/_lsp.lua b/nvim/.config/nvim/lua/plug/_lsp.lua index ee86304..beb53a2 100644 --- a/nvim/.config/nvim/lua/plug/_lsp.lua +++ b/nvim/.config/nvim/lua/plug/_lsp.lua @@ -7,9 +7,17 @@ vim.fn.sign_define("DiagnosticSignInfo", { text = "", texthl = "DiagnosticSig vim.fn.sign_define("DiagnosticSignHint", { text = "", texthl = "DiagnosticSignHint" }) lsp.ensure_installed({ - 'arduino_language_server', 'bashls', 'beancount', 'clangd', 'dockerls', - 'docker_compose_language_service', 'lua_ls', 'pyright', 'ruff_lsp', 'taplo', - 'yamlls' + 'arduino_language_server', + 'bashls', + 'beancount', + 'clangd', + 'dockerls', + 'docker_compose_language_service', + 'lua_ls', + 'pyright', + 'ruff_lsp', + 'taplo', + 'yamlls', }) lsp.preset({ name = "recommended", set_lsp_keymaps = false }) lsp.on_attach(function(client, bufnr) diff --git a/nvim/.config/nvim/lua/plug/_toggleterm.lua b/nvim/.config/nvim/lua/plug/_toggleterm.lua index 0940b82..83694d8 100644 --- a/nvim/.config/nvim/lua/plug/_toggleterm.lua +++ b/nvim/.config/nvim/lua/plug/_toggleterm.lua @@ -9,10 +9,8 @@ local lazygit = Terminal:new({ cmd = "lazygit", hidden = true, direction = 'float', - float_opts = {border = "curved"} + float_opts = { border = "curved" } }) function _Lazygit_toggle() lazygit:toggle() end -vim.cmd([[command! Lazygit :lua _Lazygit_toggle()]]) --- vim.api.nvim_set_keymap("n", "g", "lua _lazygit_toggle()", --- {noremap = true, silent = true}) +vim.cmd([[command! Lazygit :lua _Lazygit_toggle()]]) From d262c9432d5d12633a2ef39e476d42d1b028219c Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Tue, 23 May 2023 15:52:29 +0200 Subject: [PATCH 26/27] nvim: Remove automatic full-text completion Automatic completion from full-text search was draining battery and generally not too helpful. Disabled (commented) for now, can be re-enabled more specifically. --- nvim/.config/nvim/lua/plug/_lsp.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nvim/.config/nvim/lua/plug/_lsp.lua b/nvim/.config/nvim/lua/plug/_lsp.lua index beb53a2..b56c75f 100644 --- a/nvim/.config/nvim/lua/plug/_lsp.lua +++ b/nvim/.config/nvim/lua/plug/_lsp.lua @@ -157,7 +157,7 @@ cmp.setup({ { name = 'latex_symbols' }, { name = 'spell', keyword_length = 3 }, { name = 'tmux' }, - { name = 'rg', keyword_length = 5 }, + --{ name = 'rg', keyword_length = 5 }, { name = 'vCard' }, }, mapping = cmp.mapping.preset.insert({ From 72af217e8cd8142671e13f51525c42c4032181bc Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Tue, 23 May 2023 15:52:48 +0200 Subject: [PATCH 27/27] vidl: Enable automatic chapter embedding --- scripts/.local/bin/vidl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/.local/bin/vidl b/scripts/.local/bin/vidl index 9c51b86..a86ac8f 100755 --- a/scripts/.local/bin/vidl +++ b/scripts/.local/bin/vidl @@ -135,7 +135,7 @@ setup() { DL_FOLDER="${DL_FOLDER:-${XDG_VIDEOS_DIR:-$HOME/videos}/inbox}" ARCHIVE_FOLDER="${ARCHIVE_FOLDER:-${XDG_VIDEOS_DIR:-$HOME/videos}/archive}" YT_DL_CMD="${YT_DL_CMD:-yt-dlp}" - yt_default_opts=(-f "best[height\<=1080]" --retries 15 --embed-subs --sub-lang "en,de,es,fr") + yt_default_opts=(-f "best[height\<=1080]" --retries 15 --embed-chapters --embed-subs --sub-lang "en,de,es,fr") declare -a YT_DL_OPTS=${YT_DL_OPTS:-( "${yt_default_opts[@]}" )} YT_DL_TITLE="${YT_DL_TITLE:-%(channel)s_%(title)s_%(id)s}" # this title needs to be without extension