Jump to content

User:Guycn2/sandbox4.js

From Meta, a Wikimedia project coordination wiki

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/*


== Multi Patrol ==

Use this tool to mark multiple revisions as patrolled at the
touch of a button, directly from the revision history page.

See also:
* [[MediaWiki:סקריפטים/113.css]] – for the corresponding style sheet
* [[MediaWiki:סקריפטים/113.css/LTR.css]] – for the corresponding style sheet, adapted for left-to-right interface
* [[MediaWiki:סקריפטים/113.css/RTL.css]] – for the corresponding style sheet, adapted for right-to-left interface

Skins supported:
Vector (both 2022 and 2010), Monobook, Timeless, and Minerva.
Also fully supported on the mobile interface.

Dependencies:
* mediawiki.api
* mediawiki.util
* oojs-ui-core


Written by: [[User:Guycn2]]

__________________________________________________


== סקריפט לסימון מרובה של גרסאות כבדוקות ==

כלי זה מאפשר לסמן גרסאות מרובות כבדוקות בלחיצת כפתור,
ישירות מתוך דף הגרסאות הקודמות.

ראו גם:
* [[מדיה ויקי:סקריפטים/113.css]] – לגיליון הסגנונות המשויך
* [[מדיה ויקי:סקריפטים/113.css/LTR.css]] – לגיליון הסגנונות המשויך, מותאם לממשק שמוצג משמאל לימין
* [[מדיה ויקי:סקריפטים/113.css/RTL.css]] – לגיליון הסגנונות המשויך, מותאם לממשק שמוצג מימין לשמאל

עיצובים נתמכים:
וקטור (2022 ו־2010), מונובוק, מחוץ לזמן, מינרווה נויה.
הסקריפט נתמך במלואו גם בממשק למכשירים ניידים.


נכתב ע"י: [[משתמש:Guycn2]]


*/

/**
 * @file MultiPatrol - A MediaWiki user script that enables batch patrolling of
 *       multiple revisions directly from the page history interface. Users can
 *       select multiple revisions using checkboxes and mark them all as patrolled
 *       with a single button click, with real-time progress feedback.
 *
 * @author Guycn2
 * @version 1.0
 * @license CC-BY-SA-4.0
 * @requires mediawiki.api
 * @requires mediawiki.util
 * @requires oojs-ui-core
 */

