Greasy Fork is available in English.
Copy links for transfer.it with custom player support. Handles single and multi-file transfers.
// ==UserScript==
// @name Transfer.it Tools Enhanced
// @namespace https://tampermonkey.net/
// @version 1.0
// @description Copy links for transfer.it with custom player support. Handles single and multi-file transfers.
// @author pandamoon21
//
// @match https://transfer.it/*
//
// @icon https://www.google.com/s2/favicons?sz=64&domain=transfer.it
//
// @grant GM_xmlhttpRequest
// @grant GM_setClipboard
// @grant GM_addStyle
// @grant GM_addElement
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @connect mega.co.nz
// @connect *.mega.co.nz
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// --- CONFIGURATION ---
const PRESETS = {
potplayer: { name: "PotPlayer", scheme: "potplayer://" },
vlc: { name: "VLC Media Player", scheme: "vlc://" },
mpv: { name: "MPV", scheme: "mpv://" },
kmplayer: { name: "KMPlayer", scheme: "kmplayer://" },
iina: { name: "IINA (Mac)", scheme: "iina://" }
};
const MEGA_API_BASE = 'https://bt7.api.mega.co.nz';
const ANTILEAK_HEADER = '/cs?id=418284579&v=3&lang=en&wcv=2.155.1163&domain=transferit&bb=3&bc=1';
// Per-file caches
const fileMap = new Map();
const directUrlCache = new Map();
let transferInfoCache = null;
let fileListPromise = null;
// --- PLAYER MANAGEMENT ---
function getCurrentPlayer() {
const key = GM_getValue('selectedPlayer', 'potplayer');
if (key === 'custom') {
return { key: 'custom', name: "Custom Player", scheme: GM_getValue('customPlayerScheme', '') };
}
return PRESETS[key] ? { key, ...PRESETS[key] } : { key: 'potplayer', ...PRESETS.potplayer };
}
function registerMenus() {
const current = getCurrentPlayer();
for (const [key, player] of Object.entries(PRESETS)) {
const label = (current.key === key ? '✅ ' : '⚪ ') + player.name;
GM_registerMenuCommand(`Change Player: ${label}`, () => {
GM_setValue('selectedPlayer', key);
location.reload();
});
}
const customLabel = (current.key === 'custom' ? '✅ ' : '⚪ ') + "Custom Player";
GM_registerMenuCommand(`Change Player: ${customLabel}`, () => {
const savedScheme = GM_getValue('customPlayerScheme', '');
const input = prompt("Input Video Player URI Scheme:\n(example: 'potplayer://' or 'mpc-be://')", savedScheme);
if (input !== null) {
const cleanInput = input.trim();
if (cleanInput) {
GM_setValue('customPlayerScheme', cleanInput);
GM_setValue('selectedPlayer', 'custom');
location.reload();
} else {
alert("Scheme can not be empty!");
}
}
});
}
registerMenus();
// --- MEGA API ---
function megaApiPost(body, extraHeaders = {}) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "POST",
url: `${MEGA_API_BASE}/cs?`,
headers: { "Content-Type": "text/plain;charset=UTF-8", "Accept": "*/*", "Origin": "https://transfer.it", "Referer": "https://transfer.it/", ...extraHeaders },
data: JSON.stringify(body),
onload: function(response) {
if (response.status === 200) {
try { resolve(JSON.parse(response.responseText)); }
catch (e) { reject(new Error('Failed to parse MEGA API response')); }
} else {
reject(new Error(`MEGA API error: ${response.status}`));
}
},
onerror: function() { reject(new Error('Network error calling MEGA API')); }
});
});
}
function isHashcashResponse(data) {
if (!Array.isArray(data) || data.length === 0) return false;
const first = data[0];
return typeof first === 'object' && first !== null && (first.p !== undefined || first.hashcash !== undefined);
}
async function megaApiWithHashcash(body, xhValue, extraHeaders = {}) {
let data = await megaApiPost(body, extraHeaders);
if (isHashcashResponse(data)) {
console.log('[TransferIt] Hashcash challenge, solving...');
const hcData = await megaApiPost([{ a: 'hc', xh: xhValue }], { 'MEGA-Chrome-Antileak': ANTILEAK_HEADER });
if (Array.isArray(hcData) && hcData[0]?.hc) {
data = await megaApiPost(body, { ...extraHeaders, 'X-Hashcash': String(hcData[0].hc) });
}
}
return data;
}
// --- URL HELPERS ---
function getTransferId() {
const match = window.location.pathname.match(/^\/t\/([a-zA-Z0-9_-]+)/);
return match ? match[1] : null;
}
// --- TRANSFER INFO ---
async function getTransferInfo(transferId) {
if (transferInfoCache) return transferInfoCache;
const data = await megaApiWithHashcash([{ a: 'xi', xh: transferId }], transferId);
if (!Array.isArray(data) || !data[0] || typeof data[0] !== 'object') throw new Error('Invalid transfer info response');
const info = data[0];
// Single file: {zp, size}
// Multi file: {t, z, m, size, se}
transferInfoCache = {
title: info.t ? atob(info.t.replace(/-/g, '+').replace(/_/g, '/')) : '',
nonce: info.z || null,
message: info.m ? atob(info.m.replace(/-/g, '+').replace(/_/g, '/')) : '',
totalSize: Array.isArray(info.size) ? info.size[0] : 0,
fileCount: Array.isArray(info.size) ? info.size[1] : 0,
sender: info.se || '',
expiry: info.zp || 0,
isSingleFile: !info.t && !info.z
};
return transferInfoCache;
}
// --- FILE LIST ---
async function getFileList(transferId) {
const data = await megaApiWithHashcash(
[{ a: 'f', c: 1, r: 1, xnc: 1 }],
transferId,
{ 'MEGA-Chrome-Antileak': `${ANTILEAK_HEADER}&x=${transferId}` }
);
if (!Array.isArray(data) || !data[0]?.f) {
throw new Error('Invalid file list response');
}
// Filter leaf files (t=0), sort by timestamp
const files = data[0].f
.filter(f => f.t === 0)
.sort((a, b) => (a.ts || 0) - (b.ts || 0));
return files.map(f => ({
handle: f.h,
size: f.s || 0,
timestamp: f.ts || 0
}));
}
// --- BUILD FILE MAP ---
async function buildFileMap(transferId) {
if (fileMap.size > 0) return fileMap;
if (fileListPromise) return fileListPromise;
fileListPromise = (async () => {
const [apiFiles, domFilenames] = await Promise.all([
getFileList(transferId),
waitForDOMFilenames()
]);
console.log(`[TransferIt] API files: ${apiFiles.length}, DOM filenames: ${domFilenames.length}`);
const count = Math.min(apiFiles.length, domFilenames.length);
for (let i = 0; i < count; i++) {
const filename = domFilenames[i];
const file = apiFiles[i];
fileMap.set(filename, {
handle: file.handle,
size: file.size,
index: i
});
}
return fileMap;
})();
return fileListPromise;
}
async function waitForDOMFilenames() {
for (let i = 0; i < 20; i++) {
const names = getDOMFilenames();
if (names.length > 0) return names;
await new Promise(r => setTimeout(r, 250));
}
return [];
}
function getDOMFilenames() {
const results = [];
// File manager view
const fmItems = document.querySelectorAll('.js-fm-section:not(.hidden) .it-grid-item');
fmItems.forEach(item => {
const el = item.querySelector('span.md-font-size, span.pr-color');
if (el) {
const text = el.textContent.trim();
if (text) results.push(text);
}
});
if (results.length > 0) return results;
// Link-ready view step-2
const lrItems = document.querySelectorAll('.js-link-ready-section .body.step-2 .it-grid-item');
lrItems.forEach(item => {
const el = item.querySelector('span.md-font-size, span.pr-color');
if (el) {
const text = el.textContent.trim();
if (text) results.push(text);
}
});
if (results.length > 0) return results;
// Fallback: video extensions
const allEls = document.querySelectorAll('span, div.file-name, div.name');
const seen = new Set();
for (const el of allEls) {
if (el.offsetParent === null && !el.closest('.js-fm-section, .js-link-ready-section, .js-ready-to-dl-section')) continue;
const text = el.textContent.trim();
if (text.match(/\.(mkv|mp4|avi|mov|webm|m4v|flv|wmv|mp3|wav|flac|aac|ogg|srt|ass|sub|zip|rar|7z)$/i) && text.length < 300 && !seen.has(text)) {
seen.add(text);
results.push(text);
}
}
return results;
}
// --- DOWNLOAD URL ---
function getDownloadUrl(transferId, fileHandle, filename) {
const params = new URLSearchParams({ x: transferId, n: fileHandle, fn: filename });
return `${MEGA_API_BASE}/cs/g?${params.toString()}`;
}
// --- ZIP DOWNLOAD URL ---
function getZipDownloadUrl(transferId, nonce, folderName) {
// Pattern: fn = folderName.zip (or transferID+nonce.zip if no folderName)
const fn = folderName ? `${folderName}.zip` : `${transferId}${nonce}.zip`;
const params = new URLSearchParams({ x: transferId, n: nonce, fn: fn });
return `${MEGA_API_BASE}/cs/g?${params.toString()}`;
}
async function resolveZipDownloadUrl(transferId) {
const info = await getTransferInfo(transferId);
if (!info.nonce) {
throw new Error('Transfer does not support zip download');
}
// Use transfer title as folder name (clean up invalid filename chars)
let folderName = info.title || `${transferId}${info.nonce}`;
folderName = folderName.replace(/[<>:"/\\|?*]/g, '_').trim();
const zipUrl = getZipDownloadUrl(transferId, info.nonce, folderName);
const directUrl = await fetchDirectLink(zipUrl);
return { url: directUrl, filename: `${folderName}.zip` };
}
// --- FETCH DIRECT LINK ---
function fetchDirectLink(downloadUrl) {
return fetch(downloadUrl, {
method: 'GET',
redirect: 'manual',
headers: {
'Referer': 'https://transfer.it/',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
}
}).then(response => {
if (response.type === 'opaqueredirect' || response.status === 0) {
return fetchDirectLinkFallback(downloadUrl);
}
const location = response.headers.get('Location') || response.headers.get('location');
if (location) return location;
if (response.redirected && response.url && response.url !== downloadUrl) return response.url;
throw new Error('No redirect Location found');
}).catch(err => {
console.warn('[TransferIt] fetch redirect:manual failed:', err);
return fetchDirectLinkFallback(downloadUrl);
});
}
function fetchDirectLinkFallback(downloadUrl) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url: downloadUrl,
headers: { "Referer": "https://transfer.it/", "Range": "bytes=0-0" },
onload: function(response) {
const headers = response.responseHeaders || '';
const locationMatch = headers.match(/location:\s*(.+)/i);
if (locationMatch) { resolve(locationMatch[1].trim()); return; }
if (response.finalUrl && response.finalUrl !== downloadUrl) { resolve(response.finalUrl); return; }
reject(new Error('No redirect Location found'));
},
onerror: function() { reject(new Error('Network error fetching direct link')); }
});
});
}
// --- RESOLVE DIRECT LINK FOR A FILE ---
async function resolveDirectLinkForFile(transferId, filename) {
if (directUrlCache.has(filename)) {
return directUrlCache.get(filename);
}
await buildFileMap(transferId);
const fileInfo = fileMap.get(filename);
if (!fileInfo) {
throw new Error(`File "${filename}" not found. Available: ${[...fileMap.keys()].join(', ')}`);
}
const downloadUrl = getDownloadUrl(transferId, fileInfo.handle, filename);
console.log(`[TransferIt] Fetching: "${filename}" (handle=${fileInfo.handle})`);
const directUrl = await fetchDirectLink(downloadUrl);
directUrlCache.set(filename, directUrl);
console.log(`[TransferIt] Got URL for "${filename}"`);
return directUrl;
}
// --- ICONS ---
const ICONS = {
play: '<svg viewBox="0 0 24 24"><path d="M8 5.14v14l11-7-11-7z"/></svg>',
copy: '<svg viewBox="0 0 24 24"><path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>',
download: '<svg viewBox="0 0 24 24"><path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/></svg>',
check: '<svg viewBox="0 0 24 24"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>',
loading: '<svg viewBox="0 0 24 24"><path d="M12 4V2A10 10 0 0 0 2 12h2a8 8 0 0 1 8-8z"/></svg>'
};
// --- STYLES ---
GM_addStyle(`
.ti-file-actions {
display: inline-flex !important;
align-items: center !important;
gap: 3px !important;
margin-left: 4px !important;
vertical-align: middle !important;
}
.ti-file-btn {
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
width: 28px !important;
height: 28px !important;
border: 1px solid rgba(255,255,255,0.2) !important;
border-radius: 6px !important;
background: rgba(255,255,255,0.1) !important;
color: #ccc !important;
cursor: pointer !important;
padding: 0 !important;
transition: all 0.15s ease !important;
font-size: 0 !important;
line-height: 0 !important;
}
.ti-file-btn:hover {
background: rgba(255,255,255,0.2) !important;
border-color: rgba(255,255,255,0.4) !important;
color: #fff !important;
transform: scale(1.15) !important;
}
.ti-file-btn svg { width: 14px !important; height: 14px !important; fill: currentColor !important; }
.ti-file-btn.play-btn { color: #4ade80 !important; border-color: rgba(74,222,128,0.3) !important; }
.ti-file-btn.copy-btn { color: #60a5fa !important; border-color: rgba(96,165,250,0.3) !important; }
.ti-file-btn.play-btn:hover { color: #86efac !important; border-color: rgba(74,222,128,0.6) !important; background: rgba(74,222,128,0.15) !important; }
.ti-file-btn.copy-btn:hover { color: #93c5fd !important; border-color: rgba(96,165,250,0.6) !important; background: rgba(96,165,250,0.15) !important; }
.ti-file-btn.loading svg { animation: ti-spin 0.8s linear infinite !important; fill: #fbbf24 !important; }
.ti-file-btn:disabled { opacity: 0.3 !important; cursor: not-allowed !important; transform: none !important; }
.ti-fab {
position: fixed !important;
bottom: 24px !important;
left: 24px !important;
z-index: 2147483646 !important;
width: 48px !important;
height: 48px !important;
border-radius: 50% !important;
background: #3B82F6 !important;
border: 2px solid rgba(255,255,255,0.15) !important;
color: #fff !important;
cursor: pointer !important;
box-shadow: 0 4px 20px rgba(59,130,246,0.4), 0 2px 8px rgba(0,0,0,0.3) !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
transition: all 0.2s ease !important;
padding: 0 !important;
}
.ti-fab:hover {
background: #2563EB !important;
transform: scale(1.1) !important;
box-shadow: 0 6px 28px rgba(37,99,235,0.5), 0 2px 12px rgba(0,0,0,0.4) !important;
}
.ti-fab svg { width: 22px !important; height: 22px !important; fill: currentColor !important; }
.ti-fab.panel-open {
background: #60A5FA !important;
box-shadow: 0 4px 20px rgba(96,165,250,0.5), 0 2px 8px rgba(0,0,0,0.3) !important;
}
.ti-panel {
position: fixed !important;
bottom: 82px !important;
left: 24px !important;
z-index: 2147483647 !important;
background: #1F2937 !important;
border-radius: 12px !important;
padding: 20px !important;
color: #F9FAFB !important;
font-family: 'Nunito Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !important;
font-size: 13px !important;
min-width: 340px !important;
max-width: 460px !important;
box-shadow: 0 8px 32px rgba(0,0,0,0.15), 0 2px 8px rgba(0,0,0,0.1) !important;
}
.ti-panel.hidden { display: none !important; }
.ti-header {
display: flex !important;
align-items: center !important;
gap: 10px !important;
margin-bottom: 12px !important;
padding-bottom: 12px !important;
border-bottom: 1px solid #374151 !important;
}
.ti-header svg { width: 20px !important; height: 20px !important; fill: #60A5FA !important; flex-shrink: 0 !important; }
.ti-title { font-weight: 700 !important; font-size: 15px !important; color: #F9FAFB !important; }
.ti-file-info {
font-size: 13px !important;
color: #D1D5DB !important;
word-break: break-all !important;
margin-bottom: 4px !important;
line-height: 1.5 !important;
font-weight: 600 !important;
}
.ti-file-meta {
font-size: 11px !important;
color: #9CA3AF !important;
margin-bottom: 16px !important;
}
.ti-actions { display: flex !important; gap: 8px !important; flex-wrap: wrap !important; }
.ti-btn {
display: inline-flex !important;
align-items: center !important;
gap: 6px !important;
padding: 10px 18px !important;
border: none !important;
border-radius: 8px !important;
background: #374151 !important;
color: #D1D5DB !important;
cursor: pointer !important;
font-size: 12px !important;
font-weight: 600 !important;
font-family: inherit !important;
transition: all 0.2s ease !important;
white-space: nowrap !important;
}
.ti-btn:hover {
background: #4B5563 !important;
color: #F9FAFB !important;
transform: translateY(-1px) !important;
box-shadow: 0 4px 12px rgba(0,0,0,0.3) !important;
}
.ti-btn:active { transform: translateY(0) !important; }
.ti-btn svg { width: 14px !important; height: 14px !important; fill: currentColor !important; flex-shrink: 0 !important; }
.ti-btn.play-btn {
background: #3B82F6 !important;
color: #FFFFFF !important;
}
.ti-btn.play-btn:hover {
background: #2563EB !important;
box-shadow: 0 4px 12px rgba(59,130,246,0.4) !important;
}
.ti-btn.copy-btn:hover { color: #60A5FA !important; }
.ti-btn.dl-btn:hover { color: #60A5FA !important; }
.ti-btn.loading svg { animation: ti-spin 0.8s linear infinite !important; fill: #60A5FA !important; }
.ti-btn:disabled { opacity: 0.4 !important; cursor: not-allowed !important; transform: none !important; }
.ti-status {
font-size: 11px !important;
color: rgba(255,255,255,0.35) !important;
margin-top: 10px !important;
min-height: 16px !important;
}
.ti-status.success { color: #4ade80 !important; }
.ti-status.error { color: #f87171 !important; }
@keyframes ti-spin { 100% { transform: rotate(360deg); } }
`);
// --- HELPERS ---
function formatSize(bytes) {
if (!bytes || bytes <= 0) return '';
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
let i = 0, size = bytes;
while (size >= 1024 && i < units.length - 1) { size /= 1024; i++; }
return `${size.toFixed(i > 0 ? 2 : 0)} ${units[i]}`;
}
function setPanelStatus(text, type) {
const el = document.querySelector('.ti-status');
if (el) { el.textContent = text; el.className = `ti-status ${type || ''}`; }
}
function setBtnLoading(btn, isLoading) {
if (isLoading) {
btn.dataset.originalIcon = btn.querySelector('svg')?.outerHTML || '';
const svg = btn.querySelector('svg');
if (svg) svg.outerHTML = ICONS.loading;
btn.classList.add('loading');
btn.disabled = true;
} else {
btn.classList.remove('loading');
btn.disabled = false;
const svg = btn.querySelector('svg');
if (svg && btn.dataset.originalIcon) svg.outerHTML = btn.dataset.originalIcon;
}
}
// --- ACTION HANDLER ---
async function doAction(type, btn, filename) {
const transferId = getTransferId();
if (!transferId) return;
const player = getCurrentPlayer();
if (type === 'play' && player.key === 'custom' && !player.scheme) {
alert("Please set custom scheme first via Tampermonkey menu.");
return;
}
const hasFileList = document.querySelector('.js-fm-section:not(.hidden) .it-grid-item') ||
document.querySelector('.js-link-ready-section .body.step-2 .it-grid-item');
if (!hasFileList) {
const viewBtn = document.querySelector('.js-ready-to-dl-section .js-view-content');
if (viewBtn && !viewBtn.classList.contains('hidden')) {
viewBtn.click();
setPanelStatus('Opening file list... click again!', '');
return;
}
setPanelStatus('Click "View Content" first!', 'error');
return;
}
setBtnLoading(btn, true);
try {
const url = await resolveDirectLinkForFile(transferId, filename);
if (type === 'copy') {
GM_setClipboard(url);
setBtnLoading(btn, false);
const svg = btn.querySelector('svg');
if (svg) svg.outerHTML = ICONS.check;
setPanelStatus(`Copied: ${filename}`, 'success');
setTimeout(() => {
const s = btn.querySelector('svg');
if (s) s.outerHTML = ICONS.copy;
setPanelStatus('', '');
}, 2000);
} else if (type === 'play') {
setBtnLoading(btn, false);
window.location.href = `${player.scheme}${url}`;
}
} catch (err) {
setBtnLoading(btn, false);
console.error(`[TransferIt] ${type} error for "${filename}":`, err);
setPanelStatus(`Error: ${err.message}`, 'error');
}
}
// --- CREATE PER-FILE BUTTONS (Copy + Play only, no Download) ---
function createFileActions(filename) {
const group = document.createElement('div');
group.className = 'ti-file-actions';
group.dataset.filename = filename;
const copyBtn = document.createElement('button');
copyBtn.className = 'ti-file-btn copy-btn';
copyBtn.title = `Copy link: ${filename}`;
copyBtn.innerHTML = ICONS.copy;
copyBtn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); doAction('copy', copyBtn, filename); });
const playBtn = document.createElement('button');
playBtn.className = 'ti-file-btn play-btn';
playBtn.title = `Play: ${filename}`;
playBtn.innerHTML = ICONS.play;
playBtn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); doAction('play', playBtn, filename); });
group.appendChild(copyBtn);
group.appendChild(playBtn);
return group;
}
// --- INJECT INLINE BUTTONS ---
function injectInlineButtons() {
const fmItems = document.querySelectorAll('.js-fm-section:not(.hidden) .it-grid-item');
fmItems.forEach(item => {
if (item.querySelector('.ti-file-actions')) return;
const nameEl = item.querySelector('span.md-font-size, span.pr-color');
if (!nameEl) return;
const filename = nameEl.textContent.trim();
if (!filename) return;
const cols = item.querySelectorAll(':scope > .col');
if (cols.length === 0) return;
cols[cols.length - 1].appendChild(createFileActions(filename));
});
const lrItems = document.querySelectorAll('.js-link-ready-section .body.step-2 .it-grid-item');
lrItems.forEach(item => {
if (item.querySelector('.ti-file-actions')) return;
const nameEl = item.querySelector('span.md-font-size, span.pr-color');
if (!nameEl) return;
const filename = nameEl.textContent.trim();
if (!filename) return;
const cols = item.querySelectorAll(':scope > .col');
if (cols.length === 0) return;
cols[cols.length - 1].appendChild(createFileActions(filename));
});
}
// --- FLOATING PANEL ---
let panelHost = null;
function createPanelHost() {
if (panelHost) return panelHost;
panelHost = GM_addElement(document.documentElement, 'div', { id: 'ti-panel-host' });
panelHost.style.cssText = 'position:fixed!important;z-index:2147483647!important;pointer-events:none!important;top:0!important;left:0!important;width:0!important;height:0!important;overflow:visible!important;';
const fab = GM_addElement(panelHost, 'button', { class: 'ti-fab', title: 'Toggle Transfer.it Tools' });
fab.innerHTML = ICONS.download;
fab.style.pointerEvents = 'auto';
fab.addEventListener('click', togglePanel);
const panel = GM_addElement(panelHost, 'div', { class: 'ti-panel hidden' });
panel.style.pointerEvents = 'auto';
panel.innerHTML = `
<div class="ti-header">
<svg viewBox="0 0 24 24"><path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/></svg>
<span class="ti-title">Transfer.it Tools</span>
</div>
<div class="ti-file-info" id="ti-info">Loading...</div>
<div class="ti-file-meta" id="ti-meta"></div>
<div class="ti-actions" id="ti-actions"></div>
<div class="ti-status" id="ti-status">Waiting...</div>
`;
return panelHost;
}
function togglePanel() {
const host = createPanelHost();
const panel = host.querySelector('.ti-panel');
const fab = host.querySelector('.ti-fab');
const isHidden = panel.classList.contains('hidden');
if (isHidden) {
panel.classList.remove('hidden');
fab.classList.add('panel-open');
fab.innerHTML = '<svg viewBox="0 0 24 24"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>';
} else {
panel.classList.add('hidden');
fab.classList.remove('panel-open');
fab.innerHTML = ICONS.download;
}
}
async function updatePanel(host) {
const transferId = getTransferId();
if (!transferId) return;
try {
const info = await getTransferInfo(transferId);
const panel = host.querySelector('.ti-panel');
if (!panel) return;
const infoEl = panel.querySelector('#ti-info');
const metaEl = panel.querySelector('#ti-meta');
const actionsEl = panel.querySelector('#ti-actions');
const isSingleFile = info.isSingleFile || info.fileCount === 1;
if (infoEl) infoEl.textContent = info.title || (isSingleFile ? 'Single file transfer' : `${info.fileCount} files`);
if (metaEl) metaEl.textContent = `${formatSize(info.totalSize)}${info.sender ? ' • from ' + info.sender : ''}`;
// Build action buttons based on file count
actionsEl.innerHTML = '';
if (isSingleFile) {
// Single file: Copy only
actionsEl.innerHTML = `
<button class="ti-btn copy-btn" id="ti-copy" disabled>${ICONS.copy} Copy Link</button>
`;
panel.querySelector('#ti-copy').addEventListener('click', async function() {
const filenames = getDOMFilenames();
if (filenames.length === 0) {
setPanelStatus('No file found', 'error');
return;
}
await doAction('copy', this, filenames[0]);
});
} else {
// Multi file: Copy All Links + Copy Zipped Link
actionsEl.innerHTML = `
<button class="ti-btn copy-btn" id="ti-copy-all" disabled>${ICONS.copy} Copy All Links</button>
<button class="ti-btn copy-btn" id="ti-copy-zip" disabled>${ICONS.copy} Copy Zipped Link</button>
`;
panel.querySelector('#ti-copy-all').addEventListener('click', async function() {
const originalHtml = this.innerHTML;
setBtnLoading(this, true);
try {
const names = getDOMFilenames();
if (names.length === 0) {
throw new Error('No files found in DOM. Make sure file list is visible.');
}
const urls = [];
for (let i = 0; i < names.length; i++) {
try {
const url = await resolveDirectLinkForFile(transferId, names[i]);
urls.push(url);
} catch (fileErr) {
console.warn(`[TransferIt] Copy All: Failed for ${names[i]}:`, fileErr);
}
}
if (urls.length === 0) {
throw new Error('Failed to get any download URLs');
}
GM_setClipboard(urls.join('\n'));
setBtnLoading(this, false);
this.innerHTML = `${ICONS.check} Copied ${urls.length} links!`;
this.style.background = '#10B981';
this.style.color = '#FFFFFF';
setPanelStatus(`Copied ${urls.length} of ${names.length} links!`, 'success');
setTimeout(() => {
this.innerHTML = originalHtml;
this.style.background = '';
this.style.color = '';
}, 2000);
} catch (err) {
setBtnLoading(this, false);
setPanelStatus(`Error: ${err.message}`, 'error');
}
});
panel.querySelector('#ti-copy-zip').addEventListener('click', async function() {
const originalHtml = this.innerHTML;
setBtnLoading(this, true);
try {
const result = await resolveZipDownloadUrl(transferId);
GM_setClipboard(result.url);
setBtnLoading(this, false);
this.innerHTML = `${ICONS.check} Copied!`;
this.style.background = '#10B981';
this.style.color = '#FFFFFF';
setPanelStatus('Zip link copied to clipboard!', 'success');
setTimeout(() => {
this.innerHTML = originalHtml;
this.style.background = '';
this.style.color = '';
}, 2000);
} catch (err) {
setBtnLoading(this, false);
setPanelStatus(`Error: ${err.message}`, 'error');
}
});
}
// Enable buttons
actionsEl.querySelectorAll('.ti-btn').forEach(btn => btn.disabled = false);
setPanelStatus(`Ready — ${getCurrentPlayer().name}`, 'success');
} catch (err) {
console.error('[TransferIt] Panel update error:', err);
setPanelStatus(err.message, 'error');
}
}
// --- INIT ---
let isInitializing = false;
let lastUrl = '';
async function init() {
const transferId = getTransferId();
if (!transferId) {
if (panelHost) { panelHost.remove(); panelHost = null; }
return;
}
injectInlineButtons();
const host = createPanelHost();
if (!isInitializing && lastUrl !== window.location.href) {
isInitializing = true;
lastUrl = window.location.href;
fileMap.clear();
directUrlCache.clear();
transferInfoCache = null;
fileListPromise = null;
await updatePanel(host);
isInitializing = false;
}
}
let retryCount = 0;
const MAX_RETRIES = 40;
const RETRY_MS = 500;
function tryInit() {
init();
const transferId = getTransferId();
if (transferId && retryCount < MAX_RETRIES) {
retryCount++;
setTimeout(tryInit, RETRY_MS);
}
}
let lastPathname = window.location.pathname;
setInterval(() => {
if (window.location.pathname !== lastPathname) {
lastPathname = window.location.pathname;
retryCount = 0;
isInitializing = false;
fileMap.clear();
directUrlCache.clear();
transferInfoCache = null;
fileListPromise = null;
if (panelHost) { panelHost.remove(); panelHost = null; }
tryInit();
}
}, 500);
const observer = new MutationObserver(() => {
if (getTransferId()) {
injectInlineButtons();
if (!panelHost && !isInitializing) {
retryCount = 0;
tryInit();
}
}
});
observer.observe(document.body || document.documentElement, { childList: true, subtree: true });
tryInit();
})();