Puzzmo Crossword Navigator

Navigate to most recent incomplete crossword

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Puzzmo Crossword Navigator
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Navigate to most recent incomplete crossword
// @author       Michael Abon
// @match        https://www.puzzmo.com/*
// @grant        none
// @esversion    8
// @license MIT
// ==/UserScript==

(function() {
    'use strict';
    
    function createButton(id = 'crossword-nav-btn') {
        const button = document.createElement('button');
        button.id = id;
        button.textContent = 'Go to Latest Incomplete Crossword';
        button.style.cssText = `
            padding: 0.25rem 1.5rem;
            width: 100%;
            background: var(--theme-player);
            color: var(--theme-playerFG);
            border: none;
            border-radius: 0.300rem;
            font-size: 13px;
            cursor: pointer;
            transition: background 0.2s;
            font-family: "Poppins-SemiBold", "Poppins SemiBold", "Poppins";
        `;
        
        button.addEventListener('click', navigateToLatestCrossword);
        return button;
    }
    
    function injectButton() {
        const todayIntro = document.getElementById('today-intro');
        if (!todayIntro || document.getElementById('crossword-nav-btn')) return;
        
        const button = createButton();
                
        // Insert before the target element
        const container = document.createElement('div');
        container.style.cssText = `
            margin-top: 1rem;
            display: flex;
            justify-content: flex-start;
        `;
        container.appendChild(button);
        todayIntro.parentNode.insertBefore(container, todayIntro);
    }
    
    function injectButtonInSidebar() {
        // Look for the specific link in the puzzle complete sidebar with "More puzzles" text
        const targetLinks = document.querySelectorAll('a[aria-label="Go to [object Object]"][href^="/today"]');
        const targetLink = Array.from(targetLinks).find(link => link.innerText.trim() === 'More puzzles');
        
        if (!targetLink || document.getElementById('crossword-nav-btn-sidebar')) return;
        
        const button = createButton('crossword-nav-btn-sidebar');
        
        // Create container for the button - this will be a sibling of the parent with onclick
        const container = document.createElement('div');
        container.style.cssText = `
            margin-bottom: 12px;
            display: flex;
            justify-content: flex-start;
        `;
        container.appendChild(button);
        
        // Get the parent with onclick and its parent (grandparent)
        const parentWithOnClick = targetLink.parentElement;
        const grandparent = parentWithOnClick.parentElement;
        
        if (grandparent) {
            // Insert our container before the parent with onclick
            grandparent.insertBefore(container, parentWithOnClick);
        }
    }
    
    function getAuthData() {
        try {
            const puzmoAuth = localStorage.getItem('puzmoAuth');
            if (!puzmoAuth) {
                throw new Error('No puzmoAuth found in localStorage');
            }
            
            const authData = JSON.parse(puzmoAuth);
            return {
                token: authData.token,
                userID: authData.data.userID
            };
        } catch (error) {
            console.error('Failed to parse auth data:', error);
            return null;
        }
    }
    
    async function getAuthHeaders() {
        const authData = getAuthData();
        if (!authData) {
            throw new Error('Could not retrieve authentication data');
        }
        
        return {
            'Accept': '*/*',
            'Accept-Language': 'en-CA,en-US;q=0.7,en;q=0.3',
            'auth-provider': 'custom',
            'authorization': `Bearer ${authData.token}`,
            'puzzmo-gameplay-id': authData.userID,
            'runtime': 'web',
            'Content-Type': 'application/json',
            'Origin': 'https://www.puzzmo.com',
            'Referer': window.location.href
        };
    }
    
    async function fetchCalendarData(month, year) {
        const query = `
            query CalendarQuery(
              $month: Int!
              $year: Int!
              $gameFilter: String
            ) {
              dailiesForAMonth(month: $month, year: $year, gameFilter: $gameFilter) {
                day
                puzzles(cacheKey: "calendar") {
                  series {
                    id
                  }
                  lock {
                    start
                    type
                    overrideRoles
                  }
                  status
                  urlPath
                  puzzle {
                    id
                    slug
                    name
                    emoji
                    variantID
                    subvariantID
                    seriesNumber
                    game {
                      slug
                      displayName
                      flagsArr
                      id
                    }
                    gameNameOverride
                    currentAccountGamePlayed {
                      slug
                      completed
                      pointsAwarded
                      id
                    }
                  }
                  id
                }
                id
              }
            }`;
        
        const headers = await getAuthHeaders();
        
        try {
            const response = await fetch('https://www.puzzmo.com/_api/prod/graphql?CalendarQuery=', {
                method: 'POST',
                headers,
                body: JSON.stringify({
                    query,
                    operationName: 'CalendarQuery',
                    variables: {
                        month,
                        year,
                        gameFilter: null
                    }
                })
            });
            
            if (!response.ok) {
                throw new Error(`HTTP error! status: ${response.status}`);
            }
            
            return await response.json();
        } catch (error) {
            console.error('Failed to fetch calendar data:', error);
            return null;
        }
    }
    
    function findIncompleteCrosswords(calendarData) {
        if (!calendarData?.data?.dailiesForAMonth) return [];
        
        const incompleteCrosswords = [];
        
        calendarData.data.dailiesForAMonth.forEach(daily => {
            if (!daily.puzzles) return;
            
            daily.puzzles.forEach(puzzleEntry => {
                const puzzle = puzzleEntry.puzzle;
                
                // Check if it's a crossword game
                if (puzzle?.game?.slug === 'crossword') {
                    const gamePlay = puzzle.currentAccountGamePlayed;
                    
                    // Check if incomplete (null gamePlay or completed: false)
                    if (!gamePlay || !gamePlay.completed) {
                        incompleteCrosswords.push({
                            day: daily.day,
                            urlPath: puzzleEntry.urlPath,
                            name: puzzle.name || puzzle.gameNameOverride || 'Crossword',
                            emoji: puzzle.emoji || '📝',
                            seriesNumber: puzzle.seriesNumber
                        });
                    }
                }
            });
        });
        
        // Sort by seriesNumber (most recent first - higher numbers are newer)
        return incompleteCrosswords.sort((a, b) => b.seriesNumber - a.seriesNumber);
    }
    
    async function navigateToLatestCrossword() {
        const button = document.getElementById('crossword-nav-btn') || document.getElementById('crossword-nav-btn-sidebar');
        const originalText = button.textContent;
        
        button.textContent = 'Searching...';
        button.disabled = true;
        
        try {
            const now = new Date();
            let currentMonth = now.getMonth(); // 0-indexed
            let currentYear = now.getFullYear();
            
            // Search back indefinitely (with reasonable safety limit of 5 years)
            const maxMonthsBack = 60; // 5 years
            
            for (let monthsBack = 0; monthsBack < maxMonthsBack; monthsBack++) {
                console.log(`Searching ${currentYear}-${currentMonth + 1}...`);
                button.textContent = `Searching ${currentYear}-${String(currentMonth + 1).padStart(2, '0')}...`;
                
                const calendarData = await fetchCalendarData(currentMonth, currentYear);
                if (!calendarData) {
                    console.error('Failed to fetch calendar data');
                    break;
                }
                
                const incompleteCrosswords = findIncompleteCrosswords(calendarData);
                
                if (incompleteCrosswords.length > 0) {
                    const latest = incompleteCrosswords[0];
                    console.log(`Found incomplete crossword: ${latest.name} on day ${latest.day}`);
                    button.textContent = `Found ${latest.name}! Navigating...`;
                    
                    // Navigate to the puzzle
                    window.location.href = `https://www.puzzmo.com/puzzle/${latest.urlPath}`;
                    return;
                }
                
                // Go back one month
                currentMonth--;
                if (currentMonth < 0) {
                    currentMonth = 11;
                    currentYear--;
                }
                
                // Small delay to avoid hammering the API
                await new Promise(resolve => setTimeout(resolve, 100));
            }
            
            alert(`No incomplete crosswords found in the last ${maxMonthsBack / 12} years!`);
            
        } catch (error) {
            console.error('Error finding crossword:', error);
            alert('Error finding incomplete crossword. Check console for details.');
        } finally {
            button.textContent = originalText;
            button.disabled = false;
        }
    }
    
    function injectAllButtons() {
        injectButton();
        injectButtonInSidebar();
    }
    
    // Wait for page to load, then inject
    function init() {
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', injectAllButtons);
        } else {
            injectAllButtons();
        }
        
        // Also watch for dynamic content changes
        const observer = new MutationObserver(injectAllButtons);
        observer.observe(document.body, { childList: true, subtree: true });
    }
    
    init();
})();