Greasy Fork is available in English.

mydealz Script

mydealz Deal-Management: Deals filtern und ausblenden, alle Einstellungen per UI verwalten.

目前为 2025-01-01 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name mydealz Script
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.8.1
  5. // @description mydealz Deal-Management: Deals filtern und ausblenden, alle Einstellungen per UI verwalten.
  6. // @author Moritz Baumeister (https://www.mydealz.de/profile/BobBaumeister), Flo (https://www.mydealz.de/profile/Basics0119)
  7. // @license MIT
  8. // @match https://www.mydealz.de/*
  9. // @icon https://www.google.com/s2/favicons?sz=64&domain=mydealz.de
  10. // @grant none
  11. // ==/UserScript==
  12.  
  13. // Versions-Änderungen:
  14. // Fix: Touchscreen: UI lässt sich nun auch auf Geräten mit Touchscreen korrekt benutzen. UI schließt sich jetzt nicht mehr komplett, nachdem ein Wort aus der Liste gelöscht wurde.
  15.  
  16. // Einbinden von Font Awesome für Icons
  17. const fontAwesomeLink = document.createElement('link');
  18. fontAwesomeLink.rel = 'stylesheet';
  19. fontAwesomeLink.href = 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css';
  20. document.head.appendChild(fontAwesomeLink);
  21.  
  22. // Add constant for touch detection
  23. const IS_TOUCH_DEVICE = ('ontouchstart' in window) ||
  24. (navigator.maxTouchPoints > 0) ||
  25. (navigator.msMaxTouchPoints > 0);
  26.  
  27. // --- 1. Initial Setup ---
  28. const EXCLUDE_WORDS_KEY = 'excludeWords';
  29. const EXCLUDE_MERCHANTS_KEY = 'excludeMerchantIDs';
  30. const HIDDEN_DEALS_KEY = 'hiddenDeals';
  31. const EXCLUDE_MERCHANTS_DATA_KEY = 'excludeMerchantsData';
  32. const MERCHANT_PAGE_SELECTOR = '.merchant-banner';
  33.  
  34. // Load data immediately
  35. let excludeWords, excludeMerchantIDs, hiddenDeals;
  36. let dealThatOpenedSettings = null; // Add at top with other globals
  37. let settingsDiv = null;
  38. let isSettingsOpen = false;
  39. let merchantListDiv = null;
  40. let wordsListDiv = null;
  41. let uiClickOutsideHandler = null; // Add at top with other globals
  42. let activeSubUI = null; // Add at top with other globals
  43.  
  44. try {
  45. excludeWords = JSON.parse(localStorage.getItem(EXCLUDE_WORDS_KEY)) || [];
  46. excludeMerchantIDs = JSON.parse(localStorage.getItem(EXCLUDE_MERCHANTS_KEY)) || [];
  47. hiddenDeals = JSON.parse(localStorage.getItem(HIDDEN_DEALS_KEY)) || [];
  48. } catch (error) {
  49. excludeWords = [];
  50. excludeMerchantIDs = [];
  51. hiddenDeals = [];
  52. }
  53.  
  54. // --- 1. Core Functions ---
  55. function processArticles() {
  56. const deals = document.querySelectorAll('article.thread--deal, article.thread--voucher');
  57. deals.forEach(deal => {
  58. const dealId = deal.getAttribute('id');
  59.  
  60. // Check if manually hidden
  61. if (hiddenDeals.includes(dealId)) {
  62. hideDeal(deal);
  63. return;
  64. }
  65.  
  66. // Check if should be hidden by rules
  67. if (shouldExcludeArticle(deal)) {
  68. hideDeal(deal);
  69. return;
  70. }
  71.  
  72. // Show deal if not excluded
  73. deal.style.display = 'block';
  74. deal.style.opacity = '1';
  75. });
  76. }
  77.  
  78. // Define observer
  79. const observer = new MutationObserver((mutations) => {
  80. mutations.forEach(mutation => {
  81. if (mutation.addedNodes.length) {
  82. processArticles();
  83. addSettingsButton();
  84. addHideButtons();
  85. }
  86. });
  87. });
  88.  
  89. // Initialize everything
  90. (function init() {
  91. processArticles();
  92. addSettingsButton();
  93. addHideButtons();
  94.  
  95. observer.observe(document.body, {
  96. childList: true,
  97. subtree: true
  98. });
  99. })();
  100.  
  101. // --- 2. Hilfsfunktionen ---
  102. function shouldExcludeArticle(article) {
  103. const titleElement = article.querySelector('.thread-title');
  104. const merchantLink = article.querySelector('a[href*="merchant-id="]');
  105.  
  106. if (titleElement && excludeWords.some(word => titleElement.textContent.toLowerCase().includes(word.toLowerCase()))) {
  107. return true;
  108. }
  109.  
  110. if (merchantLink) {
  111. const merchantIDMatch = merchantLink.getAttribute('href').match(/merchant-id=(\d+)/);
  112. if (merchantIDMatch) {
  113. const merchantID = merchantIDMatch[1];
  114. if (excludeMerchantIDs.includes(merchantID)) {
  115. return true;
  116. }
  117. }
  118. }
  119.  
  120. return false;
  121. }
  122.  
  123. function hideDeal(deal) {
  124. deal.style.display = 'none';
  125. }
  126.  
  127. // Funktion zum Speichern der ausgeblendeten Deals
  128. function saveHiddenDeals() {
  129. localStorage.setItem(HIDDEN_DEALS_KEY, JSON.stringify(hiddenDeals));
  130. }
  131.  
  132. // Speichern der `excludeWords` und `excludeMerchantIDs`
  133. function saveExcludeWords(words) {
  134. localStorage.setItem('excludeWords', JSON.stringify(words));
  135. }
  136. function loadExcludeWords() {
  137. return JSON.parse(localStorage.getItem('excludeWords')) || [];
  138. }
  139.  
  140. // Update save function to handle merchant data objects
  141. function saveExcludeMerchants(merchantsData) {
  142. // Filter out invalid entries
  143. const validMerchants = merchantsData.filter(m =>
  144. m &&
  145. typeof m.id !== 'undefined' &&
  146. m.id !== null &&
  147. typeof m.name !== 'undefined' &&
  148. m.name !== null
  149. );
  150.  
  151. // Extract IDs for backwards compatibility
  152. const ids = validMerchants.map(m => m.id);
  153.  
  154. localStorage.setItem(EXCLUDE_MERCHANTS_KEY, JSON.stringify(ids));
  155. localStorage.setItem(EXCLUDE_MERCHANTS_DATA_KEY, JSON.stringify(validMerchants));
  156.  
  157. // Update global array
  158. excludeMerchantIDs = ids;
  159. }
  160.  
  161. // Load function for merchant data
  162. function loadExcludeMerchants() {
  163. const merchantsData = JSON.parse(localStorage.getItem(EXCLUDE_MERCHANTS_DATA_KEY)) || [];
  164. const legacyIds = JSON.parse(localStorage.getItem(EXCLUDE_MERCHANTS_KEY)) || [];
  165.  
  166. // Filter out invalid entries
  167. const validMerchants = merchantsData.filter(m =>
  168. m &&
  169. typeof m.id !== 'undefined' &&
  170. m.id !== null &&
  171. typeof m.name !== 'undefined' &&
  172. m.name !== null
  173. );
  174.  
  175. // Convert legacy IDs if needed
  176. if (validMerchants.length === 0 && legacyIds.length > 0) {
  177. return legacyIds
  178. .filter(id => id && typeof id !== 'undefined')
  179. .map(id => ({ id, name: id }));
  180. }
  181.  
  182. return validMerchants;
  183. }
  184.  
  185. // Clean up existing data on script init
  186. (function cleanupMerchantData() {
  187. const merchants = loadExcludeMerchants();
  188. saveExcludeMerchants(merchants);
  189. })();
  190.  
  191. // Fügt Event Listener hinzu, um Auto-Speichern zu ermöglichen
  192. function addAutoSaveListeners() {
  193. // Event Listener für Eingabefelder
  194. const excludeWordsInput = document.getElementById('excludeWordsInput');
  195. excludeWordsInput.addEventListener('input', () => {
  196. const newWords = excludeWordsInput.value.split('\n').map(w => w.trim()).filter(Boolean);
  197. saveExcludeWords(newWords);
  198. excludeWords = newWords;
  199. processArticles();
  200. });
  201.  
  202. const excludeMerchantIDsInput = document.getElementById('excludeMerchantIDsInput');
  203. excludeMerchantIDsInput.addEventListener('input', () => {
  204. const newMerchantIDs = excludeMerchantIDsInput.value.split('\n').map(id => id.trim()).filter(Boolean);
  205. saveExcludeMerchants(newMerchantIDs);
  206. excludeMerchantIDs = newMerchantIDs;
  207. processArticles();
  208. });
  209. }
  210.  
  211. // Remove highlighting-related event listeners
  212.  
  213. // UI-Button zum Öffnen der Einstellungen wird nur einmal erstellt
  214. const settingsButton = document.createElement('button');
  215. settingsButton.innerHTML = '⚙️';
  216. settingsButton.style.padding = '10px';
  217. settingsButton.style.background = 'transparent'; // Hintergrund transparent machen
  218. settingsButton.style.color = 'white';
  219. settingsButton.style.border = 'none';
  220. settingsButton.style.borderRadius = '5px';
  221. settingsButton.style.cursor = 'pointer';
  222.  
  223. // UI-Button zum Verbergen der Deals wird nur einmal erstellt
  224. const hideButton = document.createElement('button');
  225. hideButton.innerHTML = '❌';
  226. hideButton.style.marginLeft = '10px';
  227. hideButton.title = 'Deal verbergen'; // Tooltip hinzufügen
  228. hideButton.style.padding = '10px';
  229. hideButton.style.background = 'transparent'; // Hintergrund transparent machen
  230. hideButton.style.color = 'white';
  231. hideButton.style.border = 'none';
  232. hideButton.style.borderRadius = '5px';
  233. hideButton.style.cursor = 'pointer';
  234.  
  235. // Funktion, um das Verbergen der Deals zu ermöglichen
  236. hideButton.addEventListener('click', function(event) {
  237. const deal = event.target.closest('article');
  238. if (deal) {
  239. const dealId = deal.getAttribute('id');
  240. hiddenDeals.push(dealId);
  241. saveHiddenDeals();
  242. hideDeal(deal);
  243. }
  244. });
  245.  
  246. // Add debug logging to settings button click handler
  247. function addSettingsButton() {
  248. const deals = document.querySelectorAll('article.thread--deal, article.thread--voucher');
  249.  
  250. deals.forEach(deal => {
  251. if (deal.hasAttribute('data-settings-added')) return;
  252.  
  253. const settingsBtn = document.createElement('button');
  254. settingsBtn.innerHTML = '⚙️';
  255. settingsBtn.className = 'vote-button overflow--visible settings-button';
  256. settingsBtn.title = 'Einstellungen';
  257. settingsBtn.style.cssText = `
  258. border: none;
  259. cursor: pointer;
  260. height: 32px;
  261. width: 32px;
  262. display: flex;
  263. align-items: center;
  264. justify-content: center;
  265. font-size: 16px;
  266. padding: 0;
  267. background: transparent;
  268. z-index: 1000;
  269. `;
  270.  
  271. settingsBtn.onclick = (e) => {
  272. e.preventDefault();
  273. e.stopPropagation();
  274. dealThatOpenedSettings = deal;
  275. createSettingsUI();
  276. return false;
  277. };
  278.  
  279. const settingsContainer = document.createElement('div');
  280. settingsContainer.className = 'vote-box settings-container';
  281. settingsContainer.style.cssText = `
  282. display: flex;
  283. align-items: center;
  284. justify-content: center;
  285. margin-left: 8px;
  286. height: 32px;
  287. width: 32px;
  288. border-radius: 50%;
  289. background: #f2f2f2;
  290. border: 1px solid #e4e4e4;
  291. padding: 4px;
  292. z-index: 1000;
  293. `;
  294.  
  295. settingsContainer.appendChild(settingsBtn);
  296.  
  297. const voteBox = deal.querySelector('.vote-box');
  298. if (voteBox && voteBox.parentNode) {
  299. voteBox.parentNode.insertBefore(settingsContainer, voteBox.nextSibling);
  300. deal.setAttribute('data-settings-added', 'true');
  301. }
  302. });
  303. }
  304.  
  305. // Add to document ready handler
  306. document.addEventListener('DOMContentLoaded', () => {
  307. processArticles();
  308. addSettingsButton();
  309. });
  310.  
  311. function addHideButtons() {
  312. const deals = document.querySelectorAll('article:not([data-button-added])');
  313.  
  314. deals.forEach(deal => {
  315. if (deal.hasAttribute('data-button-added')) return;
  316.  
  317. const voteTemp = deal.querySelector('.cept-vote-temp');
  318. if (!voteTemp) return;
  319.  
  320. // Remove popover
  321. const popover = voteTemp.querySelector('.popover-origin');
  322. if (popover) popover.remove();
  323.  
  324. const hideButtonContainer = document.createElement('div');
  325. hideButtonContainer.style.cssText = `
  326. position: absolute;
  327. left: 0;
  328. top: 0;
  329. width: 100%;
  330. height: 100%;
  331. display: none;
  332. z-index: 10001;
  333. pointer-events: none;
  334. `;
  335.  
  336. const hideButton = document.createElement('button');
  337. hideButton.innerHTML = '❌';
  338. hideButton.className = 'vote-button overflow--visible';
  339. hideButton.title = 'Deal verbergen';
  340. hideButton.style.cssText = `
  341. position: absolute;
  342. left: 50%;
  343. top: 50%;
  344. transform: translate(-50%, -50%);
  345. z-index: 10002;
  346. background: rgba(255, 255, 255, 0.9);
  347. border: none;
  348. border-radius: 50%;
  349. cursor: pointer;
  350. padding: 8px;
  351. width: 32px;
  352. height: 32px;
  353. display: flex;
  354. align-items: center;
  355. justify-content: center;
  356. pointer-events: all;
  357. `;
  358.  
  359. // Touch device handling
  360. if ('ontouchstart' in window) {
  361. let buttonVisible = false;
  362.  
  363. voteTemp.addEventListener('touchstart', (e) => {
  364. e.preventDefault();
  365. e.stopPropagation();
  366.  
  367. if (!buttonVisible) {
  368. buttonVisible = true;
  369. hideButtonContainer.style.display = 'block';
  370. } else {
  371. // Second touch on container: hide deal
  372. const dealId = deal.getAttribute('id');
  373. hiddenDeals.push(dealId);
  374. saveHiddenDeals();
  375. hideDeal(deal);
  376. }
  377. }, true);
  378.  
  379. // Reset visibility when touch moves outside
  380. voteTemp.addEventListener('touchend', () => {
  381. if (!buttonVisible) {
  382. hideButtonContainer.style.display = 'none';
  383. }
  384. }, true);
  385. } else {
  386. // Desktop hover behavior
  387. voteTemp.addEventListener('mouseenter', () => {
  388. hideButtonContainer.style.display = 'block';
  389. }, true);
  390.  
  391. voteTemp.addEventListener('mouseleave', () => {
  392. hideButtonContainer.style.display = 'none';
  393. }, true);
  394.  
  395. hideButton.onclick = (e) => {
  396. e.preventDefault();
  397. e.stopPropagation();
  398. const dealId = deal.getAttribute('id');
  399. hiddenDeals.push(dealId);
  400. saveHiddenDeals();
  401. hideDeal(deal);
  402. return false;
  403. };
  404. }
  405.  
  406. hideButtonContainer.appendChild(hideButton);
  407. voteTemp.appendChild(hideButtonContainer);
  408. deal.setAttribute('data-button-added', 'true');
  409. });
  410. }
  411.  
  412. // HTML Decoder Funktion hinzufügen
  413. function decodeHtml(html) {
  414. const txt = document.createElement('textarea');
  415. txt.innerHTML = html;
  416. return txt.value;
  417. }
  418.  
  419. // --- 3. Backup- und Restore-Funktionen ---
  420. function backupData() {
  421. const backup = {
  422. excludeWords: excludeWords,
  423. excludeMerchantIDs: excludeMerchantIDs,
  424. merchantsData: loadExcludeMerchants()
  425. };
  426.  
  427. const now = new Date();
  428. const timestamp = now.getFullYear() + '-' +
  429. String(now.getMonth() + 1).padStart(2, '0') + '-' +
  430. String(now.getDate()).padStart(2, '0') + '_' +
  431. String(now.getHours()).padStart(2, '0') + '.' +
  432. String(now.getMinutes()).padStart(2, '0');
  433.  
  434. const blob = new Blob([JSON.stringify(backup, null, 2)], { type: 'application/json' });
  435. const url = URL.createObjectURL(blob);
  436. const a = document.createElement('a');
  437. a.href = url;
  438. a.download = `mydealz_backup_${timestamp}.json`;
  439. a.click();
  440. URL.revokeObjectURL(url);
  441. }
  442.  
  443. // Restore-Funktion
  444. function restoreData(event) {
  445. const file = event.target.files[0];
  446. if (file && file.type === 'application/json') {
  447. const reader = new FileReader();
  448. reader.onload = function(e) {
  449. const restoredData = JSON.parse(e.target.result);
  450. if (restoredData.excludeWords && (restoredData.excludeMerchantIDs || restoredData.merchantsData)) {
  451. // Restore words
  452. saveExcludeWords(restoredData.excludeWords);
  453. excludeWords = restoredData.excludeWords;
  454.  
  455. // Restore merchant data
  456. const merchantsData = restoredData.merchantsData ||
  457. restoredData.excludeMerchantIDs.map(id => ({ id, name: id }));
  458.  
  459. saveExcludeMerchants(merchantsData);
  460.  
  461. // Update UI
  462. document.getElementById('excludeWordsInput').value = excludeWords.join('\n');
  463.  
  464. // Refresh deals
  465. processArticles();
  466. } else {
  467. alert('Die Backup-Datei ist nicht im richtigen Format.');
  468. }
  469. };
  470. reader.readAsText(file);
  471. } else {
  472. alert('Bitte wählen Sie eine gültige JSON-Datei aus.');
  473. }
  474. }
  475.  
  476. // --- 4. Benutzeroberfläche (UI) ---
  477. function getSubUIPosition() {
  478. if (IS_TOUCH_DEVICE) {
  479. return `
  480. position: fixed;
  481. top: 50%;
  482. left: 50%;
  483. transform: translate(-50%, -50%);
  484. `;
  485. }
  486. return `
  487. position: fixed;
  488. top: 50%;
  489. left: calc(50% + 310px);
  490. transform: translate(-50%, -50%);
  491. `;
  492. }
  493.  
  494. function createMerchantListUI() {
  495. // Remove existing instance if it exists
  496. if (merchantListDiv && merchantListDiv.parentNode) {
  497. merchantListDiv.remove();
  498. }
  499.  
  500. merchantListDiv = document.createElement('div');
  501. merchantListDiv.style.cssText = `
  502. ${getSubUIPosition()}
  503. padding: 15px;
  504. background: #f9f9f9;
  505. border: 1px solid #ccc;
  506. border-radius: 5px;
  507. z-index: 1001; // Higher than settings UI
  508. width: 300px;
  509. `;
  510.  
  511. const currentMerchants = loadExcludeMerchants();
  512.  
  513. const merchantListHTML = currentMerchants.map(merchant => `
  514. <div class="merchant-item" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px; padding: 5px; background: #f0f0f0; border-radius: 3px;">
  515. <div style="display: flex; flex-direction: column;">
  516. <span style="font-weight: bold;">${merchant.name}</span>
  517. <span style="color: #666; font-size: 0.8em;">ID: ${merchant.id}</span>
  518. </div>
  519. <button class="delete-merchant" data-id="${merchant.id}" style="background: none; border: none; cursor: pointer; color: #666;">
  520. <i class="fas fa-times"></i>
  521. </button>
  522. </div>
  523. `).join('');
  524.  
  525. merchantListDiv.innerHTML = `
  526. <h4 style="margin-bottom: 10px;">Ausgeblendete Händler</h4>
  527. <input type="text" id="merchantSearch" placeholder="Händler suchen..." style="width: 100%; padding: 5px; margin-bottom: 10px; border: 1px solid #ccc; border-radius: 3px;">
  528. <div style="margin-bottom: 15px;">
  529. <div id="merchantList" style="margin-bottom: 10px; max-height: 200px; overflow-y: auto; padding-right: 5px;">
  530. ${merchantListHTML}
  531. </div>
  532. <button id="clearMerchantListButton" style="width: 100%; padding: 5px 10px; background: #f0f0f0; border: 1px solid #ccc; border-radius: 3px; cursor: pointer; margin-top: 10px;">
  533. <i class="fas fa-trash"></i> Alle Händler entfernen
  534. </button>
  535. </div>
  536. <div style="text-align: right;">
  537. <button id="closeMerchantListButton" style="padding: 8px 12px; background: none; border: none; cursor: pointer;" title="Schließen">
  538. <i class="fas fa-times"></i>
  539. </button>
  540. </div>
  541. `;
  542.  
  543. // Add the div to the document body
  544. document.body.appendChild(merchantListDiv);
  545. setupClickOutsideHandler();
  546.  
  547. // Add search functionality
  548. const searchInput = document.getElementById('merchantSearch');
  549. searchInput.addEventListener('input', (e) => {
  550. const searchTerm = e.target.value.toLowerCase();
  551. document.querySelectorAll('.merchant-item').forEach(item => {
  552. const merchantName = item.querySelector('span').textContent.toLowerCase();
  553. const merchantId = item.querySelector('span:last-child').textContent.toLowerCase();
  554. if (merchantName.includes(searchTerm) || merchantId.includes(searchTerm)) {
  555. item.style.display = 'flex';
  556. } else {
  557. item.style.display = 'none';
  558. }
  559. });
  560. });
  561.  
  562. // Add clear all button handler
  563. document.getElementById('clearMerchantListButton').addEventListener('click', () => {
  564. if (confirm('Möchten Sie wirklich alle Händler aus der Liste entfernen?')) {
  565. saveExcludeMerchants([]);
  566. document.getElementById('merchantList').innerHTML = '';
  567. processArticles();
  568. }
  569. });
  570.  
  571. // Update delete button handlers
  572. document.querySelectorAll('.delete-merchant').forEach(button => {
  573. button.addEventListener('click', function(e) {
  574. // Prevent event bubbling
  575. e.preventDefault();
  576. e.stopPropagation();
  577.  
  578. // Get merchant ID to delete
  579. const deleteButton = e.target.closest('.delete-merchant');
  580. if (!deleteButton) return;
  581.  
  582. const idToDelete = deleteButton.dataset.id;
  583.  
  584. // Update merchant data
  585. const merchantsData = loadExcludeMerchants();
  586. const updatedMerchants = merchantsData.filter(m => m.id !== idToDelete);
  587.  
  588. // Save updated data
  589. saveExcludeMerchants(updatedMerchants);
  590.  
  591. // Remove from UI
  592. deleteButton.closest('.merchant-item').remove();
  593.  
  594. // Refresh deals
  595. processArticles();
  596. });
  597. });
  598.  
  599. // Update close button handlers in createMerchantListUI
  600. document.getElementById('closeMerchantListButton').addEventListener('click', (e) => {
  601. e.stopPropagation(); // Prevent event bubbling
  602. closeActiveSubUI();
  603. });
  604. }
  605.  
  606. function createExcludeWordsUI() {
  607. // Remove existing instance if it exists
  608. if (wordsListDiv && wordsListDiv.parentNode) {
  609. wordsListDiv.remove();
  610. }
  611.  
  612. wordsListDiv = document.createElement('div');
  613. wordsListDiv.style.cssText = `
  614. ${getSubUIPosition()}
  615. padding: 15px;
  616. background: #f9f9f9;
  617. border: 1px solid #ccc;
  618. border-radius: 5px;
  619. z-index: 1001; // Higher than settings UI
  620. width: 300px;
  621. `;
  622.  
  623. const currentWords = loadExcludeWords();
  624.  
  625. const wordsListHTML = currentWords.map(word => `
  626. <div class="word-item" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px; padding: 5px; background: #f0f0f0; border-radius: 3px;">
  627. <span style="word-break: break-word;">${word}</span>
  628. <button class="delete-word" data-word="${word}" style="background: none; border: none; cursor: pointer; color: #666;">
  629. <i class="fas fa-times"></i>
  630. </button>
  631. </div>
  632. `).join('');
  633.  
  634. wordsListDiv.innerHTML = `
  635. <h4 style="margin-bottom: 10px;">Ausgeblendete Wörter</h4>
  636. <input type="text" id="wordSearch" placeholder="Wörter suchen..." style="width: 100%; padding: 5px; margin-bottom: 10px; border: 1px solid #ccc; border-radius: 3px;">
  637. <div style="margin-bottom: 15px;">
  638. <div id="wordsList" style="margin-bottom: 10px; max-height: 200px; overflow-y: auto; padding-right: 5px;">
  639. ${wordsListHTML}
  640. </div>
  641. <button id="clearWordsListButton" style="width: 100%; padding: 5px 10px; background: #f0f0f0; border: 1px solid #ccc; border-radius: 3px; cursor: pointer; margin-top: 10px;">
  642. <i class="fas fa-trash"></i> Alle Wörter entfernen
  643. </button>
  644. </div>
  645. <div style="text-align: right;">
  646. <button id="closeWordsListButton" style="padding: 8px 12px; background: none; border: none; cursor: pointer;" title="Schließen">
  647. <i class="fas fa-times"></i>
  648. </button>
  649. </div>
  650. `;
  651.  
  652. // Add the div to the document body
  653. document.body.appendChild(wordsListDiv);
  654. setupClickOutsideHandler();
  655.  
  656. // Add search functionality
  657. const searchInput = document.getElementById('wordSearch');
  658. searchInput.addEventListener('input', (e) => {
  659. const searchTerm = e.target.value.toLowerCase();
  660. document.querySelectorAll('.word-item').forEach(item => {
  661. const word = item.querySelector('span').textContent.toLowerCase();
  662. item.style.display = word.includes(searchTerm) ? 'flex' : 'none';
  663. });
  664. });
  665.  
  666. // Add clear all button handler
  667. document.getElementById('clearWordsListButton').addEventListener('click', () => {
  668. if (confirm('Möchten Sie wirklich alle Wörter aus der Liste entfernen?')) {
  669. saveExcludeWords([]);
  670. document.getElementById('wordsList').innerHTML = '';
  671. excludeWords = [];
  672. processArticles();
  673. }
  674. });
  675.  
  676. // Add delete handlers
  677. document.querySelectorAll('.delete-word').forEach(button => {
  678. button.addEventListener('click', (e) => {
  679. // Prevent event bubbling
  680. e.preventDefault();
  681. e.stopPropagation();
  682.  
  683. const deleteButton = e.target.closest('.delete-word');
  684. if (!deleteButton) return;
  685.  
  686. const wordToDelete = deleteButton.dataset.word;
  687. excludeWords = excludeWords.filter(word => word !== wordToDelete);
  688. saveExcludeWords(excludeWords);
  689.  
  690. // Remove only the specific word item
  691. deleteButton.closest('.word-item').remove();
  692.  
  693. // Update deals without closing UI
  694. processArticles();
  695. });
  696. });
  697.  
  698. // Update close button handlers in createExcludeWordsUI
  699. document.getElementById('closeWordsListButton').addEventListener('click', (e) => {
  700. e.stopPropagation(); // Prevent event bubbling
  701. closeActiveSubUI();
  702. });
  703. }
  704.  
  705. function getWordsFromTitle(dealElement) {
  706. const title = dealElement.querySelector('.thread-title').textContent;
  707. return title.split(/\s+/)
  708. .map(word => word.replace(/[^a-zA-Z0-9äöüÄÖÜß]/g, ''))
  709. .filter(word => word.length > 2);
  710. }
  711.  
  712. // Add debug logging to settings UI creation
  713. function createSettingsUI() {
  714. if (isSettingsOpen) return;
  715. isSettingsOpen = true;
  716.  
  717. settingsDiv = document.createElement('div');
  718. settingsDiv.style.cssText = `
  719. position: fixed;
  720. top: 50%;
  721. left: 50%;
  722. transform: translate(-50%, -50%);
  723. padding: 15px;
  724. background: #f9f9f9;
  725. border: 1px solid #ccc;
  726. border-radius: 5px;
  727. z-index: 1000;
  728. max-width: 300px;
  729. overflow: auto;
  730. `;
  731.  
  732. // Get merchant info from current deal
  733. let merchantName = null;
  734. let showMerchantButton = false;
  735.  
  736. if (dealThatOpenedSettings) {
  737. const merchantLink = dealThatOpenedSettings.querySelector('a[data-t="merchantLink"]');
  738. if (merchantLink) {
  739. merchantName = merchantLink.textContent.trim();
  740. showMerchantButton = true;
  741. }
  742. }
  743.  
  744. // Process articles when opening settings
  745. processArticles();
  746.  
  747. const currentExcludeWords = JSON.parse(localStorage.getItem(EXCLUDE_WORDS_KEY)) || [];
  748. const currentExcludeMerchantIDs = JSON.parse(localStorage.getItem(EXCLUDE_MERCHANTS_KEY)) || [];
  749.  
  750. const dealWords = dealThatOpenedSettings ? getWordsFromTitle(dealThatOpenedSettings) : [];
  751.  
  752. // Conditional merchant button HTML - only show if merchant exists
  753. const merchantButtonHtml = showMerchantButton ? `
  754. <button id="hideMerchantButton" style="width: 100%; margin-top: 5px; padding: 5px 10px; background: #f0f0f0; border: 1px solid #ccc; border-radius: 3px; cursor: pointer;">
  755. <i class="fas fa-store-slash"></i> Alle Deals von <span style="font-weight: bold">${merchantName}</span> ausblenden
  756. </button>
  757. ` : '';
  758.  
  759. settingsDiv.innerHTML = `
  760. <h4 style="margin-bottom: 15px;">Einstellungen zum Ausblenden</h4>
  761.  
  762. <!-- Word Input Section -->
  763. <div style="margin-bottom: 20px;">
  764. <div style="display: flex; align-items: center; gap: 5px;">
  765. <input list="wordSuggestions"
  766. id="newWordInput"
  767. placeholder="Neues Wort..."
  768. title="Deals mit hier eingetragenen Wörtern im Titel werden ausgeblendet."
  769. style="flex: 1; padding: 8px; border: 1px solid #ccc; border-radius: 3px;">
  770. <datalist id="wordSuggestions">
  771. ${dealWords.map(word => `<option value="${word}">`).join('')}
  772. </datalist>
  773. <button id="addWordButton"
  774. style="padding: 8px; background: #f0f0f0; border: 1px solid #ccc; border-radius: 3px; cursor: pointer;">
  775. <i class="fas fa-plus"></i>
  776. </button>
  777. </div>
  778. </div>
  779.  
  780. <!-- Merchant Hide Button -->
  781. ${merchantButtonHtml}
  782.  
  783. <!-- List Management Section -->
  784. <div style="margin-top: 20px; display: flex; flex-direction: column; gap: 10px;">
  785. <button id="showWordsListButton"
  786. style="width: 100%; padding: 8px; background: #f0f0f0; border: 1px solid #ccc; border-radius: 3px; cursor: pointer;">
  787. <i class="fas fa-list"></i> Wortfilter verwalten
  788. </button>
  789. <button id="showMerchantListButton"
  790. style="width: 100%; padding: 8px; background: #f0f0f0; border: 1px solid #ccc; border-radius: 3px; cursor: pointer;">
  791. <i class="fas fa-store"></i> Händlerfilter verwalten
  792. </button>
  793. </div>
  794.  
  795. <!-- Action Buttons -->
  796. <div style="margin-top: 20px; text-align: right; display: flex; justify-content: flex-end; gap: 5px;">
  797. <button id="createBackupButton" style="padding: 8px; background: none; border: none; cursor: pointer;" title="Backup erstellen">
  798. <i class="fas fa-file-export"></i>
  799. </button>
  800. <button id="restoreBackupButton" style="padding: 8px; background: none; border: none; cursor: pointer;" title="Wiederherstellen">
  801. <i class="fas fa-file-import"></i>
  802. </button>
  803. <input type="file" id="restoreFileInput" style="display: none;" />
  804. <button id="closeSettingsButton" style="padding: 8px; background: none; border: none; cursor: pointer;" title="Schließen">
  805. <i class="fas fa-times"></i>
  806. </button>
  807. </div>`;
  808.  
  809. // Explicitly add to DOM
  810. document.body.appendChild(settingsDiv);
  811. setupClickOutsideHandler();
  812.  
  813. // Add word input handler
  814. document.getElementById('addWordButton').addEventListener('click', () => {
  815. const newWord = document.getElementById('newWordInput').value.trim();
  816. if (newWord && !excludeWords.includes(newWord)) {
  817. // Add new word at start of array
  818. excludeWords.unshift(newWord);
  819. saveExcludeWords(excludeWords);
  820. document.getElementById('newWordInput').value = '';
  821. processArticles();
  822. }
  823. });
  824.  
  825. // Add enter key handler for input
  826. document.getElementById('newWordInput').addEventListener('keypress', (e) => {
  827. if (e.key === 'Enter') {
  828. document.getElementById('addWordButton').click();
  829. }
  830. });
  831.  
  832. // Only add merchant button listener if button exists
  833. const hideMerchantButton = document.getElementById('hideMerchantButton');
  834. if (hideMerchantButton && showMerchantButton) {
  835. hideMerchantButton.addEventListener('click', () => {
  836. if (!dealThatOpenedSettings) return;
  837.  
  838. const merchantLink = dealThatOpenedSettings.querySelector('a[href*="merchant-id="]');
  839. if (!merchantLink) return;
  840.  
  841. const merchantIDMatch = merchantLink.getAttribute('href').match(/merchant-id=(\d+)/);
  842. if (!merchantIDMatch) return;
  843.  
  844. const merchantID = merchantIDMatch[1];
  845. const merchantName = dealThatOpenedSettings.querySelector('a[data-t="merchantLink"]').textContent.trim();
  846.  
  847. // Load current data
  848. const merchantsData = loadExcludeMerchants();
  849.  
  850. // Check if ID already exists
  851. if (!merchantsData.some(m => m.id === merchantID)) {
  852. // Add new merchant at start of array
  853. merchantsData.unshift({ id: merchantID, name: merchantName });
  854. saveExcludeMerchants(merchantsData);
  855. processArticles();
  856. }
  857. });
  858. }
  859.  
  860. // Add merchant list button listener
  861. document.getElementById('showMerchantListButton').addEventListener('click', () => {
  862. closeActiveSubUI();
  863. createMerchantListUI();
  864. activeSubUI = 'merchant';
  865. });
  866.  
  867. // Add words list button listener
  868. document.getElementById('showWordsListButton').addEventListener('click', () => {
  869. closeActiveSubUI();
  870. createExcludeWordsUI();
  871. activeSubUI = 'words';
  872. });
  873.  
  874. // Always ensure close button works
  875. document.getElementById('closeSettingsButton').addEventListener('click', (e) => {
  876. e.stopPropagation(); // Prevent event bubbling
  877. if (settingsDiv?.parentNode) {
  878. settingsDiv.remove();
  879. isSettingsOpen = false;
  880. }
  881. // Close merchant list if open
  882. if (merchantListDiv && merchantListDiv.parentNode) {
  883. merchantListDiv.remove();
  884. }
  885. // Close words list if open
  886. if (wordsListDiv && wordsListDiv.parentNode) {
  887. wordsListDiv.remove();
  888. }
  889. });
  890.  
  891. // Backup/Restore Event Listeners
  892. document.getElementById('createBackupButton').addEventListener('click', backupData);
  893.  
  894. document.getElementById('restoreBackupButton').addEventListener('click', () => {
  895. document.getElementById('restoreFileInput').click();
  896. });
  897.  
  898. document.getElementById('restoreFileInput').addEventListener('change', restoreData);
  899. }
  900.  
  901. function initObserver() {
  902. observer.observe(document.body, {
  903. childList: true,
  904. subtree: true
  905. });
  906. }
  907.  
  908. // Call initial setup
  909. document.addEventListener('DOMContentLoaded', () => {
  910. processArticles();
  911. initObserver();
  912. });
  913.  
  914. // Update merchant list UI - add new item at top
  915. function addMerchantToList(merchant, merchantList) {
  916. const div = document.createElement('div');
  917. div.className = 'merchant-item';
  918. div.style.cssText = 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px; padding: 5px; background: #f0f0f0; border-radius: 3px;';
  919. div.innerHTML = `
  920. <div style="display: flex; flex-direction: column;">
  921. <span style="font-weight: bold;">${merchant.name}</span>
  922. <span style="color: #666; font-size: 0.8em;">ID: ${merchant.id}</span>
  923. </div>
  924. <button class="delete-merchant" data-id="${merchant.id}" style="background: none; border: none; cursor: pointer; color: #666;">
  925. <i class="fas fa-times"></i>
  926. </button>
  927. `;
  928.  
  929. // Insert at beginning of list
  930. merchantList.insertBefore(div, merchantList.firstChild);
  931. }
  932.  
  933. // Update word list UI - add new item at top
  934. function addWordToList(word, wordsList) {
  935. const div = document.createElement('div');
  936. div.className = 'word-item';
  937. div.style.cssText = 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px; padding: 5px; background: #f0f0f0; border-radius: 3px;';
  938. div.innerHTML = `
  939. <span style="word-break: break-word;">${word}</span>
  940. <button class="delete-word" data-word="${word}" style="background: none; border: none; cursor: pointer; color: #666;">
  941. <i class="fas fa-times"></i>
  942. </button>
  943. `;
  944.  
  945. // Insert at beginning of list
  946. wordsList.insertBefore(div, wordsList.firstChild);
  947. }
  948.  
  949. function setupClickOutsideHandler() {
  950. if (uiClickOutsideHandler) {
  951. document.removeEventListener('click', uiClickOutsideHandler);
  952. }
  953.  
  954. uiClickOutsideHandler = (e) => {
  955. const settingsOpen = settingsDiv?.parentNode;
  956. const merchantsOpen = merchantListDiv?.parentNode;
  957. const wordsOpen = wordsListDiv?.parentNode;
  958.  
  959. if (e.target.closest('.settings-button') ||
  960. e.target.closest('#showMerchantListButton') ||
  961. e.target.closest('#showWordsListButton')) {
  962. return;
  963. }
  964.  
  965. const clickedOutside = (!settingsOpen || !settingsDiv.contains(e.target)) &&
  966. (!merchantsOpen || !merchantListDiv.contains(e.target)) &&
  967. (!wordsOpen || !wordsListDiv.contains(e.target));
  968.  
  969. if (clickedOutside) {
  970. if (settingsOpen) {
  971. settingsDiv.remove();
  972. isSettingsOpen = false;
  973. }
  974. if (merchantsOpen) merchantListDiv.remove();
  975. if (wordsOpen) wordsListDiv.remove();
  976.  
  977. document.removeEventListener('click', uiClickOutsideHandler);
  978. uiClickOutsideHandler = null;
  979. }
  980. };
  981.  
  982. setTimeout(() => document.addEventListener('click', uiClickOutsideHandler), 100);
  983. }
  984.  
  985. // Add helper function to close sub-UIs
  986. function closeActiveSubUI() {
  987. if (activeSubUI === 'merchant' && merchantListDiv?.parentNode) {
  988. merchantListDiv.remove();
  989. } else if (activeSubUI === 'words' && wordsListDiv?.parentNode) {
  990. wordsListDiv.remove();
  991. }
  992. activeSubUI = null;
  993. }
  994.  
  995. // Add new function to handle merchant pages
  996. function addMerchantPageHideButton() {
  997. // Check if we're on a merchant page
  998. const urlParams = new URLSearchParams(window.location.search);
  999. const merchantId = urlParams.get('merchant-id');
  1000. if (!merchantId) return;
  1001.  
  1002. // Find merchant header
  1003. const merchantBanner = document.querySelector(MERCHANT_PAGE_SELECTOR);
  1004. if (!merchantBanner) return;
  1005.  
  1006. // Get merchant name from page
  1007. const merchantName = document.querySelector('.merchant-banner__title')?.textContent.trim();
  1008. if (!merchantName) return;
  1009.  
  1010. // Create hide button container
  1011. const hideButtonContainer = document.createElement('div');
  1012. hideButtonContainer.style.cssText = `
  1013. display: inline-flex;
  1014. align-items: center;
  1015. margin-left: 10px;
  1016. `;
  1017.  
  1018. // Create hide button
  1019. const hideButton = document.createElement('button');
  1020. hideButton.innerHTML = '<i class="fas fa-store-slash"></i>';
  1021. hideButton.title = `Alle Deals von ${merchantName} ausblenden`;
  1022. hideButton.style.cssText = `
  1023. padding: 8px;
  1024. background: #f0f0f0;
  1025. border: 1px solid #ccc;
  1026. border-radius: 3px;
  1027. cursor: pointer;
  1028. `;
  1029.  
  1030. // Add click handler
  1031. hideButton.addEventListener('click', () => {
  1032. const merchantsData = loadExcludeMerchants();
  1033.  
  1034. // Check if ID already exists
  1035. if (!merchantsData.some(m => m.id === merchantId)) {
  1036. // Add new merchant at start of array
  1037. merchantsData.unshift({ id: merchantId, name: merchantName });
  1038. saveExcludeMerchants(merchantsData);
  1039. processArticles();
  1040. }
  1041. });
  1042.  
  1043. // Add button to page
  1044. hideButtonContainer.appendChild(hideButton);
  1045. merchantBanner.appendChild(hideButtonContainer);
  1046. }
  1047.  
  1048. // Call function on page load
  1049. document.addEventListener('DOMContentLoaded', () => {
  1050. addMerchantPageHideButton();
  1051. });