MediaWiki:Gadget-musicMap-core.js

From Old School Near-Reality Wiki
Jump to navigation Jump to search

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)
  • Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
  • Opera: Press Ctrl-F5.
  1 /* Music map
  2  * Generates an interactive music map that has toggleable polygons on it. Can be used as a 'checklist' to track music track unlock progression.
  3  * See [[Map:Music tracks]]
  4  */
  5 
  6 var MM = {};
  7 MM.touch = false;
  8 MM.getUnlocked = function() {
  9 	var ls = localStorage.getItem('musicMap-'+mw.config.get('wgPageName'));
 10 	if (!ls) return [];
 11 	// map characters back to numbers and convert to 
 12 	var bitstr = Array.prototype.map.call(ls, function(x) { // go through each character in the string
 13 		var str = '00000' + parseInt(x, 32).toString(2); // parse back to bit string with sufficient leading zeroes to turn it into 5 bits long
 14 		return str.slice(-5); // the actual bits that were parsed, plus any leading zeroes if needed
 15 	}).join(''); // convert array of bitstrings to single long strong
 16 	// convert bitstring back into an array of bools
 17 	var bits = Array.prototype.map.call(bitstr, function(x) { return parseInt(x); });
 18 	return bits;
 19 }
 20 MM.saveUnlocked = function(arr) {
 21 	var bits = [];
 22 	for (var i=0; i<arr.length; i++) bits[i] = arr[i] ? 1 : 0; // fill in empty array elements
 23 	var bitstr = bits.join(''); // array in bit representation
 24 	// Split up in chunks of 5 bits. The array length should be a multiple of 5 based on the musicMap function.
 25 	// Use 32-bit string, because toString(64) is not supported in plain JS.
 26 	var b32 = bitstr.match(/.{1,5}/g).map(function(x) { return parseInt(x, 2).toString(32); });
 27 	localStorage.setItem('musicMap-'+mw.config.get('wgPageName'), b32.join(''));
 28 }
 29 
 30 MM.arrIdx = function(arr, i) {
 31 	if (i < 0) {
 32 		return arr[arr.length + i];
 33 	} else {
 34 		return arr[i];
 35 	}
 36 }
 37 
 38 MM.toggleTrack = function(track, ids, state, $targets) {
 39 	if ($targets) {
 40 		$targets = $targets.add('.mw-kartographer-interactive');
 41 	} else {
 42 		$targets = $('.mw-kartographer-interactive');
 43 	}
 44 	if (state == undefined) {
 45     	state = MM.arrIdx(unlockedTracks, track) ? 0 : 1;
 46 	}
 47 	// update all maps when one map is clicked
 48 	$targets.each(function() {
 49 		for (var i in ids) {
 50 	        var el = $(this).find('path.leaflet-interactive').eq(ids[i]);
 51 	        if (state) el.addClass('unlocked'); else el.removeClass('unlocked');
 52 		}
 53 	});
 54     if (window.unlockedTracks) {
 55 	    unlockedTracks[track] = state;
 56     }
 57 }
 58 	
 59 MM.unlockTrack = function(e) {
 60 	if (!e.ctrlKey && !e.metaKey && !(MM.touch && e.type == 'selectstart')) {
 61 		// not ctrl+click, AND not cmd+click, AND not a long press touch event
 62 		return; // neither ctrl+click nor longpress
 63 	}
 64 	var map = $(e.target).closest('.mw-kartographer-interactive').data('musicMap');
 65 	var i = $(e.target).index();
 66 	var el = $('#musicMap [value~="'+i+'"]');
 67 	MM.toggleTrack(parseInt(el.html()), el.val().split(' ').map(Number));
 68 	MM.saveUnlocked(unlockedTracks);
 69 	map.closePopup(); // close popups on current map if there were any that were open.
 70 	e.preventDefault();
 71 	e.stopPropagation();
 72 	return false;
 73 }
 74 
 75 MM.unlockAll = function(state) {
 76 	var btn = this;
 77 	btn.setDisabled(true);
 78 	// doing the track toggles ensures the button gets disabled properly before rendering the other DOM changes
 79 	setTimeout(function() {
 80 		$('#musicMap data').each(function() {
 81 			var track = parseInt(this.innerHTML);
 82 			var ids = this.value.split(' ').map(Number);
 83 			MM.toggleTrack(track, ids, state ? 1 : 0);
 84 		});
 85 		MM.saveUnlocked(unlockedTracks); // save once at the end
 86 	}, 1);
 87 	setTimeout(function() { // prevent doubleclicking the button: disable for 3 seconds
 88 		btn.setDisabled(false);
 89 	}, 3000);
 90 }
 91 
 92 MM.musicMap = function(map) {
 93 	if ($('#musicMap').length == 0) return;
 94 	$target = $(map._container);
 95 	if ($target.data('musicMap')) return; // already added event handlers
 96 	$target.data('musicMap', map);
 97 
 98 	/* Local storage format:
 99 	 * base32-encoded string
100 	 * All songs with an associated cache ID will be in the array at that position
101 	 * A gap to make this array's total length a multiple of 5 bits (since 2^5 = 32)
102 	 * A gap of 20 to prevent newly released songs from being marked as unlocked
103 	 * All N songs without a cache ID will be placed at the end, alphabetically sorted:
104 	 *  with [length-1] being a, and [length-N] being z.
105 	 */
106 	var ls = MM.getUnlocked();
107 	var unlocked = [],
108 		idless = [];
109 	$('#musicMap data').each(function() {
110 		// rebuild local storage data based on the <data>, because the track list might have changed.
111 		var track = parseInt(this.innerHTML);
112 		var ids = this.value.split(' ').map(Number);
113 		if (MM.arrIdx(ls, track)) {
114 			MM.toggleTrack(track, ids, 1, $target);
115 		}
116 		if (track >= 0) {
117 			unlocked[track] = MM.arrIdx(ls, track) ? 1 : 0;
118 		} else {
119 			idless[-track - 1] = MM.arrIdx(ls, track) ? 1 : 0;
120 		}
121 	});
122 	// gap of 5-(lengths%5) to make unlocked part a multiple of 5 bits (for base32enc). 20 empty slots as a spacer.
123 	window.unlockedTracks = unlocked.concat(Array(5-((unlocked.length+idless.length) % 5) + 20)).concat(idless);
124 	MM.saveUnlocked(unlockedTracks);
125 	
126 	$target.find('path').click(MM.unlockTrack).dblclick(function(e) {
127 		if (e.ctrlKey || e.metaKey) {
128 			// ctrl+dblclick already gets handled by the click handler; don't fullscreen etc.
129 			e.preventDefault();
130 			e.stopPropagation();
131 		}
132 	}).on('touchstart', function(e) { // Handle long-press touch events to unlock tracks: https://stackoverflow.com/q/66546226/1256925
133 		MM.touch = true;
134 	}).on('touchend', function(e) {
135 		MM.touch = false;
136 	}).on('selectstart', MM.unlockTrack);
137 }
138 
139 MM.playTrack = function(e) {
140 	// This handler will trigger before the audioplayer.js event handler, because
141 	// this handler is tied to the map container, and that handler is tied to body.
142 	e.preventDefault();
143 	var $clone = $(e.target).clone(); // make a copy to put back in the tooltip
144 	var parent = e.target.parentElement; // where to insert the copy
145 	$('#music-playlist .player').html(''); // remove previous player
146 	$(e.target).appendTo('#music-playlist .player'); // move the song that will play to the play-box; audioplayer.js will replace this with <audio>.
147 	$clone.appendTo(parent); // put the link back in the tooltip
148 }
149 
150 MM.initMap = function(map, fullscreen) {
151 	if (!$('#musicMap').length) return;
152 	if (map instanceof Array) map = map[0];
153 	// wait for this map to be ready and make it a musicmap
154 	map.on('kartographerisready', function() { MM.musicMap(this); });
155 	if (fullscreen === false) {
156 		// make the fullscreen maps that may be created also turn into musicmaps
157 		L.Map.addInitHook(function() {MM.initMap(this, true);})
158 	}
159 	if ($('#music-playlist .player').length == 0) {
160 		$('#music-playlist').show().append('<div class="player">Click a link in a map tooltip to play that track.</div>');
161 	}
162 	$(map._container).on('click', 'a[href^="/w/File:"][href$=".ogg"]', MM.playTrack)
163 	$(map._container).on('click', 'a:not([href^="/w/File:"][href$=".ogg"])', function() {
164 		this.target = '_blank'; // open song links in new tab to prevent having to reload the map
165 	});
166 	if ($('.musicMap-buttons').length == 0) {
167 		var unlockbtn = new OO.ui.ButtonWidget( {
168 			flags: [ 'progressive' ],
169 			label: 'Unlock all tracks',
170 		} );
171 		unlockbtn.on('click', MM.unlockAll.bind(unlockbtn, 1));
172 		var lockbtn = new OO.ui.ButtonWidget( {
173 			flags: [ 'destructive' ],
174 			label: 'Lock all tracks',
175 		} );
176 		lockbtn.on('click', MM.unlockAll.bind(lockbtn, 0));
177 		// place in a wrapper and add to body
178 		$('<div>').addClass('musicMap-buttons').append(unlockbtn.$element, lockbtn.$element).appendTo('#musicMap-info');
179 	}
180 	return;
181 }
182 
183 // hook init to maps loading
184 mw.hook('wikipage.maps').add(MM.initMap);