// ==UserScript==
// @name Vadapav Utils
// @namespace ff-utils@vadapav
// @version 1.0
// @description Utility to enhance Vadapav experience
// @author im.nbn
// @match *://*.vadapav.mov/*
// @license GPL-3.0-or-later
// @grant none
// ==/UserScript==
(function () {
'use strict';
const defaultSettings = {
openInIcon: '↗',
copyIcon: '🔗',
downloadIcon: '↓',
checkIcon: '✔︎',
urlOpenerPrefix: 'iina://open?url=',
horizontalIcon: '↔',
verticalIcon: '↕',
autoOpenIn: false,
linkSeparator: '\n',
containerOrientation: 'vertical',
};
let openInIcon = localStorage.getItem('openInIcon') || defaultSettings.openInIcon;
let copyIcon = localStorage.getItem('copyIcon') || defaultSettings.copyIcon;
let downloadIcon = localStorage.getItem('downloadIcon') || defaultSettings.downloadIcon;
let urlOpenerPrefix = localStorage.getItem('urlOpenerPrefix') || defaultSettings.urlOpenerPrefix;
let autoOpenIn = localStorage.getItem('autoOpenIn') === 'true';
let linkSeparator = localStorage.getItem('linkSeparator') || defaultSettings.linkSeparator;
let containerOrientation = localStorage.getItem('containerOrientation') || defaultSettings.containerOrientation;
let isOpenInMode = false;
function toggleOpenerLinks(forceEnable) {
isOpenInMode = forceEnable !== undefined ? forceEnable : !isOpenInMode;
document.querySelectorAll('a.file-entry.wrap').forEach(anchor => {
if (isOpenInMode) {
if (!anchor.href.startsWith(urlOpenerPrefix)) {
anchor.href = `${urlOpenerPrefix}${anchor.href}`;
}
} else {
if (anchor.href.startsWith(urlOpenerPrefix)) {
anchor.href = anchor.href.replace(urlOpenerPrefix, '');
}
}
});
openinButton.style.backgroundColor = isOpenInMode ? '#28a745' : '#666';
}
function copyLinksToClipboard() {
const links = Array.from(document.querySelectorAll('a.file-entry.wrap'))
.map(anchor => anchor.href.replace(urlOpenerPrefix, ''))
.join(linkSeparator);
const textarea = document.createElement('textarea');
textarea.value = links;
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
animateButton(copyButton, defaultSettings.checkIcon);
}
function downloadAllFiles() {
document.querySelectorAll('a.file-entry.wrap').forEach(anchor => {
const link = document.createElement('a');
link.href = anchor.href.replace(urlOpenerPrefix, '');
link.download = '';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
});
animateButton(downloadAllButton, defaultSettings.checkIcon);
}
function animateButton(button, symbol) {
const originalContent = button.innerHTML;
button.classList.add('rotate-icon');
setTimeout(() => {
button.innerHTML = symbol;
button.style.backgroundColor = '#28a745';
button.classList.remove('rotate-icon');
setTimeout(() => {
button.innerHTML = originalContent;
button.style.backgroundColor = '#666';
}, 1000);
}, 500);
}
const style = document.createElement('style');
style.innerHTML = `
.button-bounce {
transition: transform 0.2s ease-in-out;
}
.button-bounce:hover {
transform: scale(1.1);
}
.rotate-icon {
animation: rotate 0.5s linear;
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.draggable {
cursor: move;
}
.overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
cursor: move;
z-index: 1;
display: none;
}
label {
user-select: none;
}
`;
document.head.appendChild(style);
const container = createContainer('containerTop', 'containerLeft', '10000');
const buttonStyle = `
background-color: #666;
color: white;
border: none;
padding: 10px;
cursor: pointer;
font-size: 16px;
border-radius: 50%;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.3s;
`;
const openinButton = createButton(openInIcon, buttonStyle, 'Toggle Opener', () => toggleOpenerLinks());
const copyButton = createButton(copyIcon, buttonStyle, 'Copy links to clipboard', copyLinksToClipboard);
const downloadAllButton = createButton(downloadIcon, buttonStyle, 'Download all files', downloadAllFiles);
const editButton = createButton('✎', buttonStyle, 'Edit settings', toggleSettings);
container.append(openinButton, copyButton, downloadAllButton, editButton);
document.body.appendChild(container);
let settingsContainer;
function toggleSettings() {
if (settingsContainer) {
document.body.removeChild(settingsContainer);
settingsContainer = null;
editButton.innerHTML = '✎';
} else {
settingsContainer = createSettingsContainer();
document.body.appendChild(settingsContainer);
editButton.innerHTML = '✖';
makeDraggable(settingsContainer, 'settingsContainerTop', 'settingsContainerLeft');
initializeSettingsContainerPosition(settingsContainer);
}
}
function createButton(innerHTML, style, title, onClick) {
const button = document.createElement('button');
button.innerHTML = innerHTML;
button.style = style;
button.classList.add('button-bounce');
button.title = title;
button.onclick = onClick;
return button;
}
function createContainer(topKey, leftKey, zIndex) {
const container = document.createElement('div');
container.style.position = 'fixed';
container.style.top = localStorage.getItem(topKey) || '50px';
container.style.left = localStorage.getItem(leftKey) || 'calc(100% - 60px)';
container.style.zIndex = zIndex;
container.style.backgroundColor = '#444';
container.style.border = '1px solid #555';
container.style.padding = '10px 15px';
container.style.boxShadow = '0px 2px 5px rgba(0, 0, 0, 0.5)';
container.style.borderRadius = '10px';
container.style.display = 'flex';
container.style.flexDirection = containerOrientation === 'vertical' ? 'column' : 'row';
container.style.gap = '10px';
container.classList.add('draggable');
makeDraggable(container, topKey, leftKey);
return container;
}
function createSettingsContainer() {
const settingsContainer = document.createElement('div');
settingsContainer.style.position = 'fixed';
settingsContainer.style.top = localStorage.getItem('settingsContainerTop') || '100px';
settingsContainer.style.left = localStorage.getItem('settingsContainerLeft') || 'calc(100% - 280px)';
settingsContainer.style.zIndex = '10001';
settingsContainer.style.backgroundColor = '#444';
settingsContainer.style.border = '1px solid #555';
settingsContainer.style.padding = '20px';
settingsContainer.style.boxShadow = '0px 2px 5px rgba(0, 0, 0, 0.5)';
settingsContainer.style.borderRadius = '10px';
settingsContainer.style.display = 'flex';
settingsContainer.style.flexDirection = 'column';
settingsContainer.style.gap = '10px';
const overlay = document.createElement('div');
overlay.className = 'overlay';
settingsContainer.appendChild(overlay);
const inputStyle = `
background-color: white;
border: 1px solid #ccc;
padding: 8px;
font-size: 14px;
border-radius: 5px;
width: 100%;
box-sizing: border-box;
`;
settingsContainer.append(
createInput('text', openInIcon, 'OpenIn Icon', inputStyle, val => openInIcon = val),
createInput('text', copyIcon, 'Copy Icon', inputStyle, val => copyIcon = val),
createInput('text', downloadIcon, 'Download Icon', inputStyle, val => downloadIcon = val),
createInput('text', urlOpenerPrefix, 'iina://open?url=', inputStyle, val => urlOpenerPrefix = val),
createAutoOpenInLabel(),
createSeparatorLabel(inputStyle),
createOrientationButton(buttonStyle),
createSaveButton(buttonStyle)
);
return settingsContainer;
}
function createInput(type, value, placeholder, style, onChange) {
const input = document.createElement('input');
input.type = type;
input.value = value;
input.placeholder = placeholder;
input.style = style;
input.oninput = () => onChange(input.value);
return input;
}
function createAutoOpenInLabel() {
const autoOpenInLabel = document.createElement('label');
const autoOpenInCheckbox = document.createElement('input');
autoOpenInCheckbox.type = 'checkbox';
autoOpenInCheckbox.checked = autoOpenIn;
autoOpenInLabel.style = `
display: flex;
align-items: center;
gap: 10px;
`;
autoOpenInLabel.appendChild(autoOpenInCheckbox);
autoOpenInLabel.appendChild(document.createTextNode('Auto apply opener prefix'));
autoOpenInCheckbox.onchange = () => {
autoOpenIn = autoOpenInCheckbox.checked;
localStorage.setItem('autoOpenIn', autoOpenIn);
toggleOpenerLinks(autoOpenIn);
};
return autoOpenInLabel;
}
function createSeparatorLabel(inputStyle) {
const separatorLabel = document.createElement('label');
const separatorSelect = document.createElement('select');
const separatorOptions = [
{ value: '\n', label: 'New Line (\\n)' },
{ value: ',', label: 'Comma (,)' },
{ value: ';', label: 'Semi-Colon (;)' },
{ value: 'custom', label: 'Custom' }
];
separatorOptions.forEach(option => {
const opt = document.createElement('option');
opt.value = option.value;
opt.textContent = option.label;
if (linkSeparator === option.value) {
opt.selected = true;
}
separatorSelect.appendChild(opt);
});
const customSeparatorInput = document.createElement('input');
customSeparatorInput.type = 'text';
customSeparatorInput.placeholder = 'Custom Separator';
customSeparatorInput.style = inputStyle;
customSeparatorInput.value = linkSeparator;
separatorSelect.onchange = () => {
if (separatorSelect.value === 'custom') {
customSeparatorInput.style.display = 'block';
linkSeparator = customSeparatorInput.value;
} else {
customSeparatorInput.style.display = 'none';
linkSeparator = separatorSelect.value;
}
localStorage.setItem('linkSeparator', linkSeparator);
};
customSeparatorInput.oninput = () => {
linkSeparator = customSeparatorInput.value;
localStorage.setItem('linkSeparator', linkSeparator);
};
if (linkSeparator !== '\n' && linkSeparator !== ',' && linkSeparator !== ';') {
separatorSelect.value = 'custom';
customSeparatorInput.style.display = 'block';
} else {
customSeparatorInput.style.display = 'none';
}
separatorLabel.style = `
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 10px;
`;
separatorLabel.appendChild(document.createTextNode('Link Separator for download'));
separatorLabel.appendChild(separatorSelect);
separatorLabel.appendChild(customSeparatorInput);
return separatorLabel;
}
function createOrientationButton(buttonStyle) {
const toggleOrientationButton = createButton(
containerOrientation === 'vertical' ? defaultSettings.verticalIcon : defaultSettings.horizontalIcon,
buttonStyle,
'Toggle Orientation',
() => {
toggleOrientation();
toggleOrientationButton.innerHTML = containerOrientation === 'vertical' ? defaultSettings.verticalIcon : defaultSettings.horizontalIcon;
}
);
return toggleOrientationButton;
}
function createSaveButton(buttonStyle) {
const saveButton = document.createElement('button');
saveButton.innerHTML = 'Save';
saveButton.style = `
background-color: #28a745;
color: white;
border: none;
padding: 10px;
cursor: pointer;
font-size: 16px;
border-radius: 5px;
`;
saveButton.onclick = () => {
localStorage.setItem('openInIcon', openInIcon);
localStorage.setItem('copyIcon', copyIcon);
localStorage.setItem('downloadIcon', downloadIcon);
localStorage.setItem('urlOpenerPrefix', urlOpenerPrefix);
openinButton.innerHTML = openInIcon;
copyButton.innerHTML = copyIcon;
downloadAllButton.innerHTML = downloadIcon;
document.body.removeChild(settingsContainer);
settingsContainer = null;
editButton.innerHTML = '✎';
};
return saveButton;
}
function toggleOrientation() {
containerOrientation = containerOrientation === 'vertical' ? 'horizontal' : 'vertical';
container.style.flexDirection = containerOrientation === 'vertical' ? 'column' : 'row';
const containerRect = container.getBoundingClientRect();
if (containerRect.right > window.innerWidth) {
container.style.left = (window.innerWidth - containerRect.width) + 'px';
}
if (containerRect.bottom > window.innerHeight) {
container.style.top = (window.innerHeight - containerRect.height) + 'px';
}
localStorage.setItem('containerOrientation', containerOrientation);
localStorage.setItem('containerTop', container.style.top);
localStorage.setItem('containerLeft', container.style.left);
}
function makeDraggable(element, topKey, leftKey) {
let offsetX = 0, offsetY = 0, initialMouseX = 0, initialMouseY = 0;
element.onmousedown = function (e) {
if (['INPUT', 'BUTTON', 'SELECT', 'TEXTAREA'].includes(e.target.tagName)) {
return;
}
e.preventDefault();
initialMouseX = e.clientX;
initialMouseY = e.clientY;
offsetX = initialMouseX - element.getBoundingClientRect().left;
offsetY = initialMouseY - element.getBoundingClientRect().top;
document.onmousemove = function (e) {
e.preventDefault();
const newMouseX = e.clientX;
const newMouseY = e.clientY;
let newTop = newMouseY - offsetY;
let newLeft = newMouseX - offsetX;
if (newTop < 0) newTop = 0;
if (newLeft < 0) newLeft = 0;
if (newTop + element.clientHeight > window.innerHeight) {
newTop = window.innerHeight - element.clientHeight;
}
if (newLeft + element.clientWidth > window.innerWidth) {
newLeft = window.innerWidth - element.clientWidth;
}
element.style.top = newTop + 'px';
element.style.left = newLeft + 'px';
localStorage.setItem(topKey, element.style.top);
localStorage.setItem(leftKey, element.style.left);
};
document.onmouseup = function () {
document.onmouseup = null;
document.onmousemove = null;
};
};
}
function initializeSettingsContainerPosition(settingsContainer) {
const settingsLeftPercent = localStorage.getItem('settingsContainerLeftPercent') || 'calc(100% - 280px)';
const settingsTopPercent = localStorage.getItem('settingsContainerTopPercent') || '100px';
settingsContainer.style.left = `${settingsLeftPercent}%`;
settingsContainer.style.top = `${settingsTopPercent}%`;
}
if (autoOpenIn) {
toggleOpenerLinks(true);
}
window.addEventListener('resize', () => {
const containerLeftPercent = localStorage.getItem('containerLeftPercent');
const containerTopPercent = localStorage.getItem('containerTopPercent');
if (containerLeftPercent !== null && containerTopPercent !== null) {
container.style.left = `${containerLeftPercent}%`;
container.style.top = `${containerTopPercent}%`;
}
const settingsLeftPercent = localStorage.getItem('settingsContainerLeftPercent');
const settingsTopPercent = localStorage.getItem('settingsContainerTopPercent');
if (settingsContainer && settingsLeftPercent !== null && settingsTopPercent !== null) {
settingsContainer.style.left = `${settingsLeftPercent}%`;
settingsContainer.style.top = `${settingsTopPercent}%`;
}
keepWithinBounds(container);
if (settingsContainer) {
keepWithinBounds(settingsContainer);
}
});
function keepWithinBounds(element) {
const rect = element.getBoundingClientRect();
if (rect.right > window.innerWidth) {
element.style.left = (window.innerWidth - rect.width) + 'px';
}
if (rect.bottom > window.innerHeight) {
element.style.top = (window.innerHeight - rect.height) + 'px';
}
}
container.addEventListener('mouseup', saveContainerPosition);
if (settingsContainer) {
settingsContainer.addEventListener('mouseup', saveSettingsContainerPosition);
}
function saveContainerPosition() {
const containerRect = container.getBoundingClientRect();
const containerLeftPercent = (containerRect.left / window.innerWidth) * 100;
const containerTopPercent = (containerRect.top / window.innerHeight) * 100;
localStorage.setItem('containerLeftPercent', containerLeftPercent);
localStorage.setItem('containerTopPercent', containerTopPercent);
}
function saveSettingsContainerPosition() {
if (settingsContainer) {
const settingsRect = settingsContainer.getBoundingClientRect();
const settingsLeftPercent = (settingsRect.left / window.innerWidth) * 100;
const settingsTopPercent = (settingsRect.top / window.innerHeight) * 100;
localStorage.setItem('settingsContainerLeftPercent', settingsLeftPercent);
localStorage.setItem('settingsContainerTopPercent', settingsTopPercent);
}
}
if (autoOpenIn) {
toggleOpenerLinks(true);
}
makeDraggable(container, 'containerTop', 'containerLeft');
container.addEventListener('mouseup', saveContainerPosition);
if (settingsContainer) {
makeDraggable(settingsContainer, 'settingsContainerTop', 'settingsContainerLeft');
settingsContainer.addEventListener('mouseup', saveSettingsContainerPosition);
}
window.addEventListener('resize', () => {
const containerLeftPercent = localStorage.getItem('containerLeftPercent');
const containerTopPercent = localStorage.getItem('containerTopPercent');
if (containerLeftPercent !== null && containerTopPercent !== null) {
container.style.left = `${containerLeftPercent}%`;
container.style.top = `${containerTopPercent}%`;
}
const settingsLeftPercent = localStorage.getItem('settingsContainerLeftPercent');
const settingsTopPercent = localStorage.getItem('settingsContainerTopPercent');
if (settingsContainer && settingsLeftPercent !== null && settingsTopPercent !== null) {
settingsContainer.style.left = `${settingsLeftPercent}%`;
settingsContainer.style.top = `${settingsTopPercent}%`;
}
keepWithinBounds(container);
if (settingsContainer) {
keepWithinBounds(settingsContainer);
}
});
function keepWithinBounds(element) {
const rect = element.getBoundingClientRect();
if (rect.right > window.innerWidth) {
element.style.left = (window.innerWidth - rect.width) + 'px';
}
if (rect.bottom > window.innerHeight) {
element.style.top = (window.innerHeight - rect.height) + 'px';
}
}
if (settingsContainer) {
initializeSettingsContainerPosition(settingsContainer);
}
})();