您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Watch favorite users for new/unsolved puzzles on Logic Masters Deutschland
// ==UserScript== // @name Logic Masters Puzzle Watcher // @namespace http://tampermonkey.net/ // @version 2.1 // @description Watch favorite users for new/unsolved puzzles on Logic Masters Deutschland // @author Oliver Burgert // @match https://logic-masters.de/* // @license GPL-3.0-or-later // @grant GM_setValue // @grant GM_getValue // @grant GM_xmlhttpRequest // ==/UserScript== (function() { 'use strict'; // Configuration const CHECK_INTERVAL = 60 * 60 * 1000; // 1 hour in milliseconds const DAILY_CHECK_INTERVAL = 24 * 60 * 60 * 1000; // 24 hours in milliseconds // Storage keys const STORAGE_KEYS = { FAVORITE_USERS: 'lm_favorite_users', LAST_CHECK: 'lm_last_check', PUZZLE_DATA: 'lm_puzzle_data' }; // Initialize storage if needed function initStorage() { if (!GM_getValue(STORAGE_KEYS.FAVORITE_USERS)) { GM_setValue(STORAGE_KEYS.FAVORITE_USERS, JSON.stringify([])); } if (!GM_getValue(STORAGE_KEYS.PUZZLE_DATA)) { GM_setValue(STORAGE_KEYS.PUZZLE_DATA, JSON.stringify({})); } } // Get favorite users list function getFavoriteUsers() { return JSON.parse(GM_getValue(STORAGE_KEYS.FAVORITE_USERS, '[]')); } // Save favorite users list function saveFavoriteUsers(users) { GM_setValue(STORAGE_KEYS.FAVORITE_USERS, JSON.stringify(users)); } // Get puzzle data function getPuzzleData() { return JSON.parse(GM_getValue(STORAGE_KEYS.PUZZLE_DATA, '{}')); } // Save puzzle data function savePuzzleData(data) { GM_setValue(STORAGE_KEYS.PUZZLE_DATA, JSON.stringify(data)); } // Get last check timestamp function getLastCheck() { return GM_getValue(STORAGE_KEYS.LAST_CHECK, 0); } // Save last check timestamp function saveLastCheck(timestamp) { GM_setValue(STORAGE_KEYS.LAST_CHECK, timestamp); } // Check if we need to update data function shouldCheck() { const lastCheck = getLastCheck(); const now = Date.now(); return (now - lastCheck) >= CHECK_INTERVAL; } // Parse puzzle data from HTML function parsePuzzleData(html, username) { const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); // Check for error message first const errorElement = doc.querySelector('p.rp_error'); if (errorElement) { throw new Error('User does not exist'); } const puzzleRows = doc.querySelectorAll('table.rp_raetselliste tr'); const puzzles = []; for (let i = 1; i < puzzleRows.length; i++) { // Skip header row const row = puzzleRows[i]; const cells = row.querySelectorAll('td'); if (cells.length >= 4) { const statusImg = cells[0].querySelector('img'); const puzzleLink = cells[1].querySelector('a'); const solvedCount = cells[2].textContent.trim(); const ratingCell = cells[3]; if (statusImg && puzzleLink) { const status = statusImg.getAttribute('title'); const difficultyImg = ratingCell.querySelector('img'); const ratingSpan = ratingCell.querySelector('span'); // Extract description span text (e.g. "von <user> (gelöst am ...)") const descriptionSpan = cells[1].querySelector('span'); const descriptionText = descriptionSpan ? descriptionSpan.textContent.trim().toLowerCase() : ''; // Only include new or unsolved puzzles (German and English versions) // Matches "gelöst heute", "gelöst am", "solved today", "solved on" const isSolved = /gelöst (am|heute|gestern)|solved (on|today|yesterday)/.test(descriptionText); if ( ((status === 'neu' || status === 'new') && !isSolved) || status === 'ungeloest' || status === 'unsolved' ) { puzzles.push({ name: puzzleLink.textContent.trim(), link: puzzleLink.getAttribute('href'), status: status, solved: solvedCount, difficulty: difficultyImg ? difficultyImg.getAttribute('alt') : '?', difficultyTitle: difficultyImg ? difficultyImg.getAttribute('title') : '', rating: ratingSpan ? ratingSpan.textContent.trim() : 'N/A' }); } } } } return puzzles; } // Fetch puzzle data for a user function fetchUserPuzzles(username) { return new Promise((resolve, reject) => { const url = `https://logic-masters.de/Raetselportal/Benutzer/eingestellt.php?name=${username}`; GM_xmlhttpRequest({ method: 'GET', url: url, onload: function(response) { if (response.status === 200) { try { const puzzles = parsePuzzleData(response.responseText, username); resolve({ username, puzzles, error: null }); } catch (e) { resolve({ username, puzzles: [], error: 'Failed to parse puzzle data' }); } } else { resolve({ username, puzzles: [], error: 'User does not exist' }); } }, onerror: function() { resolve({ username, puzzles: [], error: 'Network error' }); } }); }); } // Update puzzle data for all favorite users async function updatePuzzleData() { const favoriteUsers = getFavoriteUsers(); const puzzleData = {}; const promises = favoriteUsers.map(user => fetchUserPuzzles(user)); const results = await Promise.all(promises); results.forEach(result => { puzzleData[result.username] = { puzzles: result.puzzles, error: result.error, lastUpdate: Date.now() }; }); savePuzzleData(puzzleData); saveLastCheck(Date.now()); return puzzleData; } // Create the widget HTML function createWidget() { const widget = document.createElement('div'); widget.className = 'box menu'; widget.id = 'puzzle-watcher-widget'; widget.innerHTML = ` <h2>Puzzle Watcher</h2> <div style="margin-bottom: 10px; display: flex; align-items: center; gap: 5px; flex-wrap: wrap;"> <input type="text" id="new-user-input" placeholder="Username" style="width: 100px;"> <button id="add-user-btn" style="font-size: 11px;">Add User</button> <button id="refresh-btn" style="font-size: 11px;">Refresh</button> </div> <div id="puzzle-results"></div> `; return widget; } // Update widget display function updateWidgetDisplay() { const resultsDiv = document.getElementById('puzzle-results'); if (!resultsDiv) return; const favoriteUsers = getFavoriteUsers(); const puzzleData = getPuzzleData(); let html = ''; if (favoriteUsers.length === 0) { html = '<p style="font-size: 11px;">No users being watched.</p>'; } else { favoriteUsers.forEach(username => { const userData = puzzleData[username]; html += `<div style="border-bottom: 1px solid #ccc; margin-bottom: 5px; padding: 3px 0;">`; html += ` <div style="display: flex; justify-content: space-between; align-items: center; font-size: 12px; font-weight: bold;"> <a href="/Raetselportal/Benutzer/eingestellt.php?name=${username}" style="text-decoration: none;">${username}</a> <button class="remove-user-btn" data-username="${username}" style="font-size: 9px;">Remove</button> </div> `; if (!userData) { html += `<p style="font-size: 10px; color: #666;">Not checked yet...</p>`; } else if (userData.error) { html += `<p style="font-size: 10px; color: #f00;">${userData.error}</p>`; } else if (userData.puzzles.length === 0) { html += `<p style="font-size: 10px; color: #666;">No new puzzles</p>`; } else { userData.puzzles.forEach(puzzle => { html += `<div style="margin: 2px 0 2px 15px; font-size: 10px; display: flex; align-items: baseline; gap: 4px; flex-wrap: wrap;">`; html += `<a href="${puzzle.link}" style="text-decoration: none; display: inline;">${puzzle.name}</a>`; html += `<span style="color: #666; font-size: 9px; display: inline;">(Level ${puzzle.difficulty}, ${puzzle.rating}, ${puzzle.solved} solved)</span>`; html += `</div>`; }); } html += `</div>`; }); } resultsDiv.innerHTML = html; const removeButtons = resultsDiv.querySelectorAll('.remove-user-btn'); removeButtons.forEach(button => { button.addEventListener('click', function () { const username = this.getAttribute('data-username'); removeUser(username); }); }); } // Add user to favorites function addUser() { const input = document.getElementById('new-user-input'); const username = input.value.trim(); if (username) { const favoriteUsers = getFavoriteUsers(); if (!favoriteUsers.includes(username)) { favoriteUsers.push(username); saveFavoriteUsers(favoriteUsers); refreshData(); updateWidgetDisplay(); } input.value = ''; } } // Remove user from favorites function removeUser(username) { const favoriteUsers = getFavoriteUsers(); const index = favoriteUsers.indexOf(username); if (index > -1) { favoriteUsers.splice(index, 1); saveFavoriteUsers(favoriteUsers); // Also remove their data const puzzleData = getPuzzleData(); delete puzzleData[username]; savePuzzleData(puzzleData); updateWidgetDisplay(); } } // Refresh puzzle data async function refreshData() { const refreshBtn = document.getElementById('refresh-btn'); if (refreshBtn) { refreshBtn.textContent = 'Loading...'; refreshBtn.disabled = true; } try { await updatePuzzleData(); updateWidgetDisplay(); } finally { if (refreshBtn) { refreshBtn.textContent = 'Refresh'; refreshBtn.disabled = false; } } } // Make functions globally available for onclick handlers window.removeUser = removeUser; // Initialize everything function init() { initStorage(); // Find the left column and add our widget const leftColumn = document.querySelector('.leftcolumn'); if (leftColumn) { const widget = createWidget(); leftColumn.appendChild(widget); // Add event listeners document.getElementById('add-user-btn').addEventListener('click', addUser); document.getElementById('refresh-btn').addEventListener('click', refreshData); // Allow adding user with Enter key document.getElementById('new-user-input').addEventListener('keypress', function(e) { if (e.key === 'Enter') { addUser(); } }); // Update display with current data updateWidgetDisplay(); // Check if we need to update data automatically if (shouldCheck()) { refreshData(); } } } // Wait for the page to load if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();