// ==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();
}
})();