sonstiges

This commit is contained in:
MDeeApp
2025-10-06 00:01:54 +02:00
parent be7b65b799
commit 327a663bcf
4 changed files with 422 additions and 98 deletions

View File

@@ -1,25 +1,51 @@
// Background script for service worker
// Currently minimal, can be extended for additional functionality
chrome.runtime.onInstalled.addListener(() => {
console.log('Facebook Post Tracker extension installed');
// Set default profile if not set
chrome.storage.sync.get(['profileNumber'], (result) => {
if (!result.profileNumber) {
chrome.storage.sync.set({ profileNumber: 1 });
function setupContextMenus() {
chrome.contextMenus.removeAll(() => {
if (chrome.runtime.lastError) {
console.warn('FB Tracker: context menu reset warning:', chrome.runtime.lastError.message);
}
});
// Create context menu for manual post parsing
chrome.contextMenus.create({
id: 'fb-tracker-reparse',
title: 'FB Tracker: Post neu parsen',
contexts: ['all'],
documentUrlPatterns: ['*://*.facebook.com/*']
chrome.contextMenus.create({
id: 'fb-tracker-reparse',
title: 'FB Tracker: Post neu parsen',
contexts: ['all'],
documentUrlPatterns: ['*://*.facebook.com/*']
});
chrome.contextMenus.create({
id: 'fb-tracker-selection-ai',
title: 'FB Tracker: Auswahl mit AI beantworten',
contexts: ['selection'],
documentUrlPatterns: ['*://*.facebook.com/*']
});
});
}
chrome.runtime.onInstalled.addListener((details) => {
console.log('Facebook Post Tracker extension installed/updated');
if (details.reason === 'install') {
chrome.storage.sync.get(['profileNumber'], (result) => {
if (!result.profileNumber) {
chrome.storage.sync.set({ profileNumber: 1 });
}
});
}
setupContextMenus();
});
if (chrome.runtime.onStartup) {
chrome.runtime.onStartup.addListener(() => {
setupContextMenus();
});
}
// Ensure menus exist after service worker restarts
setupContextMenus();
chrome.contextMenus.onClicked.addListener((info, tab) => {
if (info.menuItemId === 'fb-tracker-reparse') {
chrome.tabs.sendMessage(tab.id, {
@@ -27,6 +53,16 @@ chrome.contextMenus.onClicked.addListener((info, tab) => {
x: info.pageX || 0,
y: info.pageY || 0
});
} else if (info.menuItemId === 'fb-tracker-selection-ai') {
chrome.tabs.sendMessage(tab.id, {
type: 'generateSelectionAI',
selectionText: info.selectionText || ''
}, (response) => {
if (chrome.runtime.lastError) {
console.warn('FB Tracker selection AI error:', chrome.runtime.lastError.message);
}
return true;
});
}
});

View File

@@ -18,6 +18,7 @@ const sessionSearchRecordedUrls = new Set();
const sessionSearchInfoCache = new Map();
const trackerElementsByPost = new WeakMap();
const postAdditionalNotes = new WeakMap();
const REELS_PATH_PREFIX = '/reel/';
@@ -2614,6 +2615,11 @@ document.addEventListener('contextmenu', (event) => {
// Listen for manual reparse command
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message && message.type === 'generateSelectionAI') {
handleSelectionAIRequest(message.selectionText || '', sendResponse);
return true;
}
if (message && message.type === 'reparsePost') {
console.log('[FB Tracker] Manual reparse triggered');
@@ -2752,6 +2758,38 @@ function showToast(message, type = 'info') {
}, 3000);
}
async function copyTextToClipboard(text) {
if (typeof text !== 'string') {
return false;
}
if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
try {
await navigator.clipboard.writeText(text);
return true;
} catch (error) {
console.warn('[FB Tracker] navigator.clipboard.writeText failed:', error);
}
}
try {
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.top = '-999px';
textarea.style.left = '-999px';
document.body.appendChild(textarea);
textarea.focus();
textarea.select();
const success = document.execCommand('copy');
document.body.removeChild(textarea);
return success;
} catch (error) {
console.warn('[FB Tracker] execCommand copy fallback failed:', error);
return false;
}
}
/**
* Extract post text from a Facebook post element
*/
@@ -3328,6 +3366,38 @@ async function generateAIComment(postText, profileNumber, options = {}) {
}
}
async function handleSelectionAIRequest(selectionText, sendResponse) {
try {
const normalizedSelection = normalizeSelectionText(selectionText);
if (!normalizedSelection) {
showToast('Keine gültige Auswahl gefunden', 'error');
sendResponse({ success: false, error: 'Keine gültige Auswahl gefunden' });
return;
}
showToast('AI verarbeitet Auswahl...', 'info');
const profileNumber = await getProfileNumber();
const comment = await generateAIComment(normalizedSelection, profileNumber, {});
if (!comment) {
throw new Error('Keine Antwort vom AI-Dienst erhalten');
}
const copied = await copyTextToClipboard(comment);
if (!copied) {
throw new Error('Antwort konnte nicht in die Zwischenablage kopiert werden');
}
showToast('AI-Antwort in die Zwischenablage kopiert', 'success');
sendResponse({ success: true, comment });
} catch (error) {
console.error('[FB Tracker] Selection AI error:', error);
showToast(`${error.message || 'Fehler bei AI-Anfrage'}`, 'error');
sendResponse({ success: false, error: error.message || 'Unbekannter Fehler' });
}
}
/**
* Check if AI is enabled
*/
@@ -3419,9 +3489,6 @@ async function addAICommentButton(container, postElement) {
dropdown.className = 'fb-tracker-ai-dropdown';
dropdown.style.cssText = `
display: none;
position: absolute;
right: 0;
top: calc(100% + 6px);
min-width: 220px;
background: #ffffff;
border-radius: 8px;
@@ -3435,6 +3502,75 @@ async function addAICommentButton(container, postElement) {
wrapper.appendChild(dropdown);
container.appendChild(wrapper);
const baseButtonText = button.textContent;
const resolvePostContexts = () => {
const contextCandidate = container.closest('div[aria-posinset], article[role="article"], article, div[role="complementary"]');
const normalizedContext = contextCandidate ? ensurePrimaryPostElement(contextCandidate) : null;
const fallbackContext = postElement ? ensurePrimaryPostElement(postElement) : null;
const postContext = normalizedContext || fallbackContext || contextCandidate || postElement || container;
return { postContext, contextCandidate, fallbackContext, normalizedContext };
};
const resolvePostContext = () => resolvePostContexts().postContext;
const getAdditionalNote = () => {
const context = resolvePostContext();
return context ? (postAdditionalNotes.get(context) || '') : '';
};
let notePreviewElement = null;
let noteClearButton = null;
const truncateNoteForPreview = (note) => {
if (!note) {
return '';
}
return note.length > 120 ? `${note.slice(0, 117)}` : note;
};
const updateNoteIndicator = () => {
const note = getAdditionalNote();
const hasNote = note.trim().length > 0;
button.dataset.aiOriginalText = hasNote ? `${baseButtonText}` : baseButtonText;
if ((button.dataset.aiState || 'idle') === 'idle' && !button._aiContext) {
button.textContent = button.dataset.aiOriginalText;
}
button.title = hasNote
? 'Generiere automatisch einen passenden Kommentar (mit Zusatzinfo)'
: 'Generiere automatisch einen passenden Kommentar';
};
const updateNotePreview = () => {
updateNoteIndicator();
if (notePreviewElement) {
const note = getAdditionalNote();
notePreviewElement.textContent = note
? `Aktuelle Zusatzinfo: ${truncateNoteForPreview(note)}`
: 'Keine Zusatzinfo gesetzt';
if (noteClearButton) {
const hasNote = note.trim().length > 0;
noteClearButton.disabled = !hasNote;
noteClearButton.style.opacity = hasNote ? '1' : '0.6';
noteClearButton.style.cursor = hasNote ? 'pointer' : 'default';
}
}
};
const setAdditionalNote = (value) => {
const context = resolvePostContext();
if (!context) {
return;
}
const trimmed = (value || '').trim();
if (trimmed) {
postAdditionalNotes.set(context, trimmed);
} else {
postAdditionalNotes.delete(context);
}
updateNotePreview();
};
button.addEventListener('mouseenter', () => {
if ((button.dataset.aiState || 'idle') === 'idle') {
button.style.transform = 'translateY(-2px)';
@@ -3470,10 +3606,8 @@ async function addAICommentButton(container, postElement) {
});
button.addEventListener('pointerdown', () => {
const contextElement = container.closest('div[aria-posinset], article[role="article"], article, div[role="complementary"]');
const normalized = contextElement ? ensurePrimaryPostElement(contextElement) : null;
const fallbackNormalized = postElement ? ensurePrimaryPostElement(postElement) : null;
const target = normalized || fallbackNormalized || contextElement || postElement || container;
const context = resolvePostContext();
const target = context || postElement || container;
cacheSelectionForPost(target);
});
@@ -3481,6 +3615,32 @@ async function addAICommentButton(container, postElement) {
button.dataset.aiOriginalText = button.textContent;
let dropdownOpen = false;
let dropdownPortalParent = null;
const resolveDropdownPortalParent = () => {
if (dropdownPortalParent && dropdownPortalParent.isConnected) {
return dropdownPortalParent;
}
const candidate = document.body || document.documentElement;
dropdownPortalParent = candidate;
return dropdownPortalParent;
};
const mountDropdownInPortal = () => {
const portalParent = resolveDropdownPortalParent();
if (!portalParent) {
return;
}
if (dropdown.parentElement !== portalParent) {
portalParent.appendChild(dropdown);
}
};
const restoreDropdownToWrapper = () => {
if (dropdown.parentElement !== wrapper) {
wrapper.appendChild(dropdown);
}
};
const closeDropdown = () => {
if (!dropdownOpen) {
@@ -3497,10 +3657,18 @@ async function addAICommentButton(container, postElement) {
dropdownButton.style.transform = 'translateY(0)';
button.style.boxShadow = 'none';
dropdownButton.style.boxShadow = 'none';
dropdown.style.position = '';
dropdown.style.top = '';
dropdown.style.left = '';
dropdown.style.maxHeight = '';
dropdown.style.overflowY = '';
restoreDropdownToWrapper();
window.removeEventListener('scroll', repositionDropdown, true);
window.removeEventListener('resize', repositionDropdown);
};
const handleOutsideClick = (event) => {
if (!wrapper.contains(event.target)) {
if (!wrapper.contains(event.target) && !dropdown.contains(event.target)) {
closeDropdown();
}
};
@@ -3518,82 +3686,187 @@ async function addAICommentButton(container, postElement) {
loading.style.cssText = 'padding: 8px 14px; font-size: 13px; color: #555;';
dropdown.appendChild(loading);
const appendNoteUI = () => {
noteClearButton = null;
const noteSection = document.createElement('div');
noteSection.style.cssText = 'padding: 10px 14px; display: flex; flex-direction: column; gap: 6px;';
notePreviewElement = document.createElement('div');
notePreviewElement.style.cssText = 'font-size: 12px; color: #65676b; white-space: normal;';
noteSection.appendChild(notePreviewElement);
const buttonsRow = document.createElement('div');
buttonsRow.style.cssText = 'display: flex; gap: 8px; flex-wrap: wrap;';
const editButton = document.createElement('button');
editButton.type = 'button';
editButton.textContent = 'Zusatzinfo bearbeiten';
editButton.style.cssText = 'padding: 6px 10px; border-radius: 6px; border: 1px solid #ccd0d5; background: #fff; cursor: pointer; font-size: 12px; font-weight: 600; color: #1d2129;';
editButton.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
const existingNote = getAdditionalNote();
const input = window.prompt('Zusatzinfo für den AI-Prompt eingeben:', existingNote);
if (input === null) {
return;
}
const trimmed = (input || '').trim();
setAdditionalNote(trimmed);
if (trimmed) {
showToast('Zusatzinfo gespeichert', 'success');
} else {
showToast('Zusatzinfo entfernt', 'success');
}
});
buttonsRow.appendChild(editButton);
const clearButton = document.createElement('button');
clearButton.type = 'button';
clearButton.textContent = 'Zurücksetzen';
clearButton.style.cssText = 'padding: 6px 10px; border-radius: 6px; border: 1px solid #ccd0d5; background: #fff; cursor: pointer; font-size: 12px; font-weight: 600; color: #a11900;';
clearButton.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
if (!getAdditionalNote()) {
return;
}
setAdditionalNote('');
showToast('Zusatzinfo entfernt', 'success');
});
buttonsRow.appendChild(clearButton);
noteClearButton = clearButton;
noteSection.appendChild(buttonsRow);
dropdown.appendChild(noteSection);
updateNotePreview();
};
try {
const credentials = await fetchActiveAICredentials();
dropdown.innerHTML = '';
appendNoteUI();
if (!credentials || credentials.length === 0) {
const empty = document.createElement('div');
empty.textContent = 'Keine aktiven AI-Anbieter gefunden';
empty.style.cssText = 'padding: 8px 14px; font-size: 13px; color: #888;';
dropdown.appendChild(empty);
return;
}
} else {
const divider = document.createElement('div');
divider.style.cssText = 'border-top: 1px solid #e4e6eb; margin: 6px 0;';
dropdown.appendChild(divider);
credentials.forEach((credential) => {
const option = document.createElement('button');
option.type = 'button';
option.className = 'fb-tracker-ai-option';
option.style.cssText = `
width: 100%;
padding: 8px 14px;
background: transparent;
border: none;
text-align: left;
cursor: pointer;
font-size: 13px;
display: flex;
flex-direction: column;
gap: 2px;
`;
credentials.forEach((credential) => {
const option = document.createElement('button');
option.type = 'button';
option.className = 'fb-tracker-ai-option';
option.style.cssText = `
width: 100%;
padding: 8px 14px;
background: transparent;
border: none;
text-align: left;
cursor: pointer;
font-size: 13px;
display: flex;
flex-direction: column;
gap: 2px;
`;
option.addEventListener('mouseenter', () => {
option.style.background = '#f0f2f5';
});
option.addEventListener('mouseenter', () => {
option.style.background = '#f0f2f5';
});
option.addEventListener('mouseleave', () => {
option.style.background = 'transparent';
});
option.addEventListener('mouseleave', () => {
option.style.background = 'transparent';
});
const label = document.createElement('span');
label.textContent = formatAICredentialLabel(credential);
label.style.cssText = 'font-weight: 600; color: #1d2129;';
const label = document.createElement('span');
label.textContent = formatAICredentialLabel(credential);
label.style.cssText = 'font-weight: 600; color: #1d2129;';
const metaParts = [];
if (credential.provider) {
metaParts.push(`Provider: ${credential.provider}`);
}
if (credential.model) {
metaParts.push(`Modell: ${credential.model}`);
}
if (metaParts.length > 0) {
const meta = document.createElement('span');
meta.textContent = metaParts.join(' · ');
meta.style.cssText = 'font-size: 12px; color: #65676b;';
option.appendChild(label);
option.appendChild(meta);
} else {
option.appendChild(label);
}
option.addEventListener('click', () => {
closeDropdown();
if ((button.dataset.aiState || 'idle') === 'idle') {
cacheSelectionForPost(postElement);
startAIFlow(credential.id);
const metaParts = [];
if (credential.provider) {
metaParts.push(`Provider: ${credential.provider}`);
}
if (credential.model) {
metaParts.push(`Modell: ${credential.model}`);
}
});
dropdown.appendChild(option);
});
if (metaParts.length > 0) {
const meta = document.createElement('span');
meta.textContent = metaParts.join(' · ');
meta.style.cssText = 'font-size: 12px; color: #65676b;';
option.appendChild(label);
option.appendChild(meta);
} else {
option.appendChild(label);
}
option.addEventListener('click', () => {
closeDropdown();
if ((button.dataset.aiState || 'idle') === 'idle') {
cacheSelectionForPost(postElement);
startAIFlow(credential.id);
}
});
dropdown.appendChild(option);
});
}
} catch (error) {
dropdown.innerHTML = '';
appendNoteUI();
const errorItem = document.createElement('div');
errorItem.textContent = error.message || 'AI-Anbieter konnten nicht geladen werden';
errorItem.style.cssText = 'padding: 8px 14px; font-size: 13px; color: #c0392b; white-space: normal;';
dropdown.appendChild(errorItem);
}
};
const positionDropdown = () => {
if (!dropdownOpen) {
return;
}
mountDropdownInPortal();
dropdown.style.position = 'fixed';
dropdown.style.maxHeight = `${Math.max(200, Math.floor(window.innerHeight * 0.6))}px`;
dropdown.style.overflowY = 'auto';
const rect = wrapper.getBoundingClientRect();
const dropdownRect = dropdown.getBoundingClientRect();
const margin = 8;
let top = rect.top - dropdownRect.height - margin;
if (top < margin) {
top = rect.bottom + margin;
}
const viewportPadding = 8;
let left = rect.right - dropdownRect.width;
if (left < viewportPadding) {
left = viewportPadding;
}
const maxLeft = window.innerWidth - dropdownRect.width - viewportPadding;
if (left > maxLeft) {
left = Math.max(viewportPadding, maxLeft);
}
const maxTop = window.innerHeight - dropdownRect.height - margin;
if (top > maxTop) {
top = Math.max(viewportPadding, maxTop);
}
dropdown.style.top = `${top}px`;
dropdown.style.left = `${left}px`;
};
const repositionDropdown = () => {
if (dropdownOpen) {
positionDropdown();
}
};
const toggleDropdown = async () => {
@@ -3609,12 +3882,16 @@ async function addAICommentButton(container, postElement) {
dropdownOpen = true;
wrapper.classList.add('fb-tracker-ai-wrapper--open');
dropdownButton.textContent = '▴';
mountDropdownInPortal();
dropdown.style.display = 'block';
dropdownButton.setAttribute('aria-expanded', 'true');
document.addEventListener('click', handleOutsideClick, true);
document.addEventListener('keydown', handleKeydown, true);
await renderDropdownItems();
positionDropdown();
window.addEventListener('scroll', repositionDropdown, true);
window.addEventListener('resize', repositionDropdown);
};
dropdownButton.addEventListener('click', (event) => {
@@ -3708,10 +3985,8 @@ async function addAICommentButton(container, postElement) {
button.textContent = '⏳ Generiere...';
try {
const contextCandidate = container.closest('div[aria-posinset], article[role="article"], article, div[role="complementary"]');
const normalizedContext = contextCandidate ? ensurePrimaryPostElement(contextCandidate) : null;
const fallbackContext = postElement ? ensurePrimaryPostElement(postElement) : null;
const postContext = normalizedContext || fallbackContext || contextCandidate || postElement || container;
const contexts = resolvePostContexts();
const { postContext, contextCandidate, fallbackContext } = contexts;
const selectionKeys = [];
if (postContext) {
@@ -3723,6 +3998,12 @@ async function addAICommentButton(container, postElement) {
if (contextCandidate && contextCandidate !== postContext && contextCandidate !== postElement) {
selectionKeys.push(contextCandidate);
}
if (fallbackContext
&& fallbackContext !== postContext
&& fallbackContext !== postElement
&& fallbackContext !== contextCandidate) {
selectionKeys.push(fallbackContext);
}
const resolveRecentSelection = () => {
for (const key of selectionKeys) {
@@ -3775,15 +4056,20 @@ async function addAICommentButton(container, postElement) {
throw new Error('Konnte Post-Text nicht extrahieren');
}
selectionKeys.forEach((key) => {
if (key) {
postSelectionCache.delete(key);
}
});
selectionKeys.forEach((key) => {
if (key) {
postSelectionCache.delete(key);
}
});
throwIfCancelled();
const additionalNote = postContext ? (postAdditionalNotes.get(postContext) || '') : '';
if (additionalNote) {
postText = `${postText}\n\nZusatzinfo:\n${additionalNote}`;
}
console.log('[FB Tracker] Generating AI comment for:', postText.substring(0, 100));
throwIfCancelled();
console.log('[FB Tracker] Generating AI comment for:', postText.substring(0, 100));
const profileNumber = await getProfileNumber();

View File

@@ -7,7 +7,8 @@
"storage",
"activeTab",
"tabs",
"contextMenus"
"contextMenus",
"clipboardWrite"
],
"host_permissions": [
"<all_urls>",

View File

@@ -77,18 +77,19 @@ async function initProfileSelect() {
initProfileSelect();
chrome.storage.sync.get(['debugLoggingEnabled'], (result) => {
const enabled = Boolean(result && result.debugLoggingEnabled);
debugToggle.checked = enabled;
});
debugToggle.addEventListener('change', () => {
const enabled = debugToggle.checked;
chrome.storage.sync.set({ debugLoggingEnabled: enabled }, () => {
updateStatus(`Debug-Logging ${enabled ? 'aktiviert' : 'deaktiviert'}`, true);
reloadFacebookTabs();
if (debugToggle) {
chrome.storage.sync.get(['debugLoggingEnabled'], (result) => {
const enabled = Boolean(result && result.debugLoggingEnabled);
debugToggle.checked = enabled;
});
});
debugToggle.addEventListener('change', () => {
const enabled = debugToggle.checked;
chrome.storage.sync.set({ debugLoggingEnabled: enabled }, () => {
updateStatus(`Debug-Logging ${enabled ? 'aktiviert' : 'deaktiviert'} (wirksam ohne Reload)`, true);
});
});
}
function reloadFacebookTabs() {
chrome.tabs.query({ url: ['https://www.facebook.com/*', 'https://facebook.com/*'] }, (tabs) => {