MediaWiki:Common.js: Difference between revisions

From PickiPedia: A knowledge base of bluegrass, old time psychedelic jams, and other public domain music
Jump to navigationJump to search
Magent (talk | contribs)
Use PF's mw.hook(pf.formValidationBefore) instead of native form submit event
Magent (talk | contribs)
v24772700: require all three (make+model+serial) when no nickname provided
 
(2 intermediate revisions by the same user not shown)
Line 302: Line 302:
})();
})();
/**
/**
  * Instrument form — validation + page name computation
  * Instrument form — validation + page name computation (v24772700)
* Requires at least a nickname or one of make/model/serial.
* When nickname is empty, fills it with make/model/serial so the
* page name formula <Nickname[Instrument]> resolves correctly.
*
* Uses PF's mw.hook('pf.formValidationBefore') since PF bypasses
* native form submit events.
  */
  */
(function() {
(function() {
     'use strict';
     'use strict';
    console.log('PickiPedia Common.js instrument module v24772700 loaded');


     mw.hook('pf.formValidationBefore').add(function() {
     mw.hook('pf.formValidation').add(function(args) {
         var nickname = document.querySelector('input[name="Instrument[nickname]"]');
         var nickname = document.querySelector('input[name="Instrument[nickname]"]');
         if (!nickname) return; // not the instrument form
         if (!nickname) return;


         var make = document.querySelector('input[name="Instrument[make]"]');
         var make = document.querySelector('input[name="Instrument[make]"]');
Line 327: Line 322:


         var hasNickname = nickVal !== '';
         var hasNickname = nickVal !== '';
         var hasMaker = makeVal !== '' || modelVal !== '' || serialVal !== '';
         var hasMakerComplete = makeVal !== '' && modelVal !== '' && serialVal !== '';


         if (!hasNickname && !hasMaker) {
         if (!hasNickname && !hasMakerComplete) {
             alert('Please provide either a nickname or at least one of make, model, or serial number.');
             args.numErrors += 1;
             // Add an error to block submission
            if (makeVal !== '' || modelVal !== '' || serialVal !== '') {
            var errorMsg = $('<span>').addClass('errorMessage')
                alert('Maker identification requires all three: make, model, and serial number. Or provide a nickname instead.');
                 .text('A nickname or maker identification is required.');
             } else {
             $(nickname).closest('.formtable').before(errorMsg);
                 alert('Please provide either a nickname or maker identification (make, model, and serial number).');
             }
             return;
             return;
         }
         }
Line 340: Line 336:
         // If no nickname, fill it from make/model/serial for page naming
         // If no nickname, fill it from make/model/serial for page naming
         if (!hasNickname) {
         if (!hasNickname) {
             var parts = [makeVal, modelVal, serialVal].filter(function(s) {
             nickname.value = makeVal + ' ' + modelVal + ' ' + serialVal;
                return s !== '';
            });
            nickname.value = parts.join(' ');
         }
         }
     });
     });
})();
})();

Latest revision as of 22:32, 30 March 2026

/**
* Blue Railroad Submission - Date to Block Height Converter
* Adds a datepicker that converts dates to Ethereum block heights
* Also shows time ago for news templates
*/

(function() {
    'use strict';
    
    // Only run on the Blue Railroad Submission form
    if (!document.querySelector('input[name="Blue Railroad Submission[block_height]"]')) {
        return;
    }
    
    // Reference point: known block and timestamp
    // Post-merge average block time is ~12.12 seconds
    const AVG_BLOCK_TIME = 12.12;
    
    // Get current block from footer (format: "24,328,442")
    function getCurrentBlockFromFooter() {
        const footerLink = document.querySelector('a[href*="etherscan.io/block/"]');
        if (footerLink) {
            const match = footerLink.href.match(/block\/(\d+)/);
            if (match) return parseInt(match[1]);
        }
        return null;
    }
    
    // Calculate block height from date
    function dateToBlockHeight(targetDate, refBlock, refTimestamp) {
        const targetTimestamp = targetDate.getTime() / 1000;
        const secondsDiff = refTimestamp - targetTimestamp;
        const blocksDiff = Math.round(secondsDiff / AVG_BLOCK_TIME);
        return refBlock - blocksDiff;
    }
    
    // Add the datepicker UI
    function addDatePicker() {
        const blockInput = document.querySelector('input[name="Blue Railroad Submission[block_height]"]');
        if (!blockInput) return;
        
        const container = document.createElement('div');
        container.style.marginTop = '8px';
        container.innerHTML = `
            <label style="display: block; margin-bottom: 4px; font-size: 0.9em;">
                Or pick a date/time:
            </label>
            <input type="datetime-local" id="br-datepicker" style="padding: 4px; margin-right: 8px;">
            <button type="button" id="br-convert-btn" style="padding: 4px 12px; cursor: pointer;">
                Convert to Block
            </button>
            <span id="br-status" style="margin-left: 8px; font-size: 0.9em; color: #666;"></span>
        `;
        
        blockInput.parentNode.appendChild(container);
        
        const datePicker = document.getElementById('br-datepicker');
        const convertBtn = document.getElementById('br-convert-btn');
        const status = document.getElementById('br-status');
        
        // Set default to now
        const now = new Date();
        datePicker.value = now.toISOString().slice(0, 16);
        
        convertBtn.addEventListener('click', function() {
            const selectedDate = new Date(datePicker.value);
            if (isNaN(selectedDate.getTime())) {
                status.textContent = 'Invalid date';
                status.style.color = 'red';
                return;
            }
            
            const currentBlock = getCurrentBlockFromFooter();
            if (!currentBlock) {
                status.textContent = 'Could not find reference block';
                status.style.color = 'red';
                return;
            }
            
            const currentTimestamp = Date.now() / 1000;
            const estimatedBlock = dateToBlockHeight(selectedDate, currentBlock, currentTimestamp);
            
            if (estimatedBlock > currentBlock) {
                status.textContent = 'Date is in the future!';
                status.style.color = 'orange';
            } else {
                status.textContent = '~estimated';
                status.style.color = 'green';
            }
            
            blockInput.value = estimatedBlock;
        });
    }
    
    // Run when DOM is ready
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', addDatePicker);
    } else {
        addDatePicker();
    }
})();

