User:Wyang/AddAudio.js

From Wiktionary, the free dictionary
Jump to navigation Jump to search

Note: You may have to bypass your browser’s cache to see the changes. In addition, after saving a sitewide CSS file such as MediaWiki:Common.css, it will take 5-10 minutes before the changes take effect, even if you clear your cache.

  • Mozilla / Firefox / Safari: hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (Command-R on a Macintosh);
  • Konqueror and Chrome: click Reload or press F5;
  • Opera: clear the cache in Tools → Preferences;
  • Internet Explorer: hold Ctrl while clicking Refresh, or press Ctrl-F5.


// 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 = $( '<div>', { 
				addClass: 'YRAddAudio-Box',
				append: [
					$recordButton = $( '<span>', { 
						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 = $( '<span>' )
											.css( { display: 'inline-block' } )
											.insertBefore( $saveButton )
											.hide();
										$.each( accentList[ langcode ], function ( accentcode, accentname ) {
											$accentList.append(
												accentElemList[ accentcode ] = $( '<span>' )
													.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 = $( '<span>', { 
						text: playIcon,
						addClass: 'YRAddAudio-PlayButton',
						css: {
							//display: 'none'
						},
						title: 'Play recording',
						click: function () {
							recorder.play();
						}
					} ),
					$saveButton = $( '<span>', { 
						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 || 
									$( '<span>' ).append( $( '<h3>').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( $( '<audio>' ).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: '+[[File:' + filename + ']]'
								},
								$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;
				}
			} );
		} );
	}
} );