New:Socket for Asana Is Now Available.Learn more
Get Started

YT Account Manager

Package Overview
Versions
2
Alerts
File Explorer
Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

ytmanager@accounts - firefox Package Compare versions

Comparing version
3.3
to
3.5
+95
commenter.js
// ================================================================
// YT Account Manager – commenter.js
// Postet automatisch einen Kommentar, wenn _ytmc=1 in der URL ist
// ================================================================
(async () => {
if (!location.search.includes('_ytmc=1')) return;
// Warte bis Seite geladen
if (document.readyState !== 'complete') {
await new Promise(r => window.addEventListener('load', r, { once: true }));
}
await sleep(2500);
// Pending Comment vom Background holen (der kennt unseren cookieStoreId)
let resp;
try {
resp = await browser.runtime.sendMessage({ action: 'get_my_pending_comment' });
} catch (e) { return; }
if (!resp || !resp.comment) return;
const ok = await postComment(resp.comment);
// URL säubern (Parameter entfernen)
try {
const url = new URL(location.href);
url.searchParams.delete('_ytmc');
history.replaceState(null, '', url.toString());
} catch (_) {}
// Status zurückmelden
try {
await browser.runtime.sendMessage({
action: 'comment_result',
tabId: resp.tabId,
ok
});
} catch (_) {}
})();
// ── Kommentar posten ─────────────────────────────────────────────
async function postComment(text) {
try {
// 1. Placeholder finden und anklicken um Kommentarbox zu öffnen
const placeholder = await waitForEl('#simplebox-placeholder', 12000);
if (!placeholder) return false;
placeholder.click();
await sleep(700);
// 2. Editable-Bereich finden
const box = await waitForEl('#contenteditable-root.yt-formatted-string', 6000)
|| await waitForEl('#contenteditable-root', 6000);
if (!box) return false;
box.focus();
// Text einfügen (simuliert echte Tastatureingabe für YouTube's SPA)
document.execCommand('selectAll', false, null);
document.execCommand('insertText', false, text);
box.dispatchEvent(new InputEvent('input', { bubbles: true, data: text }));
await sleep(500);
// 3. Submit-Button finden und klicken
const submit = document.querySelector(
'#submit-button button, ' +
'ytd-button-renderer#submit-button button, ' +
'#submit-button yt-button-shape button'
);
if (!submit) return false;
submit.removeAttribute('disabled');
submit.click();
await sleep(800);
return true;
} catch (e) {
console.error('[YTM Commenter]', e);
return false;
}
}
// ── Hilfsfunktionen ──────────────────────────────────────────────
function sleep(ms) {
return new Promise(r => setTimeout(r, ms));
}
function waitForEl(selector, timeout) {
return new Promise(resolve => {
const el = document.querySelector(selector);
if (el) { resolve(el); return; }
const obs = new MutationObserver(() => {
const found = document.querySelector(selector);
if (found) { obs.disconnect(); resolve(found); }
});
obs.observe(document.documentElement, { childList: true, subtree: true });
setTimeout(() => { obs.disconnect(); resolve(null); }, timeout);
});
}
// hd_unlocker.js v3.2 — YouTube Höchste Qualität Unlocker
// Nutzt YouTubes eigene interne Player-API direkt
(function () {
'use strict';
let enabled = false;
let navObserver = null;
let qualityInterval = null;
let lastUrl = location.href;
// ── Qualitätsstufen: höchste zuerst ──────────────────────────
const QUALITY_LEVELS = [
'hd2160', // 4K
'hd1440', // 1440p
'hd1080', // 1080p
'hd720', // 720p
'large', // 480p
];
// ── Storage: Aktivierungsstatus laden ────────────────────────
browser.storage.local.get('hdUnlockerEnabled').then(d => {
enabled = !!d.hdUnlockerEnabled;
if (enabled) init();
});
browser.storage.onChanged.addListener((changes) => {
if ('hdUnlockerEnabled' in changes) {
enabled = !!changes.hdUnlockerEnabled.newValue;
if (enabled) init(); else shutdown();
}
});
// ── Hauptfunktion: setzt höchste Qualität ────────────────────
function setHighestQuality() {
if (!enabled) return;
// Methode 1: YouTube HTML5-Player API direkt
const players = document.querySelectorAll('.html5-video-player');
players.forEach(player => {
try {
// Verfügbare Qualitäten vom Player holen
const available = player.getAvailableQualityLevels
? player.getAvailableQualityLevels()
: [];
if (available && available.length > 0) {
// Höchste verfügbare Qualität aus unserer Liste wählen
const best = QUALITY_LEVELS.find(q => available.includes(q)) || available[0];
if (best) {
const current = player.getPlaybackQuality ? player.getPlaybackQuality() : null;
if (current !== best) {
if (player.setPlaybackQualityRange) {
player.setPlaybackQualityRange(best, best);
}
if (player.setPlaybackQuality) {
player.setPlaybackQuality(best);
}
}
}
} else {
// Fallback: blind höchste setzen
if (player.setPlaybackQualityRange) player.setPlaybackQualityRange('hd2160', 'hd2160');
if (player.setPlaybackQuality) player.setPlaybackQuality('hd1440');
}
} catch (_) {}
});
// Methode 2: yt-player Element via Polymer internals
try {
const ytPlayer = document.querySelector('#movie_player') ||
document.querySelector('ytd-player #player');
if (ytPlayer) {
const available = ytPlayer.getAvailableQualityLevels
? ytPlayer.getAvailableQualityLevels() : [];
if (available.length > 0) {
const best = QUALITY_LEVELS.find(q => available.includes(q)) || available[0];
if (ytPlayer.setPlaybackQuality) ytPlayer.setPlaybackQuality(best);
if (ytPlayer.setPlaybackQualityRange) ytPlayer.setPlaybackQualityRange(best, best);
}
}
} catch (_) {}
// Methode 3: Qualitätsmenü-Click (wenn offen)
autoClickBestQuality();
}
// ── Methode 3: Klickt automatisch die beste Option im Menü ──
function autoClickBestQuality() {
try {
const panel = document.querySelector('.ytp-quality-menu');
if (!panel || panel.style.display === 'none') return;
// Alle Items sammeln mit ihrer Qualität
const items = [];
panel.querySelectorAll('.ytp-menuitem').forEach(item => {
const label = item.querySelector('.ytp-menuitem-label');
if (!label) return;
const text = label.textContent.trim();
// Qualitätsnummer extrahieren z.B. "1440p60" → 1440
const match = text.match(/^(\d+)p?/);
if (match) items.push({ item, res: parseInt(match[1], 10), text });
});
if (items.length === 0) return;
// Höchste Auflösung wählen
items.sort((a, b) => b.res - a.res);
const best = items[0];
// Nur klicken wenn nicht schon ausgewählt
const checked = best.item.getAttribute('aria-checked');
if (checked !== 'true') {
best.item.click();
}
} catch (_) {}
}
// ── Setzt Cookie/LocalStorage Präferenz für YouTube ──────────
function setQualityPreference() {
try {
// YouTube liest yt-player-quality aus localStorage
localStorage.setItem('yt-player-quality', JSON.stringify({
data: 'hd2160',
expiration: Date.now() + 86400000 * 30,
creation: Date.now()
}));
} catch (_) {}
}
// ── Qualitäts-Button im Player beobachten ───────────────────
function watchQualityButton() {
// Beobachte Öffnen/Schließen des Qualitätsmenüs
const panel = document.querySelector('.ytp-quality-menu');
if (panel) {
new MutationObserver(() => {
if (!enabled) return;
setTimeout(autoClickBestQuality, 50);
}).observe(panel, { attributes: true, attributeFilter: ['style', 'class'] });
}
}
// ── SPA Navigation erkennen ──────────────────────────────────
function watchNavigation() {
if (navObserver) return;
navObserver = new MutationObserver(() => {
if (location.href !== lastUrl) {
lastUrl = location.href;
if (enabled) {
// Kurz warten bis Player geladen ist
setTimeout(setHighestQuality, 1500);
setTimeout(setHighestQuality, 3000);
setTimeout(watchQualityButton, 2000);
}
}
});
navObserver.observe(document.body, { childList: true, subtree: true });
}
// ── Start / Stop ─────────────────────────────────────────────
function init() {
setQualityPreference();
setHighestQuality();
watchNavigation();
watchQualityButton();
// Alle 2 Sekunden prüfen (für den Fall dass YouTube die Qualität zurücksetzt)
if (!qualityInterval) {
qualityInterval = setInterval(() => {
if (enabled) setHighestQuality();
}, 2000);
}
}
function shutdown() {
if (qualityInterval) { clearInterval(qualityInterval); qualityInterval = null; }
// Navigation-Observer bleibt aktiv um bei Rückkehr wieder zu starten
}
})();
+67
-89
const CONTAINER_PREFIX = "YTM: ";
const YT_URL = "https://www.youtube.com/";
const TWITCH_URL = "https://www.twitch.tv/";
const GOOGLE_LOGIN = "https://accounts.google.com/ServiceLogin?service=youtube&continue=" + encodeURIComponent(YT_URL);
// Erlaubte Domains für den Link-Öffner
const ALLOWED_DOMAINS = ["youtube.com", "youtu.be", "twitch.tv"];
function isAllowedUrl(url) {
try {
const u = new URL(url);
return ALLOWED_DOMAINS.some(d => u.hostname === d || u.hostname.endsWith("." + d));
} catch (_) { return false; }
}
async function getOrCreateContainer(account) {

@@ -128,77 +139,35 @@ try {

// ── FEATURE: Echte YT-Profilbilder ──────────────────────────────────
// Öffnet kurz einen Hintergrund-Tab im jeweiligen Account-Container,
// liest dort das öffentlich sichtbare Profilbild aus dem YouTube-Header
// aus und schließt den Tab sofort wieder. Es werden dabei keine
// Kommentare, Nachrichten oder sonstigen privaten Inhalte gelesen –
// nur die Avatar-Bild-URL, die sowieso in jedem Tab sichtbar ist.
function waitForTabComplete(tabId, timeoutMs) {
return new Promise((resolve) => {
let done = false;
const finish = () => {
if (done) return;
done = true;
browser.tabs.onUpdated.removeListener(listener);
resolve();
};
const listener = (id, info) => {
if (id === tabId && info.status === "complete") finish();
};
browser.tabs.onUpdated.addListener(listener);
setTimeout(finish, timeoutMs);
});
}
async function fetchAvatarForAccount(account) {
const container = await getOrCreateContainer(account);
if (!container) return { ok: false, error: "Kein Container" };
const loggedIn = await isLoggedIn(container.cookieStoreId);
if (!loggedIn) return { ok: false, error: "Nicht eingeloggt" };
let tab;
// ── Link in Account-Container öffnen ─────────────────────────────
async function openLinkInAccount(account, url) {
try {
tab = await browser.tabs.create({ url: YT_URL, cookieStoreId: container.cookieStoreId, active: false });
await waitForTabComplete(tab.id, 9000);
const results = await browser.tabs.executeScript(tab.id, {
code: `
(function() {
const sels = [
'button#avatar-btn img',
'ytd-topbar-menu-button-renderer img',
'#avatar-btn yt-img-shadow img',
'yt-img-shadow#avatar img'
];
for (const s of sels) {
const img = document.querySelector(s);
if (img && img.src) return img.src;
}
return null;
})();
`
});
const avatarUrl = results && results[0] ? results[0] : null;
return avatarUrl ? { ok: true, avatarUrl } : { ok: false, error: "Kein Profilbild gefunden" };
} catch (e) {
return { ok: false, error: e.message };
} finally {
if (tab) { try { await browser.tabs.remove(tab.id); } catch (e) {} }
}
// URL normalisieren
let finalUrl = url;
if (!finalUrl.startsWith("http://") && !finalUrl.startsWith("https://")) {
finalUrl = "https://" + finalUrl;
}
const container = await getOrCreateContainer(account);
const opts = { url: finalUrl };
if (container) opts.cookieStoreId = container.cookieStoreId;
await browser.tabs.create(opts);
return { ok: true };
} catch (e) { return { ok: false, error: e.message }; }
}
// ── Comments Finder – öffnet einen beliebigen, eingefügten YouTube-Link ──
function isAllowedYoutubeUrl(url) {
// ── Kommentar in Account-Container öffnen ────────────────────────
async function openForComment(account, url, comment) {
try {
const u = new URL(url);
return (/(^|\.)youtube\.com$/i.test(u.hostname) || /(^|\.)youtu\.be$/i.test(u.hostname))
&& u.protocol === "https:";
} catch (e) { return false; }
}
let finalUrl = url;
if (!finalUrl.startsWith('http://') && !finalUrl.startsWith('https://')) {
finalUrl = 'https://' + finalUrl;
}
// _ytmc=1 signalisiert dem Content-Script dass es kommentieren soll
const u = new URL(finalUrl);
u.searchParams.set('_ytmc', '1');
finalUrl = u.toString();
async function openChannelForAccount(account, url) {
if (!isAllowedYoutubeUrl(url)) return { ok: false, error: "Ungültige URL" };
try {
const container = await getOrCreateContainer(account);
const opts = { url };
const key = 'pending_comment_' + (container ? container.cookieStoreId : 'default');
await browser.storage.local.set({ [key]: { comment, timestamp: Date.now() } });
const opts = { url: finalUrl };
if (container) opts.cookieStoreId = container.cookieStoreId;

@@ -210,15 +179,2 @@ await browser.tabs.create(opts);

async function openChannelForAllAccounts(url) {
if (!isAllowedYoutubeUrl(url)) return { opened: 0, error: "Ungültige URL" };
const data = await browser.storage.local.get("accounts");
const accounts = data.accounts || [];
let opened = 0;
for (const acc of accounts) {
const res = await openChannelForAccount(acc, url);
if (res.ok) opened++;
await new Promise(r => setTimeout(r, 350));
}
return { opened, total: accounts.length };
}
browser.runtime.onMessage.addListener((msg, sender, respond) => {

@@ -231,2 +187,5 @@ if (msg.action === "open_all") {

}
if (msg.action === "open_link") {
openLinkInAccount(msg.account, msg.url).then(respond); return true;
}
if (msg.action === "delete_container") {

@@ -245,11 +204,30 @@ deleteContainer(msg.account).then(() => respond({ ok: true })); return true;

}
if (msg.action === "open_channel_single") {
openChannelForAccount(msg.account, msg.url).then(respond); return true;
if (msg.action === "toggle_hd_unlocker") {
browser.storage.local.set({ hdUnlockerEnabled: msg.enabled })
.then(() => respond({ ok: true })); return true;
}
if (msg.action === "open_channel_all") {
openChannelForAllAccounts(msg.url).then(respond); return true;
if (msg.action === "open_for_comment") {
openForComment(msg.account, msg.url, msg.comment).then(respond); return true;
}
if (msg.action === "get_avatar") {
fetchAvatarForAccount(msg.account).then(respond); return true;
if (msg.action === "get_my_pending_comment") {
// sender.tab.cookieStoreId ist der Container des aufrufenden Tabs
const cookieStoreId = sender.tab && sender.tab.cookieStoreId
? sender.tab.cookieStoreId
: 'firefox-default';
const key = 'pending_comment_' + cookieStoreId;
browser.storage.local.get(key).then(async data => {
const pending = data[key];
if (pending) {
await browser.storage.local.remove(key);
respond({ comment: pending.comment, tabId: sender.tab.id });
} else {
respond({ comment: null });
}
});
return true;
}
if (msg.action === "comment_result") {
// Optional: könnte man für Badge-Feedback nutzen
respond({ ok: true }); return true;
}
});

@@ -290,3 +268,3 @@

} catch (e) {
// setBadge nicht auf Firefox für Android verfügbar
// Badge API not supported on this platform (e.g. Firefox for Android < 79)
}

@@ -293,0 +271,0 @@ }

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
<rect width="48" height="48" rx="10" fill="#FF0000"/>
<rect x="6" y="13" width="36" height="22" rx="4" fill="white"/>
<rect x="6" y="13" width="36" height="22" rx="4" fill="white" opacity="0.9"/>
<polygon points="20,17 20,29 31,23" fill="#FF0000"/>

@@ -5,0 +4,0 @@ <rect x="8" y="37" width="10" height="4" rx="2" fill="#FF6666"/>

{
"manifest_version": 2,
"name": "YT Account Manager",
"version": "3.3",
"description": "YouTube Multi-Account Manager \u2013 jeder Account in eigenem Container, dauerhaft gespeichert",
"version": "3.5",
"description": "YouTube Multi-Account Manager v3.3 \u2013 Link-\u00d6ffner, HD/FPS Unlocker, Multi-Account Commenter",
"permissions": [

@@ -13,4 +13,6 @@ "contextualIdentities",

"alarms",
"clipboardRead",
"*://*.youtube.com/*",
"*://*.google.com/*"
"*://*.google.com/*",
"*://*.twitch.tv/*"
],

@@ -30,2 +32,22 @@ "browser_action": {

},
"content_scripts": [
{
"matches": [
"*://*.youtube.com/*"
],
"js": [
"hd_unlocker.js"
],
"run_at": "document_idle"
},
{
"matches": [
"*://*.youtube.com/*"
],
"js": [
"commenter.js"
],
"run_at": "document_idle"
}
],
"icons": {

@@ -94,3 +116,3 @@ "48": "icons/icon.svg",

"id": "ytmanager@accounts",
"strict_min_version": "140.0",
"strict_min_version": "142.0",
"data_collection_permissions": {

@@ -101,6 +123,5 @@ "required": [

"optional": []
},
"strict_android_min_version": "142.0"
}
}
}
}
+236
-89

@@ -166,10 +166,3 @@ <!DOCTYPE html>

font-size: 12px;
overflow: hidden;
}
.avatar img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}

@@ -269,3 +262,3 @@ /* Login Status Badge */

.color-picker {
.color-picker, .icon-picker {
display: flex;

@@ -292,2 +285,17 @@ gap: 5px;

.icon-swatch {
width: 22px; height: 22px;
border-radius: 6px;
border: 2px solid transparent;
cursor: pointer;
background: #0f0f0f;
display: flex;
align-items: center;
justify-content: center;
font-size: 13px;
transition: transform 0.1s;
}
.icon-swatch:hover { transform: scale(1.15); }
.icon-swatch.selected { border-color: #ff3333; }
.btn-add {

@@ -308,63 +316,2 @@ background: #cc0000;

/* Comments Finder */
.btn-finder {
width: calc(100% - 24px);
margin: 4px 12px 10px;
padding: 9px;
background: #1a1a1a;
border: 1px solid #2a2a2a;
color: #ccc;
border-radius: 8px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: all 0.15s;
}
.btn-finder:hover { border-color: #555; color: #fff; }
.finder-panel {
margin: 0 12px 10px;
background: #1a1a1a;
border: 1px solid #2a2a2a;
border-radius: 8px;
padding: 10px;
display: none;
}
.finder-panel.open { display: block; }
.finder-row {
display: flex;
gap: 6px;
margin-bottom: 8px;
}
.finder-select {
flex: 1;
background: #0f0f0f;
border: 1px solid #333;
color: #f1f1f1;
border-radius: 6px;
padding: 7px 8px;
font-size: 12px;
outline: none;
}
.finder-actions { display: flex; gap: 6px; }
.btn-finder-action {
flex: 1;
padding: 8px;
border: none;
border-radius: 6px;
font-size: 12px;
font-weight: 600;
cursor: pointer;
color: #fff;
}
.btn-finder-single { background: #333; }
.btn-finder-single:hover { background: #444; }
.btn-finder-all { background: linear-gradient(135deg, #cc0000, #ff0000); }
.btn-finder-all:hover { background: linear-gradient(135deg, #e00, #ff3333); }
.finder-hint { font-size: 10px; color: #555; margin-top: 6px; line-height: 1.4; }
/* Info Box */

@@ -404,2 +351,181 @@ .info-box {

#importFile { display: none; }
/* ── Link Opener ── */
.link-section {
margin: 10px 12px 0;
background: #0d1a0d;
border: 1px solid #1a3a1a;
border-radius: 8px;
padding: 10px;
}
.link-section-title {
font-size: 11px;
font-weight: 600;
color: #4a8a4a;
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 8px;
}
.link-input-wrap { display: flex; gap: 6px; align-items: center; }
.link-input {
flex: 1;
background: #0f0f0f;
border: 1px solid #1a3a1a;
color: #f1f1f1;
border-radius: 6px;
padding: 7px 10px;
font-size: 12px;
outline: none;
}
.link-input:focus { border-color: #44aa44; }
.link-input::placeholder { color: #444; }
.link-paste-btn {
background: #1a2a1a;
border: 1px solid #1a3a1a;
color: #4a8a4a;
border-radius: 6px;
padding: 7px 9px;
font-size: 12px;
cursor: pointer;
transition: all 0.15s;
white-space: nowrap;
}
.link-paste-btn:hover { border-color: #44aa44; color: #66cc66; }
.btn-open-link {
width: 100%;
margin-top: 7px;
background: linear-gradient(135deg, #1a4a1a, #1f5a1f);
color: #44cc44;
border: 1px solid #2a6a2a;
border-radius: 6px;
padding: 8px;
font-size: 13px;
font-weight: 700;
cursor: pointer;
transition: all 0.15s;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
}
.btn-open-link:hover { background: linear-gradient(135deg, #225522, #267026); border-color: #44aa44; }
.btn-open-link:disabled { background: #1a1a1a; color: #444; border-color: #222; cursor: not-allowed; }
.link-status { font-size: 10px; color: #3a6a3a; margin-top: 4px; min-height: 14px; }
.link-status.err { color: #aa4444; }
/* ── Link-Öffnen bei Account-Item ── */
.btn-icon:hover.link-btn { color: #44cc44; background: #0d2a0d; }
.btn-icon.link-btn.selected {
color: #44ff44;
background: #0d3a0d;
border: 1px solid #2a6a2a;
border-radius: 5px;
}
.selected-for-link {
border-color: #44aa44 !important;
box-shadow: 0 0 0 1px #44aa44;
}
/* ── Commenter ── */
.comment-section {
margin: 4px 12px 0;
background: #0d0d1a;
border: 1px solid #1a1a3a;
border-radius: 8px;
padding: 10px;
}
.comment-section-title {
font-size: 11px;
font-weight: 600;
color: #6688cc;
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 8px;
}
.comment-video-info {
font-size: 11px;
color: #445;
margin-bottom: 6px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.comment-video-info span {
color: #5577bb;
}
.comment-textarea {
width: 100%;
background: #0f0f0f;
border: 1px solid #1a1a3a;
color: #f1f1f1;
border-radius: 6px;
padding: 7px 10px;
font-size: 12px;
font-family: 'Segoe UI', Arial, sans-serif;
resize: vertical;
min-height: 54px;
max-height: 120px;
outline: none;
box-sizing: border-box;
}
.comment-textarea:focus { border-color: #4466cc; }
.comment-textarea::placeholder { color: #444; }
.btn-send-comment {
width: 100%;
margin-top: 7px;
background: linear-gradient(135deg, #1a1a4a, #1f1f5a);
color: #6688ff;
border: 1px solid #2a2a6a;
border-radius: 6px;
padding: 8px;
font-size: 13px;
font-weight: 700;
cursor: pointer;
transition: all 0.15s;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
}
.btn-send-comment:hover:not(:disabled) {
background: linear-gradient(135deg, #222266, #272780);
border-color: #4466cc;
}
.btn-send-comment:disabled { background: #1a1a1a; color: #444; border-color: #222; cursor: not-allowed; }
.comment-status { font-size: 10px; color: #3a3a6a; margin-top: 4px; min-height: 14px; }
.comment-status.err { color: #aa4444; }
.comment-status.ok { color: #44cc44; }
/* ── HD Unlocker ── */
.hd-section {
margin: 4px 12px 4px;
display: flex;
align-items: center;
gap: 10px;
background: #1a1500;
border: 1px solid #3a3000;
border-radius: 8px;
padding: 9px 12px;
}
.hd-label { flex: 1; font-size: 12px; color: #cc9900; font-weight: 600; }
.hd-sublabel { font-size: 10px; color: #665500; font-weight: 400; display: block; }
.btn-hd-toggle {
background: #2a2000;
border: 1px solid #554400;
color: #887700;
border-radius: 6px;
padding: 6px 14px;
font-size: 12px;
font-weight: 700;
cursor: pointer;
transition: all 0.15s;
white-space: nowrap;
}
.btn-hd-toggle.active {
background: #332800;
border-color: #cc9900;
color: #ffcc00;
box-shadow: 0 0 8px rgba(200,150,0,0.3);
}
.btn-hd-toggle:hover { border-color: #887700; color: #ccaa00; }
</style>

@@ -426,2 +552,17 @@ </head>

<!-- Link Opener -->
<div class="link-section">
<div class="link-section-title">🔗 Link-Öffner</div>
<div class="link-input-wrap">
<input class="link-input" id="linkInput" type="text"
placeholder="Beliebigen Link einfügen...">
<button class="link-paste-btn" id="btnPasteLink" title="Aus Zwischenablage einfügen">📋</button>
<button class="link-paste-btn" id="btnSelectAll" title="Alle Accounts auswählen">☑ Alle</button>
</div>
<div class="link-status" id="linkStatus">👆 Bei jedem Account auf 🔗 klicken zum Auswählen</div>
<button class="btn-open-link" id="btnOpenLink" disabled>
<span>🔗</span><span id="linkBtnLabel">Zuerst Account(s) auswählen</span>
</button>
</div>
<!-- Toolbar: Suche + Export/Import -->

@@ -441,21 +582,2 @@ <div class="toolbar">

<!-- Comments Finder -->
<button class="btn-finder" id="btnToggleFinder">
<span>🔎</span><span>COMMENTS FINDER</span>
</button>
<div class="finder-panel" id="finderPanel">
<div class="finder-row">
<input class="add-input" id="finderLink" type="text" placeholder="YouTube-Link einfügen (Video, Short, Kommentar-Link ...)" style="flex:1;">
</div>
<div class="finder-row">
<select class="finder-select" id="finderAccountSelect">
<option value="__all__">🌐 ALLE Accounts</option>
</select>
</div>
<button class="btn-finder-action btn-finder-all" id="btnFinderOpen" style="width:100%;">▶ Öffnen</button>
<div class="finder-hint">
Fügt einen beliebigen YouTube-Link ein und leitet den gewählten Account (oder gleichzeitig alle Accounts) direkt dorthin weiter.
</div>
</div>
<!-- Toast -->

@@ -484,2 +606,5 @@ <div class="toast" id="toast"></div>

</div>
<div class="icon-picker" id="iconPicker">
<span class="picker-label">Icon:</span>
</div>
<div class="color-picker" id="colorPicker">

@@ -497,4 +622,26 @@ <span class="picker-label">Farbe:</span>

<!-- Commenter -->
<div class="comment-section">
<div class="comment-section-title">💬 Kommentar senden</div>
<div class="comment-video-info" id="commentVideoInfo">📺 Aktives Video wird erkannt...</div>
<textarea class="comment-textarea" id="commentText"
placeholder="Kommentar eingeben... (Accounts mit 🔗 auswählen)"></textarea>
<div class="comment-status" id="commentStatus">👆 Accounts mit 🔗 auswählen, dann Kommentar eingeben</div>
<button class="btn-send-comment" id="btnSendComment" disabled>
<span>💬</span><span id="commentBtnLabel">Accounts auswählen (🔗)</span>
</button>
</div>
<!-- HD / FPS Unlocker -->
<div class="hd-section">
<div class="hd-label">
🎬 1440p + 120fps Unlocker
<span class="hd-sublabel">Entsperrt höchste Qualität auf YouTube</span>
</div>
<button class="btn-hd-toggle" id="btnHdToggle">AUS</button>
</div>
<!-- Footer -->
<div class="footer">YT Account Manager v3.0 – Accounts bleiben dauerhaft gespeichert</div>
<div class="footer">YT Account Manager v3.2 – Accounts bleiben dauerhaft gespeichert</div>

@@ -501,0 +648,0 @@ <script src="popup.js"></script>

+439
-166

@@ -18,15 +18,16 @@ // ================================================================

const ICONS = ["📺", "🎮", "💼", "👤", "⭐", "🎵", "📚", "💻", "🎨", "🏠", "🛒", "🔥"];
let accounts = [];
let loginStatus = {};
let avatarCache = {};
let selectedColor = "red";
let selectedIcon = ICONS[0];
let editingId = null;
let searchTerm = "";
let dragSourceId = null;
let linkSelectedIds = new Set();
// ── Storage ──────────────────────────────────────────────────────
async function loadAccounts() {
const data = await browser.storage.local.get(["accounts", "loginStatusCache", "avatarCache"]);
const data = await browser.storage.local.get(["accounts", "loginStatusCache"]);
accounts = data.accounts || [];

@@ -36,3 +37,2 @@ // FEATURE: Gecachten Status vom Hintergrund-Alarm sofort anzeigen,

loginStatus = data.loginStatusCache || {};
avatarCache = data.avatarCache || {};
renderList();

@@ -62,3 +62,2 @@ // Trotzdem im Hintergrund einmal live nachprüfen für ganz aktuellen Stand

renderList();
refreshMissingAvatars();
}

@@ -68,20 +67,2 @@ } catch (e) {}

// ── FEATURE: Echte YT-Profilbilder statt Emoji-Icons ───────────────
// Holt für jeden eingeloggten Account, dessen Avatar noch nicht im Cache
// ist, das öffentliche Profilbild nach und speichert es lokal.
async function refreshMissingAvatars() {
for (const acc of accounts) {
if (loginStatus[acc.id] && !avatarCache[acc.id]) {
try {
const res = await browser.runtime.sendMessage({ action: "get_avatar", account: acc });
if (res && res.avatarUrl) {
avatarCache[acc.id] = res.avatarUrl;
await browser.storage.local.set({ avatarCache });
renderList();
}
} catch (e) {}
}
}
}
// ── ID Generator ─────────────────────────────────────────────────

@@ -109,64 +90,2 @@ function uid() {

// ── DOM-Hilfsfunktionen (kein innerHTML) ─────────────────────────
function mkEmptyState(emoji, msg) {
const div = document.createElement("div");
div.className = "empty-state";
const big = document.createElement("div");
big.className = "big";
big.textContent = emoji;
div.appendChild(big);
div.appendChild(document.createTextNode(msg));
return div;
}
function mkAvatar(colorHex, avatarUrl) {
const div = document.createElement("div");
div.className = "avatar";
div.style.background = colorHex;
if (avatarUrl) {
const img = document.createElement("img");
img.src = avatarUrl;
img.alt = "";
div.appendChild(img);
} else {
div.textContent = "👤";
}
return div;
}
function mkShortcut(index) {
if (index > 9) return null;
const span = document.createElement("span");
span.className = "shortcut-num";
span.title = "Alt+Shift+" + index;
span.textContent = String(index);
return span;
}
function mkBadge(status) {
const span = document.createElement("span");
span.className = "login-badge";
if (status === true) {
span.classList.add("logged-in");
span.textContent = "✔ Eingeloggt";
} else if (status === false) {
span.classList.add("logged-out");
span.textContent = "⚠ Nicht eingeloggt";
} else {
span.classList.add("checking");
span.textContent = "… Prüfe";
}
return span;
}
function mkIconBtn(cls, action, id, titleText, label) {
const btn = document.createElement("button");
btn.className = "btn-icon " + cls;
btn.dataset.action = action;
btn.dataset.id = id;
btn.title = titleText;
btn.textContent = label;
return btn;
}
// ── Liste rendern ─────────────────────────────────────────────────

@@ -187,4 +106,11 @@ function renderList() {

if (accounts.length === 0) {
const emptyDiv = document.createElement("div");
emptyDiv.className = "empty-state";
const bigDiv = document.createElement("div");
bigDiv.className = "big";
bigDiv.textContent = "📺";
emptyDiv.appendChild(bigDiv);
emptyDiv.appendChild(document.createTextNode(" Noch keine Accounts. Füge deinen ersten Account unten hinzu."));
list.textContent = "";
list.appendChild(mkEmptyState("📺", "Noch keine Accounts. Füge deinen ersten Account unten hinzu."));
list.appendChild(emptyDiv);
return;

@@ -194,4 +120,11 @@ }

if (filtered.length === 0) {
const emptyDiv = document.createElement("div");
emptyDiv.className = "empty-state";
const bigDiv = document.createElement("div");
bigDiv.className = "big";
bigDiv.textContent = "🔍";
emptyDiv.appendChild(bigDiv);
emptyDiv.appendChild(document.createTextNode(" Kein Account gefunden für \"" + esc(searchTerm) + "\"."));
list.textContent = "";
list.appendChild(mkEmptyState("🔍", "Kein Account gefunden für \"" + searchTerm + "\"." ));
list.appendChild(emptyDiv);
return;

@@ -205,9 +138,12 @@ }

const isEditing = editingId === acc.id;
const avatarUrl = avatarCache[acc.id];
const icon = acc.icon || "📺";
// Tastenkürzel-Nummer basiert auf Position in der ECHTEN (ungefilterten) Liste
const shortcutIndex = accounts.findIndex(a => a.id === acc.id) + 1;
// Login-Status ermitteln
const status = loginStatus[acc.id];
const item = document.createElement("div");
item.className = "account-item";
item.className = "account-item" + (linkSelectedIds.has(acc.id) ? " selected-for-link" : "");
item.dataset.id = acc.id;

@@ -217,20 +153,52 @@ item.draggable = !isEditing && !searchTerm; // Drag&Drop nur in ungefilterter Ansicht sinnvoll

if (isEditing) {
item.appendChild(mkAvatar(colorObj.hex, avatarUrl));
const inp2 = document.createElement("input");
inp2.className = "account-name-edit";
inp2.id = "edit-" + acc.id;
inp2.value = acc.name;
inp2.maxLength = 40;
item.appendChild(inp2);
item.appendChild(mkIconBtn("save-btn", "save", acc.id, "Speichern", "✔"));
item.appendChild(mkIconBtn("del-btn", "cancel", acc.id, "Abbrechen", "✕"));
const avatar = document.createElement("div");
avatar.className = "avatar";
avatar.style.background = colorObj.hex;
avatar.textContent = icon;
const input = document.createElement("input");
input.className = "account-name-edit";
input.id = "edit-" + acc.id;
input.value = acc.name;
input.maxLength = 40;
const saveBtn = document.createElement("button");
saveBtn.className = "btn-icon save-btn";
saveBtn.dataset.action = "save";
saveBtn.dataset.id = acc.id;
saveBtn.title = "Speichern";
saveBtn.textContent = "✔";
const cancelBtn = document.createElement("button");
cancelBtn.className = "btn-icon del-btn";
cancelBtn.dataset.action = "cancel";
cancelBtn.dataset.id = acc.id;
cancelBtn.title = "Abbrechen";
cancelBtn.textContent = "✕";
item.appendChild(avatar);
item.appendChild(input);
item.appendChild(saveBtn);
item.appendChild(cancelBtn);
} else {
const drag = document.createElement("span");
drag.className = "drag-handle";
drag.title = "Ziehen zum Sortieren";
drag.textContent = "☰";
item.appendChild(drag);
const sc = mkShortcut(shortcutIndex);
if (sc) item.appendChild(sc);
item.appendChild(mkAvatar(colorObj.hex, avatarUrl));
const dragHandle = document.createElement("span");
dragHandle.className = "drag-handle";
dragHandle.title = "Ziehen zum Sortieren";
dragHandle.textContent = "☰";
item.appendChild(dragHandle);
if (shortcutIndex <= 9) {
const shortcutSpan = document.createElement("span");
shortcutSpan.className = "shortcut-num";
shortcutSpan.title = "Alt+Shift+" + shortcutIndex;
shortcutSpan.textContent = String(shortcutIndex);
item.appendChild(shortcutSpan);
}
const avatar = document.createElement("div");
avatar.className = "avatar";
avatar.style.background = colorObj.hex;
avatar.textContent = icon;
item.appendChild(avatar);
const nameSpan = document.createElement("span");

@@ -241,6 +209,49 @@ nameSpan.className = "account-name";

item.appendChild(nameSpan);
item.appendChild(mkBadge(status));
item.appendChild(mkIconBtn("open-btn", "open", acc.id, "Öffnen", "▶"));
item.appendChild(mkIconBtn("edit-btn", "edit", acc.id, "Umbenennen", "✎"));
item.appendChild(mkIconBtn("del-btn", "delete", acc.id, "Entfernen", "✕"));
// Badge
const badge = document.createElement("span");
badge.className = "login-badge";
if (status === true) {
badge.classList.add("logged-in");
badge.textContent = "✔ Eingeloggt";
} else if (status === false) {
badge.classList.add("logged-out");
badge.textContent = "⚠ Nicht eingeloggt";
} else {
badge.classList.add("checking");
badge.textContent = "… Prüfe";
}
item.appendChild(badge);
const openBtn = document.createElement("button");
openBtn.className = "btn-icon open-btn";
openBtn.dataset.action = "open";
openBtn.dataset.id = acc.id;
openBtn.title = "Öffnen";
openBtn.textContent = "▶";
item.appendChild(openBtn);
const linkBtn = document.createElement("button");
linkBtn.className = "btn-icon link-btn" + (linkSelectedIds.has(acc.id) ? " selected" : "");
linkBtn.dataset.action = "linkselect";
linkBtn.dataset.id = acc.id;
linkBtn.title = "Für Link-Öffner auswählen";
linkBtn.textContent = "🔗";
item.appendChild(linkBtn);
const editBtn = document.createElement("button");
editBtn.className = "btn-icon edit-btn";
editBtn.dataset.action = "edit";
editBtn.dataset.id = acc.id;
editBtn.title = "Umbenennen";
editBtn.textContent = "✎";
item.appendChild(editBtn);
const delBtn = document.createElement("button");
delBtn.className = "btn-icon del-btn";
delBtn.dataset.action = "delete";
delBtn.dataset.id = acc.id;
delBtn.title = "Entfernen";
delBtn.textContent = "✕";
item.appendChild(delBtn);
}

@@ -310,2 +321,3 @@

if (action === "open") openSingle(id);
if (action === "linkselect") selectForLink(id);
if (action === "edit") startEdit(id);

@@ -341,3 +353,3 @@ if (action === "save") saveEdit(id);

btn.disabled = true;
btn.textContent = "⏳ WIRD GEÖFFNET...";
btn.innerHTML = "<span>⏳</span><span>WIRD GEÖFFNET...</span>";
try {

@@ -354,3 +366,3 @@ const res = await browser.runtime.sendMessage({ action: "open_all" });

btn.disabled = accounts.length === 0;
btn.textContent = "▶ ALLE ACCOUNTS ÖFFNEN";
btn.innerHTML = "<span>▶</span><span>ALLE ACCOUNTS ÖFFNEN</span>";
setTimeout(updateLoginStatus, 3000);

@@ -378,3 +390,3 @@ });

const newAcc = { id: uid(), name, color: selectedColor };
const newAcc = { id: uid(), name, color: selectedColor, icon: selectedIcon };
accounts.push(newAcc);

@@ -420,4 +432,2 @@ loginStatus[newAcc.id] = false; // noch nicht eingeloggt

delete loginStatus[id];
delete avatarCache[id];
await browser.storage.local.set({ avatarCache });
await saveAccounts();

@@ -445,2 +455,17 @@ await browser.runtime.sendMessage({ action: "delete_container", account: acc });

function renderIconPicker() {
const ip = document.getElementById("iconPicker");
ICONS.forEach(icon => {
const sw = document.createElement("div");
sw.className = "icon-swatch" + (icon === selectedIcon ? " selected" : "");
sw.textContent = icon;
sw.addEventListener("click", () => {
selectedIcon = icon;
document.querySelectorAll(".icon-swatch").forEach(s => s.classList.remove("selected"));
sw.classList.add("selected");
});
ip.appendChild(sw);
});
}
// ── FEATURE: Suche ───────────────────────────────────────────────

@@ -503,3 +528,4 @@ document.getElementById("searchInput").addEventListener("input", e => {

name: acc.name,
color: COLORS.find(c => c.id === acc.color) ? acc.color : "red"
color: COLORS.find(c => c.id === acc.color) ? acc.color : "red",
icon: ICONS.includes(acc.icon) ? acc.icon : ICONS[0]
});

@@ -516,70 +542,317 @@ added++;

// ── FEATURE: Comments Finder (Link-basiert) ────────────────────────
// Nimmt einen beliebigen YouTube-Link entgegen und leitet den gewählten
// Account (oder gleichzeitig alle Accounts) direkt dorthin weiter.
function normalizeYoutubeLink(raw) {
let url = String(raw || "").trim();
if (!url) return null;
if (!/^https?:\/\//i.test(url)) url = "https://" + url;
try {
const u = new URL(url);
if (!/(^|\.)youtube\.com$|(^|\.)youtu\.be$/i.test(u.hostname)) return null;
return u.href;
} catch (e) { return null; }
// ── Init ─────────────────────────────────────────────────────────
renderColorPicker();
renderIconPicker();
loadAccounts();
// ================================================================
// NEUE FEATURES v3.2
// ================================================================
// ── Link-Öffner: Multi-Account-Auswahl ──────────────────────────
function selectForLink(id) {
const acc = accounts.find(a => a.id === id);
if (!acc) return;
if (linkSelectedIds.has(id)) {
linkSelectedIds.delete(id);
} else {
linkSelectedIds.add(id);
}
updateLinkBtn();
renderList();
if (typeof updateCommentBtn === 'function') updateCommentBtn();
}
function populateFinderAccountSelect() {
const sel = document.getElementById("finderAccountSelect");
const current = sel.value;
sel.textContent = ""; const allOpt = document.createElement("option"); allOpt.value = "__all__"; allOpt.textContent = "🌐 ALLE Accounts"; sel.appendChild(allOpt);
accounts.forEach(acc => {
const opt = document.createElement("option");
opt.value = acc.id;
opt.textContent = acc.name;
sel.appendChild(opt);
});
if (current && (current === "__all__" || accounts.find(a => a.id === current))) sel.value = current;
function selectAllForLink() {
if (linkSelectedIds.size === accounts.length) {
linkSelectedIds.clear();
showToast("🔗 Alle abgewählt", "info");
} else {
accounts.forEach(a => linkSelectedIds.add(a.id));
showToast(`🔗 Alle ${accounts.length} Accounts ausgewählt`, "info");
}
updateLinkBtn();
renderList();
if (typeof updateCommentBtn === 'function') updateCommentBtn();
}
document.getElementById("btnToggleFinder").addEventListener("click", () => {
const panel = document.getElementById("finderPanel");
panel.classList.toggle("open");
if (panel.classList.contains("open")) {
populateFinderAccountSelect();
document.getElementById("finderLink").focus();
function updateLinkBtn() {
const btn = document.getElementById("btnOpenLink");
const label = document.getElementById("linkBtnLabel");
const status = document.getElementById("linkStatus");
const allBtn = document.getElementById("btnSelectAll");
if (!btn) return;
const selected = accounts.filter(a => linkSelectedIds.has(a.id));
// "Alle auswählen"-Button Text
if (allBtn) {
if (accounts.length > 0 && linkSelectedIds.size === accounts.length) {
allBtn.textContent = "✕ Alle abwählen";
allBtn.classList.add("active");
} else {
allBtn.textContent = "☑ Alle auswählen";
allBtn.classList.remove("active");
}
}
if (selected.length === 0) {
btn.disabled = true;
label.textContent = "Zuerst Account(s) auswählen (🔗 klicken)";
if (status) { status.textContent = "👆 Accounts unten bei 🔗 auswählen"; status.classList.remove("err"); }
} else if (selected.length === 1) {
btn.disabled = false;
label.textContent = `Mit „${selected[0].icon || "📺"} ${selected[0].name}" öffnen`;
if (status) { status.textContent = `✅ 1 Account: ${selected[0].name}`; status.classList.remove("err"); }
} else {
btn.disabled = false;
label.textContent = `Mit ${selected.length} Accounts öffnen`;
const names = selected.slice(0, 3).map(a => a.name).join(", ") + (selected.length > 3 ? "..." : "");
if (status) { status.textContent = `✅ ${selected.length} Accounts: ${names}`; status.classList.remove("err"); }
}
}
// 📋 Einfügen
document.getElementById("btnPasteLink").addEventListener("click", async () => {
try {
const text = await navigator.clipboard.readText();
if (text) {
document.getElementById("linkInput").value = text.trim();
const status = document.getElementById("linkStatus");
if (status) status.textContent = "📋 Eingefügt — jetzt Öffnen klicken";
}
} catch (_) {
document.getElementById("linkInput").focus();
const status = document.getElementById("linkStatus");
if (status) status.textContent = "💡 Strg+V zum Einfügen";
}
});
document.getElementById("finderLink").addEventListener("keydown", e => {
if (e.key === "Enter") document.getElementById("btnFinderOpen").click();
// ☑ Alle auswählen
document.getElementById("btnSelectAll").addEventListener("click", selectAllForLink);
// 🔗 Link öffnen (alle ausgewählten Accounts)
document.getElementById("btnOpenLink").addEventListener("click", async () => {
const url = document.getElementById("linkInput").value.trim();
const status = document.getElementById("linkStatus");
const selected = accounts.filter(a => linkSelectedIds.has(a.id));
if (!url) {
status.textContent = "❌ Bitte einen Link eingeben!";
status.classList.add("err"); return;
}
if (selected.length === 0) {
status.textContent = "❌ Bitte mindestens einen Account auswählen (🔗)!";
status.classList.add("err"); return;
}
let finalUrl = url;
if (!url.startsWith("http://") && !url.startsWith("https://")) {
finalUrl = "https://" + url;
}
status.textContent = `⏳ Öffne mit ${selected.length} Account(s)...`;
status.classList.remove("err");
let ok = 0, fail = 0;
for (const acc of selected) {
try {
const res = await browser.runtime.sendMessage({
action: "open_link", account: acc, url: finalUrl
});
if (res && res.ok) ok++; else fail++;
} catch (_) { fail++; }
}
if (fail === 0) {
status.textContent = `✅ ${ok} Tab(s) geöffnet!`;
document.getElementById("linkInput").value = "";
} else {
status.textContent = `⚠️ ${ok} OK, ${fail} Fehler`;
status.classList.add("err");
}
});
document.getElementById("btnFinderOpen").addEventListener("click", async () => {
const url = normalizeYoutubeLink(document.getElementById("finderLink").value);
if (!url) { showToast("Bitte einen gültigen YouTube-Link einfügen.", "error"); return; }
// Enter im Link-Feld
document.getElementById("linkInput").addEventListener("keydown", e => {
if (e.key === "Enter") document.getElementById("btnOpenLink").click();
});
const choice = document.getElementById("finderAccountSelect").value;
if (!choice) { showToast("Bitte einen Account oder ALLE auswählen.", "error"); return; }
// Live-Feedback beim Tippen
document.getElementById("linkInput").addEventListener("input", () => {
const status = document.getElementById("linkStatus");
const sel = accounts.filter(a => linkSelectedIds.has(a.id));
if (sel.length > 0 && document.getElementById("linkInput").value.trim()) {
status.textContent = `✅ ${sel.length} Account(s) — Enter oder Öffnen drücken`;
status.classList.remove("err");
}
});
const btn = document.getElementById("btnFinderOpen");
btn.disabled = true;
updateLinkBtn();
// ── HD / FPS Unlocker Toggle ──────────────────────────────────────
let hdEnabled = false;
async function loadHdState() {
const d = await browser.storage.local.get("hdUnlockerEnabled");
hdEnabled = !!d.hdUnlockerEnabled;
renderHdBtn();
}
function renderHdBtn() {
const btn = document.getElementById("btnHdToggle");
if (!btn) return;
if (hdEnabled) {
btn.textContent = "✅ AN";
btn.classList.add("active");
} else {
btn.textContent = "AUS";
btn.classList.remove("active");
}
}
document.getElementById("btnHdToggle").addEventListener("click", async () => {
hdEnabled = !hdEnabled;
renderHdBtn();
await browser.runtime.sendMessage({ action: "toggle_hd_unlocker", enabled: hdEnabled });
showToast(
hdEnabled ? "🎬 HD-Unlocker AN! Seite neu laden." : "🎬 HD-Unlocker AUS.",
hdEnabled ? "success" : "info"
);
});
loadHdState();
// ================================================================
// COMMENTER v3.3
// ================================================================
let currentVideoUrl = null;
// Aktuelles Tab / Video erkennen
async function detectCurrentVideo() {
const infoEl = document.getElementById('commentVideoInfo');
const btnSend = document.getElementById('btnSendComment');
try {
if (choice === "__all__") {
if (accounts.length === 0) { showToast("Keine Accounts konfiguriert.", "error"); btn.disabled = false; return; }
const res = await browser.runtime.sendMessage({ action: "open_channel_all", url });
showToast(`✅ In ${res.opened} Account(s) geöffnet!`, "success");
const tabs = await browser.tabs.query({ active: true, currentWindow: true });
const tab = tabs[0];
if (tab && tab.url && tab.url.includes('youtube.com/watch')) {
currentVideoUrl = tab.url;
const urlObj = new URL(tab.url);
const vid = urlObj.searchParams.get('v') || '';
infoEl.textContent = '';
infoEl.appendChild(document.createTextNode('📺 Video erkannt: '));
const titleSpan = document.createElement('span');
titleSpan.textContent = tab.title || vid;
infoEl.appendChild(titleSpan);
} else if (tab && tab.url && (tab.url.includes('youtube.com/shorts') || tab.url.includes('youtube.com/live'))) {
currentVideoUrl = tab.url;
infoEl.textContent = '';
infoEl.appendChild(document.createTextNode('📺 Video erkannt: '));
const titleSpan = document.createElement('span');
titleSpan.textContent = tab.title || tab.url;
infoEl.appendChild(titleSpan);
} else {
const acc = accounts.find(a => a.id === choice);
if (!acc) { showToast("Account nicht gefunden.", "error"); btn.disabled = false; return; }
await browser.runtime.sendMessage({ action: "open_channel_single", account: acc, url });
showToast(`▶ Geöffnet in "${acc.name}"`, "success");
currentVideoUrl = null;
infoEl.textContent = '⚠️ Kein YouTube-Video im aktiven Tab';
}
} catch (e) {
showToast("Fehler: " + e.message, "error");
currentVideoUrl = null;
infoEl.textContent = '⚠️ Tab nicht erkannt';
}
updateCommentBtn();
}
function updateCommentBtn() {
const btn = document.getElementById('btnSendComment');
const label = document.getElementById('commentBtnLabel');
const status = document.getElementById('commentStatus');
const text = (document.getElementById('commentText')?.value || '').trim();
const selected = accounts.filter(a => linkSelectedIds.has(a.id));
if (!currentVideoUrl) {
btn.disabled = true;
label.textContent = '⚠️ Kein YouTube-Video aktiv';
return;
}
if (selected.length === 0) {
btn.disabled = true;
label.textContent = 'Accounts auswählen (🔗)';
if (status) { status.textContent = '👆 Accounts mit 🔗 auswählen'; status.className = 'comment-status'; }
return;
}
if (!text) {
btn.disabled = true;
label.textContent = 'Kommentar eingeben...';
if (status) { status.textContent = `✅ ${selected.length} Account(s) bereit – Kommentar fehlt`; status.className = 'comment-status'; }
return;
}
btn.disabled = false;
label.textContent = `An ${selected.length} Account(s) senden`;
if (status) { status.textContent = `✅ Bereit – ${selected.length} Account(s) ausgewählt`; status.className = 'comment-status'; }
}
document.getElementById('commentText').addEventListener('input', updateCommentBtn);
document.getElementById('btnSendComment').addEventListener('click', async () => {
const text = document.getElementById('commentText').value.trim();
const selected = accounts.filter(a => linkSelectedIds.has(a.id));
const status = document.getElementById('commentStatus');
const btn = document.getElementById('btnSendComment');
if (!text || !currentVideoUrl || selected.length === 0) return;
btn.disabled = true;
btn.textContent = '';
const loadingIcon = document.createElement('span');
loadingIcon.textContent = '⏳';
const loadingText = document.createElement('span');
loadingText.textContent = 'Wird gesendet...';
btn.appendChild(loadingIcon);
btn.appendChild(loadingText);
status.textContent = `⏳ Öffne ${selected.length} Tab(s)...`;
status.className = 'comment-status';
let ok = 0, fail = 0;
for (const acc of selected) {
try {
const res = await browser.runtime.sendMessage({
action: 'open_for_comment',
account: acc,
url: currentVideoUrl,
comment: text
});
if (res && res.ok) ok++; else fail++;
} catch (_) { fail++; }
await new Promise(r => setTimeout(r, 400));
}
if (fail === 0) {
status.textContent = `✅ ${ok} Tab(s) geöffnet – Kommentar wird automatisch gepostet!`;
status.className = 'comment-status ok';
document.getElementById('commentText').value = '';
} else {
status.textContent = `⚠️ ${ok} OK, ${fail} Fehler`;
status.className = 'comment-status err';
}
btn.textContent = '';
const doneIcon = document.createElement('span');
doneIcon.textContent = '💬';
const doneLabel = document.createElement('span');
doneLabel.id = 'commentBtnLabel';
doneLabel.textContent = `An ${selected.length} Account(s) senden`;
btn.appendChild(doneIcon);
btn.appendChild(doneLabel);
btn.disabled = false;
updateCommentBtn();
});
// ── Init ─────────────────────────────────────────────────────────
renderColorPicker();
loadAccounts();
// Video erkennen beim Öffnen
detectCurrentVideo();

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet