MediaWiki:Gadget-musicMap-core.js
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.
/* 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);