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.
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);