( () => {

	'use strict';

	if (
		mw.config.get( 'multiPatrolLoaded' ) ||
		mw.config.get( 'wgAction' ) !== 'history' ||
		!mw.config.get( 'wgArticleId' )
	)
		return;

	mw.config.set( 'multiPatrolLoaded', true );

	/**
	 * Main class for the MultiPatrol functionality. Handles the user interface,
	 * API calls for patrolling revisions, and progress tracking. Supports both
	 * English and Hebrew localization.
	 *
	 * @class MultiPatrol
	 */
	class MultiPatrol {
		#messages;
		#lang;
		#$elements;
		#alreadyRun;
		#api;
		#progressBar;
		#panel;

		/**
		 * Creates a new MultiPatrol instance. Initializes localization messages for
		 * English and Hebrew, sets the current user language, and prepares internal
		 * state properties for tracking UI elements and run status.
		 *
		 * @function constructor
		 */
		constructor() {
			this.#messages = {
				en: {
					btnText: 'Mark selected revisions as patrolled',
					noRevsSelected: 'No revisions were selected.',
					statusRunning: 'Script is running',
					statusCount: 'revision $1 of $2',
					finishedRunning: 'Script has finished running',
					finishedTime: '$1 seconds',
					successCount: '$1 revisions were marked as patrolled (this may include revisions that had already been patrolled before).',
					failureCount: '$1 revisions could not be marked as patrolled. They may be too old (more than 30 days).',
					viewLog: 'View the patrol log for this page »'
				},
				he: {
					btnText: 'סימון הגרסאות שנבחרו כבדוקות',
					noRevsSelected: 'לא נבחרו גרסאות.',
					statusRunning: 'הסקריפט בפעולה',
					statusCount: 'גרסה $1 מתוך $2',
					finishedRunning: 'פעולת הסקריפט הושלמה',
					finishedTime: '$1 שניות',
					successCount: '$1 גרסאות סומנו כבדוקות (ייתכן שזה כולל גרסאות שכבר היו מסומנות כבדוקות לפני כן).',
					failureCount: 'לא ניתן היה לסמן $1 גרסאות כבדוקות. ייתכן שהן ישנות מדי (מעל 30 יום).',
					viewLog: 'לצפייה ביומן השינויים הבדוקים של דף זה »'
				}
			}; // messages END

			this.#lang = mw.config.get( 'wgUserLanguage' );
			this.#$elements = {};
			this.#alreadyRun = false;
			this.#api = null;
			this.#progressBar = null;
			this.#panel = null;
		} // constructor END

		/**
		 * Retrieves a localized message string based on the user's interface language.
		 * Falls back to English if the message is not available in the user's language,
		 * or returns the key itself if no translation exists.
		 *
		 * @function #i18n
		 * @private
		 * @param {string} key - The message key to look up in the messages object.
		 * @returns {string} The localized message string, English fallback, or the
		 *          original key if no translation is found.
		 */
		#i18n( key ) {
			return this.#messages[ this.#lang ]?.[ key ] || this.#messages.en[ key ] || key;
		}

		/**
		 * Loads the required CSS stylesheets for the MultiPatrol interface. Fetches
		 * the base stylesheet and a direction-specific stylesheet (RTL or LTR) based
		 * on the current document direction, both from Hebrew Wikipedia.
		 *
		 * @function loadCSS
		 * @public
		 */
		loadCSS() {
			mw.loader.load(
				'https://he.wikipedia.org/w/index.php?title=מדיה_ויקי:סקריפטים/113.css&action=raw&ctype=text/css',
				'text/css'
			);

			const dir = document.documentElement.dir === 'rtl' ? 'RTL' : 'LTR';

			mw.loader.load(
				`https://he.wikipedia.org/w/index.php?title=מדיה_ויקי:סקריפטים/113.css/${ dir }.css&action=raw&ctype=text/css`,
				'text/css'
			);
		}

		/**
		 * Sets up the multi-patrol button in the page history interface. Attaches a
		 * click event listener to the buttons area that creates and inserts the patrol
		 * button when the advanced options are shown. The button triggers the batch
		 * patrolling process when clicked.
		 *
		 * @function setupMultiPatrolBtn
		 * @public
		 */
		setupMultiPatrolBtn() {
			this.#$elements.$buttonsArea = $( '.mw-history-compareselectedversions' );

			const $compareBtn =
				this.#$elements.$buttonsArea.find( '.mw-history-compareselectedversions-button' );

			if ( !$compareBtn.length )
				return;

			this.#$elements.$buttonsArea.on( 'click', evt => {
				// The 'history-show-advanced-btn' element is specific to hewiki,
				// and is created locally by [[MediaWiki:FixHistPage.js]].
				// In order for this script to work in other wikis,
				// $multiPatrolBtn must be added to the page independently
				// of the hewiki-specific 'history-show-advanced-btn' element!
				if ( evt.target.classList.contains( 'history-show-advanced-btn' ) ) {
					this.#$elements.$multiPatrolBtn = $( '<button>' )
						.addClass( [ 'cdx-button', 'history-multi-patrol-btn' ] )
						.attr( 'type', 'button' )
						.text( this.#i18n( 'btnText' ) )
						.on( 'click', () => this.#patrolSelectedRevs() )
						.insertAfter( $compareBtn );
				}
			} );
		} // setupMultiPatrolBtn END

		/**
		 * Patrols all currently selected revisions in the page history. Collects the
		 * revision IDs from checked checkboxes, sends patrol API requests sequentially
		 * with a delay between each, updates the progress bar in real time, and
		 * displays success/failure statistics upon completion.
		 *
		 * @function #patrolSelectedRevs
		 * @private
		 * @async
		 * @returns {Promise<void>} Resolves when all selected revisions have been
		 *          processed and the results have been displayed.
		 */
		async #patrolSelectedRevs() {
			if ( !this.#alreadyRun ) {
				this.#alreadyRun = true;
				await this.#prepareForFirstRun();
			}

			const idsToPatrol = [];

			this.#$elements.$revElements.each( ( index, element ) => {
				const $element = $( element );
				if ( $element.prop( 'checked' ) ) {
					const match = $element.attr( 'name' ).match( /ids\[(\d+)\]/ );
					if ( match?.[ 1 ] )
						idsToPatrol.unshift( parseInt( match[ 1 ], 10 ) );
				}
			} );

			if ( !idsToPatrol.length ) {
				OO.ui.alert( this.#i18n( 'noRevsSelected' ) );
				return;
			}

			this.#$elements.$statusCountTotal.text( idsToPatrol.length );

			this.#prepareForRun();

			let successCount = 0, failureCount = 0;

			const startTime = mw.now();

			for ( const [ index, id ] of idsToPatrol.entries() ) {
				this.#$elements.$statusCountCurr.text( index + 1 );

				try {
					const params = { action: 'patrol', revid: id };
					const data = await this.#api.postWithToken( 'patrol', params );

					if ( data?.patrol )
						successCount++;
					else
						throw 'unknown_error';
				} catch {
					failureCount++;
				}

				this.#progressBar.setProgress( ( index + 1 ) / idsToPatrol.length * 100 );

				if ( index < idsToPatrol.length - 1 )
					await this.#sleep();
			} // for loop END

			const finishTime = mw.now();

			this.#$elements.$finishedTimeSecs.text(
				( ( finishTime - startTime ) / 1000 ).toFixed( 1 )
			);

			this.#postRun( successCount, failureCount );
		} // patrolSelectedRevs END

		/**
		 * Performs one-time initialization before the first patrol run. Loads required
		 * MediaWiki modules, creates all UI elements including the status display,
		 * progress bar, and statistics panel, and initializes the MediaWiki API
		 * instance for making patrol requests.
		 *
		 * @function #prepareForFirstRun
		 * @private
		 * @async
		 * @returns {Promise<void>} Resolves when all modules are loaded and UI
		 *          elements are created and attached to the DOM.
		 */
		async #prepareForFirstRun() {
			await mw.loader.using( [ 'mediawiki.api', 'mediawiki.util', 'oojs-ui-core' ] );

			const $pageHistory = $( '#pagehistory' );

			this.#$elements.$revElements =
				$pageHistory.find(
					'.mw-contributions-list input[ form="mw-history-revisionactions" ][ type="checkbox" ]'
				);

			this.#$elements.$toggleControls =
				this.#$elements.$buttonsArea.find( '.mw-checkbox-toggle-controls' );

			const $statusCount = $( '<span>' )
				.attr( 'id', 'multi-patrol-status-count' )
				.html(
					this.#i18n( 'statusCount' )
						.replace( '$1', '<span id="multi-patrol-status-count-curr"></span>' )
						.replace( '$2', '<span id="multi-patrol-status-count-total"></span>' )
				);

			this.#$elements.$statusCountCurr =
				$statusCount.children( '#multi-patrol-status-count-curr' );

			this.#$elements.$statusCountTotal =
				$statusCount.children( '#multi-patrol-status-count-total' );

			this.#$elements.$status = $( '<p>' )
				.attr( 'id', 'multi-patrol-status' )
				.text( this.#i18n( 'statusRunning' ) )
				.append( $statusCount );

			const $finishedTime = $( '<span>' )
				.attr( 'id', 'multi-patrol-finished-time' )
				.html(
					this.#i18n( 'finishedTime' )
						.replace( '$1', '<span id="multi-patrol-finished-time-secs"></span>' )
				);

			this.#$elements.$finishedTimeSecs =
				$finishedTime.children( '#multi-patrol-finished-time-secs' );

			this.#$elements.$finished = $( '<p>' )
				.attr( 'id', 'multi-patrol-finished' )
				.text( this.#i18n( 'finishedRunning' ) )
				.append( $finishedTime );

			this.#progressBar = new OO.ui.ProgressBarWidget( {
				id: 'multi-patrol-progress-bar',
				progress: 0
			} );

			this.#$elements.$successCount = $( '<p>' )
				.html(
					this.#i18n( 'successCount' )
						.replace( '$1', '<span id="multi-patrol-success-count-num"></span>' )
				);

			this.#$elements.$successCountNum =
				this.#$elements.$successCount.children( '#multi-patrol-success-count-num' );

			this.#$elements.$failureCount = $( '<p>' )
				.html(
					this.#i18n( 'failureCount' )
						.replace( '$1', '<span id="multi-patrol-failure-count-num"></span>' )
				);

			this.#$elements.$failureCountNum =
				this.#$elements.$failureCount.children( '#multi-patrol-failure-count-num' );

			const logParams = { type: 'patrol', page: mw.config.get( 'wgPageName' ) };
			const logUrl = mw.util.getUrl( 'Special:Log', logParams );
			const $logLink = $( '<a>' ).attr( 'href', logUrl ).text( this.#i18n( 'viewLog' ) );
			const $logLinkContainer = $( '<p>' ).append( $logLink );

			this.#$elements.$stats = $( '<div>' )
				.attr( 'id', 'multi-patrol-stats' )
				.append(
					this.#$elements.$successCount,
					this.#$elements.$failureCount,
					$logLinkContainer
				);

			const $panelContent = $( '<div>' )
				.attr( 'id', 'multi-patrol-panel-content' )
				.append(
					this.#$elements.$status,
					this.#$elements.$finished,
					this.#progressBar.$element,
					this.#$elements.$stats
				);

			this.#panel = new OO.ui.PanelLayout( {
				expanded: false,
				framed: true,
				padded: true,
				$content: $panelContent
			} ).toggle( false );

			$pageHistory.before( this.#panel.$element );

			this.#api = new mw.Api( { userAgent: 'MultiPatrol/1.0' } );
		} // prepareForFirstRun END

		/**
		 * Prepares the UI state before each patrol run. Disables the patrol button
		 * and revision checkboxes to prevent concurrent runs, resets the progress bar,
		 * shows the panel with status information, and hides any previous results.
		 *
		 * @function #prepareForRun
		 * @private
		 */
		#prepareForRun() {
			[ 'multiPatrolBtn', 'revElements' ]
				.forEach( item => this.#$elements[ `$${ item }` ].attr( 'disabled', true ) );

			this.#$elements.$toggleControls.css( 'pointer-events', 'none' );
			this.#progressBar.pushPending().setProgress( 0 );
			this.#panel.toggle( true ).scrollElementIntoView();
			this.#$elements.$status.show();

			[ 'finished', 'successCount', 'failureCount', 'stats' ]
				.forEach( item => this.#$elements[ `$${ item }` ].hide() );
		}

		/**
		 * Handles the UI updates after a patrol run completes. Hides the running
		 * status, displays completion message and elapsed time, shows success and
		 * failure counts if applicable, and re-enables the patrol button and
		 * revision checkboxes for subsequent runs.
		 *
		 * @function #postRun
		 * @private
		 * @param {number} successCount - The number of revisions that were
		 *        successfully marked as patrolled.
		 * @param {number} failureCount - The number of revisions that could not be
		 *        marked as patrolled, typically due to age restrictions.
		 */
		#postRun( successCount, failureCount ) {
			this.#$elements.$status.hide();
			this.#$elements.$finished.show();

			if ( successCount ) {
				this.#$elements.$successCountNum.text( successCount );
				this.#$elements.$successCount.show();
			}

			if ( failureCount ) {
				this.#$elements.$failureCountNum.text( failureCount );
				this.#$elements.$failureCount.show();
			}

			this.#progressBar.popPending();
			this.#$elements.$stats.fadeIn();

			[ 'multiPatrolBtn', 'revElements' ]
				.forEach( item => this.#$elements[ `$${ item }` ].attr( 'disabled', false ) );

			this.#$elements.$toggleControls.css( 'pointer-events', 'auto' );
		} // postRun END

		/**
		 * Creates a promise that resolves after a 200 millisecond delay. Used to
		 * add a pause between sequential API patrol requests to avoid overwhelming
		 * the server with rapid-fire requests.
		 *
		 * @function #sleep
		 * @private
		 * @returns {Promise<void>} A promise that resolves after the delay period.
		 */
		#sleep() {
			return new Promise( resolve => setTimeout( resolve, 200 ) );
		}
	} // MultiPatrol class definition END

	const multiPatrol = new MultiPatrol();
	multiPatrol.loadCSS();
	$( () => multiPatrol.setupMultiPatrolBtn() );

} )();