aktueller stand
This commit is contained in:
@@ -42,6 +42,32 @@ function isOnReelsPage() {
|
||||
}
|
||||
}
|
||||
|
||||
function maybeRedirectPageReelsToMain() {
|
||||
try {
|
||||
const { location } = window;
|
||||
const pathname = location && location.pathname;
|
||||
if (typeof pathname !== 'string') {
|
||||
return false;
|
||||
}
|
||||
const match = pathname.match(/^\/([^/]+)\/reels\/?$/i);
|
||||
if (!match) {
|
||||
return false;
|
||||
}
|
||||
const pageSlug = match[1];
|
||||
if (!pageSlug) {
|
||||
return false;
|
||||
}
|
||||
const targetUrl = `${location.origin}/${pageSlug}/`;
|
||||
if (location.href === targetUrl) {
|
||||
return false;
|
||||
}
|
||||
location.replace(targetUrl);
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
let debugLoggingEnabled = false;
|
||||
|
||||
const originalConsoleLog = console.log.bind(console);
|
||||
@@ -705,6 +731,140 @@ function expandPhotoUrlHostVariants(url) {
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchPostByUrl(url) {
|
||||
const normalizedUrl = normalizeFacebookPostUrl(url);
|
||||
if (!normalizedUrl) {
|
||||
return null;
|
||||
}
|
||||
const response = await backendFetch(`${API_URL}/posts/by-url?url=${encodeURIComponent(normalizedUrl)}`);
|
||||
if (!response.ok) {
|
||||
return null;
|
||||
}
|
||||
const data = await response.json();
|
||||
return data && data.id ? data : null;
|
||||
}
|
||||
|
||||
async function fetchPostById(postId) {
|
||||
if (!postId) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const response = await backendFetch(`${API_URL}/posts`);
|
||||
if (!response.ok) {
|
||||
return null;
|
||||
}
|
||||
const posts = await response.json();
|
||||
if (!Array.isArray(posts)) {
|
||||
return null;
|
||||
}
|
||||
return posts.find(post => post && post.id === postId) || null;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function buildSimilarityPayload(postElement) {
|
||||
let postText = null;
|
||||
try {
|
||||
postText = extractPostText(postElement) || null;
|
||||
} catch (error) {
|
||||
console.debug('[FB Tracker] Failed to extract post text for similarity:', error);
|
||||
}
|
||||
const imageInfo = await getFirstPostImageInfo(postElement);
|
||||
return {
|
||||
postText,
|
||||
firstImageHash: imageInfo.hash,
|
||||
firstImageUrl: imageInfo.url
|
||||
};
|
||||
}
|
||||
|
||||
async function findSimilarPost({ url, postText, firstImageHash }) {
|
||||
if (!url) {
|
||||
return null;
|
||||
}
|
||||
if (!postText && !firstImageHash) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const payload = {
|
||||
url,
|
||||
post_text: postText || null,
|
||||
first_image_hash: firstImageHash || null
|
||||
};
|
||||
const response = await backendFetch(`${API_URL}/posts/similar`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
if (!response.ok) {
|
||||
return null;
|
||||
}
|
||||
const data = await response.json();
|
||||
return data && data.match ? data : null;
|
||||
} catch (error) {
|
||||
console.warn('[FB Tracker] Similarity check failed:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function shortenInline(text, maxLength = 64) {
|
||||
if (!text) {
|
||||
return '';
|
||||
}
|
||||
if (text.length <= maxLength) {
|
||||
return text;
|
||||
}
|
||||
return `${text.slice(0, maxLength - 3)}...`;
|
||||
}
|
||||
|
||||
function formatSimilarityLabel(similarity) {
|
||||
if (!similarity || !similarity.match) {
|
||||
return '';
|
||||
}
|
||||
const match = similarity.match;
|
||||
const base = match.title || match.created_by_name || match.url || 'Beitrag';
|
||||
const details = [];
|
||||
if (similarity.similarity && typeof similarity.similarity.text === 'number') {
|
||||
details.push(`Text ${Math.round(similarity.similarity.text * 100)}%`);
|
||||
}
|
||||
if (similarity.similarity && typeof similarity.similarity.image_distance === 'number') {
|
||||
details.push(`Bild Δ${similarity.similarity.image_distance}`);
|
||||
}
|
||||
const detailText = details.length ? ` (${details.join(', ')})` : '';
|
||||
return `Ähnlich zu: ${shortenInline(base, 64)}${detailText}`;
|
||||
}
|
||||
|
||||
async function attachUrlToExistingPost(postId, urls, payload = {}) {
|
||||
if (!postId) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const body = {
|
||||
urls: Array.isArray(urls) ? urls : [],
|
||||
skip_content_key_check: true
|
||||
};
|
||||
if (payload.firstImageHash) {
|
||||
body.first_image_hash = payload.firstImageHash;
|
||||
}
|
||||
if (payload.firstImageUrl) {
|
||||
body.first_image_url = payload.firstImageUrl;
|
||||
}
|
||||
const response = await backendFetch(`${API_URL}/posts/${postId}/urls`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
return response.ok;
|
||||
} catch (error) {
|
||||
console.warn('[FB Tracker] Failed to attach URL to existing post:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if post is already tracked (checks all URL candidates to avoid duplicates)
|
||||
async function checkPostStatus(postUrl, allUrlCandidates = []) {
|
||||
try {
|
||||
@@ -880,15 +1040,20 @@ async function persistAlternatePostUrls(postId, urls = []) {
|
||||
}
|
||||
|
||||
// Add post to tracking
|
||||
async function markPostChecked(postId, profileNumber) {
|
||||
async function markPostChecked(postId, profileNumber, options = {}) {
|
||||
try {
|
||||
const ignoreOrder = options && options.ignoreOrder === true;
|
||||
const returnError = options && options.returnError === true;
|
||||
console.log('[FB Tracker] Marking post as checked:', postId, 'Profile:', profileNumber);
|
||||
const response = await backendFetch(`${API_URL}/posts/${postId}/check`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ profile_number: profileNumber })
|
||||
body: JSON.stringify({
|
||||
profile_number: profileNumber,
|
||||
ignore_order: ignoreOrder
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
@@ -898,15 +1063,21 @@ async function markPostChecked(postId, profileNumber) {
|
||||
}
|
||||
|
||||
if (response.status === 409) {
|
||||
console.log('[FB Tracker] Post already checked by this profile');
|
||||
return null;
|
||||
const payload = await response.json().catch(() => ({}));
|
||||
const message = payload && payload.error ? payload.error : 'Beitrag kann aktuell nicht bestätigt werden.';
|
||||
console.log('[FB Tracker] Post check blocked:', message);
|
||||
return returnError ? { error: message, status: response.status } : null;
|
||||
}
|
||||
|
||||
console.error('[FB Tracker] Failed to mark post as checked:', response.status);
|
||||
return null;
|
||||
return returnError
|
||||
? { error: 'Beitrag konnte nicht bestätigt werden.', status: response.status }
|
||||
: null;
|
||||
} catch (error) {
|
||||
console.error('[FB Tracker] Error marking post as checked:', error);
|
||||
return null;
|
||||
return (options && options.returnError)
|
||||
? { error: 'Beitrag konnte nicht bestätigt werden.', status: 0 }
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -920,7 +1091,9 @@ async function addPostToTracking(postUrl, targetCount, profileNumber, options =
|
||||
}
|
||||
|
||||
let postText = null;
|
||||
if (options && options.postElement) {
|
||||
if (options && typeof options.postText === 'string') {
|
||||
postText = options.postText;
|
||||
} else if (options && options.postElement) {
|
||||
try {
|
||||
postText = extractPostText(options.postElement) || null;
|
||||
} catch (error) {
|
||||
@@ -967,6 +1140,13 @@ async function addPostToTracking(postUrl, targetCount, profileNumber, options =
|
||||
payload.post_text = postText;
|
||||
}
|
||||
|
||||
if (options && typeof options.firstImageHash === 'string' && options.firstImageHash.trim()) {
|
||||
payload.first_image_hash = options.firstImageHash.trim();
|
||||
}
|
||||
if (options && typeof options.firstImageUrl === 'string' && options.firstImageUrl.trim()) {
|
||||
payload.first_image_url = options.firstImageUrl.trim();
|
||||
}
|
||||
|
||||
const response = await backendFetch(`${API_URL}/posts`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -1667,6 +1847,18 @@ async function captureElementScreenshot(element) {
|
||||
const endY = Math.min(documentHeight, elementBottom + verticalMargin);
|
||||
const baseDocTop = Math.max(0, elementTop - verticalMargin);
|
||||
|
||||
const restoreScrollPosition = () => {
|
||||
window.scrollTo({ top: originalScrollY, left: originalScrollX, behavior: 'auto' });
|
||||
if (document.documentElement) {
|
||||
document.documentElement.scrollTop = originalScrollY;
|
||||
document.documentElement.scrollLeft = originalScrollX;
|
||||
}
|
||||
if (document.body) {
|
||||
document.body.scrollTop = originalScrollY;
|
||||
document.body.scrollLeft = originalScrollX;
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
let iteration = 0;
|
||||
let targetScroll = startY;
|
||||
@@ -1723,7 +1915,9 @@ async function captureElementScreenshot(element) {
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
window.scrollTo({ top: originalScrollY, left: originalScrollX, behavior: 'auto' });
|
||||
restoreScrollPosition();
|
||||
await delay(0);
|
||||
restoreScrollPosition();
|
||||
}
|
||||
|
||||
if (!segments.length) {
|
||||
@@ -1845,6 +2039,125 @@ async function maybeDownscaleScreenshot(imageData) {
|
||||
}
|
||||
}
|
||||
|
||||
function isLikelyPostImage(img) {
|
||||
if (!img) {
|
||||
return false;
|
||||
}
|
||||
const src = img.currentSrc || img.src || '';
|
||||
if (!src) {
|
||||
return false;
|
||||
}
|
||||
if (src.startsWith('data:')) {
|
||||
return false;
|
||||
}
|
||||
const lowerSrc = src.toLowerCase();
|
||||
if (lowerSrc.includes('emoji') || lowerSrc.includes('static.xx') || lowerSrc.includes('sprite')) {
|
||||
return false;
|
||||
}
|
||||
const width = img.naturalWidth || img.width || 0;
|
||||
const height = img.naturalHeight || img.height || 0;
|
||||
if (width < 120 || height < 120) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function waitForImageLoad(img, timeoutMs = 1500) {
|
||||
return new Promise((resolve) => {
|
||||
if (!img) {
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
if (img.complete && img.naturalWidth > 0) {
|
||||
resolve(true);
|
||||
return;
|
||||
}
|
||||
let resolved = false;
|
||||
const finish = (value) => {
|
||||
if (resolved) return;
|
||||
resolved = true;
|
||||
resolve(value);
|
||||
};
|
||||
const timer = setTimeout(() => finish(false), timeoutMs);
|
||||
img.addEventListener('load', () => {
|
||||
clearTimeout(timer);
|
||||
finish(true);
|
||||
}, { once: true });
|
||||
img.addEventListener('error', () => {
|
||||
clearTimeout(timer);
|
||||
finish(false);
|
||||
}, { once: true });
|
||||
});
|
||||
}
|
||||
|
||||
function buildDHashFromPixels(imageData) {
|
||||
if (!imageData || !imageData.data) {
|
||||
return null;
|
||||
}
|
||||
const { data } = imageData;
|
||||
const bits = [];
|
||||
for (let y = 0; y < 8; y += 1) {
|
||||
for (let x = 0; x < 8; x += 1) {
|
||||
const leftIndex = ((y * 9) + x) * 4;
|
||||
const rightIndex = ((y * 9) + x + 1) * 4;
|
||||
const left = 0.299 * data[leftIndex] + 0.587 * data[leftIndex + 1] + 0.114 * data[leftIndex + 2];
|
||||
const right = 0.299 * data[rightIndex] + 0.587 * data[rightIndex + 1] + 0.114 * data[rightIndex + 2];
|
||||
bits.push(left > right ? 1 : 0);
|
||||
}
|
||||
}
|
||||
let hex = '';
|
||||
for (let i = 0; i < bits.length; i += 4) {
|
||||
const value = (bits[i] << 3) | (bits[i + 1] << 2) | (bits[i + 2] << 1) | bits[i + 3];
|
||||
hex += value.toString(16);
|
||||
}
|
||||
return hex.padStart(16, '0');
|
||||
}
|
||||
|
||||
async function computeDHashFromUrl(imageUrl) {
|
||||
if (!imageUrl) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const response = await fetch(imageUrl);
|
||||
if (!response.ok) {
|
||||
return null;
|
||||
}
|
||||
const blob = await response.blob();
|
||||
const bitmap = await createImageBitmap(blob);
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 9;
|
||||
canvas.height = 8;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) {
|
||||
return null;
|
||||
}
|
||||
ctx.drawImage(bitmap, 0, 0, 9, 8);
|
||||
const imageData = ctx.getImageData(0, 0, 9, 8);
|
||||
return buildDHashFromPixels(imageData);
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function getFirstPostImageInfo(postElement) {
|
||||
if (!postElement) {
|
||||
return { hash: null, url: null };
|
||||
}
|
||||
const images = Array.from(postElement.querySelectorAll('img')).filter(isLikelyPostImage);
|
||||
for (const img of images.slice(0, 5)) {
|
||||
const loaded = await waitForImageLoad(img);
|
||||
if (!loaded) {
|
||||
continue;
|
||||
}
|
||||
const src = img.currentSrc || img.src;
|
||||
const hash = await computeDHashFromUrl(src);
|
||||
if (hash) {
|
||||
return { hash, url: src };
|
||||
}
|
||||
}
|
||||
return { hash: null, url: null };
|
||||
}
|
||||
|
||||
function getStickyHeaderHeight() {
|
||||
try {
|
||||
const banner = document.querySelector('[role="banner"], header[role="banner"]');
|
||||
@@ -1939,6 +2252,9 @@ function extractDeadlineFromPostText(postElement) {
|
||||
|
||||
const extractTimeAfterIndex = (text, index) => {
|
||||
const tail = text.slice(index, index + 80);
|
||||
if (/^\s*(?:-|–|—|bis)\s*\d{1,2}\.\d{1,2}\./i.test(tail)) {
|
||||
return null;
|
||||
}
|
||||
const timeMatch = /^\s*(?:\(|\[)?\s*(?:[,;:-]|\b)?\s*(?:um|ab|bis|gegen|spätestens)?\s*(?:den|dem|am)?\s*(?:ca\.?)?\s*(\d{1,2})(?:[:.](\d{2}))?\s*(?:uhr|h)?\s*(?:\)|\])?/i.exec(tail);
|
||||
if (!timeMatch) {
|
||||
return null;
|
||||
@@ -1950,6 +2266,9 @@ function extractDeadlineFromPostText(postElement) {
|
||||
if (Number.isNaN(hour) || Number.isNaN(minute)) {
|
||||
return null;
|
||||
}
|
||||
if (hour === 24 && minute === 0) {
|
||||
return { hour: 23, minute: 59 };
|
||||
}
|
||||
if (hour < 0 || hour > 23 || minute < 0 || minute > 59) {
|
||||
return null;
|
||||
}
|
||||
@@ -1963,6 +2282,22 @@ function extractDeadlineFromPostText(postElement) {
|
||||
};
|
||||
|
||||
const foundDates = [];
|
||||
const rangePattern = /\b(\d{1,2})\.(\d{1,2})\.(\d{2,4})\s*(?:-|–|—|bis)\s*(\d{1,2})\.(\d{1,2})\.(\d{2,4})\b/i;
|
||||
const rangeMatch = rangePattern.exec(fullText);
|
||||
if (rangeMatch) {
|
||||
const endDay = parseInt(rangeMatch[4], 10);
|
||||
const endMonth = parseInt(rangeMatch[5], 10);
|
||||
let endYear = parseInt(rangeMatch[6], 10);
|
||||
if (endYear < 100) {
|
||||
endYear += 2000;
|
||||
}
|
||||
if (endMonth >= 1 && endMonth <= 12 && endDay >= 1 && endDay <= 31) {
|
||||
const endDate = new Date(endYear, endMonth - 1, endDay, 0, 0, 0, 0);
|
||||
if (endDate.getMonth() === endMonth - 1 && endDate.getDate() === endDay && endDate > today) {
|
||||
return toDateTimeLocalString(endDate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const pattern of patterns) {
|
||||
let match;
|
||||
@@ -2404,7 +2739,19 @@ async function renderTrackedStatus({
|
||||
: null;
|
||||
|
||||
const isExpired = postData.deadline_at ? new Date(postData.deadline_at) < new Date() : false;
|
||||
const canCurrentProfileCheck = postData.next_required_profile === profileNumber;
|
||||
const requiredProfiles = Array.isArray(postData.required_profiles) && postData.required_profiles.length
|
||||
? postData.required_profiles
|
||||
.map((value) => {
|
||||
const parsed = parseInt(value, 10);
|
||||
if (Number.isNaN(parsed)) {
|
||||
return null;
|
||||
}
|
||||
return Math.min(5, Math.max(1, parsed));
|
||||
})
|
||||
.filter(Boolean)
|
||||
: Array.from({ length: Math.max(1, Math.min(5, parseInt(postData.target_count, 10) || 1)) }, (_, index) => index + 1);
|
||||
const isCurrentProfileRequired = requiredProfiles.includes(profileNumber);
|
||||
const canCurrentProfileCheck = isCurrentProfileRequired && postData.next_required_profile === profileNumber;
|
||||
const isCurrentProfileDone = checks.some(check => check.profile_number === profileNumber);
|
||||
|
||||
if (isFeedHome && isCurrentProfileDone) {
|
||||
@@ -2434,16 +2781,23 @@ async function renderTrackedStatus({
|
||||
${lastCheck ? `<div style="color: #65676b; font-size: 12px; white-space: nowrap;">Letzte: ${lastCheck}</div>` : ''}
|
||||
`;
|
||||
|
||||
if (canCurrentProfileCheck && !isExpired && !completed) {
|
||||
if (!isExpired && !completed && !isCurrentProfileDone && isCurrentProfileRequired) {
|
||||
const checkButtonEnabled = canCurrentProfileCheck;
|
||||
const buttonColor = checkButtonEnabled ? '#42b72a' : '#f39c12';
|
||||
const cursorStyle = 'pointer';
|
||||
const buttonTitle = checkButtonEnabled
|
||||
? 'Beitrag bestätigen'
|
||||
: 'Wartet auf vorherige Profile';
|
||||
|
||||
statusHtml += `
|
||||
<button class="fb-tracker-check-btn" style="
|
||||
<button class="fb-tracker-check-btn" title="${buttonTitle}" style="
|
||||
padding: 4px 12px;
|
||||
background-color: #42b72a;
|
||||
background-color: ${buttonColor};
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
cursor: ${cursorStyle};
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
margin-left: 8px;
|
||||
@@ -2524,9 +2878,9 @@ async function renderTrackedStatus({
|
||||
checkBtn.disabled = true;
|
||||
checkBtn.textContent = 'Wird bestätigt...';
|
||||
|
||||
const result = await markPostChecked(postData.id, profileNumber);
|
||||
const result = await markPostChecked(postData.id, profileNumber, { returnError: true });
|
||||
|
||||
if (result) {
|
||||
if (result && !result.error) {
|
||||
await renderTrackedStatus({
|
||||
container,
|
||||
postElement,
|
||||
@@ -2540,8 +2894,13 @@ async function renderTrackedStatus({
|
||||
});
|
||||
} else {
|
||||
checkBtn.disabled = false;
|
||||
checkBtn.textContent = 'Fehler - Erneut versuchen';
|
||||
checkBtn.style.backgroundColor = '#e74c3c';
|
||||
checkBtn.textContent = '✓ Bestätigen';
|
||||
if (result && result.error) {
|
||||
showToast(result.error, 'error');
|
||||
} else {
|
||||
checkBtn.textContent = 'Fehler - Erneut versuchen';
|
||||
checkBtn.style.backgroundColor = '#e74c3c';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -2772,6 +3131,36 @@ async function createTrackerUI(postElement, buttonBar, postNum = '?', options =
|
||||
font-size: 13px;
|
||||
max-width: 160px;
|
||||
" />
|
||||
<div class="fb-tracker-similarity" style="
|
||||
display: none;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 8px;
|
||||
border: 1px solid #f0c36d;
|
||||
background: #fff6d5;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
color: #6b5b00;
|
||||
flex-basis: 100%;
|
||||
">
|
||||
<span class="fb-tracker-similarity__text"></span>
|
||||
<button class="fb-tracker-merge-btn" type="button" style="
|
||||
border: 1px solid #caa848;
|
||||
background: white;
|
||||
color: #7a5d00;
|
||||
padding: 4px 8px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
">Mergen</button>
|
||||
<a class="fb-tracker-similarity-link" href="#" target="_blank" rel="noopener" style="
|
||||
color: #7a5d00;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
display: none;
|
||||
">Öffnen</a>
|
||||
</div>
|
||||
<button class="fb-tracker-add-btn" style="
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
@@ -2792,8 +3181,109 @@ async function createTrackerUI(postElement, buttonBar, postNum = '?', options =
|
||||
const selectElement = container.querySelector(`#${selectId}`);
|
||||
const deadlineInput = container.querySelector(`#${deadlineId}`);
|
||||
selectElement.value = '2';
|
||||
const similarityBox = container.querySelector('.fb-tracker-similarity');
|
||||
const similarityText = container.querySelector('.fb-tracker-similarity__text');
|
||||
const mergeButton = container.querySelector('.fb-tracker-merge-btn');
|
||||
const similarityLink = container.querySelector('.fb-tracker-similarity-link');
|
||||
const similarityPayloadPromise = buildSimilarityPayload(postElement);
|
||||
let similarityPayload = null;
|
||||
let similarityMatch = null;
|
||||
const mainLinkUrl = postUrlData.mainUrl;
|
||||
|
||||
const resolveSimilarityPayload = async () => {
|
||||
if (!similarityPayload) {
|
||||
similarityPayload = await similarityPayloadPromise;
|
||||
}
|
||||
return similarityPayload;
|
||||
};
|
||||
|
||||
if (similarityBox && similarityText && mergeButton) {
|
||||
(async () => {
|
||||
const payload = await resolveSimilarityPayload();
|
||||
const similarity = await findSimilarPost({
|
||||
url: postUrlData.url,
|
||||
postText: payload.postText,
|
||||
firstImageHash: payload.firstImageHash
|
||||
});
|
||||
if (!similarity || !similarity.match) {
|
||||
return;
|
||||
}
|
||||
similarityMatch = similarity.match;
|
||||
similarityText.textContent = formatSimilarityLabel(similarity);
|
||||
similarityBox.style.display = 'flex';
|
||||
if (!addButton.disabled) {
|
||||
addButton.textContent = 'Neu speichern';
|
||||
}
|
||||
if (similarityLink) {
|
||||
similarityLink.style.display = 'inline';
|
||||
similarityLink.textContent = 'Öffnen';
|
||||
if (similarityMatch.url) {
|
||||
similarityLink.href = similarityMatch.url;
|
||||
similarityLink.dataset.ready = '1';
|
||||
} else {
|
||||
similarityLink.href = '#';
|
||||
similarityLink.dataset.ready = '0';
|
||||
similarityLink.dataset.postId = similarityMatch.id;
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
mergeButton.addEventListener('click', async () => {
|
||||
if (!similarityMatch) {
|
||||
return;
|
||||
}
|
||||
mergeButton.disabled = true;
|
||||
const previousLabel = mergeButton.textContent;
|
||||
mergeButton.textContent = 'Mergen...';
|
||||
|
||||
const payload = await resolveSimilarityPayload();
|
||||
const urlCandidates = [postUrlData.url, ...postUrlData.allCandidates];
|
||||
const uniqueUrls = Array.from(new Set(urlCandidates.filter(Boolean)));
|
||||
const attached = await attachUrlToExistingPost(similarityMatch.id, uniqueUrls, payload);
|
||||
|
||||
if (attached) {
|
||||
const updatedPost = await fetchPostByUrl(similarityMatch.url);
|
||||
if (updatedPost) {
|
||||
await renderTrackedStatus({
|
||||
container,
|
||||
postElement,
|
||||
postData: updatedPost,
|
||||
profileNumber,
|
||||
isFeedHome,
|
||||
isDialogContext,
|
||||
manualHideInfo,
|
||||
encodedUrl,
|
||||
postNum
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
mergeButton.disabled = false;
|
||||
mergeButton.textContent = previousLabel;
|
||||
});
|
||||
}
|
||||
|
||||
if (similarityLink && !similarityLink.dataset.bound) {
|
||||
similarityLink.dataset.bound = '1';
|
||||
similarityLink.addEventListener('click', async (event) => {
|
||||
if (similarityLink.dataset.ready === '1') {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
const postId = similarityLink.dataset.postId;
|
||||
if (!postId) {
|
||||
return;
|
||||
}
|
||||
const resolved = await fetchPostById(postId);
|
||||
if (resolved && resolved.url) {
|
||||
similarityLink.href = resolved.url;
|
||||
similarityLink.dataset.ready = '1';
|
||||
window.open(resolved.url, '_blank', 'noopener');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (mainLinkUrl) {
|
||||
const mainLinkButton = document.createElement('button');
|
||||
mainLinkButton.className = 'fb-tracker-mainlink-btn';
|
||||
@@ -2838,11 +3328,15 @@ async function createTrackerUI(postElement, buttonBar, postNum = '?', options =
|
||||
await delay(220);
|
||||
|
||||
const deadlineValue = deadlineInput ? deadlineInput.value : '';
|
||||
const payload = await resolveSimilarityPayload();
|
||||
|
||||
const result = await addPostToTracking(postUrlData.url, targetCount, profileNumber, {
|
||||
postElement,
|
||||
deadline: deadlineValue,
|
||||
candidates: postUrlData.allCandidates
|
||||
candidates: postUrlData.allCandidates,
|
||||
postText: payload.postText,
|
||||
firstImageHash: payload.firstImageHash,
|
||||
firstImageUrl: payload.firstImageUrl
|
||||
});
|
||||
|
||||
if (result) {
|
||||
@@ -3236,6 +3730,10 @@ let globalPostCounter = 0;
|
||||
|
||||
// Find all Facebook posts on the page
|
||||
function findPosts() {
|
||||
if (maybeRedirectPageReelsToMain()) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[FB Tracker] Scanning for posts...');
|
||||
|
||||
const postContainers = findPostContainers();
|
||||
@@ -3343,6 +3841,7 @@ function findPosts() {
|
||||
|
||||
// Initialize
|
||||
console.log('[FB Tracker] Initializing...');
|
||||
maybeRedirectPageReelsToMain();
|
||||
|
||||
// Run multiple times to catch loading posts
|
||||
setTimeout(findPosts, 2000);
|
||||
@@ -4055,6 +4554,32 @@ function extractPostText(postElement) {
|
||||
}
|
||||
};
|
||||
|
||||
const hasEmojiChars = (text) => /[\uD800-\uDBFF][\uDC00-\uDFFF]|[\u2600-\u27BF]/.test(text);
|
||||
|
||||
const injectEmojiLabels = (root) => {
|
||||
if (!root || typeof root.querySelectorAll !== 'function') {
|
||||
return;
|
||||
}
|
||||
const emojiNodes = root.querySelectorAll('img[alt], [role="img"][aria-label]');
|
||||
emojiNodes.forEach((node) => {
|
||||
const label = node.getAttribute('alt') || node.getAttribute('aria-label');
|
||||
if (!label || !hasEmojiChars(label)) {
|
||||
return;
|
||||
}
|
||||
const textNode = node.ownerDocument.createTextNode(label);
|
||||
node.replaceWith(textNode);
|
||||
});
|
||||
};
|
||||
|
||||
const getTextWithEmojis = (element) => {
|
||||
if (!element) {
|
||||
return '';
|
||||
}
|
||||
const clone = element.cloneNode(true);
|
||||
injectEmojiLabels(clone);
|
||||
return clone.innerText || clone.textContent || '';
|
||||
};
|
||||
|
||||
const SKIP_TEXT_CONTAINERS_SELECTOR = [
|
||||
'div[role="textbox"]',
|
||||
'[contenteditable="true"]',
|
||||
@@ -4177,7 +4702,7 @@ function extractPostText(postElement) {
|
||||
logPostText('Selector result skipped (inside disallowed region)', selector, makeSnippet(element.innerText || element.textContent || ''));
|
||||
continue;
|
||||
}
|
||||
tryAddCandidate(element.innerText || element.textContent || '', element, { selector });
|
||||
tryAddCandidate(getTextWithEmojis(element), element, { selector });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4200,11 +4725,12 @@ function extractPostText(postElement) {
|
||||
const clone = postElement.cloneNode(true);
|
||||
const elementsToRemove = clone.querySelectorAll(SKIP_TEXT_CONTAINERS_SELECTOR);
|
||||
elementsToRemove.forEach((node) => node.remove());
|
||||
injectEmojiLabels(clone);
|
||||
const cloneText = clone.innerText || clone.textContent || '';
|
||||
fallbackText = cleanCandidate(cloneText);
|
||||
logPostText('Fallback extracted from clone', makeSnippet(fallbackText || cloneText));
|
||||
} catch (error) {
|
||||
const allText = postElement.innerText || postElement.textContent || '';
|
||||
const allText = getTextWithEmojis(postElement);
|
||||
fallbackText = cleanCandidate(allText);
|
||||
logPostText('Fallback extracted from original element', makeSnippet(fallbackText || allText));
|
||||
}
|
||||
@@ -5071,19 +5597,19 @@ async function addAICommentButton(container, postElement) {
|
||||
let postId = container.dataset.postId || '';
|
||||
|
||||
if (postId) {
|
||||
latestData = await markPostChecked(postId, effectiveProfile);
|
||||
latestData = await markPostChecked(postId, effectiveProfile, { ignoreOrder: true });
|
||||
if (!latestData && decodedUrl) {
|
||||
const refreshed = await checkPostStatus(decodedUrl);
|
||||
if (refreshed && refreshed.id) {
|
||||
container.dataset.postId = refreshed.id;
|
||||
latestData = await markPostChecked(refreshed.id, effectiveProfile) || refreshed;
|
||||
latestData = await markPostChecked(refreshed.id, effectiveProfile, { ignoreOrder: true }) || refreshed;
|
||||
}
|
||||
}
|
||||
} else if (decodedUrl) {
|
||||
const refreshed = await checkPostStatus(decodedUrl);
|
||||
if (refreshed && refreshed.id) {
|
||||
container.dataset.postId = refreshed.id;
|
||||
latestData = await markPostChecked(refreshed.id, effectiveProfile) || refreshed;
|
||||
latestData = await markPostChecked(refreshed.id, effectiveProfile, { ignoreOrder: true }) || refreshed;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user