User:Wyang/AddAudio.js

// This will only work on Firefox, until the other browsers implement // MediaRecorder (which might not actually be that far away). // Written by User:Yair_rand; copied here to allow some customisation. // UI suggestions would be most welcome.

$( function {	var mediaDevices = navigator.mediaDevices || navigator,		getUserMedia = ( mediaDevices.getUserMedia || mediaDevices.webkitGetUserMedia || mediaDevices.mozGetUserMedia || mediaDevices.msGetUserMedia ),		hasTabs = 'tabbedLanguages' in window,		$sections = $( hasTabs ? languageContainers : '#mw-content-text > h2' );	var accentList = {		// I'll assume that capitalized versions of the region codes themselves 		// are fine for the template display.		// Empty strings means no special categorization.		// I have no idea whether these are the right accents to have available		// as options.		en: { us: 'U.S.', uk: 'British', ca: 'Canadian', au: 'Australian', nz: 'New Zealand' },		de: { de: , at: , ch:  },		fr: { fr: , 'ca-qc':  },		pt: { pt: , br: '' },		zh: { cmn: 'Mandarin', 'cmn-2': 'Mandarin', yue: 'Cantonese', nan: 'Min Nan', cdo: 'Min Dong', hak: 'Hakka', wuu: 'Wu', 'wuu-2': 'Wu' },	};	if ( getUserMedia && 'MediaRecorder' in window && ( mw.config.get( 'wgNamespaceNumber' ) === 0 || mw.config.get( 'wgPageName' ) === 'Wiktionary:Sandbox' ) && mw.config.get( 'wgAction' ) === 'view' && !/&printable=yes|&diff=|&oldid=/.test( window.location.search ) ) {		var stopIcon = '◼',			recordIcon = '⚫',			pauseIcon = '❚❚',			playIcon = '►',			saveIcon = '✔',			uploadsInProgress = 0;		mw.util.addCSS( '\ .YRAddAudio-Box { \ font-size: 10px; \ } \			.YRAddAudio-RecordButton { \ margin-right: 3px; \ cursor: pointer; \ } \			.YRAddAudio-RecordButton:hover { \ color: #DD0000; \ } \			.YRAddAudio-Recording { \ color: #DD0000; \ font-weight: bold; \ text-shadow: 0 0 1px #DD0000; \ } \			.YRAddAudio-PlayButton { \ display: none; \ margin-right: 3px; \ /* color: #AAAAAA; */ \ cursor: pointer; \ } \			.YRAddAudio-PlayButton:hover { \ color: #AAAAAA; \ } \			.YRAddAudio-SaveButton { \ display: none; \ margin-right: 3px; \ cursor: pointer; \ } \			.YRAddAudio-SaveButton:hover { \ color: #33FF22; \ } \			.YRAddAudio-accent { \ margin: 1px; \ padding: 1px; \ cursor: pointer; \ } \			.YRAddAudio-accent:hover { \ margin: 0px; \ border: 1px solid #AAA; \ padding: 1px; \ } \			.YRAddAudio-activeAccent { \ font-weight: bold; \ } \			'		);		function AButton( sectionIndex, language, headerLevel, oldHeader ) {			var recording = false,				recorder,				$elem,				$recordButton,				$playButton,				$saveButton,				$accentList,				accent,				langcode;			// Todo: Accent field.			// Todo: Licensing information in the form.			this.$elem = $elem = $( ' ', { addClass: 'YRAddAudio-Box', append: [ $recordButton = $( ' ', { 						text: recordIcon,						addClass: 'YRAddAudio-RecordButton',						title: 'Record audio pronunciation for this word',						click: function recordOrStop {							function startRecording {								recorder.start;								$recordButton.addClass( 'YRAddAudio-Recording' );							}							try {								if ( !recorder ) {									setupRecorder( function ( r ) { recorder = r;										startRecording; } );									langcode = findLang( sectionIndex );									if ( accentList[ langcode ] ) {										var accentElemList = {};										$accentList = $( ' ' )											.css( { display: 'inline-block' } )											.insertBefore( $saveButton )											.hide;										$.each( accentList[ langcode ], function ( accentcode, accentname ) { $accentList.append(												accentElemList[ accentcode ] = $( ' ' )													.text( accentcode.toUpperCase )													.attr( 'title', 'Accent/dialect: ' + accentcode.toUpperCase )													.addClass( 'YRAddAudio-accent' )													.on( 'click', function { if ( accent ) { accentElemList[ accent ].removeClass( 'YRAddAudio-activeAccent' ); }														if ( accent === accentcode ) { accent = undefined; } else { $( this ).addClass( 'YRAddAudio-activeAccent' ); accent = accentcode; }													} )											);										} );									}								} else {									if ( recording ) {										recorder.stop;										$recordButton.removeClass( 'YRAddAudio-Recording' );										$accentList && $accentList.show;										$playButton.show;										$saveButton.show;									} else {										startRecording;									}								}								//this.innerText = recording ? recordIcon : stopIcon;								recording = !recording;							} catch( e ) {								console.log( e, 6 );							}						}					} ), $playButton = $( ' ', { 						text: playIcon,						addClass: 'YRAddAudio-PlayButton',						css: {							//display: 'none'						},						title: 'Play recording',						click: function {							recorder.play;						}					} ), $saveButton = $( ' ', { 						text: saveIcon,						addClass: 'YRAddAudio-SaveButton',						title: 'Add this recording to the entry',						click: function {							// addEdit, tying into upload							//mw.loader.using( 'mediawiki.ForeignApi', function  { mw.loader.using( [ 'mediawiki.ForeignUpload', 'mediawiki.api.parse' ], function {								recorder.add( langcode, accent );							} ); }					} ),					'Add audio pronunciation'				],				css: {					'font-size': '10px'				},			} ); function setupRecorder( cb ) { function acceptStream( stream ) { var mimeType = MediaRecorder.isTypeSupported( 'audio/ogg' ) ? 'audio/ogg' : 'audio/webm', mediaRecorder = new MediaRecorder( stream, { mimeType: mimeType } ), chunks = [], blob; mediaRecorder.ondataavailable = function ( e ) { chunks.push( e.data ); };					mediaRecorder.onstop = function { blob = new Blob( chunks, { 'type': 'audio/ogg' } ); chunks = []; };					function editPage( langcode, accent ) { var editor = new Editor, title = mw.config.get( 'wgPageName' ), //langcode = findLang( sectionIndex ), // Todo: Accent filename = ( langcode + '-' + ( accent ? accent + '-' : '' ) + title ).replace( /[\.:]/g, '-' ) + '.ogg', // Todo: Accent text audioTemplate = '* \{\{audio|' + filename + ( accent ? '|Audio (' + accent.toUpperCase + ')' : '' ) + '|lang=' + langcode + '\}\}', addedWikitext = // Use '{\{subst:=\}\}' so as not to interfere // with other scripts. ( oldHeader ? '' : '\n\{\{subst:=\}\}==Pronunciation===\n' ) + audioTemplate; if ( !langcode ) { return editor.error( 'Language not found.' ); }						// Capitalize filename = filename.charAt( 0 ).toUpperCase + filename.slice( 1 ); ( new mw.Api ).parse( audioTemplate ).done( function ( html ) {							// Hopefully this is wrapped.							var $addedElement = $( html ),								$addedHeader = oldHeader || 									$( ' ' ).append( $( ' ').text( 'Pronunciation' ) ),								$redLink = $addedElement.find( '.audiofile a.new' );							if ( $redLink.length === 0 ) {								// No redlink, file space already exists.								// (Or there's a parsing error. Ignoring that.)								editor.error( 'Audio file already exists.' );								return;							}							// This isn't actually a direct transclusion of the							// file. Things may break or be inaccurate as a 							// result.							$redLink.replaceWith( $( ' ' ).attr( { 								'src': window.URL.createObjectURL( blob ),								controls: 'controls'							} ) );							editor.addEdit( {									edit: function ( w ) { var untilPostPronunciationHeaderReg = new RegExp( 											'((?:\n|^)==' + language + '==' + '[\\s\\S]*?' + '(?=\n={3,}(?!Alternative|Etymology).+=+\n))'										); var inPronunciationHeaderReg = new RegExp( 											'((?:\n|^)==' + language + '==' + '[\\s\\S]*?' + '\n(?:\\{\\{subst:=\\}\\}|=)=+Pronunciation' + '.+=+)'										);										if ( !oldHeader ) { w = w.replace( untilPostPronunciationHeaderReg, '$1' +												addedWikitext + 												'\n'											); } else { w = w.replace( inPronunciationHeaderReg, '$1' + '\n' + 												addedWikitext											); }										// console.log( 'wikitext', w ); // Then add '\{\{audio|' + filename + '|' + accent + '|lang=' + langcode + '\}\}' // With a header, if applicable. Maybe use the whole // '\{\{subst:=\}\}' stuff so as not to interfere // with the other scripts. return w;									}, redo: function { oldHeader || $addedHeader.insertBefore( $elem ); $addedElement.insertBefore( $elem ); $elem.hide; }, 									undo: function { oldHeader || $addedHeader.remove; $addedElement.remove; $elem.show; },									after_save: function upload { var FU = new mw.ForeignUpload, username = mw.config.get( 'wgUserName' ); if ( uploadsInProgress === 0 ) { document.body.style.cursor = 'wait'; }										uploadsInProgress++; FU.setFile( blob ); FU.setFilename( filename ); FU.setText( 											// Copied from en-us-test.ogg. 											// Dunno if it's what people usually use...											'==\{\{int:description\}\}==' +											'\n\{\{Information' +											'\n |description   = \{\{en|Pronunciation of the term in ' + ( accent ? accent + ' ' : '' ) + language + '\}\}' +											'\n |date          = ' + ( new Date ).toISOString.split( 'T' )[ 0 ] +											'\n |source         = \{\{own\}\}' +											'\n |author         = \[\[User:' + username + '|' + username + '\]\]' +											'\n |permission     =' +											'\n |other_versions =' +											'\n\}\}' +											'\n' +											'\n==\{\{int:license-header\}\}==' +											'\n\[\[Category:' + language + ' pronunciation|' + title + '\]\]' +											'\n\{\{self|Cc-by-sa-3.0\}\}'										); FU.setComment( 											'Upload ' + language + ( accent ? ' (' + accent + ')' : '' ) + ' audio for ' + 											'\[\[wikt:' + title + '|' + title + '\]\] ' + 											'(\[\[wikt:User:Yair rand/AddAudio.js|AddAudio.js\]\])'										); FU.upload.done( function {											uploadsInProgress--;											if ( uploadsInProgress === 0 ) {												document.body.style.cursor = '';												// Do a quick purge, so the file												// shows up right.												( new mw.Api ).post( { action: 'purge', titles: title } );											}										} ).fail( function {											editor.error( 'Upload failed: ' + ( FU.stateDetails.error ? FU.stateDetails.error.info : '' ) );										} ); },									summary: '+' },								$addedElement[ 0 ] );						} );					}					cb( {						start: function {							mediaRecorder.start;						},						stop: function  {							mediaRecorder.stop;							// Close the stream?						},						play: function  {							if ( blob ) {								new Audio( window.URL.createObjectURL( blob ) ).play;							}						},						add: editPage					} ); }				function noStream( err ) { $elem .css( 'font-color', '#A00' ) .text( 'Error: ' + err ); }				if ( navigator.mediaDevices ) { getUserMedia.call( mediaDevices, { audio: true } ) .then( acceptStream ) .catch( noStream ); } else { getUserMedia.call( navigator, { audio: true }, acceptStream, noStream ); }			}		}		function findLang( sectionIndex ) { // Find lang code for this section by pulling it from the headword. // If tabbedLanguages loaded after addaudio, hasTabs might be no // longer accurate. return ( 'tabbedLanguages' in window? 				$( languageContainers[ sectionIndex ] ).find( '.headword' ) :				$sections.eq( sectionIndex ).find( '~* .headword' )			).attr( 'lang' ); }		// Todo: Editor log for uploading. $sections.each( function ( sectionIndex ) {			var $section = $( this ), 				language = hasTabs ? 					tabbedLanguages[ sectionIndex ] : 					$section.find( '.mw-headline' ).text;			// Maybe use nextUntil instead of ~ here.			$section.find( hasTabs ? 'h3, h4' : '~h3, ~h4' ).each( function { var $this = $( this ), text = $this.find( '.mw-headline' ).text, $button, oldHeader; if ( !text.startsWith( 'Alternative' ) && !text.startsWith( 'Etymology' ) ) { oldHeader = text.startsWith( 'Pronunciation' ); $button = ( new AButton( sectionIndex, language, this.nodeNode, oldHeader ) ).$elem; if ( oldHeader ) { // We already have a pronunciation header. $button.insertAfter( $this ); } else { // We don't have a pronunciation header, so we have to // add one before the first pos header. $button.insertBefore( $this ); }					return false; }			} );		} );	} } );