MediaWiki:Gadget-musicMap-core.js: Difference between revisions

From RuneRealm Wiki
Jump to navigation Jump to search
Content added Content deleted
(Created page with "→‎Music map * Generates an interactive music map that has toggleable polygons on it. Can be used as a 'checklist' to track music track unlock progression. * See [[Map:Music tracks]]: var MM = {}; MM.touch = false; MM.getUnlocked = function() { var ls = localStorage.getItem('musicMap-'+mw.config.get('wgPageName')); if (!ls) return []; // map characters back to numbers and convert to var bitstr = Array.prototype.map.call(ls, function(x) { // go through each cha...")
(No difference)

Revision as of 01:51, 13 October 2024

/* Music map
 * Generates an interactive music map that has toggleable polygons on it. Can be used as a 'checklist' to track music track unlock progression.
 * See [[Map:Music tracks]]
 */

var MM = {};
MM.touch = false;
MM.getUnlocked = function() {
	var ls = localStorage.getItem('musicMap-'+mw.config.get('wgPageName'));
	if (!ls) return [];
	// map characters back to numbers and convert to 
	var bitstr = Array.prototype.map.call(ls, function(x) { // go through each character in the string
		var str = '00000' + parseInt(x, 32).toString(2); // parse back to bit string with sufficient leading zeroes to turn it into 5 bits long
		return str.slice(-5); // the actual bits that were parsed, plus any leading zeroes if needed
	}).join(''); // convert array of bitstrings to single long strong
	// convert bitstring back into an array of bools
	var bits = Array.prototype.map.call(bitstr, function(x) { return parseInt(x); });
	return bits;
}
MM.saveUnlocked = function(arr) {
	var bits = [];
	for (var i=0; i<arr.length; i++) bits[i] = arr[i] ? 1 : 0; // fill in empty array elements
	var bitstr = bits.join(''); // array in bit representation
	// Split up in chunks of 5 bits. The array length should be a multiple of 5 based on the musicMap function.
	// Use 32-bit string, because toString(64) is not supported in plain JS.
	var b32 = bitstr.match(/.{1,5}/g).map(function(x) { return parseInt(x, 2).toString(32); });
	localStorage.setItem('musicMap-'+mw.config.get('wgPageName'), b32.join(''));
}

MM.arrIdx = function(arr, i) {
	if (i < 0) {
		return arr[arr.length + i];
	} else {
		return arr[i];
	}
}

MM.toggleTrack = function(track, ids, state, $targets) {
	if ($targets) {
		$targets = $targets.add('.mw-kartographer-interactive');
	} else {
		$targets = $('.mw-kartographer-interactive');
	}
	if (state == undefined) {
    	state = MM.arrIdx(unlockedTracks, track) ? 0 : 1;
	}
	// update all maps when one map is clicked
	$targets.each(function() {
		for (var i in ids) {
	        var el = $(this).find('path.leaflet-interactive').eq(ids[i]);
	        if (state) el.addClass('unlocked'); else el.removeClass('unlocked');
		}
	});
    if (window.unlockedTracks) {
	    unlockedTracks[track] = state;
    }
}
	
MM.unlockTrack = function(e) {
	if (!e.ctrlKey && !e.metaKey && !(MM.touch && e.type == 'selectstart')) {
		// not ctrl+click, AND not cmd+click, AND not a long press touch event
		return; // neither ctrl+click nor longpress
	}
	var map = $(e.target).closest('.mw-kartographer-interactive').data('musicMap');
	var i = $(e.target).index();
	var el = $('#musicMap [value~="'+i+'"]');
	MM.toggleTrack(parseInt(el.html()), el.val().split(' ').map(Number));
	MM.saveUnlocked(unlockedTracks);
	map.closePopup(); // close popups on current map if there were any that were open.
	e.preventDefault();
	e.stopPropagation();
	return false;
}

MM.unlockAll = function(state) {
	var btn = this;
	btn.setDisabled(true);
	// doing the track toggles ensures the button gets disabled properly before rendering the other DOM changes
	setTimeout(function() {
		$('#musicMap data').each(function() {
			var track = parseInt(this.innerHTML);
			var ids = this.value.split(' ').map(Number);
			MM.toggleTrack(track, ids, state ? 1 : 0);
		});
		MM.saveUnlocked(unlockedTracks); // save once at the end
	}, 1);
	setTimeout(function() { // prevent doubleclicking the button: disable for 3 seconds
		btn.setDisabled(false);
	}, 3000);
}

