Aktueller Stand
This commit is contained in:
453
web/app.js
453
web/app.js
@@ -25,7 +25,7 @@ const PROFILE_NAMES = {
|
||||
4: 'Profil 4',
|
||||
5: 'Profil 5'
|
||||
};
|
||||
|
||||
|
||||
function apiFetch(url, options = {}) {
|
||||
const config = {
|
||||
...options,
|
||||
@@ -68,13 +68,26 @@ const autoRefreshToggle = document.getElementById('autoRefreshToggle');
|
||||
const autoRefreshIntervalSelect = document.getElementById('autoRefreshInterval');
|
||||
const sortModeSelect = document.getElementById('sortMode');
|
||||
const sortDirectionToggle = document.getElementById('sortDirectionToggle');
|
||||
const bookmarkPanelToggle = document.getElementById('bookmarkPanelToggle');
|
||||
const bookmarkPanel = document.getElementById('bookmarkPanel');
|
||||
const bookmarkPanelClose = document.getElementById('bookmarkPanelClose');
|
||||
const bookmarksList = document.getElementById('bookmarksList');
|
||||
const bookmarkForm = document.getElementById('bookmarkForm');
|
||||
const bookmarkNameInput = document.getElementById('bookmarkName');
|
||||
const bookmarkQueryInput = document.getElementById('bookmarkQuery');
|
||||
const bookmarkCancelBtn = document.getElementById('bookmarkCancelBtn');
|
||||
|
||||
const REFRESH_SETTINGS_KEY = 'trackerRefreshSettings';
|
||||
const SORT_SETTINGS_KEY = 'trackerSortSettings';
|
||||
const SORT_SETTINGS_LEGACY_KEY = 'trackerSortMode';
|
||||
const FACEBOOK_TRACKING_PARAMS = ['__cft__', '__tn__', '__eep__', 'mibextid'];
|
||||
const FACEBOOK_TRACKING_PARAMS = ['__cft__', '__tn__', '__eep__', 'mibextid', 'set', 'comment_id', 'hoisted_section_header_type'];
|
||||
const VALID_SORT_MODES = new Set(['created', 'deadline', 'smart', 'lastCheck', 'lastChange']);
|
||||
const DEFAULT_SORT_SETTINGS = { mode: 'created', direction: 'desc' };
|
||||
const BOOKMARKS_STORAGE_KEY = 'trackerSearchBookmarks';
|
||||
const BOOKMARKS_BASE_URL = 'https://www.facebook.com/search/top';
|
||||
const BOOKMARK_WINDOW_DAYS = 28;
|
||||
const DEFAULT_BOOKMARKS = [];
|
||||
const BOOKMARK_SUFFIXES = ['Gewinnspiel', 'gewinnen', 'verlosen'];
|
||||
|
||||
let autoRefreshTimer = null;
|
||||
let autoRefreshSettings = {
|
||||
@@ -89,6 +102,8 @@ let manualPostEditingId = null;
|
||||
let manualPostModalLastFocus = null;
|
||||
let manualPostModalPreviousOverflow = '';
|
||||
let activeDeadlinePicker = null;
|
||||
let bookmarkPanelVisible = false;
|
||||
let bookmarkOutsideHandler = null;
|
||||
|
||||
const INITIAL_POST_LIMIT = 10;
|
||||
const POST_LOAD_INCREMENT = 10;
|
||||
@@ -222,6 +237,429 @@ function persistSortStorage(storage) {
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeCustomBookmark(entry) {
|
||||
if (!entry || typeof entry !== 'object') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const query = typeof entry.query === 'string' ? entry.query.trim() : '';
|
||||
if (!query) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const label = typeof entry.label === 'string' && entry.label.trim() ? entry.label.trim() : query;
|
||||
const id = typeof entry.id === 'string' && entry.id ? entry.id : `custom-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
|
||||
|
||||
return {
|
||||
id,
|
||||
label,
|
||||
query,
|
||||
type: 'custom'
|
||||
};
|
||||
}
|
||||
|
||||
function loadCustomBookmarks() {
|
||||
try {
|
||||
const raw = localStorage.getItem(BOOKMARKS_STORAGE_KEY);
|
||||
if (!raw) {
|
||||
return [];
|
||||
}
|
||||
const parsed = JSON.parse(raw);
|
||||
if (!Array.isArray(parsed)) {
|
||||
return [];
|
||||
}
|
||||
return parsed.map(normalizeCustomBookmark).filter(Boolean);
|
||||
} catch (error) {
|
||||
console.warn('Konnte Bookmarks nicht laden:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function saveCustomBookmarks(bookmarks) {
|
||||
try {
|
||||
const sanitized = Array.isArray(bookmarks)
|
||||
? bookmarks.map(normalizeCustomBookmark).filter(Boolean)
|
||||
: [];
|
||||
localStorage.setItem(BOOKMARKS_STORAGE_KEY, JSON.stringify(sanitized));
|
||||
} catch (error) {
|
||||
console.warn('Konnte Bookmarks nicht speichern:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function formatFacebookDateParts(date) {
|
||||
const year = date.getFullYear();
|
||||
const month = date.getMonth() + 1;
|
||||
const day = date.getDate();
|
||||
const monthLabel = `${year}-${month}`;
|
||||
const dayLabel = `${year}-${month}-${day}`;
|
||||
return {
|
||||
year: String(year),
|
||||
monthLabel,
|
||||
dayLabel
|
||||
};
|
||||
}
|
||||
|
||||
function buildBookmarkFiltersParam() {
|
||||
const y = String(new Date().getFullYear()); // "2025"
|
||||
|
||||
// start_month = Monat von (heute - BOOKMARK_WINDOW_DAYS), auf Monatsanfang (ohne Padding)
|
||||
const windowAgo = new Date();
|
||||
windowAgo.setDate(windowAgo.getDate() - BOOKMARK_WINDOW_DAYS);
|
||||
const startMonthNum = windowAgo.getMonth() + 1; // 1..12
|
||||
const startMonthLabel = `${y}-${startMonthNum}`; // z.B. "2025-9"
|
||||
const startDayLabel = `${startMonthLabel}-1`; // z.B. "2025-9-1"
|
||||
|
||||
// Ende = Jahresende (ohne Padding), Jahre immer aktuelles Jahr als String
|
||||
const endMonthLabel = `${y}-12`;
|
||||
const endDayLabel = `${y}-12-31`;
|
||||
|
||||
// Reihenfolge wie gewünscht: top_tab zuerst, dann rp_creation_time
|
||||
const filtersPayload = {
|
||||
'top_tab_recent_posts:0': JSON.stringify({
|
||||
name: 'top_tab_recent_posts',
|
||||
args: ''
|
||||
}),
|
||||
'rp_creation_time:0': JSON.stringify({
|
||||
name: 'creation_time',
|
||||
args: JSON.stringify({
|
||||
start_year: y, // als String
|
||||
start_month: startMonthLabel,
|
||||
end_year: y, // als String
|
||||
end_month: endMonthLabel,
|
||||
start_day: startDayLabel,
|
||||
end_day: endDayLabel
|
||||
})
|
||||
})
|
||||
};
|
||||
|
||||
const serialized = JSON.stringify(filtersPayload);
|
||||
|
||||
// Rohes Base64 zurückgeben (kein encodeURIComponent!)
|
||||
if (typeof window !== 'undefined' && typeof window.btoa === 'function') {
|
||||
return window.btoa(serialized);
|
||||
} else if (typeof btoa === 'function') {
|
||||
return btoa(serialized);
|
||||
} else if (typeof Buffer !== 'undefined') {
|
||||
return Buffer.from(serialized, 'utf8').toString('base64');
|
||||
} else {
|
||||
console.warn('Base64 nicht verfügbar, gebe JSON zurück (wird von URLSearchParams encodet).');
|
||||
return serialized;
|
||||
}
|
||||
}
|
||||
/*
|
||||
function buildBookmarkFiltersParam() {
|
||||
const y = String(new Date().getFullYear()); // "2025"
|
||||
|
||||
// WICHTIG: Schlüssel-Reihenfolge wie im 'soll' → top_tab zuerst
|
||||
const filtersPayload = {
|
||||
'top_tab_recent_posts:0': JSON.stringify({
|
||||
name: 'top_tab_recent_posts',
|
||||
args: ''
|
||||
}),
|
||||
'rp_creation_time:0': JSON.stringify({
|
||||
name: 'creation_time',
|
||||
args: JSON.stringify({
|
||||
start_year: y, // als String
|
||||
start_month: `${y}-1`, // ohne Padding
|
||||
end_year: y, // als String
|
||||
end_month: `${y}-12`,
|
||||
start_day: `${y}-1-1`,
|
||||
end_day: `${y}-12-31`
|
||||
})
|
||||
})
|
||||
};
|
||||
|
||||
const serialized = JSON.stringify(filtersPayload);
|
||||
|
||||
// Base64 OHNE URL-Encode zurückgeben
|
||||
if (typeof window !== 'undefined' && typeof window.btoa === 'function') {
|
||||
return window.btoa(serialized);
|
||||
} else if (typeof btoa === 'function') {
|
||||
return btoa(serialized);
|
||||
} else if (typeof Buffer !== 'undefined') {
|
||||
return Buffer.from(serialized, 'utf8').toString('base64');
|
||||
} else {
|
||||
console.warn('Base64 nicht verfügbar, gebe JSON zurück (wird von URLSearchParams encodet).');
|
||||
return serialized; // kein encodeURIComponent!
|
||||
}
|
||||
}
|
||||
*/
|
||||
function buildBookmarkSearchUrl(query) {
|
||||
const trimmed = typeof query === 'string' ? query.trim() : '';
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const searchUrl = new URL(BOOKMARKS_BASE_URL);
|
||||
searchUrl.searchParams.set('q', trimmed);
|
||||
searchUrl.searchParams.set('filters', buildBookmarkFiltersParam());
|
||||
return searchUrl.toString();
|
||||
}
|
||||
|
||||
function buildBookmarkSearchQueries(baseQuery) {
|
||||
const trimmed = typeof baseQuery === 'string' ? baseQuery.trim() : '';
|
||||
if (!trimmed) {
|
||||
return [...BOOKMARK_SUFFIXES];
|
||||
}
|
||||
|
||||
return BOOKMARK_SUFFIXES.map((suffix) => `${trimmed} ${suffix}`.trim());
|
||||
}
|
||||
|
||||
function openBookmark(bookmark) {
|
||||
if (!bookmark) {
|
||||
return;
|
||||
}
|
||||
|
||||
const queries = buildBookmarkSearchQueries(bookmark.query);
|
||||
if (!queries.length) {
|
||||
queries.push('');
|
||||
}
|
||||
|
||||
queries.forEach((searchTerm) => {
|
||||
const url = buildBookmarkSearchUrl(searchTerm);
|
||||
if (url) {
|
||||
window.open(url, '_blank', 'noopener');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function removeBookmark(bookmarkId) {
|
||||
if (!bookmarkId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const current = loadCustomBookmarks();
|
||||
const next = current.filter((bookmark) => bookmark.id !== bookmarkId);
|
||||
if (next.length === current.length) {
|
||||
return;
|
||||
}
|
||||
saveCustomBookmarks(next);
|
||||
renderBookmarks();
|
||||
}
|
||||
|
||||
function renderBookmarks() {
|
||||
if (!bookmarksList) {
|
||||
return;
|
||||
}
|
||||
|
||||
bookmarksList.innerHTML = '';
|
||||
|
||||
const items = [...DEFAULT_BOOKMARKS, ...loadCustomBookmarks()];
|
||||
|
||||
const staticDefault = {
|
||||
id: 'default-empty',
|
||||
label: 'Gewinnspiel / gewinnen / verlosen',
|
||||
query: '',
|
||||
type: 'default'
|
||||
};
|
||||
|
||||
items.unshift(staticDefault);
|
||||
|
||||
if (!items.length) {
|
||||
const empty = document.createElement('div');
|
||||
empty.className = 'bookmark-empty';
|
||||
empty.textContent = 'Noch keine Bookmarks vorhanden.';
|
||||
empty.setAttribute('role', 'listitem');
|
||||
bookmarksList.appendChild(empty);
|
||||
return;
|
||||
}
|
||||
|
||||
items.forEach((bookmark) => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'bookmark-item';
|
||||
item.setAttribute('role', 'listitem');
|
||||
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.className = 'bookmark-button';
|
||||
const label = bookmark.label || bookmark.query;
|
||||
button.textContent = label;
|
||||
|
||||
const searchVariants = buildBookmarkSearchQueries(bookmark.query);
|
||||
if (searchVariants.length) {
|
||||
button.title = searchVariants.map((variant) => `• ${variant}`).join('\n');
|
||||
} else {
|
||||
button.title = `Suche nach "${bookmark.query}" (letzte 4 Wochen)`;
|
||||
}
|
||||
button.addEventListener('click', () => openBookmark(bookmark));
|
||||
|
||||
item.appendChild(button);
|
||||
|
||||
if (bookmark.type === 'custom') {
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.type = 'button';
|
||||
removeBtn.className = 'bookmark-remove-btn';
|
||||
removeBtn.setAttribute('aria-label', `${bookmark.label || bookmark.query} entfernen`);
|
||||
removeBtn.textContent = '×';
|
||||
removeBtn.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
removeBookmark(bookmark.id);
|
||||
});
|
||||
item.appendChild(removeBtn);
|
||||
}
|
||||
|
||||
bookmarksList.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
function resetBookmarkForm() {
|
||||
if (!bookmarkForm) {
|
||||
return;
|
||||
}
|
||||
|
||||
bookmarkForm.reset();
|
||||
if (bookmarkNameInput) {
|
||||
bookmarkNameInput.value = '';
|
||||
}
|
||||
if (bookmarkQueryInput) {
|
||||
bookmarkQueryInput.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
function ensureBookmarkOutsideHandler() {
|
||||
if (bookmarkOutsideHandler) {
|
||||
return bookmarkOutsideHandler;
|
||||
}
|
||||
|
||||
bookmarkOutsideHandler = (event) => {
|
||||
if (!bookmarkPanelVisible) {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = event.target;
|
||||
const insidePanel = bookmarkPanel && bookmarkPanel.contains(target);
|
||||
const onToggle = bookmarkPanelToggle && bookmarkPanelToggle.contains(target);
|
||||
if (!insidePanel && !onToggle) {
|
||||
toggleBookmarkPanel(false);
|
||||
}
|
||||
};
|
||||
|
||||
return bookmarkOutsideHandler;
|
||||
}
|
||||
|
||||
function removeBookmarkOutsideHandler() {
|
||||
if (!bookmarkOutsideHandler) {
|
||||
return;
|
||||
}
|
||||
document.removeEventListener('mousedown', bookmarkOutsideHandler, true);
|
||||
document.removeEventListener('focusin', bookmarkOutsideHandler);
|
||||
}
|
||||
|
||||
function toggleBookmarkPanel(forceVisible) {
|
||||
if (!bookmarkPanel || !bookmarkPanelToggle) {
|
||||
return;
|
||||
}
|
||||
|
||||
const shouldShow = typeof forceVisible === 'boolean' ? forceVisible : !bookmarkPanelVisible;
|
||||
if (shouldShow === bookmarkPanelVisible) {
|
||||
return;
|
||||
}
|
||||
|
||||
bookmarkPanelVisible = shouldShow;
|
||||
bookmarkPanel.hidden = !bookmarkPanelVisible;
|
||||
bookmarkPanel.setAttribute('aria-hidden', bookmarkPanelVisible ? 'false' : 'true');
|
||||
bookmarkPanelToggle.setAttribute('aria-expanded', bookmarkPanelVisible ? 'true' : 'false');
|
||||
|
||||
if (bookmarkPanelVisible) {
|
||||
renderBookmarks();
|
||||
resetBookmarkForm();
|
||||
if (bookmarkQueryInput) {
|
||||
window.requestAnimationFrame(() => {
|
||||
bookmarkQueryInput.focus();
|
||||
});
|
||||
}
|
||||
const handler = ensureBookmarkOutsideHandler();
|
||||
window.requestAnimationFrame(() => {
|
||||
document.addEventListener('mousedown', handler, true);
|
||||
document.addEventListener('focusin', handler);
|
||||
});
|
||||
} else {
|
||||
resetBookmarkForm();
|
||||
removeBookmarkOutsideHandler();
|
||||
if (bookmarkPanelToggle) {
|
||||
bookmarkPanelToggle.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleBookmarkSubmit(event) {
|
||||
event.preventDefault();
|
||||
|
||||
if (!bookmarkForm) {
|
||||
return;
|
||||
}
|
||||
|
||||
const query = bookmarkQueryInput ? bookmarkQueryInput.value.trim() : '';
|
||||
const name = bookmarkNameInput ? bookmarkNameInput.value.trim() : '';
|
||||
|
||||
if (!query) {
|
||||
resetBookmarkForm();
|
||||
if (bookmarkQueryInput) {
|
||||
bookmarkQueryInput.focus();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const customBookmarks = loadCustomBookmarks();
|
||||
const normalizedQuery = query.toLowerCase();
|
||||
const existingIndex = customBookmarks.findIndex((bookmark) => bookmark.query.toLowerCase() === normalizedQuery);
|
||||
|
||||
const nextBookmark = {
|
||||
id: existingIndex >= 0 ? customBookmarks[existingIndex].id : `custom-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`,
|
||||
label: name || query,
|
||||
query,
|
||||
type: 'custom'
|
||||
};
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
customBookmarks[existingIndex] = nextBookmark;
|
||||
} else {
|
||||
customBookmarks.push(nextBookmark);
|
||||
}
|
||||
|
||||
saveCustomBookmarks(customBookmarks);
|
||||
renderBookmarks();
|
||||
resetBookmarkForm();
|
||||
}
|
||||
|
||||
function initializeBookmarks() {
|
||||
if (!bookmarksList) {
|
||||
return;
|
||||
}
|
||||
|
||||
renderBookmarks();
|
||||
|
||||
if (bookmarkPanel) {
|
||||
bookmarkPanel.setAttribute('aria-hidden', 'true');
|
||||
}
|
||||
|
||||
if (bookmarkPanelToggle) {
|
||||
bookmarkPanelToggle.addEventListener('click', () => {
|
||||
toggleBookmarkPanel();
|
||||
});
|
||||
}
|
||||
|
||||
if (bookmarkPanelClose) {
|
||||
bookmarkPanelClose.addEventListener('click', () => {
|
||||
toggleBookmarkPanel(false);
|
||||
});
|
||||
}
|
||||
|
||||
if (bookmarkCancelBtn) {
|
||||
bookmarkCancelBtn.addEventListener('click', () => {
|
||||
resetBookmarkForm();
|
||||
if (bookmarkQueryInput) {
|
||||
bookmarkQueryInput.focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (bookmarkForm) {
|
||||
bookmarkForm.addEventListener('submit', handleBookmarkSubmit);
|
||||
}
|
||||
}
|
||||
|
||||
function getSortSettingsPageKey() {
|
||||
try {
|
||||
const path = window.location.pathname;
|
||||
@@ -532,7 +970,9 @@ function normalizeFacebookPostUrl(rawValue) {
|
||||
const cleanedParams = new URLSearchParams();
|
||||
parsed.searchParams.forEach((paramValue, paramKey) => {
|
||||
const lowerKey = paramKey.toLowerCase();
|
||||
if (FACEBOOK_TRACKING_PARAMS.some((trackingKey) => lowerKey.startsWith(trackingKey))) {
|
||||
const isTrackingParam = FACEBOOK_TRACKING_PARAMS.some((trackingKey) => lowerKey.startsWith(trackingKey));
|
||||
const isSingleUnitParam = lowerKey === 's' && paramValue === 'single_unit';
|
||||
if (isTrackingParam || isSingleUnitParam) {
|
||||
return;
|
||||
}
|
||||
cleanedParams.append(paramKey, paramValue);
|
||||
@@ -2842,6 +3282,12 @@ document.addEventListener('keydown', (event) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (bookmarkPanelVisible) {
|
||||
event.preventDefault();
|
||||
toggleBookmarkPanel(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (screenshotModal && screenshotModal.classList.contains('open')) {
|
||||
if (screenshotModalZoomed) {
|
||||
resetScreenshotZoom();
|
||||
@@ -2858,6 +3304,7 @@ window.addEventListener('resize', () => {
|
||||
});
|
||||
|
||||
// Initialize
|
||||
initializeBookmarks();
|
||||
loadAutoRefreshSettings();
|
||||
initializeTabFromUrl();
|
||||
loadSortMode();
|
||||
|
||||
Reference in New Issue
Block a user