/**
 * TimeAgo — shows "X blocks ago" for transcluded templates.
 *
 * Usage in wikitext:
 *   <span class="timeago" data-lastmod-page="Template:NewsShorts"></span>
 *
 * Fetches the last revision timestamp from the API and renders
 * a block-based relative time string using Ethereum block heights.
 */
(function() {
    'use strict';

    // Post-merge average block time is ~12.12 seconds
    var AVG_BLOCK_TIME = 12.12;

    var spans = document.querySelectorAll('.timeago[data-lastmod-page]');
    if (!spans.length) return;

    // Format number with commas (e.g., 1234567 -> "1,234,567")
    function formatNumber(num) {
        return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
    }

    // Collect unique page titles
    var titles = [];
    spans.forEach(function(span) {
        var t = span.getAttribute('data-lastmod-page');
        if (t && titles.indexOf(t) === -1) titles.push(t);
    });

    // Batch query the API (up to 50 titles per request)
    var api = mw.config.get('wgScriptPath') + '/api.php';
    var url = api + '?action=query&prop=revisions&rvprop=timestamp&format=json&titles=' +
        encodeURIComponent(titles.join('|'));

    fetch(url).then(function(r) { return r.json(); }).then(function(data) {
        var pages = data.query && data.query.pages || {};
        var timestamps = {};

        Object.keys(pages).forEach(function(id) {
            var page = pages[id];
            if (page.revisions && page.revisions[0]) {
                timestamps[page.title] = new Date(page.revisions[0].timestamp);
            }
        });

        spans.forEach(function(span) {
            var title = span.getAttribute('data-lastmod-page');
            var ts = timestamps[title];
            if (!ts) {
                span.textContent = '';
                return;
            }
            span.textContent = formatBlocksAgo(ts);
            span.title = ts.toLocaleString();
        });
    });

    function formatBlocksAgo(date) {
        var secondsAgo = Math.floor((Date.now() - date.getTime()) / 1000);
        var blocksAgo = Math.round(secondsAgo / AVG_BLOCK_TIME);

        if (blocksAgo < 1) {
            return 'this block';
        } else if (blocksAgo === 1) {
            return '1 block ago';
        } else {
            return formatNumber(blocksAgo) + ' blocks ago';
        }
    }
})();

/**
 * HLS Video Player - Initializes HLS.js for IPFS-hosted videos
 *
 * Usage in wikitext (via Template:HLSVideo):
 *   <div class="hls-video-player" data-cid="QmXet6..."></div>
 *
 * The gadget loads hls.js and initializes players for any element
 * with the hls-video-player class and a data-cid attribute.
 *
 * Supports both HLS streams (CID/master.m3u8) and raw video files.
 * Tries HLS first; if the manifest 404s, falls back to direct playback.
 */
