PageSourceSearch

https://eng.musicshake.com/musicshake_web/js/web-shim.js?v=1790269258

js musicshake.com collected 2026-09-24 20:39:17 UTC 40,163 bytes, 736 lines download raw bytes

1/*
2 * musicshake_web — runs the original Lite3 "Make Music" page without Flash (2026-09-23).
3 * Loaded after the unchanged Lite3 scripts from /create/js/. It replaces only what needed Flash:
4 *   __swf        the four SWFs become JavaScript objects with the same method names, at the same element ids
5 *                (Lite3 reaches them with document.getElementById(Lite3.id + "-swf-" + name)):
6 *                  bootstrap -> "Flash is ready"; mixer2 -> MusicshakeMixer2 (js/mixer2.js);
7 *                  preview   -> plays one block through svc.php; encoder -> mixdown, MP3 and publish.php
8 *   Lite3.bootstrap.run   no Flash version check
9 *   __xhr3       catalog JSONP over https (an https page can't load http scripts)
10 */
11(function (window, $) {
12	"use strict";
13	var SVC = '/musicshake_web/svc.php';
14
15	// swfobject is only read for the Flash version (lite3-bootstrap.js check_browser) and the Quick Tour video
16	window.swfobject = window.swfobject || {
17		getFlashPlayerVersion: function () { return {major: 0, minor: 0, release: 0}; },
18		hasFlashPlayerVersion: function () { return false; },
19		embedSWF: function () {}
20	};
21
22	// ---- stand-in "SWF" elements ---------------------------------------------------------------------------
23	function holder(name) {
24		var id = Lite3.id + '-swf-' + name;
25		$('#' + id).remove();
26		var el = document.createElement('div');
27		el.id = id;
28		el.style.display = 'none';
29		document.body.appendChild(el);
30		return el;
31	}
32
33	function previewPlayer(el) {
34		var audio = new window.Audio(), timer = null;
35		audio.preload = 'auto';
36		function stopped() { clearInterval(timer); timer = null; try { Lite3.preview.stopped(); } catch (e) {} }
37		audio.addEventListener('playing', function () {
38			try { Lite3.preview.starting(); } catch (e) {}
39			clearInterval(timer);
40			timer = setInterval(function () {
41				if (audio.duration) try { Lite3.preview.progress(Math.round(audio.currentTime / audio.duration * 100)); } catch (e) {}
42			}, 100);
43		});
44		audio.addEventListener('ended', stopped);
45		audio.addEventListener('error', stopped);
46		el.preview_load = function (id, type) {
47			if (!/^[0-9]+$/.test(String(id)) || !/^[SDVRFE]$/.test(String(type))) return;
48			audio.src = SVC + '?do=sample&id=' + id + '&type=' + type;
49			var p = audio.play();
50			if (p && p.catch) p.catch(stopped);
51		};
52		el.preview_play = function () { var p = audio.play(); if (p && p.catch) p.catch(stopped); };
53		el.preview_stop = function () { audio.pause(); stopped(); };
54	}
55
56	// ---- encoder.swf: mixdown -> MP3 -> upload (publish) ---------------------------------------------------
57	// Same sequence and page callbacks as the SWF: Lite3.post.status_ready -> setparams(params) -> status_mixdown(%)
58	// -> status_encoding(%) -> status_upload -> done(song_num). Like encoder.swf the file ends with the text
59	// "musicshake" and an ID3v1 tag. Errors close the status window with a message (the SWF just hung).
60	var ID3_GENRE = {1: 7, 2: 17, 3: 14, 4: 3, 5: 52, 6: 13, 7: 116, 8: 8, 9: 86, 10: 10, 11: 42};
61	function id3v1(title, artist, genre) {
62		var tag = new Uint8Array(128);
63		function put(text, at, len) { text = String(text || ''); for (var i = 0; i < len && i < text.length; i++) { var c = text.charCodeAt(i); tag[at + i] = c < 256 ? c : 63; } }
64		put('TAG', 0, 3); put(title, 3, 30); put(artist, 33, 30); put('Musicshake', 63, 30);
65		put(String(new Date().getFullYear()), 93, 4); put('License: CC BY-NC-SA 3.0', 97, 30);
66		tag[127] = ID3_GENRE[+genre] || 0;
67		return tag;
68	}
69
70	function encoder(el) {
71		var alive = true, worker = null, abort = window.AbortController ? new AbortController() : null;
72		function gone() { return !alive || !document.getElementById(el.id); }
73		function fail(message) {
74			alive = false;
75			if (worker) worker.terminate();
76			$('#' + Lite3.id + '-post-status').remove();
77			$('#mask').remove();
78			__error(message || 'Posting failed. Please try again.');
79			$('#' + el.id).remove();
80		}
81		// Cancel in the status window unloads the "SWF": stop everything
82		new MutationObserver(function (m, obs) { if (!document.getElementById(el.id)) { alive = false; if (worker) worker.terminate(); if (abort) abort.abort(); obs.disconnect(); } })
83			.observe(document.body, {childList: true});
84
85		el.setparams = function (params) {
86			var msw = MusicshakeMixer2.data.serialize(), cb = function (name, arg) { try { Lite3.post[name](arg); } catch (e) {} };
87			MusicshakeMixer2.mixdown(function (p) { if (!gone()) cb('status_mixdown', p); }).then(function (pcm) {
88				if (gone()) return;
89				return new Promise(function (resolve, reject) {
90					worker = new Worker(MusicshakeMixer2.mp3Worker || '/musicshake_web/js/mp3-worker.js');
91					worker.onmessage = function (e) {
92						if (e.data.progress !== undefined) cb('status_encoding', e.data.progress);
93						else if (e.data.mp3) resolve(e.data.mp3);
94						else reject(new Error(e.data.error || 'encoding failed'));
95					};
96					worker.onerror = function (e) { reject(new Error(e.message || 'encoding failed')); };
97					worker.postMessage({left: pcm.left, right: pcm.right}, [pcm.left.buffer, pcm.right.buffer]);
98				});
99			}).then(function (mp3) {
100				if (gone() || !mp3) return;
101				worker.terminate(); worker = null;
102				var mark = new TextEncoder().encode('musicshake'), tag = id3v1(params.title, params.user_id, params.genre);
103				var file = new Blob([mp3, mark, tag], {type: 'audio/mpeg'});
104				cb('status_upload');
105				var form = new FormData();
106				['title', 'description', 'genre', 'mood', 'theme', 'image', 'contest', 'allow_remix', 'show_instruments'].forEach(function (k) {
107					form.append(k, params[k] === undefined || params[k] === null ? '' : params[k]);
108				});
109				['tempo', 'key', 'fadeout', 'chords', 'tracks', 'blocks', 'effects', 'parent'].forEach(function (k) { form.append(k, msw[k]); });
110				form.append('mp3', file, 'song.mp3');
111				return fetch('/musicshake_web/publish.php', {method: 'POST', body: form, credentials: 'same-origin', signal: abort ? abort.signal : undefined})
112					.then(function (r) { return r.json().catch(function () { return {error: 'Server error (' + r.status + ')'}; }); })
113					.then(function (res) {
114						if (gone()) return;
115						if (res && res.song_num) { alive = false; cb('done', res.song_num); }
116						else fail(res && res.error);
117					});
118			}).catch(function (err) {
119				if (!gone()) fail(err && err.name === 'AbortError' ? null : 'Posting failed: ' + (err && err.message || err));
120			});
121		};
122		setTimeout(function () { if (!gone()) { try { Lite3.post.status_ready(); } catch (e) { fail(); } } }, 0);
123	}
124
125	window.__swf = {
126		required: 0,
127		load: function (swf) {
128			var el = holder(swf);
129			if (swf === 'bootstrap') {
130				setTimeout(function () { Lite3.bootstrap.ok(); }, 0);
131			} else if (swf === 'mixer2') {
132				var api = window.MusicshakeMixer2.create();
133				for (var k in api) el[k] = api[k];
134				setTimeout(function () { Lite3.Mixer.event_connect(); }, 0);
135			} else if (swf === 'preview') {
136				previewPlayer(el);
137			} else if (swf === 'encoder') {
138				encoder(el);
139			}
140		},
141		embed: function () {},
142		unload: function (swf) { $('#' + Lite3.id + '-swf-' + swf).remove(); }
143	};
144
145	// ---- use the whole page ----------------------------------------------------------------------------
146	// Lite3 created the editor at a fixed 500x500 that could be moved (by its header) and resized (by its edges).
147	// Here it is fixed: it fills the page below the site's menu bar (in the browser) or the whole screen (installed
148	// app: no menu bar, see below) and follows the window's size and orientation.
149	var PAD = 42;   // #lite3: padding 0 20px + 1px border each side
150	// the screen's safe area (iPhone notch / home bar; 0 elsewhere), read from a hidden probe padded with env()
151	function insets() {
152		var p = document.getElementById('msw-safe');
153		if (!p) {
154			p = document.createElement('div');
155			p.id = 'msw-safe';
156			p.style.cssText = 'position:fixed;left:0;top:0;width:0;height:0;visibility:hidden;pointer-events:none;' +
157				'padding:env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left)';
158			document.body.appendChild(p);
159		}
160		var c = window.getComputedStyle(p);
161		return {t: parseFloat(c.paddingTop) || 0, r: parseFloat(c.paddingRight) || 0, b: parseFloat(c.paddingBottom) || 0, l: parseFloat(c.paddingLeft) || 0};
162	}
163	function fit() {
164		var app = $('#' + Lite3.id);
165		if (!app.length || !$('#' + Lite3.id + '-grid').length) return;   // only the mixer view (not the Start button)
166		var safe = insets(), nav = $('#header');
167		var top = (nav.is(':visible') ? nav.outerHeight() : safe.t) + 10;
168		var bottom = 10 + safe.b;   // the footer links line is hidden (css/web.css)
169		// the real screen width: on phones the 500px editor first stretches the layout viewport, so innerWidth alone
170		// would keep it too wide
171		// (iOS reports screen.width as the portrait width even in landscape: use the larger side when landscape)
172		var sw = window.screen && screen.width ? (window.innerWidth > window.innerHeight ? Math.max(screen.width, screen.height) : screen.width) : Infinity;
173		var vw = Math.min(window.innerWidth, window.visualViewport ? window.visualViewport.width : Infinity, sw);
174		var w = Math.max(240, vw - 20 - safe.l - safe.r - PAD);
175		var h = Math.max(300, window.innerHeight - top - bottom);
176		Lite3.UI.__minimum_width = Math.min(500, w);
177		Lite3.UI.__minimum_height = Math.min(500, h);
178		app.css({left: 10 + safe.l, top: top, width: w, height: h}).toggleClass('narrow', w < 500);
179		Lite3.UI.resize_and_scroll();
180	}
181	var fitTimer = null;
182	function fitSoon() { clearTimeout(fitTimer); fitTimer = setTimeout(fit, 60); }
183	// only the browser window's own resize: jQuery UI's "resize" events from the dialogs bubble up to window too
184	$(window).on('resize orientationchange', function (e) { if (e.target !== window) return; fitSoon(); });
185	if (window.visualViewport) window.visualViewport.addEventListener('resize', fitSoon);
186	// no moving or resizing by hand: the editor's draggable/resizable widgets (and their edge handles) are removed
187	function fixed() {
188		var app = $('#' + Lite3.id);
189		try { if (app.data('draggable')) app.draggable('destroy'); } catch (e) {}
190		try { if (app.data('resizable')) app.resizable('destroy'); } catch (e) {}
191		app.children('.ui-resizable-handle').remove();
192	}
193	var _create = Lite3.UI.create;
194	Lite3.UI.create = function () { var r = _create.apply(this, arguments); fixed(); arrangeHeader(); fitSoon(); return r; };
195	var _sync = Lite3.Mixer.event_sync;
196	Lite3.Mixer.event_sync = function () { var r = _sync.apply(this, arguments); fitSoon(); return r; };
197	window.MusicshakeWeb = {fit: fit};
198
199	// ---- the editor's toolbar -------------------------------------------------------------------------------
200	// In the browser: the site's menu bar is on top as on every eng page; the toolbar has [Save] [Install] [Classic].
201	// Installed app (html.app-standalone, js/app.js): the whole screen is the editor. The menu bar (#header) is hidden
202	// (css/web.css) and its right side, #header-login — Login (logged out) or bell + user id + user menu (logged
203	// in), filled by Lite3's login script — is moved as it is, with its click handlers, into the toolbar:
204	// [Save] [Install] [bell] [user id | Login] [menu]; the menu has the site's page links and the Classic version.
205	// Both: "My saved songs" (Menu > My songs) and "Auto save" (always on) are dropped.
206	var APP = !!(window.MSApp && MSApp.standalone);
207	var SITE = [['Home', '/'], ['Listen', '/recent'], ['Chart', '/charts'], ['People', '/people'], ['Store', '/store']];
208	function toolbar() {
209		var head = $('#' + Lite3.id + '-header');
210		if (!head.length) return null;
211		var bar = $('#msw-toolbar');
212		if (!bar.length) {
213			bar = $('<div id="msw-toolbar"></div>').append(
214				$('<a class="save" href="#" title="Save this song to My songs" style="display:none">Save</a>')
215					.click(function (e) { e.preventDefault(); Lite3.save.save_slot(); }),
216				$('<a class="install" href="#" title="Install Musicshake as an app: full screen, from your home screen" style="display:none">Install<span class="long">&nbsp;app</span></a>')
217					.click(function (e) { e.preventDefault(); if (window.MSApp) MSApp.install(); }),
218				$('<a class="classic" href="/create/" target="_blank" title="The original Make Music (needs Adobe Flash)">Classic<span class="long">&nbsp;Flash version</span></a>'),
219				$('<a class="sitemenu" href="#" title="Musicshake menu" aria-label="Musicshake menu" aria-haspopup="true"></a>')
220					.click(function (e) { e.preventDefault(); e.stopPropagation(); siteMenu(this); }));
221			head.append(bar);
222		}
223		var login = $('#header-login');
224		if (APP && login.length && !login.parent().is(bar)) bar.children('.sitemenu').before(login);
225		return bar;
226	}
227	function siteMenu(anchor) {
228		var m = $('#msw-sitemenu');
229		if (m.length) { m.remove(); return; }
230		m = $('<div id="msw-sitemenu" role="menu"></div>');
231		$.each(SITE, function (i, l) { m.append($('<a role="menuitem"></a>').attr('href', l[1]).text(l[0])); });
232		m.append('<div class="sep"></div>', '<a role="menuitem" href="/create/" target="_blank" title="The original Make Music (needs Adobe Flash)">Classic Flash version</a>');
233		$('body').append(m);
234		var r = anchor.getBoundingClientRect();
235		m.css({top: r.bottom + 8, right: Math.max(10, window.innerWidth - r.right)});
236	}
237	$(document).on('click touchend', function (e) {
238		if (!$(e.target).closest('#msw-sitemenu, #msw-toolbar .sitemenu').length) $('#msw-sitemenu').remove();
239	});
240	// Lite3 adds the user menu (My profile, Settings, Logout...) to #header, which is hidden: it is moved to the page
241	// and opened under the user id
242	function placeUserMenu() {
243		var u = $('#header > #header-login-usermenu');
244		if (!u.length) return;
245		var id = $('#header-login .usermenu')[0];
246		u.addClass('msw-usermenu').appendTo('body');
247		if (id) { var r = id.getBoundingClientRect(); u.css({top: r.bottom + 8, right: Math.max(10, window.innerWidth - r.right)}); }
248	}
249	function arrangeHeader() {
250		var login = $('#header-login');
251		toolbar();
252		if (!login.length) return;
253		var mysongs = login.children('a').filter(function () { return $(this).text() === 'My saved songs'; }), autosave = login.children('#autosave');
254		if (mysongs.length || autosave.length) { mysongs.remove(); autosave.remove(); autosaveOn(); }
255		// logged out: Login only (as on the site, whose Signup link is hidden)
256		login.children('a').filter(function () { return $(this).text() === 'Signup'; }).hide();
257		if (login.children('.usermenu').length && !login.children('.notification').length) {
258			var n = (Login.userdata && Login.userdata.new_notifications) ? parseInt(Login.userdata.new_notifications, 10) || 0 : 0;
259			login.prepend('<a href="/my_notifications" class="notification" title="Notifications"><span class="label">Notifications: </span><span class="val' + (n ? ' highlight' : '') + '">' + n + '</span></a>');
260		}
261	}
262	$(function () {
263		var login = document.getElementById('header-login'), header = document.getElementById('header');
264		if (window.MutationObserver) {
265			if (login) new MutationObserver(arrangeHeader).observe(login, {childList: true});
266			if (header && APP) new MutationObserver(placeUserMenu).observe(header, {childList: true});
267		}
268		arrangeHeader();
269	});
270
271	// ---- start with the last song (or One click start) -----------------------------------------------------
272	// No "New song" window on arrival: the last song worked on in this browser is reopened — a saved song from the
273	// account (same user logged in), otherwise the copy kept here — or, with nothing kept, One click start.
274	// A Remix link (?sn=) still opens that song. The start window is still in Menu > New.
275	var LAST = 'msw.last', lastKept = '', starting = false;
276	function remember() {
277		try {
278			var d = MusicshakeMixer2.data, swf = Lite3.Mixer.swf, st = swf && swf.mixer_state ? swf.mixer_state() : '';
279			// only a finished song (not one half-way through an edit or a load)
280			if (starting || (st !== 'pause' && st !== 'play') || !d.chords.length || !d.tracks.length || (d.complete && !d.complete())) return;
281			var user = Login.userdata && Login.userdata.num ? String(Login.userdata.num) : '';
282			var msw = d.serialize(), key = user + '|' + (Lite3.slot.num || '') + '|' + JSON.stringify(msw);
283			if (key === lastKept) return;
284			lastKept = key;
285			localStorage.setItem(LAST, JSON.stringify({user: user, slot: Lite3.slot.num ? String(Lite3.slot.num) : '', title: Lite3.slot.title || '', msw: msw, at: Date.now()}));
286		} catch (e) {}
287	}
288	function forget() { try { localStorage.removeItem(LAST); } catch (e) {} lastKept = ''; }
289	function lastSong() { try { return JSON.parse(localStorage.getItem(LAST) || 'null'); } catch (e) { return null; } }
290	var _ok = Lite3.bootstrap.ok;
291	Lite3.bootstrap.ok = function () {
292		if (!Lite3.startopt) {
293			var last = lastSong(), me = Login.userdata && Login.userdata.num ? String(Login.userdata.num) : '';
294			// a song kept by another account on this browser is not opened
295			if (last && last.user && last.user !== me) last = null;
296			if (last && last.slot && me) {
297				Lite3.startopt = {type: 'slot', slot_num: last.slot};
298				setTimeout(function () { Lite3.slot.update(last.slot, last.title); }, 0);
299			} else if (last && last.msw) {
300				Lite3.startopt = {type: 'local', msw: last.msw};
301			} else {
302				Lite3.startopt = {type: 'shake'};
303			}
304			// if that song can't be opened (deleted, another account, bad copy), it is forgotten and a new one starts
305			if (Lite3.startopt.type !== 'shake') starting = true;
306		}
307		return _ok.apply(this, arguments);
308	};
309	var _sync2 = Lite3.Mixer.event_sync;
310	Lite3.Mixer.event_sync = function () { starting = false; var r = _sync2.apply(this, arguments); setTimeout(remember, 0); return r; };
311	window.addEventListener('pagehide', remember);
312	document.addEventListener('visibilitychange', function () { if (document.visibilityState === 'hidden') remember(); });
313	setInterval(function () { if (document.visibilityState !== 'hidden') remember(); }, 15000);   // block on/off and volume changes don't re-sync
314
315	// ---- menu, genre and block variation: popovers next to their button ---------------------------------
316	// __dialog_sticky centres these windows over a white 30% mask; here the mask is clear (a tap outside still
317	// closes the window) and the window opens beside what was tapped, styled apart from the editor (css/web.css).
318	function popover(id, anchor, side) {
319		var d = $('#' + id), a = $(anchor);
320		if (!d.length) return;
321		d.addClass('msw-pop');
322		$('#mask').addClass('msw-clear').off('click.msw').on('click.msw', function () { __dialog_close_sticky(); });
323		if (!a.length) return;
324		var r = a[0].getBoundingClientRect(), w = d.outerWidth(), h = d.outerHeight(), vw = window.innerWidth, vh = window.innerHeight, top, left;
325		if (side === 'side') {
326			left = r.right + 10; if (left + w > vw - 10) left = r.left - w - 10;
327			top = r.top - 20;
328		} else {
329			left = r.left + r.width / 2 - w / 2;
330			top = r.top - h - 12; if (top < 10) top = r.bottom + 12;
331		}
332		left = Math.max(10, Math.min(vw - w - 10, left));
333		top = Math.max(10, Math.min(vh - h - 10, top));
334		d.css({position: 'fixed', left: left, top: top, marginLeft: 0});
335	}
336	// Lite3's dim layer behind its windows was white 30% (#mask, no colour given): black 45% from the first frame,
337	// so no white flash while a window waits for a login check or a catalog request. See-through (0) and the
338	// post-status 80% black stay as they are.
339	var _mask = window.__mask;
340	window.__mask = function (on, opacity, bgcolor) {
341		if (on && opacity !== 0 && !bgcolor) { opacity = 0.45; bgcolor = '#000'; }
342		return _mask.call(this, on, opacity, bgcolor);
343	};
344
345	// Save Song, Choose Tempo, My Songs: the same dark windows, centred over a dim background (tap outside closes)
346	function modal(id) {
347		var d = $('#' + id);
348		if (!d.length) return false;
349		d.addClass('msw-pop msw-modal');
350		$('#mask').removeClass('msw-clear').addClass('msw-dim').off('click.msw').on('click.msw', function () { __dialog_close_sticky(); });
351		return true;
352	}
353	function whenOpen(id) {   // these windows open after a login check or a catalog request: wait for them
354		var tries = 0, t = setInterval(function () { if (modal(id) || ++tries > 40) clearInterval(t); }, 50);
355	}
356	['save_slot', 'saveas_slot'].forEach(function (f) {
357		var orig = Lite3.save[f];
358		if (orig) Lite3.save[f] = function () { var r = orig.apply(this, arguments); whenOpen(Lite3.id + '-save'); return r; };
359	});
360	var _tempo = Lite3.tempo.show;
361	Lite3.tempo.show = function () { var r = _tempo.apply(this, arguments); whenOpen(Lite3.id + '-tempo'); return r; };
362	var _mysongs = Lite3.mysongs;
363	Lite3.mysongs = function () { var r = _mysongs.apply(this, arguments); whenOpen(Lite3.id + '-mysongs'); watchSongs(); return r; };
364	// My Songs rows: one "…" button opens Load / Rename / Copy / Delete (the four buttons crowded narrow windows);
365	// tempo as "140 BPM"
366	function tidySongs() {
367		$('#' + Lite3.id + '-mysongs .item').each(function () {
368			var it = $(this);
369			var t = it.children('.tempo'); if (t.length && /Beats per minute/.test(t.text())) t.text(t.text().replace(/\s*Beats per minute/, ' BPM'));
370			if (it.children('.msw-more').length || !it.children('.buttons').length) return;
371			$('<button class="msw-more" aria-label="More">&hellip;</button>').insertBefore(it.children('.buttons')).click(function (e) {
372				e.stopPropagation();
373				var open = it.hasClass('msw-open');
374				$('#' + Lite3.id + '-mysongs .item.msw-open').removeClass('msw-open');
375				if (!open) it.addClass('msw-open');
376			});
377			it.children('.buttons').on('click', 'button', function () { it.removeClass('msw-open'); });
378			// tapping the song itself loads it (Load left the … menu)
379			it.addClass('msw-loadable').on('click', function (e) {
380				if ($(e.target).closest('.msw-more, .buttons, .rename_box, .handle, input, button').length) return;
381				it.children('.buttons').find('button.load').trigger('click');
382			});
383		});
384	}
385	var songsObserver = null;
386	function watchSongs() {
387		var tries = 0, t = setInterval(function () {
388			var list = document.querySelector('#' + Lite3.id + '-mysongs .alist');
389			if (!list && ++tries < 40) return;
390			clearInterval(t);
391			if (!list || !window.MutationObserver) return;
392			if (songsObserver) songsObserver.disconnect();
393			songsObserver = new MutationObserver(tidySongs);
394			songsObserver.observe(list, {childList: true, subtree: true});
395			tidySongs();
396		}, 50);
397	}
398	$(document).on('click', function (e) { if (!$(e.target).closest('.msw-more, .buttons').length) $('#' + Lite3.id + '-mysongs .item.msw-open').removeClass('msw-open'); });
399
400	// confirm box ("A new will load and any unsaved information …"): the dark design, Confirm in orange
401	if (window.__msg) __msg.MIXER_CONFIRM_SHAKE = 'A new song will load and any unsaved changes to the current song will be lost.';
402	var _confirm = window.__confirm;
403	window.__confirm = function () {
404		var r = _confirm.apply(this, arguments);
405		var d = $('div.dialog#confirm');
406		if (d.length) { d.addClass('msw-confirm'); modal('confirm'); }
407		return r;
408	};
409
410	// Choose Chord, Choose Instrument, Post Song: fit narrow windows; when opened as a step of a flow (Start from
411	// scratch, Add part / track) their background is the dim dark one, not Lite3's white 30% layer
412	// Wherever they are opened from (a part number, a track icon, + menu, a step of a flow), these windows look the
413	// same: fitted to the screen and centred (the chord list scrolls natively: css/web.css)
414	function fitDialog(id) {
415		var d = $('#' + id);
416		if (!d.length) return false;
417		d.addClass('msw-fit');
418		if (id === Lite3.id + '-post') d.addClass('msw-pop msw-postdlg');
419		var m = $('#mask');
420		if (m.length && !m.hasClass('msw-clear')) m.addClass('msw-dim');
421		centre(d);
422		return true;
423	}
424	function centre(d) {
425		if (!d || !d.length || !d.is(':visible')) return;
426		var vw = window.innerWidth, vh = window.innerHeight, w = d.outerWidth(), h = d.outerHeight(), safe = insets();
427		d.css({position: 'fixed', marginLeft: 0, marginTop: 0,
428			left: Math.max(10 + safe.l, Math.round((vw - w) / 2)), top: Math.max(10 + safe.t, Math.round((vh - h) / 2))});
429	}
430	var FITTED = ['chords', 'instruments', 'effect-browser', 'post'];
431	function whenFit(id) {
432		var tries = 0, last = -1, t = setInterval(function () {
433			var d = $('#' + id);
434			if (d.length && !d.hasClass('msw-fit')) fitDialog(id);
435			// re-centre while its content (catalog lists) arrives and changes its height
436			if (d.length && d.outerHeight() !== last) { last = d.outerHeight(); centre(d); }
437			if (++tries > 40 || (tries > 4 && !d.length)) clearInterval(t);
438		}, 50);
439	}
440	$(window).on('resize orientationchange', function (e) {
441		if (e.target !== window) return;
442		setTimeout(function () { FITTED.forEach(function (n) { centre($('#' + Lite3.id + '-' + n + '.msw-fit')); }); }, 100);
443	});
444	var _cbrowse = Lite3.chords.browse;
445	Lite3.chords.browse = function () { var r = _cbrowse.apply(this, arguments); whenFit(Lite3.id + '-chords'); return r; };
446	var _ibrowse = Lite3.instruments.browse;
447	Lite3.instruments.browse = function () { var r = _ibrowse.apply(this, arguments); whenFit(Lite3.id + '-instruments'); return r; };
448	var _ebrowse = Lite3.effects.browse;
449	Lite3.effects.browse = function () { var r = _ebrowse.apply(this, arguments); whenFit(Lite3.id + '-effect-browser'); return r; };
450	var _post = Lite3.post.start;
451	Lite3.post.start = function () { var r = _post.apply(this, arguments); whenFit(Lite3.id + '-post'); return r; };
452	var _menu = Lite3.menu.show;
453	Lite3.menu.show = function () { var r = _menu.apply(this, arguments); popover(Lite3.id + '-menu', '#' + Lite3.id + '-control-menu'); return r; };
454	var _genre = Lite3.genre.show;
455	Lite3.genre.show = function () { var r = _genre.apply(this, arguments); popover(Lite3.id + '-genre', '#' + Lite3.id + '-control-genre'); return r; };
456	// Block Variation: a popover beside the block; the current variation (or the first) plays as soon as the list is
457	// there, like the chord and instrument windows. One timer: a re-open cancels the previous one.
458	var _bv = Lite3.blockvariations.show, bvTimer = null;
459	Lite3.blockvariations.show = function (row, col) {
460		var r = _bv.apply(this, arguments), id = Lite3.id + '-blockvariation', block = '#' + Lite3.id + '-block-' + row + '_' + col, dlg = $('#' + id)[0];
461		popover(id, block, 'side');
462		clearInterval(bvTimer);
463		var tries = 0, played = false;
464		bvTimer = setInterval(function () {
465			var d = $('#' + id);
466			if (!d.length || (dlg && d[0] !== dlg) || ++tries > 60) { clearInterval(bvTimer); return; }
467			popover(id, block, 'side');   // its list arrives later: place it again once it has its size
468			var list = d.find('.playable');
469			if (!list.length || played) return;
470			played = true;
471			var item = list.filter('.selected').first();
472			if (!item.length) item = list.first().trigger('click');
473			item.children('.play').trigger('click');
474			setTimeout(function () { popover(id, block, 'side'); clearInterval(bvTimer); }, 300);
475		}, 100);
476		return r;
477	};
478
479	// ---- menu: My songs (the top "My saved songs" link is gone) --------------------------------------------
480	var _buttons = Lite3.menu.buttons;
481	Lite3.menu.buttons = function () {
482		var list = _buttons.apply(this, arguments);
483		var mine = $('<div class="pad clickable"><div class="label b">My songs</div><p>Open and manage your saved songs</p></div>')
484			.click(function () { __dialog_close_sticky(); Login.check(function () { Lite3.mysongs(); }); });
485		list.children().first().after(mine);
486		return list;
487	};
488
489	// ---- New song window: no "My songs" (it is in the Menu); styled like the popovers, centred ------------------
490	var _start = Lite3.startmenu.show;
491	Lite3.startmenu.show = function () {
492		var r = _start.apply(this, arguments), id = Lite3.id + '-startmenu';
493		$('#' + id + ' .alist > .pad').filter(function () { return $(this).find('.label').text() === 'My songs'; }).remove();
494		popover(id);
495		confirmNew(id);
496		return r;
497	};
498
499	// ---- no "Save your work" panel beside the editor ---------------------------------------------------------
500	// Lite3 put it in the page's sidebar for every song not yet saved. Instead: Menu > Save (asks for a title the
501	// first time) and a Save button in the toolbar, shown while the song has no slot yet. Once saved, auto save
502	// (always on) keeps it up to date.
503	Lite3.save.create = function () {};
504	function saveButton() {
505		var bar = toolbar();
506		if (bar) bar.children('.save').toggle(!Lite3.slot.num);
507	}
508	// ---- Install app (js/app.js): the browser's install prompt, or the Add to Home Screen steps on iPhone/iPad ----
509	function installButton() {
510		var bar = toolbar();
511		if (bar && window.MSApp) bar.children('.install').toggle(MSApp.canInstall());
512	}
513	document.addEventListener('msapp:installable', installButton);
514	var _slot = Lite3.slot.update;
515	Lite3.slot.update = function () { var r = _slot.apply(this, arguments); saveButton(); return r; };
516	var _sync3 = Lite3.Mixer.event_sync;
517	Lite3.Mixer.event_sync = function () { var r = _sync3.apply(this, arguments); saveButton(); installButton(); return r; };
518	var _saved = Lite3.Mixer.event_saved;   // a first save sets the slot from the mixer's save callback
519	if (_saved) Lite3.Mixer.event_saved = function () { var r = _saved.apply(this, arguments); setTimeout(saveButton, 0); return r; };
520
521	// ---- auto save is always on ---------------------------------------------------------------------------
522	function autosaveOn() {
523		if (window.__settings) window.__settings.AUTOSAVE = 1;
524		if (Lite3.Mixer.swf && Lite3.Mixer.swf.mixer_autosave) Lite3.Mixer.swf.mixer_autosave(1);
525	}
526	var _connect = Lite3.Mixer.event_connect;
527	Lite3.Mixer.event_connect = function () { if (window.__settings) window.__settings.AUTOSAVE = 1; return _connect.apply(this, arguments); };
528
529	// ---- no Flash check --------------------------------------------------------------------------------
530	Lite3.bootstrap.run = function () {
531		if ($('#' + Lite3.id).length > 0) $('#' + Lite3.id).remove();
532		__swf.load('bootstrap', '300', '300');
533	};
534
535	// ---- catalog over https ----------------------------------------------------------------------------
536	window.__xhr3 = function (url, data, success, failure) {
537		if (Login.userdata && Login.userdata.num) data += '&user_num=' + Login.userdata.num;
538		$.ajax({
539			url: 'https://usminimsgs.musicshake.com/msgs' + url,
540			type: 'GET',
541			data: data,
542			cache: true,
543			dataType: 'jsonp',
544			success: function (d) { if (d && typeof d === 'object' && success) success(d); },
545			error: function () {
546				if (failure) { failure(); return; }
547				// callers mostly pass no failure handler: close what waits for this and say so
548				$('#alert, #alert2').remove();
549				__dialog_close_sticky();
550				__error(navigator.onLine === false ? OFFLINE : "The music catalog isn't responding. Please try again in a moment.");
551			}
552		});
553	};
554
555	// ---- 2026-09-24 audit fixes ------------------------------------------------------------------------------
556	var OFFLINE = "You're offline. Check your connection and try again.";
557
558	// no ads, no dead social widgets: the ad frames Lite3 still builds (every shake, preview and post) are dropped
559	window.__advertisement = function () {};
560	var _html = $.fn.html;
561	$.fn.html = function (v) {
562		if (typeof v === 'string' && v.indexOf('/ads/create-adsense') >= 0) return _html.call(this, '');
563		return _html.apply(this, arguments);
564	};
565
566	// server calls (/xhr/*): a failed request says so (Lite3 dropped the error silently, and windows waited forever)
567	window.__xhr = function (request, data, success, failure) {
568		$.ajax({
569			url: '/xhr/' + request, type: 'POST', data: data || '', dataType: 'json',
570			error: function () {
571				$('#alert, #alert2').remove();
572				if (typeof failure === 'function') failure({result: 'failure'});
573				__error(navigator.onLine === false ? OFFLINE : __msg.SYSTEM_FAULT);
574			},
575			success: function (d) {
576				if (!d || typeof d !== 'object') return;
577				if (d.result !== 'success') {
578					if (d.output && typeof d.output === 'string') __error(d.output);
579					if (typeof failure === 'function') failure(d);
580				} else if (typeof success === 'function') success(d);
581			}
582		});
583	};
584
585	// genre / mood / theme names: read-only, so fetched by GET with retries; a built-in genre list until then
586	// (without them the editor could not draw)
587	var GENRES = {1: 'Hiphop', 2: 'Rock/Metal', 3: 'R&B', 4: 'Dance/Disco', 5: 'Electronic/Techno', 6: 'Pop', 7: 'Ballad', 8: 'Jazz', 9: 'Latin', 10: 'Classical/New Age'};
588	Lite3.category.init = function () {
589		var tries = 0;
590		Lite3.category.genre = Lite3.category.genre || $.extend({}, GENRES);
591		(function get() {
592			$.ajax({url: '/xhr/category', type: 'GET', dataType: 'json', timeout: 10000}).done(function (d) {
593				if (!d || !d.output || !d.output.genre) { fail(); return; }
594				delete d.output.genre[11];
595				Lite3.category.genre = d.output.genre;
596				Lite3.category.mood = d.output.mood;
597				Lite3.category.theme = d.output.theme;
598			}).fail(fail);
599			function fail() { if (++tries < 3) setTimeout(get, 1500 * tries); }
600		})();
601	};
602	var _gname = Lite3.genre.name;
603	Lite3.genre.name = function () { if (!Lite3.category.genre) Lite3.category.genre = $.extend({}, GENRES); return _gname.apply(this, arguments); };
604	// no genre (Start from scratch): a placeholder instead of an empty box
605	var _gselect = Lite3.genre.select;
606	Lite3.genre.select = function () {
607		var r = _gselect.apply(this, arguments), b = $('#' + Lite3.id + '-control-genre');
608		if (!$.trim(b.text())) b.text('Choose genre');
609		return r;
610	};
611
612	// engine messages: looked up directly (Lite3 used eval on the code), the "Preparing..." layer is removed, and
613	// sounds that failed are fetched again when the connection is back
614	$.extend(__msg, {
615		MIXER_DOWNLOAD_ERROR: "Some sounds couldn't be downloaded. Check your connection; they'll load on your next change.",
616		MIXER_UNDO_ERROR: "Sorry, that couldn't be undone.",
617		SLOT_SAVE_ERROR: "Your song couldn't be saved just now. It will be saved with your next change.",
618		SONG_TOO_LONG: 'This song is too long to post (30 minutes at most).'
619	});
620	var _merror = Lite3.Mixer.error, lastError = 0;
621	Lite3.Mixer.error = function (code) {
622		code = /^[A-Z][A-Z0-9_]{1,40}$/.test(String(code)) ? String(code) : 'MIXER_DOWNLOAD_ERROR';
623		if (!$('div.dialog.sticky:visible').length) $('#mask').remove();
624		$('#alert, #alert2').remove();
625		if (code === 'PLEASE_WAIT') return;
626		if (starting) {   // the remembered song could not be opened: forget it and start a new one
627			starting = false; forget();
628			setTimeout(function () { var s = Lite3.Mixer.swf; if (s && s.mixer_load) s.mixer_load({type: 'shake'}); }, 0);
629		}
630		if (code === 'MIXER_DOWNLOAD_ERROR') window.addEventListener('online', function () { var s = Lite3.Mixer.swf; if (s && s.mixer_retry) s.mixer_retry(); }, {once: true});
631		if (Date.now() - lastError < 1500) return;   // one message per burst
632		lastError = Date.now();
633		if (navigator.onLine === false && /DOWNLOAD|SAVE|MSW/.test(code)) { __error(OFFLINE); return; }
634		if (__msg[code] && !/^(MIXER_MSW_ERROR|INVALID_VERSION|LOGIN_REQUIRED)$/.test(code)) { __error(__msg[code]); return; }
635		return _merror.call(this, code);
636	};
637
638	// New song options replace the current song: ask first, like Shake does (the handlers are private to Lite3's
639	// start menu, so each button's own click handler is wrapped)
640	function confirmNew(id) {
641		$('#' + id + ' .alist > .pad').each(function () {
642			var pad = $(this), label = pad.find('.label').text(), ev = $._data(this, 'events');
643			if (!/^(One click start|Genre tempo wizard|Start from scratch)$/.test(label) || pad.data('msw-confirm') || !ev || !ev.click) return;
644			var handlers = ev.click.map(function (h) { return h.handler; });
645			pad.data('msw-confirm', 1).off('click').on('click', function (e) {
646				var self = this, run = function () { handlers.forEach(function (h) { h.call(self, e); }); };
647				if (!MusicshakeMixer2.data.tracks.length) { run(); return; }
648				__dialog_close_sticky();
649				__confirm(__msg.MIXER_CONFIRM_SHAKE, run);
650			});
651		});
652	}
653
654	// a window reused by Lite3 (__dialog_sticky empties the open one and renames it) starts clean: no classes or
655	// position left over from the previous window, no leftover tap-outside handler on the mask
656	var _sticky = window.__dialog_sticky;
657	window.__dialog_sticky = function () {
658		var old = $('div.dialog.sticky');
659		if (old.length) {
660			old.removeClass(function (i, c) { return (c.match(/\bmsw-\S+/g) || []).join(' '); });
661			old.css({position: '', left: '', top: '', marginLeft: '', marginTop: ''});
662		}
663		$('#mask').off('click.msw');
664		return _sticky.apply(this, arguments);
665	};
666	var _mask2 = window.__mask;
667	window.__mask = function (on) { if (!on) $('#mask').off('click.msw'); return _mask2.apply(this, arguments); };
668
669	// the login window: always the dark centred one (it was plain when opened from Save or My songs)
670	if (typeof window.open_loginform === 'function') {
671		var _login = window.open_loginform;
672		window.open_loginform = function () { var r = _login.apply(this, arguments); modal('login-form'); return r; };
673	}
674
675	// Instrument window: Select with nothing chosen says what to do
676	$(document).on('click', '#' + Lite3.id + '-instruments .bottom button', function () {
677		var d = $('#' + Lite3.id + '-instruments')[0];
678		setTimeout(function () {
679			var now = $('#' + Lite3.id + '-instruments');
680			if (!now.length || now[0] !== d || now.find('.msw-hint').length) return;
681			$('<span class="msw-hint">Choose an instrument and a sound first</span>').appendTo(now.find('.bottom'));
682			setTimeout(function () { now.find('.msw-hint').remove(); }, 2500);
683		}, 250);
684	});
685
686	// the track / part / effect menus beside the grid stay on screen
687	function clampMenus() {
688		$('.grid_menu:visible').each(function () {
689			var m = $(this), r = this.getBoundingClientRect(), vw = window.innerWidth, dx = 0;
690			if (r.right > vw - 6) dx = vw - 6 - r.right;
691			if (r.left + dx < 6) dx = 6 - r.left;
692			if (dx) m.css('left', (parseFloat(m.css('left')) || 0) + dx);
693		});
694	}
695	$(document).on('click touchend', '#' + Lite3.id + '-grid', function () { setTimeout(clampMenus, 30); });
696
697	// volume: − and + step it by 10%
698	$(document).on('click', '#' + Lite3.id + '-control-volume .minus, #' + Lite3.id + '-control-volume .plus', function () {
699		var s = $('#' + Lite3.id + '-control-volume .ui-slider');
700		if (!s.length) return;
701		var v = Math.max(0, Math.min(1, Math.round((s.slider('value') + ($(this).hasClass('plus') ? 0.1 : -0.1)) * 10) / 10));
702		s.slider('value', v);
703		Lite3.Mixer.volume(v);
704	});
705
706	// a new song from a template starts with every block off: say how to start (once)
707	var hinted = false;
708	var _sync4 = Lite3.Mixer.event_sync;
709	Lite3.Mixer.event_sync = function (d) {
710		var r = _sync4.apply(this, arguments);
711		try {
712			if (!hinted && d && d.blocks && d.blocks.length && !d.blocks.some(function (row) { return row.some(function (b) { return b.on; }); })) {
713				hinted = true;
714				setTimeout(function () { if (!$('div.dialog:visible, #error:visible').length) __alert2('Tap the blocks to turn them on, then press Play.', true); }, 1500);
715			}
716		} catch (e) {}
717		return r;
718	};
719
720	// keyboard and screen readers: the editor's controls are buttons with names
721	var LABELS = {'control-play': 'Play', 'control-pause': 'Pause', 'control-shake': 'Shake: a new song', 'control-menu': 'Menu', 'control-genre': 'Genre'};
722	function label() {
723		$.each(LABELS, function (id, text) {
724			var el = $('#' + Lite3.id + '-' + id);
725			if (el.length && !el.attr('role')) el.attr({role: 'button', tabindex: 0, 'aria-label': text});
726		});
727		$('.dialog .top .close').not('[role]').attr({role: 'button', tabindex: 0, 'aria-label': 'Close'});
728	}
729	$(document).on('keydown', '[role=button]', function (e) {
730		if ((e.key === 'Enter' || e.key === ' ') && !/^(A|BUTTON|INPUT|TEXTAREA|SELECT)$/.test(e.target.tagName)) { e.preventDefault(); $(this).trigger('click'); }
731	});
732	var _create2 = Lite3.UI.create;
733	Lite3.UI.create = function () { var r = _create2.apply(this, arguments); setTimeout(label, 0); return r; };
734	var _sync5 = Lite3.Mixer.event_sync;
735	Lite3.Mixer.event_sync = function () { var r = _sync5.apply(this, arguments); setTimeout(label, 0); return r; };
736})(window, window.jQuery);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.