|
|
| (4 intermediate revisions by the same user not shown) |
| Line 250: |
Line 250: |
| /* End of includePage */ | | /* End of includePage */ |
|
| |
|
| /**
| | importScript( 'MediaWiki:Common-mainpage.js' ); |
| * BattlestarWiki — Main Page JavaScript
| | importScript( 'MediaWiki:Common-votd.js' ); |
| * Append to MediaWiki:Common.js
| | importScript( 'MediaWiki:Common.portal.js' ); |
| *
| |
| * Handles:
| |
| * 1. Hero slideshow auto-rotation + article image fetching
| |
| * 2. Featured Article of the Day — daily deterministic pick,
| |
| * excluding Category:Stub_Pages, with extract + thumbnail
| |
| * 3. Media wiki file count (from media.battlestarwiki.org)
| |
| * 4. Recent changes tab switching (EN / DE / Media wikis)
| |
| * 5. Red-link card filtering
| |
| *
| |
| * All logic is scoped to .bsw-main-page; safe to load globally.
| |
| */
| |
| | |
| ( function () {
| |
| 'use strict';
| |
| | |
| var API = 'https://en.battlestarwiki.org/w/api.php';
| |
| var MEDIA_API = 'https://media.battlestarwiki.org/w/api.php';
| |
| var DE_API = 'https://de.battlestarwiki.org/w/api.php';
| |
| | |
| /* ── Shared utilities ─────────────────────────────────────────── */
| |
| | |
| function dailySeed( dateStr ) {
| |
| var d;
| |
| if ( dateStr ) {
| |
| var p = dateStr.split( '-' );
| |
| d = Date.UTC( +p[0], +p[1] - 1, +p[2] );
| |
| } else {
| |
| var now = new Date();
| |
| d = Date.UTC( now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() );
| |
| }
| |
| return Math.floor( d / 86400000 );
| |
| }
| |
| | |
| function esc( s ) {
| |
| return String( s || '' )
| |
| .replace( /&/g, '&' )
| |
| .replace( /</g, '<' )
| |
| .replace( />/g, '>' )
| |
| .replace( /"/g, '"' );
| |
| }
| |
| | |
| function apiGetFrom( baseUrl, params ) {
| |
| params.format = 'json';
| |
| params.origin = '*';
| |
| var qs = Object.keys( params )
| |
| .map( function ( k ) { return encodeURIComponent( k ) + '=' + encodeURIComponent( params[k] ); } )
| |
| .join( '&' );
| |
| return fetch( baseUrl + '?' + qs, { headers: { Accept: 'application/json' } } )
| |
| .then( function ( r ) {
| |
| if ( !r.ok ) throw new Error( 'API HTTP ' + r.status );
| |
| return r.json();
| |
| } );
| |
| }
| |
| | |
| function apiGet( params ) {
| |
| return apiGetFrom( API, params );
| |
| }
| |
| | |
| /* ════════════════════════════════════════════════════════════════
| |
| 1. HERO SLIDESHOW
| |
| Dynamically populated from the wiki's most-viewed articles.
| |
| Uses a daily seed so all visitors see the same 3 slides each day.
| |
| Falls back to the static Lua-rendered slides if the API fails.
| |
| ════════════════════════════════════════════════════════════════ */
| |
| | |
| /* Series classification for badge color + label */
| |
| var SERIES_CATS = [
| |
| { pat: /\(RDM\)|\(TRS\)|Re-imagined|Reimagined/i, label: 'Re-imagined Series', color: 'var(--color-primary)' },
| |
| { pat: /\(TOS\)|Original Series|\bTOS\b/i, label: 'Original Series', color: '#ef9f27' },
| |
| { pat: /\(1980\)|Galactica.?1980|Category:1980\b/i, label: 'Galactica 1980', color: '#97c459' },
| |
| { pat: /\(Caprica\)|Caprica \(series\)|Category:Caprica\b/i, label: 'Caprica', color: '#d4537e' },
| |
| { pat: /\(BAC\)|Blood and Chrome|Blood.*Chrome|\bBAC\b/i, label: 'Blood & Chrome', color: '#e24b4a' }
| |
| ];
| |
| | |
| function classifyArticle( title, cats, extract ) {
| |
| /* Check category names first */
| |
| if ( cats ) {
| |
| for ( var i = 0; i < SERIES_CATS.length; i++ ) {
| |
| for ( var j = 0; j < cats.length; j++ ) {
| |
| if ( SERIES_CATS[i].pat.test( cats[j].title ) ) return SERIES_CATS[i];
| |
| }
| |
| }
| |
| }
| |
| /* Fall back to title matching */
| |
| for ( var k = 0; k < SERIES_CATS.length; k++ ) {
| |
| if ( SERIES_CATS[k].pat.test( title ) ) return SERIES_CATS[k];
| |
| }
| |
| /* Final fallback: scan extract for series keywords */
| |
| if ( extract ) {
| |
| var text = extract.replace( /<[^>]+>/g, '' );
| |
| for ( var m = 0; m < SERIES_CATS.length; m++ ) {
| |
| if ( SERIES_CATS[m].pat.test( text ) ) return SERIES_CATS[m];
| |
| }
| |
| }
| |
| return { label: 'Battlestar Wiki', color: 'var(--color-primary)' };
| |
| }
| |
| | |
| function buildSlideHTML( article, series, imgUrl ) {
| |
| var title = article.title;
| |
| var url = '/wiki/' + encodeURIComponent( title.replace( / /g, '_' ) );
| |
| var desc = ( article.extract || '' ).replace( /<[^>]+>/g, '' ).slice( 0, 160 );
| |
| if ( desc.length === 160 ) desc = desc.replace( /\s\S+$/, '' ) + '…';
| |
| | |
| var badgeStyle = series.color ? ' style="background:' + series.color + '"' : '';
| |
| | |
| return '<div class="bsw-slide bsw-active" data-article="' + esc( title ) + '">' +
| |
| '<div class="bsw-slide-bg"></div>' +
| |
| '<div class="bsw-slide-overlay"></div>' +
| |
| '<div class="bsw-slide-content">' +
| |
| '<div class="bsw-slide-badge">' +
| |
| '<span class="bsw-slide-badge-dot"' + badgeStyle + '></span>' +
| |
| '<span>' + esc( series.label ) + '</span>' +
| |
| '</div>' +
| |
| '<div class="bsw-slide-title"><a href="' + esc( url ) + '">' + esc( title ) + '</a></div>' +
| |
| '<div class="bsw-slide-desc">' + esc( desc ) + '</div>' +
| |
| '</div>' +
| |
| '</div>';
| |
| }
| |
| | |
| function buildDotsAndNav( count ) {
| |
| var dots = '<div class="bsw-hero-dots">';
| |
| for ( var i = 0; i < count; i++ ) {
| |
| var cls = 'bsw-hero-dot' + ( i === 0 ? ' bsw-active' : '' );
| |
| dots += '<span class="' + cls + '" role="button" tabindex="0" onclick="bswGoSlide(' + i + ')"></span>';
| |
| }
| |
| dots += '</div>';
| |
| var nav = '<div class="bsw-hero-nav">' +
| |
| '<span class="bsw-hero-btn" role="button" tabindex="0" id="bsw-hero-prev" onclick="bswPrevSlide()">‹</span>' +
| |
| '<span class="bsw-hero-btn" role="button" tabindex="0" id="bsw-hero-next" onclick="bswNextSlide()">›</span>' +
| |
| '</div>';
| |
| return dots + nav;
| |
| }
| |
| | |
| function initHeroSlideshow( hero, slides ) {
| |
| var dots = hero.querySelectorAll( '.bsw-hero-dot' );
| |
| var current = 0;
| |
| var total = slides.length;
| |
| var timer;
| |
| | |
| function goTo( n ) {
| |
| slides[ current ].classList.remove( 'bsw-active' );
| |
| dots[ current ] && dots[ current ].classList.remove( 'bsw-active' );
| |
| current = ( n + total ) % total;
| |
| slides[ current ].classList.add( 'bsw-active' );
| |
| dots[ current ] && dots[ current ].classList.add( 'bsw-active' );
| |
| resetTimer();
| |
| }
| |
| | |
| function resetTimer() {
| |
| clearInterval( timer );
| |
| timer = setInterval( function () { goTo( current + 1 ); }, 6000 );
| |
| }
| |
| | |
| window.bswGoSlide = function ( n ) { goTo( n ); };
| |
| window.bswPrevSlide = function () { goTo( current - 1 ); };
| |
| window.bswNextSlide = function () { goTo( current + 1 ); };
| |
| | |
| dots.forEach( function ( dot, i ) {
| |
| dot.addEventListener( 'click', function () { goTo( i ); } );
| |
| } );
| |
| | |
| var prevBtn = document.getElementById( 'bsw-hero-prev' );
| |
| var nextBtn = document.getElementById( 'bsw-hero-next' );
| |
| if ( prevBtn ) prevBtn.addEventListener( 'click', function () { goTo( current - 1 ); } );
| |
| if ( nextBtn ) nextBtn.addEventListener( 'click', function () { goTo( current + 1 ); } );
| |
| | |
| hero.addEventListener( 'mouseenter', function () { clearInterval( timer ); } );
| |
| hero.addEventListener( 'mouseleave', resetTimer );
| |
| | |
| resetTimer();
| |
| }
| |
| | |
| function fetchSlideImages( hero, slides ) {
| |
| var titles = Array.from( slides ).map( function ( s ) { return s.dataset.article; } ).filter( Boolean );
| |
| if ( !titles.length ) return;
| |
| | |
| apiGet( {
| |
| action: 'query',
| |
| titles: titles.join( '|' ),
| |
| prop: 'pageimages',
| |
| piprop: 'thumbnail|original',
| |
| pithumbsize: '1200',
| |
| redirects: '1'
| |
| } ).then( function ( data ) {
| |
| var pages = data.query.pages;
| |
| var imageMap = {};
| |
| var normalized = {};
| |
| if ( data.query.normalized ) {
| |
| data.query.normalized.forEach( function ( n ) { normalized[ n.to ] = n.from; } );
| |
| }
| |
| var redirects = {};
| |
| if ( data.query.redirects ) {
| |
| data.query.redirects.forEach( function ( r ) { redirects[ r.to ] = r.from; } );
| |
| }
| |
| Object.values( pages ).forEach( function ( page ) {
| |
| if ( !page.title ) return;
| |
| var img = ( page.original && page.original.source ) ||
| |
| ( page.thumbnail && page.thumbnail.source ) || '';
| |
| if ( !img ) return;
| |
| imageMap[ page.title ] = img;
| |
| if ( redirects[ page.title ] ) imageMap[ redirects[ page.title ] ] = img;
| |
| if ( normalized[ page.title ] ) imageMap[ normalized[ page.title ] ] = img;
| |
| } );
| |
| | |
| slides.forEach( function ( slide ) {
| |
| var imgUrl = imageMap[ slide.dataset.article ];
| |
| if ( !imgUrl ) return;
| |
| var bg = slide.querySelector( '.bsw-slide-bg' );
| |
| if ( !bg ) return;
| |
| var bgImg = document.createElement( 'img' );
| |
| bgImg.src = imgUrl;
| |
| bgImg.alt = '';
| |
| bgImg.className = 'bsw-slide-bg-blur';
| |
| bgImg.setAttribute( 'aria-hidden', 'true' );
| |
| var fgImg = document.createElement( 'img' );
| |
| fgImg.src = imgUrl;
| |
| fgImg.alt = '';
| |
| fgImg.className = 'bsw-slide-bg-img';
| |
| bg.appendChild( bgImg );
| |
| bg.appendChild( fgImg );
| |
| } );
| |
| } ).catch( function () {} );
| |
| }
| |
| | |
| function initHero() {
| |
| var hero = document.querySelector( '.bsw-hero' );
| |
| if ( !hero ) return;
| |
| | |
| var seed = dailySeed();
| |
| var SLIDE_COUNT = 3;
| |
| var FETCH_BATCH = 20; /* MW caps rnlimit at 20 for non-bots */
| |
| | |
| /* Fetch two batches of random articles for a larger candidate pool */
| |
| Promise.all( [
| |
| apiGet( { action: 'query', list: 'random', rnnamespace: '0', rnlimit: String( FETCH_BATCH ) } ),
| |
| apiGet( { action: 'query', list: 'random', rnnamespace: '0', rnlimit: String( FETCH_BATCH ) } )
| |
| ] ).then( function ( results ) {
| |
| var combined = results[0].query.random.concat( results[1].query.random );
| |
| /* Deduplicate and filter */
| |
| var seen = {};
| |
| var items = combined.filter( function ( p ) {
| |
| if ( seen[ p.title ] ) return false;
| |
| seen[ p.title ] = true;
| |
| return p.title !== 'Main Page' &&
| |
| p.title !== 'Main Page/New' &&
| |
| p.title.indexOf( '/' ) === -1;
| |
| } );
| |
| if ( !items.length ) throw new Error( 'No articles' );
| |
| | |
| /* Cap at 20 titles to stay within MW API limits */
| |
| items = items.slice( 0, 20 );
| |
| | |
| /* Fetch extracts + images + categories for all candidates in one call */
| |
| return apiGet( {
| |
| action: 'query',
| |
| titles: items.map( function ( p ) { return p.title; } ).join( '|' ),
| |
| prop: 'extracts|pageimages|categories|pageprops',
| |
| exintro: '1',
| |
| exchars: '200',
| |
| exsectionformat: 'plain',
| |
| piprop: 'thumbnail|original',
| |
| pithumbsize: '1200',
| |
| cllimit: '500',
| |
| ppprop: 'disambiguation',
| |
| redirects: '1'
| |
| } );
| |
| } ).then( function ( data ) {
| |
| var pages = Object.values( data.query.pages || {} );
| |
| | |
| /* Keep only articles that have an image and aren't disambiguation pages */
| |
| var withImages = pages.filter( function ( p ) {
| |
| if ( !p.thumbnail && !p.original ) return false;
| |
| var cats = ( p.categories || [] ).map( function ( c ) { return c.title; } );
| |
| var isDisamb = ( p.pageprops && p.pageprops.disambiguation !== undefined ) ||
| |
| cats.some( function ( c ) { return c === 'Category:Disambiguation'; } );
| |
| var isStub = cats.some( function ( c ) { return /[Ss]tub/.test( c ); } );
| |
| return !isDisamb && !isStub;
| |
| } );
| |
| | |
| /* Fall back to all non-disambiguation pages if not enough have images */
| |
| var pool = withImages.length >= SLIDE_COUNT ? withImages : pages.filter( function ( p ) {
| |
| var cats = ( p.categories || [] ).map( function ( c ) { return c.title; } );
| |
| var isDisamb = ( p.pageprops && p.pageprops.disambiguation !== undefined ) ||
| |
| cats.some( function ( c ) { return c === 'Category:Disambiguation'; } );
| |
| var isStub = cats.some( function ( c ) { return /[Ss]tub/.test( c ); } );
| |
| return !isDisamb && !isStub;
| |
| } );
| |
| if ( pool.length < SLIDE_COUNT ) throw new Error( 'Not enough articles' );
| |
| | |
| /* Pick SLIDE_COUNT articles using daily seed with spacing */
| |
| var picks = [];
| |
| for ( var i = 0; i < SLIDE_COUNT; i++ ) {
| |
| picks.push( pool[ ( seed + i * 7 ) % pool.length ] );
| |
| }
| |
| | |
| /* Build slide HTML */
| |
| var slideHTML = picks.map( function ( article, i ) {
| |
| var cats = article.categories || [];
| |
| var series = classifyArticle( article.title, cats, article.extract );
| |
| var html = buildSlideHTML( article, series, '' );
| |
| return i === 0 ? html : html.replace( 'bsw-slide bsw-active', 'bsw-slide' );
| |
| } ).join( '' );
| |
| | |
| hero.innerHTML = slideHTML + buildDotsAndNav( picks.length );
| |
| | |
| var newSlides = hero.querySelectorAll( '.bsw-slide' );
| |
| initHeroSlideshow( hero, newSlides );
| |
| | |
| /* Inject images directly from the data we already fetched */
| |
| picks.forEach( function ( article, i ) {
| |
| var imgUrl = ( article.original && article.original.source ) ||
| |
| ( article.thumbnail && article.thumbnail.source ) || '';
| |
| if ( !imgUrl ) return;
| |
| var bg = newSlides[ i ] && newSlides[ i ].querySelector( '.bsw-slide-bg' );
| |
| if ( !bg ) return;
| |
| var bgImg = document.createElement( 'img' );
| |
| bgImg.src = imgUrl;
| |
| bgImg.alt = '';
| |
| bgImg.className = 'bsw-slide-bg-blur';
| |
| bgImg.setAttribute( 'aria-hidden', 'true' );
| |
| var fgImg = document.createElement( 'img' );
| |
| fgImg.src = imgUrl;
| |
| fgImg.alt = '';
| |
| fgImg.className = 'bsw-slide-bg-img';
| |
| bg.appendChild( bgImg );
| |
| bg.appendChild( fgImg );
| |
| } );
| |
| | |
| } ).catch( function ( err ) {
| |
| /* Fallback: use static Lua-rendered slides */
| |
| var staticSlides = hero.querySelectorAll( '.bsw-slide' );
| |
| if ( staticSlides.length ) {
| |
| /* Ensure first slide is active */
| |
| staticSlides.forEach( function( s ) { s.classList.remove( 'bsw-active' ); } );
| |
| staticSlides[0].classList.add( 'bsw-active' );
| |
| initHeroSlideshow( hero, staticSlides );
| |
| fetchSlideImages( hero, staticSlides );
| |
| }
| |
| } );
| |
| }
| |
| | |
| /* ════════════════════════════════════════════════════════════════
| |
| 2. FEATURED ARTICLE OF THE DAY
| |
| Daily deterministic pick from all mainspace articles,
| |
| excluding Category:Stub_Pages.
| |
| ════════════════════════════════════════════════════════════════ */
| |
| | |
| function initFeatured() {
| |
| var container = document.getElementById( 'bsw-featured-inner' );
| |
| if ( !container ) return;
| |
| | |
| var seed = dailySeed();
| |
| | |
| /* We fetch batches of 20 random articles, check for stubs,
| |
| and pick the first clean one. Two batches covers ~99.99%
| |
| of cases given only ~9% of articles are stubs. */
| |
| fetchCandidateBatch( seed )
| |
| .then( function ( pages ) {
| |
| var clean = pages.filter( function ( p ) {
| |
| /* If categories array is present and contains Stub_Pages,
| |
| exclude. Absence of categories key means not a stub. */
| |
| if ( !p.categories ) return true;
| |
| return !p.categories.some( function ( c ) {
| |
| return c.title === 'Category:Stub_Pages';
| |
| } );
| |
| } );
| |
| | |
| if ( !clean.length ) {
| |
| /* Extremely unlikely — fall back to second batch */
| |
| return fetchCandidateBatch( seed + 1 );
| |
| }
| |
| return clean;
| |
| } )
| |
| .then( function ( pages ) {
| |
| /* Pick deterministically from the clean pool */
| |
| var pick = pages[ seed % pages.length ];
| |
| renderFeatured( container, pick );
| |
| } )
| |
| .catch( function ( e ) {
| |
| container.innerHTML =
| |
| '<div class="bsw-error">Could not load featured article. ' + esc( e.message ) + '</div>';
| |
| } );
| |
| }
| |
| | |
| /**
| |
| * Fetch 20 random mainspace articles with extract, thumbnail,
| |
| * and stub-category membership check in a single API call.
| |
| * Uses rnstart derived from seed for determinism.
| |
| */
| |
| function fetchCandidateBatch( seed ) {
| |
| /* Step 1: get 20 random page IDs */
| |
| return apiGet( {
| |
| action: 'query',
| |
| list: 'random',
| |
| rnnamespace: '0',
| |
| rnlimit: '20'
| |
| } ).then( function ( data ) {
| |
| var ids = data.query.random.map( function ( p ) { return p.id; } ).join( '|' );
| |
| | |
| /* Step 2: batch fetch extract + thumbnail + stub check */
| |
| return apiGet( {
| |
| action: 'query',
| |
| pageids: ids,
| |
| prop: 'extracts|pageimages|categories',
| |
| exintro: '1',
| |
| exchars: '600',
| |
| exsectionformat: 'plain',
| |
| piprop: 'thumbnail',
| |
| pithumbsize: '320',
| |
| clcategories: 'Category:Stub_Pages',
| |
| cllimit: '1'
| |
| } );
| |
| } ).then( function ( data ) {
| |
| return Object.values( data.query.pages );
| |
| } );
| |
| }
| |
| | |
| function renderFeatured( container, page ) {
| |
| var title = page.title || '';
| |
| var url = 'https://en.battlestarwiki.org/wiki/' + encodeURIComponent( title.replace( / /g, '_' ) );
| |
| var extract = page.extract || '';
| |
| var thumb = page.thumbnail ? page.thumbnail.source : '';
| |
| | |
| var thumbHtml;
| |
| if ( thumb ) {
| |
| thumbHtml = '<img class="bsw-fa-thumb" src="' + esc( thumb ) + '" alt="' + esc( title ) + '">';
| |
| } else {
| |
| thumbHtml = '<img class="bsw-fa-thumb" src="https://en.battlestarwiki.org/resources/BSGWIKILOGO.png" alt="Battlestar Wiki">';
| |
| }
| |
| | |
| container.innerHTML =
| |
| thumbHtml +
| |
| '<div>' +
| |
| '<div class="bsw-fa-title">' +
| |
| '<a href="' + esc( url ) + '">' + esc( title ) + '</a>' +
| |
| '</div>' +
| |
| '<div class="bsw-fa-extract">' + extract + '</div>' +
| |
| '<div style="margin-top:0.375rem">' +
| |
| '<a href="' + esc( url ) + '" style="font-size:0.75rem;color:var(--color-link)">Read more \u2192</a>' +
| |
| '</div>' +
| |
| '</div>';
| |
| | |
| /* Update the "Read more" span in the card header to link to the article */
| |
| var hdrLink = document.getElementById( 'bsw-featured-link' );
| |
| if ( hdrLink ) {
| |
| /* Replace the span with a proper anchor now that we have a URL */
| |
| var a = document.createElement( 'a' );
| |
| a.href = url;
| |
| a.textContent = 'Read more \u2192';
| |
| hdrLink.parentNode.replaceChild( a, hdrLink );
| |
| }
| |
| }
| |
| | |
| /**
| |
| * Fetch file count from media.battlestarwiki.org and populate
| |
| * the #bsw-stat-files span in the statistics card.
| |
| */
| |
| function initMediaFileCount() {
| |
| var el = document.getElementById( 'bsw-stat-files' );
| |
| if ( !el ) return;
| |
| | |
| apiGetFrom( MEDIA_API, {
| |
| action: 'query',
| |
| meta: 'siteinfo',
| |
| siprop: 'statistics'
| |
| } ).then( function ( data ) {
| |
| var files = data.query.statistics.images || 0;
| |
| el.textContent = files.toLocaleString();
| |
| } ).catch( function () {
| |
| el.textContent = '—';
| |
| } );
| |
| }
| |
| | |
| /**
| |
| * Recent changes tab switching.
| |
| * Fetches live recent changes from the selected wiki's API
| |
| * and replaces the #bsw-rc-content div content.
| |
| */
| |
| var rcCurrentWiki = 'all';
| |
| var rcCache = {};
| |
| | |
| function loadRecentChanges( wiki ) {
| |
| var wrap = document.getElementById( 'bsw-rc-content' );
| |
| if ( !wrap ) return;
| |
| | |
| if ( rcCache[ wiki ] ) {
| |
| wrap.innerHTML = rcCache[ wiki ];
| |
| return;
| |
| }
| |
| | |
| /* 'all' shows the cached Special:RecentChanges transclude */
| |
| if ( wiki === 'all' ) {
| |
| if ( rcCache.all ) {
| |
| wrap.innerHTML = rcCache.all;
| |
| }
| |
| return;
| |
| }
| |
| | |
| wrap.innerHTML = '<p style="color:var(--color-base--subtle);font-size:0.8125rem;padding:0.5rem 0">Loading\u2026</p>';
| |
| | |
| var apiMap = { en: API, de: DE_API, media: MEDIA_API };
| |
| var base = apiMap[ wiki ];
| |
| if ( !base ) return;
| |
| | |
| apiGetFrom( base, {
| |
| action: 'query',
| |
| list: 'recentchanges',
| |
| rcprop: 'title|timestamp|user|sizes',
| |
| rcnamespace: '0',
| |
| rclimit: '8',
| |
| rctype: 'edit|new'
| |
| } ).then( function ( data ) {
| |
| var changes = data.query.recentchanges || [];
| |
| if ( !changes.length ) {
| |
| wrap.innerHTML = '<p style="color:var(--color-base--subtle);font-size:0.8125rem;padding:0.5rem 0">No recent changes.</p>';
| |
| return;
| |
| }
| |
| | |
| var baseUrl = {
| |
| en: 'https://en.battlestarwiki.org/wiki/',
| |
| de: 'https://de.battlestarwiki.org/wiki/',
| |
| media: 'https://media.battlestarwiki.org/wiki/'
| |
| }[ wiki ];
| |
| | |
| var html = '<ul class="bsw-recent">' + changes.map( function ( rc ) {
| |
| var title = rc.title;
| |
| var url = baseUrl + encodeURIComponent( title.replace( / /g, '_' ) );
| |
| var ts = new Date( rc.timestamp );
| |
| var ago = timeAgo( ts );
| |
| return '<li class="ri">' +
| |
| '<a href="' + esc( url ) + '" target="_blank">' + esc( title ) + '</a>' +
| |
| '<span class="bsw-recent-time">' + esc( ago ) + '</span>' +
| |
| '</li>';
| |
| } ).join( '' ) + '</ul>';
| |
| | |
| rcCache[ wiki ] = html;
| |
| wrap.innerHTML = html;
| |
| } ).catch( function () {
| |
| wrap.innerHTML = '<p style="color:var(--color-base--subtle);font-size:0.8125rem;padding:0.5rem 0">Could not load recent changes.</p>';
| |
| } );
| |
| }
| |
| | |
| function timeAgo( date ) {
| |
| var diff = Math.floor( ( Date.now() - date.getTime() ) / 1000 );
| |
| if ( diff < 60 ) return diff + 's ago';
| |
| if ( diff < 3600 ) return Math.floor( diff / 60 ) + ' min ago';
| |
| if ( diff < 86400 ) return Math.floor( diff / 3600 ) + ' hr ago';
| |
| return Math.floor( diff / 86400 ) + ' days ago';
| |
| }
| |
| | |
| /**
| |
| * Newest article — pulls from Special:NewPages via API,
| |
| * skips subpages, redirects, and disambiguation pages.
| |
| */
| |
| function initNewestArticle() {
| |
| var inner = document.getElementById( 'bsw-newest-inner' );
| |
| if ( !inner ) return;
| |
| | |
| apiGet( {
| |
| action: 'query',
| |
| list: 'logevents',
| |
| letype: 'create',
| |
| leaction: 'create/create',
| |
| lenamespace: '0',
| |
| lelimit: '20',
| |
| leprop: 'title|timestamp|user'
| |
| } ).then( function ( data ) {
| |
| var entries = ( data.query.logevents || [] ).filter( function ( e ) {
| |
| return e.title.indexOf( '/' ) === -1;
| |
| } );
| |
| | |
| if ( !entries.length ) throw new Error( 'No entries' );
| |
| | |
| var entry = entries[0];
| |
| var title = entry.title;
| |
| var url = 'https://en.battlestarwiki.org/wiki/' + encodeURIComponent( title.replace( / /g, '_' ) );
| |
| var ts = new Date( entry.timestamp );
| |
| var date = ts.toLocaleDateString( 'en-US', { year: 'numeric', month: 'long', day: 'numeric' } );
| |
| | |
| return apiGet( {
| |
| action: 'query',
| |
| titles: title,
| |
| prop: 'extracts|pageimages|pageprops',
| |
| exintro: '1',
| |
| exchars: '300',
| |
| exsectionformat: 'plain',
| |
| piprop: 'thumbnail',
| |
| pithumbsize: '200',
| |
| ppprop: 'disambiguation'
| |
| } ).then( function ( d ) {
| |
| var page = Object.values( d.query.pages || {} )[0] || {};
| |
| /* Skip disambiguation pages, try next */
| |
| if ( page.pageprops && page.pageprops.disambiguation !== undefined ) {
| |
| var next = entries[1];
| |
| if ( !next ) throw new Error( 'No suitable page' );
| |
| title = next.title;
| |
| url = 'https://en.battlestarwiki.org/wiki/' + encodeURIComponent( title.replace( / /g, '_' ) );
| |
| date = new Date( next.timestamp ).toLocaleDateString( 'en-US', { year: 'numeric', month: 'long', day: 'numeric' } );
| |
| return apiGet( {
| |
| action: 'query', titles: title,
| |
| prop: 'extracts|pageimages', exintro: '1', exchars: '300',
| |
| exsectionformat: 'plain', piprop: 'thumbnail', pithumbsize: '200'
| |
| } ).then( function ( d2 ) {
| |
| return { page: Object.values( d2.query.pages || {} )[0] || {}, title: title, url: url, date: date };
| |
| } );
| |
| }
| |
| return { page: page, title: title, url: url, date: date };
| |
| } );
| |
| | |
| } ).then( function ( result ) {
| |
| var page = result.page;
| |
| var title = result.title || page.title || '';
| |
| var url = result.url || ( 'https://en.battlestarwiki.org/wiki/' + encodeURIComponent( ( page.title || '' ).replace( / /g, '_' ) ) );
| |
| var thumb = page.thumbnail ? page.thumbnail.source : '';
| |
| var extract = page.extract || '';
| |
| | |
| var thumbHtml = thumb
| |
| ? '<img src="' + esc( thumb ) + '" alt="' + esc( title ) + '" class="bsw-fa-thumb">'
| |
| : '<img src="https://en.battlestarwiki.org/resources/BSGWIKILOGO.png" alt="Battlestar Wiki" class="bsw-fa-thumb">';
| |
| | |
| inner.innerHTML =
| |
| thumbHtml +
| |
| '<div>' +
| |
| '<div class="bsw-fa-title"><a href="' + esc( url ) + '">' + esc( title ) + '</a></div>' +
| |
| '<div style="font-size:0.6875rem;color:var(--color-base--subtle);margin-bottom:0.25rem">Created ' + esc( result.date ) + '</div>' +
| |
| '<div class="bsw-fa-extract">' + extract + '</div>' +
| |
| '</div>';
| |
| | |
| } ).catch( function ( err ) {
| |
| inner.innerHTML = '<div style="font-size:0.8125rem;color:var(--color-base--subtle);padding:0.5rem 0">Could not load newest article.</div>';
| |
| } );
| |
| }
| |
| | |
| /* ── Entry point ──────────────────────────────────────────────── */
| |
| | |
| /**
| |
| * Hide any .bsw-card whose .bsw-card-body contains nothing but
| |
| * a red link (missing page transclusion). This catches subpages
| |
| * like BattlestarWiki:Did_you_know, OTD/Month_Day etc. that
| |
| * haven't been created yet.
| |
| */
| |
| function filterRedlinkCards() {
| |
| document.querySelectorAll( '.bsw-main-page .bsw-card' ).forEach( function ( card ) {
| |
| var body = card.querySelector( '.bsw-card-body' );
| |
| if ( !body ) return;
| |
| | |
| /* Remove non-content elements before checking — date labels,
| |
| loading spinners etc. shouldn't count as "content" */
| |
| var clone = body.cloneNode( true );
| |
| | |
| /* Remove known non-content wrappers */
| |
| clone.querySelectorAll( '.bsw-otd-date, .bsw-spinner, .bsw-loading' )
| |
| .forEach( function ( el ) { el.remove(); } );
| |
| | |
| var text = clone.textContent.trim();
| |
| var links = clone.querySelectorAll( 'a' );
| |
| var redLinks = clone.querySelectorAll( 'a.new' );
| |
| | |
| /* Hide if nothing left after stripping non-content */
| |
| if ( text === '' ) {
| |
| card.style.display = 'none';
| |
| return;
| |
| }
| |
| | |
| /* Hide if the only remaining content is red links */
| |
| if ( redLinks.length > 0 && redLinks.length === links.length ) {
| |
| clone.querySelectorAll( 'a' ).forEach( function ( a ) { a.remove(); } );
| |
| if ( clone.textContent.trim() === '' ) {
| |
| card.style.display = 'none';
| |
| }
| |
| }
| |
| } );
| |
| }
| |
| | |
| /* bswSetTab — exposed globally for inline onclick on span tabs */
| |
| window.bswSetTab = function ( el, wiki ) {
| |
| document.querySelectorAll( '.bsw-wtab' ).forEach( function ( t ) {
| |
| t.classList.remove( 'bsw-active' );
| |
| } );
| |
| el.classList.add( 'bsw-active' );
| |
| rcCurrentWiki = wiki;
| |
| loadRecentChanges( wiki );
| |
| };
| |
| | |
| mw.hook( 'wikipage.content' ).add( function () {
| |
| if ( !document.querySelector( '.bsw-main-page' ) ) return;
| |
| initHero();
| |
| initFeatured();
| |
| initMediaFileCount();
| |
| initNewestArticle();
| |
| filterRedlinkCards();
| |
| /* Cache the initial 'all' RC content so tab switching can restore it */
| |
| var rcWrap = document.getElementById( 'bsw-rc-content' );
| |
| if ( rcWrap ) rcCache.all = rcWrap.innerHTML;
| |
| } );
| |
| | |
| }() );
| |
| | |
| /**
| |
| * BattlestarWiki — Video of the Day loader
| |
| * Append to MediaWiki:Common.js (after the main page JS block)
| |
| *
| |
| * Fetches from battlestarpegasus.com MediaCMS API.
| |
| * Selects a video deterministically by date.
| |
| * Supports manual override via Battlestar_Wiki:Video_of_the_Day/YYYY-MM-DD subpage.
| |
| */
| |
| | |
| ( function () {
| |
| 'use strict';
| |
| | |
| var PEGASUS = 'https://battlestarpegasus.com';
| |
| var PAGE_SIZE = 50;
| |
| | |
| function esc( s ) {
| |
| return String( s || '' )
| |
| .replace( /&/g, '&' ).replace( /</g, '<' )
| |
| .replace( />/g, '>' ).replace( /"/g, '"' );
| |
| }
| |
| | |
| function dailySeed() {
| |
| var now = new Date();
| |
| return Math.floor( Date.UTC(
| |
| now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()
| |
| ) / 86400000 );
| |
| }
| |
| | |
| function fmtDuration( secs ) {
| |
| secs = Math.round( secs || 0 );
| |
| var m = Math.floor( secs / 60 ), s = secs % 60;
| |
| return m + ':' + ( s < 10 ? '0' : '' ) + s;
| |
| }
| |
| | |
| function pegasusGet( path ) {
| |
| return fetch( PEGASUS + path, { headers: { Accept: 'application/json' } } )
| |
| .then( function ( r ) {
| |
| if ( !r.ok ) throw new Error( 'HTTP ' + r.status );
| |
| return r.json();
| |
| } );
| |
| }
| |
| | |
| function fetchAllVideos() {
| |
| return pegasusGet( '/api/v1/media/?media_type=video&ordering=add_date&page_size=' + PAGE_SIZE )
| |
| .then( function ( data ) {
| |
| var results = data.results || [];
| |
| if ( data.next ) {
| |
| return pegasusGet( '/api/v1/media/?media_type=video&ordering=add_date&page_size=' + PAGE_SIZE + '&page=2' )
| |
| .then( function ( d2 ) { return results.concat( d2.results || [] ); } );
| |
| }
| |
| return results;
| |
| } );
| |
| }
| |
| | |
| function fetchVideoDetail( token ) {
| |
| return pegasusGet( '/api/v1/media/' + token );
| |
| }
| |
| | |
| function renderVotd( container, media ) {
| |
| var token = media.friendly_token;
| |
| var watchUrl = media.url || ( PEGASUS + '/view?m=' + token );
| |
| var thumb = media.thumbnail_url ? PEGASUS + media.thumbnail_url : '';
| |
| var poster = media.poster_url ? PEGASUS + media.poster_url : thumb;
| |
| | |
| /* ── Build video URLs from API response ── */
| |
| var hlsSrc = '';
| |
| var mp4Src = '';
| |
| | |
| if ( media.hls_info && media.hls_info.master_file ) {
| |
| hlsSrc = PEGASUS + media.hls_info.master_file;
| |
| }
| |
| /* Prefer 720p MP4 fallback, cascade down */
| |
| var enc = media.encodings_info || {};
| |
| var mp4Res = [ '720', '480', '360', '1080', '240' ];
| |
| for ( var i = 0; i < mp4Res.length; i++ ) {
| |
| var r = enc[ mp4Res[i] ];
| |
| if ( r && r.h264 && r.h264.url && r.h264.status === 'success' ) {
| |
| mp4Src = PEGASUS + r.h264.url;
| |
| break;
| |
| }
| |
| }
| |
| | |
| /* ── Player HTML: <video> with poster ── */
| |
| var videoId = 'bsw-votd-video';
| |
| var playerHtml =
| |
| '<video id="' + videoId + '" ' +
| |
| 'style="width:100%;aspect-ratio:16/9;display:block;background:#000" ' +
| |
| 'controls playsinline preload="none" ' +
| |
| ( poster ? 'poster="' + esc( poster ) + '"' : '' ) + '>' +
| |
| ( mp4Src ? '<source src="' + esc( mp4Src ) + '" type="video/mp4">' : '' ) +
| |
| '</video>';
| |
| | |
| /* ── Description sidebar ── */
| |
| var tags = ( media.tags || [] ).slice( 0, 8 ).map( function ( t ) {
| |
| var label = typeof t === 'string' ? t : ( t.title || t.name || '' );
| |
| return label ? '<span class="bsw-votd-tag">' + esc( label ) + '</span>' : '';
| |
| } ).join( '' );
| |
| | |
| var stats = [];
| |
| if ( media.duration ) stats.push( '\u23f1 ' + fmtDuration( media.duration ) );
| |
| if ( media.views ) stats.push( '\ud83d\udc41 ' + media.views.toLocaleString() );
| |
| if ( media.size ) stats.push( media.size );
| |
| | |
| var desc = media.description || '';
| |
| | |
| var infoHtml =
| |
| '<div class="bsw-votd-title">' + esc( media.title || 'Untitled' ) + '</div>' +
| |
| ( stats.length ? '<div class="bsw-votd-stats">' + stats.join( ' \u00b7 ' ) + '</div>' : '' ) +
| |
| ( desc ? '<div class="bsw-votd-desc">' + desc + '</div>' : '' ) +
| |
| ( tags ? '<div class="bsw-votd-tags">' + tags + '</div>' : '' ) +
| |
| '<div class="bsw-votd-actions">' +
| |
| '<a class="bsw-votd-watch" href="' + esc( watchUrl ) + '" target="_blank" rel="noopener">Watch on Battlestar Pegasus \u2197</a>' +
| |
| '<a class="bsw-votd-archive" href="/Battlestar_Wiki:Video_of_the_Day">Video archive</a>' +
| |
| '</div>';
| |
| | |
| /* Inject into DOM */
| |
| var playerEl = container.querySelector( '.bsw-votd-player' );
| |
| var infoEl = document.getElementById( 'bsw-votd-info' );
| |
| | |
| if ( playerEl ) {
| |
| playerEl.innerHTML = playerHtml;
| |
| | |
| /* Wire HLS.js if HLS src available and browser needs it */
| |
| if ( hlsSrc ) {
| |
| var videoEl = document.getElementById( videoId );
| |
| if ( videoEl ) {
| |
| if ( videoEl.canPlayType( 'application/vnd.apple.mpegurl' ) ) {
| |
| /* Native HLS (Safari) */
| |
| videoEl.src = hlsSrc;
| |
| } else {
| |
| /* Load hls.js dynamically from cdnjs */
| |
| var script = document.createElement( 'script' );
| |
| script.src = 'https://cdnjs.cloudflare.com/ajax/libs/hls.js/1.4.12/hls.min.js';
| |
| script.onload = function () {
| |
| if ( window.Hls && Hls.isSupported() ) {
| |
| var hls = new Hls( { startLevel: 3 } ); /* start at 720p */
| |
| hls.loadSource( hlsSrc );
| |
| hls.attachMedia( videoEl );
| |
| }
| |
| };
| |
| document.head.appendChild( script );
| |
| }
| |
| }
| |
| }
| |
| }
| |
| | |
| if ( infoEl ) {
| |
| infoEl.innerHTML = infoHtml;
| |
| infoEl.style.display = '';
| |
| }
| |
| }
| |
| | |
| function showVotdError( container, msg ) {
| |
| var playerEl = container.querySelector( '.bsw-votd-player' );
| |
| if ( playerEl ) {
| |
| playerEl.innerHTML =
| |
| '<div class="bsw-error" style="padding:1.5rem;text-align:center">' +
| |
| 'Could not load video. ' +
| |
| '<a href="' + esc( PEGASUS ) + '" target="_blank" rel="noopener">Visit Battlestar Pegasus \u2197</a>' +
| |
| '</div>';
| |
| }
| |
| }
| |
| | |
| mw.hook( 'wikipage.content' ).add( function () {
| |
| var container = document.getElementById( 'bsw-votd-container' );
| |
| if ( !container ) return;
| |
| | |
| var dateStr = container.dataset.date || '';
| |
| var overrideEl = document.getElementById( 'bsw-votd-override' );
| |
| var override = overrideEl ? overrideEl.textContent.trim() : '';
| |
| | |
| var seed = ( function () {
| |
| if ( dateStr ) {
| |
| var p = dateStr.split( '-' );
| |
| return Math.floor( Date.UTC( +p[0], +p[1] - 1, +p[2] ) / 86400000 );
| |
| }
| |
| return dailySeed();
| |
| }() );
| |
| | |
| if ( override ) {
| |
| fetchVideoDetail( override )
| |
| .then( function ( media ) { renderVotd( container, media ); } )
| |
| .catch( function ( e ) { showVotdError( container, e.message ); } );
| |
| } else {
| |
| fetchAllVideos()
| |
| .then( function ( items ) {
| |
| if ( !items.length ) throw new Error( 'No videos available.' );
| |
| var pick = items[ seed % items.length ];
| |
| return fetchVideoDetail( pick.friendly_token );
| |
| } )
| |
| .then( function ( media ) { renderVotd( container, media ); } )
| |
| .catch( function ( e ) { showVotdError( container, e.message ); } );
| |
| }
| |
| } );
| |
| | |
| }() );
| |