(function() {
    'use strict';

    var IPFS_GATEWAY = 'https://ipfs.delivery-kid.cryptograss.live/ipfs';
    var HLS_JS_URL = 'https://cdn.jsdelivr.net/npm/hls.js@latest';
    var hlsLoadPromise = null;

    // CIDv1 (bafy...) is Base32 lowercase, but MediaWiki capitalizes page titles.
    // CIDv0 (Qm...) is Base58 case-sensitive — must not be lowercased.
    function normalizeCid(cid) {
        if (cid && cid.substring(0, 4) === 'Bafy') {
            return cid.toLowerCase();
        }
        return cid;
    }

    function loadHls() {
        if (typeof Hls !== 'undefined') {
            return Promise.resolve();
        }
        if (hlsLoadPromise) {
            return hlsLoadPromise;
        }
        hlsLoadPromise = new Promise(function(resolve) {
            var script = document.createElement('script');
            script.src = HLS_JS_URL;
            script.onload = resolve;
            document.head.appendChild(script);
        });
        return hlsLoadPromise;
    }

    function initPlayers() {
        var containers = document.querySelectorAll('.hls-video-player[data-cid]:not([data-initialized])');
        if (!containers.length) return;

        // Mark ALL containers immediately to prevent race conditions
        containers.forEach(function(c) {
            c.setAttribute('data-initialized', 'true');
        });

        // Then load hls.js and initialize
        loadHls().then(function() {
            containers.forEach(initPlayer);
        });
    }

    function createVideoElement(width, maxWidth) {
        var video = document.createElement('video');
        video.controls = true;
        video.playsInline = true;
        video.style.width = width;
        video.style.maxWidth = maxWidth;
        video.style.backgroundColor = '#000';
        video.style.display = 'block';
        return video;
    }

    function fallbackToDirectVideo(container, cid, width, maxWidth) {
        // CID is a raw video file, not an HLS stream — play directly
        var directUrl = IPFS_GATEWAY + '/' + normalizeCid(cid);
        container.innerHTML = '';
        var video = createVideoElement(width, maxWidth);
        video.src = directUrl;
        video.addEventListener('error', function() {
            container.innerHTML = '<p style="color: red; padding: 1em;">Error loading video. The IPFS content may not be available.</p>';
        });
        container.appendChild(video);
    }

    function initPlayer(container) {
        var cid = container.getAttribute('data-cid');
        var normalCid = normalizeCid(cid);
        var width = container.getAttribute('data-width') || '100%';
        var maxWidth = container.getAttribute('data-max-width') || '800px';
        
        var hlsSrc = IPFS_GATEWAY + '/' + normalCid + '/master.m3u8';

        var video = createVideoElement(width, maxWidth);
        container.appendChild(video);

        if (video.canPlayType('application/vnd.apple.mpegurl')) {
            // Safari native HLS — try HLS first, fall back on error
            video.src = hlsSrc;
            video.addEventListener('error', function() {
                fallbackToDirectVideo(container, cid, width, maxWidth);
            });
        } else if (typeof Hls !== 'undefined' && Hls.isSupported()) {
            var hls = new Hls();
            hls.loadSource(hlsSrc);
            hls.attachMedia(video);
            hls.on(Hls.Events.ERROR, function(event, data) {
                if (data.fatal) {
                    // HLS failed (likely no master.m3u8) — try direct video
                    hls.destroy();
                    fallbackToDirectVideo(container, cid, width, maxWidth);
                }
            });
        } else {
            // No HLS support — try direct video
            fallbackToDirectVideo(container, cid, width, maxWidth);
        }
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initPlayers);
    } else {
        initPlayers();
    }

    if (typeof mw !== 'undefined' && mw.hook) {
        mw.hook('wikipage.content').add(function($content) {
            initPlayers();
        });
    }
})();
/**
 * Instrument form — validation + page name computation (v24772700)
 */
(function() {
    'use strict';
    console.log('PickiPedia Common.js instrument module v24772700 loaded');

    mw.hook('pf.formValidation').add(function(args) {
        var nickname = document.querySelector('input[name="Instrument[nickname]"]');
        if (!nickname) return;

        var make = document.querySelector('input[name="Instrument[make]"]');
        var model = document.querySelector('input[name="Instrument[model]"]');
        var serial = document.querySelector('input[name="Instrument[serial]"]');

        var nickVal = nickname.value.trim();
        var makeVal = make ? make.value.trim() : '';
        var modelVal = model ? model.value.trim() : '';
        var serialVal = serial ? serial.value.trim() : '';

        var hasNickname = nickVal !== '';
        var hasMakerComplete = makeVal !== '' && modelVal !== '' && serialVal !== '';

        if (!hasNickname && !hasMakerComplete) {
            args.numErrors += 1;
            if (makeVal !== '' || modelVal !== '' || serialVal !== '') {
                alert('Maker identification requires all three: make, model, and serial number. Or provide a nickname instead.');
            } else {
                alert('Please provide either a nickname or maker identification (make, model, and serial number).');
            }
            return;
        }

        // If no nickname, fill it from make/model/serial for page naming
        if (!hasNickname) {
            nickname.value = makeVal + ' ' + modelVal + ' ' + serialVal;
        }
    });
})();