diff --git a/torrent-to-transmission.user.js b/torrent-to-transmission.user.js new file mode 100644 index 0000000..e69078e --- /dev/null +++ b/torrent-to-transmission.user.js @@ -0,0 +1,780 @@ +// ==UserScript== +// @name Transmission Remote Script +// @namespace https://git.ponywave.de/Akamaru/Userscripts +// @version 1.0 +// @description Sendet Magnet-Links und .torrent-Dateien an Transmission im Netzwerk mit Pfad-Auswahl und Verwaltung +// @author Akamaru +// @match *://*/* +// @grant GM_registerMenuCommand +// @grant GM_xmlhttpRequest +// @grant GM_setValue +// @grant GM_getValue +// @updateURL https://git.ponywave.de/Akamaru/Userscripts/raw/branch/master/torrent-to-transmission.user.js +// ==/UserScript== + +(function() { + 'use strict'; + + // --- Hilfsfunktionen für Storage --- + async function getConfig() { + let config = await GM_getValue('transmissionConfig'); + if (config) return JSON.parse(config); + return await askForConfig(); + } + async function setConfig(config) { + await GM_setValue('transmissionConfig', JSON.stringify(config)); + } + async function askForConfig() { + let ipport = prompt('Transmission IP und Port (z.B. 192.168.1.100:9091):'); + if (!ipport) return null; + let user = prompt('Benutzername (leer lassen, falls nicht benötigt):'); + let pass = prompt('Passwort (leer lassen, falls nicht benötigt):'); + let url = 'http://' + ipport + '/transmission/rpc'; + let config = { url, user, pass }; + await setConfig(config); + return config; + } + + async function getPaths() { + let paths = await GM_getValue('transmissionPaths'); + if (paths) return JSON.parse(paths); + return []; + } + async function setPaths(paths) { + await GM_setValue('transmissionPaths', JSON.stringify(paths)); + } + + // --- Hilfsfunktionen für modale Dialoge --- + function showModalDialog({title, content, onOk, onCancel, okText = 'OK', cancelText = 'Abbrechen', showCancel = true}) { + let overlay = document.createElement('div'); + overlay.style.position = 'fixed'; + overlay.style.top = 0; + overlay.style.left = 0; + overlay.style.width = '100vw'; + overlay.style.height = '100vh'; + overlay.style.background = 'rgba(0,0,0,0.5)'; + overlay.style.zIndex = 99999; + overlay.style.display = 'flex'; + overlay.style.alignItems = 'center'; + overlay.style.justifyContent = 'center'; + + let dialog = document.createElement('div'); + dialog.style.background = '#222'; + dialog.style.color = '#fff'; + dialog.style.padding = '24px 32px'; + dialog.style.borderRadius = '10px'; + dialog.style.minWidth = '480px'; + dialog.style.boxShadow = '0 4px 24px rgba(0,0,0,0.4)'; + dialog.style.display = 'flex'; + dialog.style.flexDirection = 'column'; + dialog.style.gap = '16px'; + + if (title) { + let h2 = document.createElement('h2'); + h2.textContent = title; + h2.style.margin = '0 0 8px 0'; + dialog.appendChild(h2); + } + if (typeof content === 'string') { + let p = document.createElement('div'); + p.innerHTML = content; + dialog.appendChild(p); + } else if (content instanceof Node) { + dialog.appendChild(content); + } + + let btnRow = document.createElement('div'); + btnRow.style.display = 'flex'; + btnRow.style.justifyContent = 'flex-end'; + btnRow.style.gap = '12px'; + + if (showCancel) { + let cancelBtn = document.createElement('button'); + cancelBtn.textContent = cancelText; + cancelBtn.style.background = '#666'; + cancelBtn.style.color = '#fff'; + cancelBtn.style.border = 'none'; + cancelBtn.style.padding = '8px 18px'; + cancelBtn.style.borderRadius = '4px'; + cancelBtn.style.cursor = 'pointer'; + cancelBtn.onclick = function() { + document.body.removeChild(overlay); + if (onCancel) onCancel(); + }; + btnRow.appendChild(cancelBtn); + } + + let okBtn = document.createElement('button'); + okBtn.textContent = okText; + okBtn.style.background = '#6c3'; + okBtn.style.color = '#222'; + okBtn.style.border = 'none'; + okBtn.style.padding = '8px 18px'; + okBtn.style.borderRadius = '4px'; + okBtn.style.cursor = 'pointer'; + okBtn.onclick = function() { + document.body.removeChild(overlay); + if (onOk) onOk(); + }; + btnRow.appendChild(okBtn); + + dialog.appendChild(btnRow); + overlay.appendChild(dialog); + document.body.appendChild(overlay); + return {overlay, dialog}; + } + + // --- Eigener Dialog für Pfadverwaltung --- + async function editPathsMenu() { + let paths = await getPaths(); + let container = document.createElement('div'); + container.style.display = 'flex'; + container.style.flexDirection = 'column'; + container.style.gap = '10px'; + + let list = document.createElement('ul'); + list.style.listStyle = 'none'; + list.style.padding = '0'; + list.style.margin = '0'; + function renderList() { + list.innerHTML = ''; + paths.forEach((p, i) => { + let li = document.createElement('li'); + li.style.display = 'flex'; + li.style.alignItems = 'center'; + li.style.gap = '8px'; + let span = document.createElement('span'); + span.textContent = p; + li.appendChild(span); + let delBtn = document.createElement('button'); + delBtn.textContent = 'Löschen'; + delBtn.style.background = '#a33'; + delBtn.style.color = '#fff'; + delBtn.style.border = 'none'; + delBtn.style.padding = '2px 8px'; + delBtn.style.borderRadius = '4px'; + delBtn.style.cursor = 'pointer'; + delBtn.onclick = async function() { + paths.splice(i, 1); + await setPaths(paths); + renderList(); + }; + li.appendChild(delBtn); + list.appendChild(li); + }); + } + renderList(); + container.appendChild(list); + + let addRow = document.createElement('div'); + addRow.style.display = 'flex'; + addRow.style.gap = '8px'; + let input = document.createElement('input'); + input.type = 'text'; + input.placeholder = 'Neuen Pfad eingeben...'; + input.style.flex = '1'; + input.style.padding = '4px'; + input.style.borderRadius = '4px'; + input.style.border = '1px solid #888'; + input.style.background = '#333'; + input.style.color = '#fff'; + addRow.appendChild(input); + let addBtn = document.createElement('button'); + addBtn.textContent = 'Hinzufügen'; + addBtn.style.background = '#444'; + addBtn.style.color = '#fff'; + addBtn.style.border = 'none'; + addBtn.style.padding = '4px 12px'; + addBtn.style.borderRadius = '4px'; + addBtn.style.cursor = 'pointer'; + addBtn.onclick = async function() { + let val = input.value.trim(); + if (val) { + paths.push(val); + await setPaths(paths); + input.value = ''; + renderList(); + } + }; + addRow.appendChild(addBtn); + container.appendChild(addRow); + + showModalDialog({ + title: 'Download-Pfade verwalten', + content: container, + okText: 'Schließen', + showCancel: false + }); + } + + // --- Eigener Dialog für Transmission-Konfiguration --- + async function askForConfig() { + return new Promise(resolve => { + let container = document.createElement('div'); + container.style.display = 'flex'; + container.style.flexDirection = 'column'; + container.style.gap = '10px'; + + let ipInput = document.createElement('input'); + ipInput.type = 'text'; + ipInput.placeholder = 'IP und Port (z.B. 192.168.1.100:9091)'; + ipInput.style.padding = '4px'; + ipInput.style.borderRadius = '4px'; + ipInput.style.border = '1px solid #888'; + ipInput.style.background = '#333'; + ipInput.style.color = '#fff'; + container.appendChild(ipInput); + + let userInput = document.createElement('input'); + userInput.type = 'text'; + userInput.placeholder = 'Benutzername (optional)'; + userInput.style.padding = '4px'; + userInput.style.borderRadius = '4px'; + userInput.style.border = '1px solid #888'; + userInput.style.background = '#333'; + userInput.style.color = '#fff'; + container.appendChild(userInput); + + let passInput = document.createElement('input'); + passInput.type = 'password'; + passInput.placeholder = 'Passwort (optional)'; + passInput.style.padding = '4px'; + passInput.style.borderRadius = '4px'; + passInput.style.border = '1px solid #888'; + passInput.style.background = '#333'; + passInput.style.color = '#fff'; + container.appendChild(passInput); + + showModalDialog({ + title: 'Transmission konfigurieren', + content: container, + okText: 'Speichern', + cancelText: 'Abbrechen', + showCancel: true, + onOk: async () => { + let ipport = ipInput.value.trim(); + let user = userInput.value.trim(); + let pass = passInput.value; + if (!ipport) return; + let url = 'http://' + ipport + '/transmission/rpc'; + let config = { url, user, pass }; + await setConfig(config); + resolve(config); + }, + onCancel: () => resolve(null) + }); + }); + } + + // --- Transmission Session-ID holen --- + function getSessionId(config, callback) { + let headers = {}; + if (config.user) { + headers['Authorization'] = 'Basic ' + btoa(config.user + ':' + config.pass); + } + GM_xmlhttpRequest({ + method: 'POST', + url: config.url, + headers: headers, + onload: function(response) { + const match = response.responseHeaders.match(/x-transmission-session-id:(.+)/i); + const sessionId = match ? match[1].trim() : null; + callback(sessionId); + }, + onerror: function() { + showToast('Fehler beim Abrufen der Session-ID!'); + } + }); + } + + // --- Magnet/Torrent an Transmission senden --- + function sendToTransmission(data, isMagnet, downloadDir, retry) { + getConfig().then(config => { + getSessionId(config, function(sessionId) { + let headers = { + 'Content-Type': 'application/json', + 'X-Transmission-Session-Id': sessionId + }; + if (config.user) { + headers['Authorization'] = 'Basic ' + btoa(config.user + ':' + config.pass); + } + let args = isMagnet ? { filename: data } : { metainfo: data }; + if (downloadDir) args['download-dir'] = downloadDir; + let body = { + method: 'torrent-add', + arguments: args + }; + GM_xmlhttpRequest({ + method: 'POST', + url: config.url, + headers: headers, + data: JSON.stringify(body), + onload: function(response) { + if (response.status === 409 && !retry) { + const match = response.responseHeaders.match(/x-transmission-session-id:(.+)/i); + const newSessionId = match ? match[1].trim() : null; + if (newSessionId) { + headers['X-Transmission-Session-Id'] = newSessionId; + GM_xmlhttpRequest({ + method: 'POST', + url: config.url, + headers: headers, + data: JSON.stringify(body), + onload: function(resp2) { + if (resp2.status === 409) { + showToast('Session-ID konnte nicht erneuert werden!\nResponse Headers:\n' + resp2.responseHeaders); + } else { + showToast('An Transmission gesendet!'); + } + }, + onerror: function() { + showToast('Fehler beim Senden an Transmission!'); + } + }); + } else { + showToast('Session-ID konnte nicht erneuert werden!\nResponse Headers:\n' + response.responseHeaders); + } + } else if (response.status === 409 && retry) { + showToast('Session-ID konnte nicht erneuert werden!\nResponse Headers:\n' + response.responseHeaders); + } else { + showToast('An Transmission gesendet!'); + } + }, + onerror: function() { + showToast('Fehler beim Senden an Transmission!'); + } + }); + }); + }); + } + + // --- .torrent-Datei herunterladen und in Base64 umwandeln --- + function fetchAndEncodeTorrent(url, callback) { + GM_xmlhttpRequest({ + method: 'GET', + url: url, + responseType: 'arraybuffer', + onload: function(response) { + const bytes = new Uint8Array(response.response); + let binary = ''; + for (let i = 0; i < bytes.byteLength; i++) { + binary += String.fromCharCode(bytes[i]); + } + const base64 = btoa(binary); + callback(base64); + }, + onerror: function() { + showToast('Fehler beim Herunterladen der .torrent-Datei!'); + } + }); + } + + // --- Dialog für Pfad-Auswahl und Senden --- + async function showPathDialog(onSend, onCancel, fileList) { + let paths = await getPaths(); + // Dialog-Elemente erzeugen + let overlay = document.createElement('div'); + overlay.style.position = 'fixed'; + overlay.style.top = 0; + overlay.style.left = 0; + overlay.style.width = '100vw'; + overlay.style.height = '100vh'; + overlay.style.background = 'rgba(0,0,0,0.5)'; + overlay.style.zIndex = 99999; + overlay.style.display = 'flex'; + overlay.style.alignItems = 'center'; + overlay.style.justifyContent = 'center'; + + let dialog = document.createElement('div'); + dialog.style.background = '#222'; + dialog.style.color = '#fff'; + dialog.style.padding = '24px 32px'; + dialog.style.borderRadius = '10px'; + dialog.style.minWidth = '480px'; + dialog.style.boxShadow = '0 4px 24px rgba(0,0,0,0.4)'; + dialog.style.display = 'flex'; + dialog.style.flexDirection = 'column'; + dialog.style.gap = '16px'; + + let title = document.createElement('h2'); + title.textContent = 'Download-Verzeichnis wählen'; + title.style.margin = '0 0 8px 0'; + dialog.appendChild(title); + + // Dateiliste für .torrent-Dateien mit Checkboxen + let selectedFiles = []; + let fileCheckboxes = []; + if (fileList && fileList.length > 0) { + selectedFiles = fileList.map(() => true); // standardmäßig alle ausgewählt + let fileBox = document.createElement('div'); + fileBox.style.maxHeight = '180px'; + fileBox.style.overflowY = 'auto'; + fileBox.style.background = '#181818'; + fileBox.style.border = '1px solid #444'; + fileBox.style.borderRadius = '6px'; + fileBox.style.padding = '8px 12px'; + fileBox.style.marginBottom = '8px'; + fileBox.style.width = '100%'; + let fileTitle = document.createElement('div'); + fileTitle.textContent = 'Dateien im Torrent:'; + fileTitle.style.fontWeight = 'bold'; + fileTitle.style.marginBottom = '6px'; + fileBox.appendChild(fileTitle); + + // Buttons für alle an/abwählen + let btnRow = document.createElement('div'); + btnRow.style.display = 'flex'; + btnRow.style.gap = '8px'; + btnRow.style.marginBottom = '8px'; + let allOnBtn = document.createElement('button'); + allOnBtn.textContent = 'Alle auswählen'; + allOnBtn.style.background = '#444'; + allOnBtn.style.color = '#fff'; + allOnBtn.style.border = 'none'; + allOnBtn.style.padding = '2px 10px'; + allOnBtn.style.borderRadius = '4px'; + allOnBtn.style.cursor = 'pointer'; + allOnBtn.onclick = function() { + fileCheckboxes.forEach((cb, i) => { cb.checked = true; selectedFiles[i] = true; }); + }; + btnRow.appendChild(allOnBtn); + let allOffBtn = document.createElement('button'); + allOffBtn.textContent = 'Alle abwählen'; + allOffBtn.style.background = '#444'; + allOffBtn.style.color = '#fff'; + allOffBtn.style.border = 'none'; + allOffBtn.style.padding = '2px 10px'; + allOffBtn.style.borderRadius = '4px'; + allOffBtn.style.cursor = 'pointer'; + allOffBtn.onclick = function() { + fileCheckboxes.forEach((cb, i) => { cb.checked = false; selectedFiles[i] = false; }); + }; + btnRow.appendChild(allOffBtn); + fileBox.appendChild(btnRow); + + fileList.forEach((f, idx) => { + let fileRow = document.createElement('div'); + fileRow.style.display = 'flex'; + fileRow.style.alignItems = 'center'; + fileRow.style.gap = '8px'; + fileRow.style.fontSize = '0.98em'; + fileRow.style.wordBreak = 'break-all'; + fileRow.style.whiteSpace = 'normal'; + let cb = document.createElement('input'); + cb.type = 'checkbox'; + cb.checked = true; + cb.onchange = function() { selectedFiles[idx] = cb.checked; }; + fileCheckboxes.push(cb); + fileRow.appendChild(cb); + let label = document.createElement('span'); + label.textContent = f; + fileRow.appendChild(label); + fileBox.appendChild(fileRow); + }); + dialog.appendChild(fileBox); + } + + let select = document.createElement('select'); + select.style.fontSize = '1.1em'; + select.style.padding = '6px'; + select.style.marginBottom = '8px'; + select.style.borderRadius = '4px'; + select.style.border = '1px solid #888'; + select.style.background = '#333'; + select.style.color = '#fff'; + select.style.width = '100%'; + paths.forEach(p => { + let opt = document.createElement('option'); + opt.value = p; + opt.textContent = p; + select.appendChild(opt); + }); + dialog.appendChild(select); + + let btnRow = document.createElement('div'); + btnRow.style.display = 'flex'; + btnRow.style.justifyContent = 'flex-end'; + btnRow.style.gap = '12px'; + + let cancelBtn = document.createElement('button'); + cancelBtn.textContent = 'Abbrechen'; + cancelBtn.style.background = '#666'; + cancelBtn.style.color = '#fff'; + cancelBtn.style.border = 'none'; + cancelBtn.style.padding = '8px 18px'; + cancelBtn.style.borderRadius = '4px'; + cancelBtn.style.cursor = 'pointer'; + cancelBtn.onclick = function() { + document.body.removeChild(overlay); + if (onCancel) onCancel(); + }; + btnRow.appendChild(cancelBtn); + + let sendBtn = document.createElement('button'); + sendBtn.textContent = 'Senden'; + sendBtn.style.background = '#6c3'; + sendBtn.style.color = '#222'; + sendBtn.style.border = 'none'; + sendBtn.style.padding = '8px 18px'; + sendBtn.style.borderRadius = '4px'; + sendBtn.style.cursor = 'pointer'; + sendBtn.onclick = function() { + let dir = select.value; + document.body.removeChild(overlay); + if (onSend) { + if (fileList && fileList.length > 0) { + // Übergebe die Auswahl als Array von bools + onSend(dir, selectedFiles); + } else { + onSend(dir); + } + } + }; + btnRow.appendChild(sendBtn); + + dialog.appendChild(btnRow); + overlay.appendChild(dialog); + document.body.appendChild(overlay); + } + + // --- Hilfsfunktion: Nach dem Hinzufügen eines Torrents Dateien abwählen --- + function setUnwantedFiles(torrentId, unwantedIndices, config) { + if (!unwantedIndices || unwantedIndices.length === 0) return; + let headers = { + 'Content-Type': 'application/json', + }; + if (config.user) { + headers['Authorization'] = 'Basic ' + btoa(config.user + ':' + config.pass); + } + getSessionId(config, function(sessionId) { + headers['X-Transmission-Session-Id'] = sessionId; + let body = { + method: 'torrent-set', + arguments: { + ids: [torrentId], + 'files-unwanted': unwantedIndices + } + }; + GM_xmlhttpRequest({ + method: 'POST', + url: config.url, + headers: headers, + data: JSON.stringify(body), + onload: function(response) { + // Optional: Rückmeldung anzeigen + if (response.status === 200) { + showToast('Dateiauswahl an Transmission übermittelt!'); + } else { + showToast('Fehler beim Setzen der Dateiauswahl!'); + } + }, + onerror: function() { + showToast('Fehler beim Setzen der Dateiauswahl!'); + } + }); + }); + } + + // --- Hilfsfunktion: Torrent-Dateiliste aus bencode extrahieren --- + function parseTorrentFileList(arrayBuffer) { + // Minimaler Bencode-Parser für 'info.files' oder 'info.name' (single file) + // Korrigiert: Dateinamen werden anhand der Längenangabe korrekt extrahiert + try { + let bytes = new Uint8Array(arrayBuffer); + let str = ''; + for (let i = 0; i < bytes.length; i++) str += String.fromCharCode(bytes[i]); + // Suche nach '4:info' und dann nach '5:files' oder '4:name' + let infoIdx = str.indexOf('4:info'); + if (infoIdx === -1) return []; + let filesIdx = str.indexOf('5:files', infoIdx); + if (filesIdx !== -1) { + // Multi-File Torrent: Dateinamen extrahieren + let fileList = []; + // Suche alle "4:pathl...e"-Blöcke ab filesIdx + let pathIdx = filesIdx; + while ((pathIdx = str.indexOf('4:pathl', pathIdx)) !== -1) { + pathIdx += 7; // nach '4:pathl' + // Es können mehrere Strings (Verzeichnisse + Dateiname) folgen, jeweils als ":" + let nameParts = []; + while (str[pathIdx] >= '0' && str[pathIdx] <= '9') { + // Lese Längenangabe + let lenStr = ''; + while (str[pathIdx] >= '0' && str[pathIdx] <= '9') { + lenStr += str[pathIdx++]; + } + if (str[pathIdx] !== ':') break; + pathIdx++; + let len = parseInt(lenStr, 10); + let name = str.substr(pathIdx, len); + nameParts.push(name); + pathIdx += len; + } + if (nameParts.length > 0) fileList.push(nameParts.join('/')); + // Suche nächstes '4:pathl' ab aktueller Position + } + return fileList; + } else { + // Single-File Torrent: nur Name + let nameIdx = str.indexOf('4:name', infoIdx); + if (nameIdx !== -1) { + let lenStr = ''; + let idx = nameIdx + 6; + while (str[idx] >= '0' && str[idx] <= '9') { + lenStr += str[idx++]; + } + if (str[idx] === ':') { + idx++; + let len = parseInt(lenStr, 10); + let name = str.substr(idx, len); + return [name]; + } + } + } + } catch (e) {} + return []; + } + + // --- Klicks abfangen --- + document.addEventListener('click', function(e) { + let el = e.target.closest('a'); + if (!el) return; + + // Magnet-Link + if (el.href.startsWith('magnet:?')) { + e.preventDefault(); + showPathDialog(function(dir) { + sendToTransmission(el.href, true, dir, false); + }); + } + + // .torrent-Datei + if (el.href.match(/\.torrent($|\?)/i)) { + e.preventDefault(); + GM_xmlhttpRequest({ + method: 'GET', + url: el.href, + responseType: 'arraybuffer', + onload: function(response) { + const arrayBuffer = response.response; + const base64 = btoa(String.fromCharCode.apply(null, new Uint8Array(arrayBuffer))); + const fileList = parseTorrentFileList(arrayBuffer); + showPathDialog(function(dir, selectedFiles) { + // selectedFiles: Array von bools, true = gewünscht + // Sende an Transmission, und setze unwanted falls nötig + getConfig().then(config => { + getSessionId(config, function(sessionId) { + let headers = { + 'Content-Type': 'application/json', + 'X-Transmission-Session-Id': sessionId + }; + if (config.user) { + headers['Authorization'] = 'Basic ' + btoa(config.user + ':' + config.pass); + } + let args = { metainfo: base64 }; + if (dir) args['download-dir'] = dir; + let body = { + method: 'torrent-add', + arguments: args + }; + GM_xmlhttpRequest({ + method: 'POST', + url: config.url, + headers: headers, + data: JSON.stringify(body), + onload: function(response) { + let respObj = {}; + try { respObj = JSON.parse(response.responseText); } catch (e) {} + if (response.status === 409) { + const match = response.responseHeaders.match(/x-transmission-session-id:(.+)/i); + const newSessionId = match ? match[1].trim() : null; + if (newSessionId) { + headers['X-Transmission-Session-Id'] = newSessionId; + GM_xmlhttpRequest({ + method: 'POST', + url: config.url, + headers: headers, + data: JSON.stringify(body), + onload: function(resp2) { + let respObj2 = {}; + try { respObj2 = JSON.parse(resp2.responseText); } catch (e) {} + if (resp2.status === 409) { + showToast('Session-ID konnte nicht erneuert werden!\nResponse Headers:\n' + resp2.responseHeaders); + } else { + showToast('An Transmission gesendet!'); + // Nach dem Hinzufügen: unwanted setzen + if (Array.isArray(selectedFiles) && fileList.length === selectedFiles.length) { + let unwanted = []; + for (let i = 0; i < selectedFiles.length; i++) { + if (!selectedFiles[i]) unwanted.push(i); + } + if (respObj2.result === 'success' && respObj2.arguments && respObj2.arguments['torrent-added']) { + let tid = respObj2.arguments['torrent-added'].id; + setUnwantedFiles(tid, unwanted, config); + } + } + } + }, + onerror: function() { + showToast('Fehler beim Senden an Transmission!'); + } + }); + } else { + showToast('Session-ID konnte nicht erneuert werden!\nResponse Headers:\n' + response.responseHeaders); + } + } else { + showToast('An Transmission gesendet!'); + // Nach dem Hinzufügen: unwanted setzen + if (Array.isArray(selectedFiles) && fileList.length === selectedFiles.length) { + let unwanted = []; + for (let i = 0; i < selectedFiles.length; i++) { + if (!selectedFiles[i]) unwanted.push(i); + } + if (respObj.result === 'success' && respObj.arguments && respObj.arguments['torrent-added']) { + let tid = respObj.arguments['torrent-added'].id; + setUnwantedFiles(tid, unwanted, config); + } + } + } + }, + onerror: function() { + showToast('Fehler beim Senden an Transmission!'); + } + }); + }); + }); + }, undefined, fileList); + }, + onerror: function() { + showToast('Fehler beim Herunterladen der .torrent-Datei!'); + } + }); + } + }, true); + + // --- Menüeinträge --- + if (typeof GM_registerMenuCommand !== 'undefined') { + GM_registerMenuCommand('Transmission konfigurieren', askForConfig); + GM_registerMenuCommand('Download-Pfade verwalten', editPathsMenu); + } + + // --- Rückmeldungen als Toast --- + function showToast(msg) { + let toast = document.createElement('div'); + toast.textContent = msg; + toast.style.position = 'fixed'; + toast.style.bottom = '32px'; + toast.style.right = '32px'; + toast.style.background = '#222'; + toast.style.color = '#fff'; + toast.style.padding = '12px 24px'; + toast.style.borderRadius = '8px'; + toast.style.boxShadow = '0 2px 8px rgba(0,0,0,0.2)'; + toast.style.zIndex = 999999; + document.body.appendChild(toast); + setTimeout(() => { + toast.style.transition = 'opacity 0.5s'; + toast.style.opacity = 0; + setTimeout(() => toast.remove(), 500); + }, 2200); + } +})(); \ No newline at end of file