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,24 +1,50 @@
// Background script for service worker // Background script for service worker
// Currently minimal, can be extended for additional functionality // Currently minimal, can be extended for additional functionality
chrome.runtime.onInstalled.addListener(() => { function setupContextMenus() {
console.log('Facebook Post Tracker extension installed'); chrome.contextMenus.removeAll(() => {
if (chrome.runtime.lastError) {
// Set default profile if not set console.warn('FB Tracker: context menu reset warning:', chrome.runtime.lastError.message);
chrome.storage.sync.get(['profileNumber'], (result) => {
if (!result.profileNumber) {
chrome.storage.sync.set({ profileNumber: 1 });
} }
});
// Create context menu for manual post parsing
chrome.contextMenus.create({ chrome.contextMenus.create({
id: 'fb-tracker-reparse', id: 'fb-tracker-reparse',
title: 'FB Tracker: Post neu parsen', title: 'FB Tracker: Post neu parsen',
contexts: ['all'], contexts: ['all'],
documentUrlPatterns: ['*://*.facebook.com/*'] 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) => { chrome.contextMenus.onClicked.addListener((info, tab) => {
if (info.menuItemId === 'fb-tracker-reparse') { if (info.menuItemId === 'fb-tracker-reparse') {
@@ -27,6 +53,16 @@ chrome.contextMenus.onClicked.addListener((info, tab) => {
x: info.pageX || 0, x: info.pageX || 0,
y: info.pageY || 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 sessionSearchInfoCache = new Map();
const trackerElementsByPost = new WeakMap(); const trackerElementsByPost = new WeakMap();
const postAdditionalNotes = new WeakMap();
const REELS_PATH_PREFIX = '/reel/'; const REELS_PATH_PREFIX = '/reel/';
@@ -2614,6 +2615,11 @@ document.addEventListener('contextmenu', (event) => {
// Listen for manual reparse command // Listen for manual reparse command
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message && message.type === 'generateSelectionAI') {
handleSelectionAIRequest(message.selectionText || '', sendResponse);
return true;
}
if (message && message.type === 'reparsePost') { if (message && message.type === 'reparsePost') {
console.log('[FB Tracker] Manual reparse triggered'); console.log('[FB Tracker] Manual reparse triggered');
@@ -2752,6 +2758,38 @@ function showToast(message, type = 'info') {
}, 3000); }, 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 * 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 * Check if AI is enabled
*/ */
@@ -3419,9 +3489,6 @@ async function addAICommentButton(container, postElement) {
dropdown.className = 'fb-tracker-ai-dropdown'; dropdown.className = 'fb-tracker-ai-dropdown';
dropdown.style.cssText = ` dropdown.style.cssText = `
display: none; display: none;
position: absolute;
right: 0;
top: calc(100% + 6px);
min-width: 220px; min-width: 220px;
background: #ffffff; background: #ffffff;
border-radius: 8px; border-radius: 8px;
@@ -3435,6 +3502,75 @@ async function addAICommentButton(container, postElement) {
wrapper.appendChild(dropdown); wrapper.appendChild(dropdown);
container.appendChild(wrapper); 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', () => { button.addEventListener('mouseenter', () => {
if ((button.dataset.aiState || 'idle') === 'idle') { if ((button.dataset.aiState || 'idle') === 'idle') {
button.style.transform = 'translateY(-2px)'; button.style.transform = 'translateY(-2px)';
@@ -3470,10 +3606,8 @@ async function addAICommentButton(container, postElement) {
}); });
button.addEventListener('pointerdown', () => { button.addEventListener('pointerdown', () => {
const contextElement = container.closest('div[aria-posinset], article[role="article"], article, div[role="complementary"]'); const context = resolvePostContext();
const normalized = contextElement ? ensurePrimaryPostElement(contextElement) : null; const target = context || postElement || container;
const fallbackNormalized = postElement ? ensurePrimaryPostElement(postElement) : null;
const target = normalized || fallbackNormalized || contextElement || postElement || container;
cacheSelectionForPost(target); cacheSelectionForPost(target);
}); });
@@ -3481,6 +3615,32 @@ async function addAICommentButton(container, postElement) {
button.dataset.aiOriginalText = button.textContent; button.dataset.aiOriginalText = button.textContent;
let dropdownOpen = false; 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 = () => { const closeDropdown = () => {
if (!dropdownOpen) { if (!dropdownOpen) {
@@ -3497,10 +3657,18 @@ async function addAICommentButton(container, postElement) {
dropdownButton.style.transform = 'translateY(0)'; dropdownButton.style.transform = 'translateY(0)';
button.style.boxShadow = 'none'; button.style.boxShadow = 'none';
dropdownButton.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) => { const handleOutsideClick = (event) => {
if (!wrapper.contains(event.target)) { if (!wrapper.contains(event.target) && !dropdown.contains(event.target)) {
closeDropdown(); closeDropdown();
} }
}; };
@@ -3518,17 +3686,76 @@ async function addAICommentButton(container, postElement) {
loading.style.cssText = 'padding: 8px 14px; font-size: 13px; color: #555;'; loading.style.cssText = 'padding: 8px 14px; font-size: 13px; color: #555;';
dropdown.appendChild(loading); 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 { try {
const credentials = await fetchActiveAICredentials(); const credentials = await fetchActiveAICredentials();
dropdown.innerHTML = ''; dropdown.innerHTML = '';
appendNoteUI();
if (!credentials || credentials.length === 0) { if (!credentials || credentials.length === 0) {
const empty = document.createElement('div'); const empty = document.createElement('div');
empty.textContent = 'Keine aktiven AI-Anbieter gefunden'; empty.textContent = 'Keine aktiven AI-Anbieter gefunden';
empty.style.cssText = 'padding: 8px 14px; font-size: 13px; color: #888;'; empty.style.cssText = 'padding: 8px 14px; font-size: 13px; color: #888;';
dropdown.appendChild(empty); 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) => { credentials.forEach((credential) => {
const option = document.createElement('button'); const option = document.createElement('button');
@@ -3587,8 +3814,10 @@ async function addAICommentButton(container, postElement) {
dropdown.appendChild(option); dropdown.appendChild(option);
}); });
}
} catch (error) { } catch (error) {
dropdown.innerHTML = ''; dropdown.innerHTML = '';
appendNoteUI();
const errorItem = document.createElement('div'); const errorItem = document.createElement('div');
errorItem.textContent = error.message || 'AI-Anbieter konnten nicht geladen werden'; errorItem.textContent = error.message || 'AI-Anbieter konnten nicht geladen werden';
errorItem.style.cssText = 'padding: 8px 14px; font-size: 13px; color: #c0392b; white-space: normal;'; errorItem.style.cssText = 'padding: 8px 14px; font-size: 13px; color: #c0392b; white-space: normal;';
@@ -3596,6 +3825,50 @@ async function addAICommentButton(container, postElement) {
} }
}; };
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 () => { const toggleDropdown = async () => {
if ((button.dataset.aiState || 'idle') !== 'idle') { if ((button.dataset.aiState || 'idle') !== 'idle') {
return; return;
@@ -3609,12 +3882,16 @@ async function addAICommentButton(container, postElement) {
dropdownOpen = true; dropdownOpen = true;
wrapper.classList.add('fb-tracker-ai-wrapper--open'); wrapper.classList.add('fb-tracker-ai-wrapper--open');
dropdownButton.textContent = '▴'; dropdownButton.textContent = '▴';
mountDropdownInPortal();
dropdown.style.display = 'block'; dropdown.style.display = 'block';
dropdownButton.setAttribute('aria-expanded', 'true'); dropdownButton.setAttribute('aria-expanded', 'true');
document.addEventListener('click', handleOutsideClick, true); document.addEventListener('click', handleOutsideClick, true);
document.addEventListener('keydown', handleKeydown, true); document.addEventListener('keydown', handleKeydown, true);
await renderDropdownItems(); await renderDropdownItems();
positionDropdown();
window.addEventListener('scroll', repositionDropdown, true);
window.addEventListener('resize', repositionDropdown);
}; };
dropdownButton.addEventListener('click', (event) => { dropdownButton.addEventListener('click', (event) => {
@@ -3708,10 +3985,8 @@ async function addAICommentButton(container, postElement) {
button.textContent = '⏳ Generiere...'; button.textContent = '⏳ Generiere...';
try { try {
const contextCandidate = container.closest('div[aria-posinset], article[role="article"], article, div[role="complementary"]'); const contexts = resolvePostContexts();
const normalizedContext = contextCandidate ? ensurePrimaryPostElement(contextCandidate) : null; const { postContext, contextCandidate, fallbackContext } = contexts;
const fallbackContext = postElement ? ensurePrimaryPostElement(postElement) : null;
const postContext = normalizedContext || fallbackContext || contextCandidate || postElement || container;
const selectionKeys = []; const selectionKeys = [];
if (postContext) { if (postContext) {
@@ -3723,6 +3998,12 @@ async function addAICommentButton(container, postElement) {
if (contextCandidate && contextCandidate !== postContext && contextCandidate !== postElement) { if (contextCandidate && contextCandidate !== postContext && contextCandidate !== postElement) {
selectionKeys.push(contextCandidate); selectionKeys.push(contextCandidate);
} }
if (fallbackContext
&& fallbackContext !== postContext
&& fallbackContext !== postElement
&& fallbackContext !== contextCandidate) {
selectionKeys.push(fallbackContext);
}
const resolveRecentSelection = () => { const resolveRecentSelection = () => {
for (const key of selectionKeys) { for (const key of selectionKeys) {
@@ -3781,6 +4062,11 @@ async function addAICommentButton(container, postElement) {
} }
}); });
const additionalNote = postContext ? (postAdditionalNotes.get(postContext) || '') : '';
if (additionalNote) {
postText = `${postText}\n\nZusatzinfo:\n${additionalNote}`;
}
throwIfCancelled(); throwIfCancelled();
console.log('[FB Tracker] Generating AI comment for:', postText.substring(0, 100)); console.log('[FB Tracker] Generating AI comment for:', postText.substring(0, 100));

View File

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

View File

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