MediaWiki:Gadget-musicMap-core.js

This is the current revision of this page, as edited by Alex (talk | contribs) at 12:06, 20 October 2024. The present address (URL) is a permanent link to this version.

(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

After saving, you may need to bypass your browser's cache to see the changes. For further information, see Wikipedia:Bypass your cache.

  • In most Windows and Linux browsers: Hold down Ctrl and press F5.
  • In Safari: Hold down ⇧ Shift and click the Reload button.
  • In Chrome and Firefox for Mac: Hold down both ⌘ Cmd+⇧ Shift and press R.
"use strict";

/* 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);