MM.musicMap = function(map) {
	if ($('#musicMap').length == 0) return;
	$target = $(map._container);
	if ($target.data('musicMap')) return; // already added event handlers
	$target.data('musicMap', map);

	/* Local storage format:
	 * base32-encoded string
	 * All songs with an associated cache ID will be in the array at that position
	 * A gap to make this array's total length a multiple of 5 bits (since 2^5 = 32)
	 * A gap of 20 to prevent newly released songs from being marked as unlocked
	 * All N songs without a cache ID will be placed at the end, alphabetically sorted:
	 *  with [length-1] being a, and [length-N] being z.
	 */
	var ls = MM.getUnlocked();
	var unlocked = [],
		idless = [];
	$('#musicMap data').each(function() {
		// rebuild local storage data based on the <data>, because the track list might have changed.
		var track = parseInt(this.innerHTML);
		var ids = this.value.split(' ').map(Number);
		if (MM.arrIdx(ls, track)) {
			MM.toggleTrack(track, ids, 1, $target);
		}
		if (track >= 0) {
			unlocked[track] = MM.arrIdx(ls, track) ? 1 : 0;
		} else {
			idless[-track - 1] = MM.arrIdx(ls, track) ? 1 : 0;
		}
	});
	// gap of 5-(lengths%5) to make unlocked part a multiple of 5 bits (for base32enc). 20 empty slots as a spacer.
	window.unlockedTracks = unlocked.concat(Array(5-((unlocked.length+idless.length) % 5) + 20)).concat(idless);
	MM.saveUnlocked(unlockedTracks);
	
	$target.find('path').click(MM.unlockTrack).dblclick(function(e) {
		if (e.ctrlKey || e.metaKey) {
			// ctrl+dblclick already gets handled by the click handler; don't fullscreen etc.
			e.preventDefault();
			e.stopPropagation();
		}
	}).on('touchstart', function(e) { // Handle long-press touch events to unlock tracks: https://stackoverflow.com/q/66546226/1256925
		MM.touch = true;
	}).on('touchend', function(e) {
		MM.touch = false;
	}).on('selectstart', MM.unlockTrack);
}

MM.playTrack = function(e) {
	// This handler will trigger before the audioplayer.js event handler, because
	// this handler is tied to the map container, and that handler is tied to body.
	e.preventDefault();
	var $clone = $(e.target).clone(); // make a copy to put back in the tooltip
	var parent = e.target.parentElement; // where to insert the copy
	$('#music-playlist .player').html(''); // remove previous player
	$(e.target).appendTo('#music-playlist .player'); // move the song that will play to the play-box; audioplayer.js will replace this with <audio>.
	$clone.appendTo(parent); // put the link back in the tooltip
}

MM.initMap = function(map, fullscreen) {
	if (!$('#musicMap').length) return;
	if (map instanceof Array) map = map[0];
	// wait for this map to be ready and make it a musicmap
	map.on('kartographerisready', function() { MM.musicMap(this); });
	if (fullscreen === false) {
		// make the fullscreen maps that may be created also turn into musicmaps
		L.Map.addInitHook(function() {MM.initMap(this, true);})
	}
	if ($('#music-playlist .player').length == 0) {
		$('#music-playlist').show().append('<div class="player">Click a link in a map tooltip to play that track.</div>');
		$(map._container).on('click', 'a[href^="/w/File:"][href$=".ogg"]', MM.playTrack)
		$(map._container).on('click', 'a:not([href^="/w/File:"][href$=".ogg"])', function() {
			this.target = '_blank'; // open song links in new tab to prevent having to reload the map
		});
	}
	if ($('.musicMap-buttons').length == 0) {
		var unlockbtn = new OO.ui.ButtonWidget( {
			flags: [ 'progressive' ],
			label: 'Unlock all tracks',
		} );
		unlockbtn.on('click', MM.unlockAll.bind(unlockbtn, 1));
		var lockbtn = new OO.ui.ButtonWidget( {
			flags: [ 'destructive' ],
			label: 'Lock all tracks',
		} );
		lockbtn.on('click', MM.unlockAll.bind(lockbtn, 0));
		// place in a wrapper and add to body
		$('<div>').addClass('musicMap-buttons').append(unlockbtn.$element, lockbtn.$element).appendTo('#musicMap-info');
	}
	return;
}

// hook init to maps loading
mw.hook('wikipage.maps').add(MM.initMap);