diff --git a/src/addons/addons/sounds-newgrounds-button/_manifest_entry.js b/src/addons/addons/sounds-newgrounds-button/_manifest_entry.js
new file mode 100644
index 00000000000..fa93d4a4f22
--- /dev/null
+++ b/src/addons/addons/sounds-newgrounds-button/_manifest_entry.js
@@ -0,0 +1,28 @@
+/* generated by pull.js */
+const manifest = {
+ "name": "Newgrounds Audio Import",
+ "description": "Import audio directly from Newgrounds into the sound editor."
+ "credits": [
+ {
+ "name": "SharkPool",
+ "link": "https://github.com/SharkPool-SP/"
+ }
+ ],
+ "info": [
+ {
+ "type": "notice",
+ "text": "Some audio on Newgrounds may not be licensed for public or commercial use. This addon will try to warn you or block downloads when restrictions apply.",
+ "id": "copyright-notice"
+ }
+ ],
+ "userscripts": [
+ {
+ "url": "userscript.js"
+ }
+ ],
+ "tags": ["editor", "new"],
+ "enabledByDefault": true,
+ "dynamicEnable": true,
+ "dynamicDisable": false
+};
+export default manifest;
\ No newline at end of file
diff --git a/src/addons/addons/sounds-newgrounds-button/_runtime_entry.js b/src/addons/addons/sounds-newgrounds-button/_runtime_entry.js
new file mode 100644
index 00000000000..bc67131957e
--- /dev/null
+++ b/src/addons/addons/sounds-newgrounds-button/_runtime_entry.js
@@ -0,0 +1,5 @@
+/* generated by pull.js */
+import _js from "./userscript.js";
+export const resources = {
+ "userscript.js": _js,
+};
diff --git a/src/addons/addons/sounds-newgrounds-button/userscript.js b/src/addons/addons/sounds-newgrounds-button/userscript.js
new file mode 100644
index 00000000000..0f220cacf6c
--- /dev/null
+++ b/src/addons/addons/sounds-newgrounds-button/userscript.js
@@ -0,0 +1,227 @@
+// Newgrounds Audio Button
+// By: SharkPool
+// Thanks Tom Fulp! :)
+
+export default async function() {
+ const ngIcon = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9Ii0yLjUgLTIuNSAyNSAyNSIgd2lkdGg9IjIwIiBoZWlnaHQ9IjIwIj48cGF0aCBkPSJNNy4wNjkgMS43NzhhMi4wMiAyLjAyIDAgMCAxIDIuMDIgMi4wMnYxMy44MDZhLjg0Ljg0IDAgMCAxLS44NDIuODRINi4zOTZhLjg0Ljg0IDAgMCAxLS44NDItLjg0MlY2LjExOWEuODQuODQgMCAwIDAtLjg0Mi0uODQyaC0uMzM3YS44NC44NCAwIDAgMC0uODQxLjg0MnYxMS40ODRhLjg0Ljg0IDAgMCAxLS44NDEuODQxSC44NDJBLjg0Ljg0IDAgMCAxIDAgMTcuNjAzVjIuNjE5YS44NC44NCAwIDAgMSAuODQyLS44NDF6bTEwLjkxMiAwQTIuMDIgMi4wMiAwIDAgMSAyMCAzLjc5N3YzLjQ3NGEuNjczLjY3MyAwIDAgMS0uNjczLjY3M2gtMi4zNTZhLjY3My42NzMgMCAwIDEtLjY3My0uNjczVjUuNzgzYS41MDQuNTA0IDAgMCAwLS41MDUtLjUwNWgtLjg0MmEuNTA0LjUwNCAwIDAgMC0uNTA1LjUwNXY4LjY1OGMwIC4xODYuMTUxLjMzNy4zMzcuMzM3aDEuMzQ2YS4zMzYuMzM2IDAgMCAwIC4zMzYtLjMzN3YtMS42NjNoLS4zMzZhLjY3My42NzMgMCAwIDEtLjY3My0uNjczVjkuOTUyYS42NzMuNjczIDAgMCAxIC42NzMtLjY3M2gzLjE5OGEuNjczLjY3MyAwIDAgMSAuNjczLjY3MnY2LjQ3NGEyLjAyIDIuMDIgMCAwIDEtMi4wMiAyLjAxOWgtNS4wNDlhMi4wMiAyLjAyIDAgMCAxLTIuMDItMi4wMlYzLjc5N2EyLjAyIDIuMDIgMCAwIDEgMi4wMi0yLjAxOWg1LjA0OVoiIGZpbGw9IiNmZmYiLz48L3N2Zz4=";
+ const safeIcon = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMjAuNzc0IiBoZWlnaHQ9IjEwNS45MDUiIHZpZXdCb3g9IjAgMCAxMjAuNzc0IDEwNS45MDUiPjxkZWZzPjxsaW5lYXJHcmFkaWVudCB4MT0iMjM5Ljg2IiB5MT0iMTMwLjA2IiB4Mj0iMjM5Ljg2IiB5Mj0iMjMyLjQ2NSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiIGlkPSJhIj48c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiMwMGZjMWQiLz48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMwMGI0MTYiLz48L2xpbmVhckdyYWRpZW50PjxsaW5lYXJHcmFkaWVudCB4MT0iMjM5Ljg2IiB5MT0iMTMwLjA2IiB4Mj0iMjM5Ljg2IiB5Mj0iMjMyLjQ2NSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiIGlkPSJiIj48c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiMwMGFlMTQiLz48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMwMDZkMGQiLz48L2xpbmVhckdyYWRpZW50PjwvZGVmcz48cGF0aCBkPSJtMjAwLjMgMTc0LjEwOCAyMi40NDYgMjAuNDgxIDU0LjcwOS02NC41MjkgMjEuMDQyIDE3LjM5NS03Mi45NDYgODUuMDEtNDQuMzI5LTM4Ljk5OHoiIGZpbGw9InVybCgjYSkiIHN0cm9rZT0idXJsKCNiKSIgc3Ryb2tlLXdpZHRoPSIzLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLW1pdGVybGltaXQ9IjEwIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMTc5LjQ3MiAtMTI4LjMxKSIvPjwvc3ZnPg==";
+ const warnIcon = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMjMuODYxIiBoZWlnaHQ9IjEwMC4yOTQiIHZpZXdCb3g9IjAgMCAxMjMuODYxIDEwMC4yOTQiPjxkZWZzPjxsaW5lYXJHcmFkaWVudCB4MT0iMjQwIiB5MT0iMTMwLjM0MSIgeDI9IjI0MCIgeTI9IjIyNy4xMzQiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiBpZD0iYSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjZmZlYzEwIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjZmZhYzBjIi8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgeDE9IjI0MCIgeTE9IjEzMC4zNDEiIHgyPSIyNDAiIHkyPSIyMjcuMTM0IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgaWQ9ImIiPjxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzhiNGUwMiIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzgyMjcwMCIvPjwvbGluZWFyR3JhZGllbnQ+PGxpbmVhckdyYWRpZW50IHgxPSIyNDAiIHkxPSIxNTAuODkyIiB4Mj0iMjQwIiB5Mj0iMjIxLjQ1MyIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiIGlkPSJjIj48c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiM4OTQ1MDEiLz48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiM4MjI5MDAiLz48L2xpbmVhckdyYWRpZW50PjwvZGVmcz48ZyBzdHJva2Utd2lkdGg9IjMuNSIgc3Ryb2tlLW1pdGVybGltaXQ9IjEwIj48cGF0aCBkPSJtMjQwIDEzMC4zNCA2MC4xOCA5Ni43OTRIMTc5LjgyeiIgZmlsbD0idXJsKCNhKSIgc3Ryb2tlPSJ1cmwoI2IpIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMTc4LjA3IC0xMjguNTkpIi8+PHBhdGggZD0iTTIzMi41NjUgMjE0LjAxOGE3LjQzNSA3LjQzNSAwIDEgMSAxNC44NyAwIDcuNDM1IDcuNDM1IDAgMCAxLTE0Ljg3IDBtNy4xMjItMTIuOTA2YTYgNiAwIDAgMS02LTZ2LTM4LjIyYTYgNiAwIDAgMSA2LTZoLjYyNmE2IDYgMCAwIDEgNiA2djM4LjIyYTYgNiAwIDAgMS02IDZ6IiBmaWxsPSJ1cmwoI2MpIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMTc4LjA3IC0xMjguNTkpIi8+PC9nPjwvc3ZnPg==";
+ const unsafeIcon = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMjUuNTc4IiBoZWlnaHQ9IjEyNS44ODMiIHZpZXdCb3g9IjAgMCAxMjUuNTc4IDEyNS44ODMiPjxkZWZzPjxsaW5lYXJHcmFkaWVudCB4MT0iMjM5Ljg2IiB5MT0iMTE5LjY3OSIgeDI9IjIzOS44NiIgeTI9IjI0MC42MDEiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiBpZD0iYSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjZmUwMDUwIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjOWIwMDA3Ii8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgeDE9IjIzOS44NiIgeTE9IjExOS42NzkiIHgyPSIyMzkuODYiIHkyPSIyNDAuNjAxIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgaWQ9ImIiPjxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzdlMDAyNyIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzNiMDAwMSIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxwYXRoIGQ9Im0xOTcuMjE0IDEyMC44MDIgNDIuNjQ2IDQxLjgwMyA0MS41MjMtNDIuOTI2IDE3Ljk1NiAxNy42NzYtNDEuNTIzIDQyLjM2NCA0Mi4zNjQgNDAuOTYyLTE3LjM5NCAxOC41MTctNDIuMDg1LTQxLjI0MkwxOTkuNDYgMjQwLjZsLTE4LjIzNy0xNy42NzVMMjIyLjE4NCAxODBsLTQyLjY0NS00MC45NjJ6IiBmaWxsPSJ1cmwoI2EpIiBzdHJva2U9InVybCgjYikiIHN0cm9rZS13aWR0aD0iMy41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1taXRlcmxpbWl0PSIxMCIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTE3Ny4wNTkgLTExNy4xOTQpIi8+PC9zdmc+";
+
+ const proxy1 = "https://corsproxy.io?url=";
+ const proxy2 = "https://api.codetabs.com/v1/proxy?quest=";
+
+ let ngButtonElement;
+
+ async function safeFetch(url, respondType) {
+ const proxies = [proxy1, proxy2];
+ for (const proxy of proxies) {
+ try {
+ const response = await fetch(proxy + url);
+
+ if (response.ok) return await response[respondType]();
+ if (response.status === 400) return undefined;
+ continue;
+ } catch (e) {
+ console.warn(`Failed to fetch ${url} with proxy: ${proxy}`, e);
+ }
+ }
+ return undefined;
+ }
+
+ async function addTrack2Editor(url, name) {
+ const buffer = await safeFetch(url, "arrayBuffer");
+ if (!buffer) {
+ alert("Failed to Fetch Song!");
+ return;
+ }
+
+ const storage = vm.runtime.storage;
+ const asset = storage.createAsset(
+ storage.AssetType.Sound, storage.DataFormat.MP3,
+ new Uint8Array(buffer), null, true
+ );
+
+ try {
+ await vm.addSound(
+ {
+ asset, name,
+ md5: asset.assetId + "." + asset.dataFormat,
+ },
+ vm.editingTarget.id
+ );
+ } catch (e) {
+ console.warn(e);
+ }
+ }
+
+ async function openNewgroundsPopup() {
+ let url, name, songURL;
+ let infoBox = undefined;
+
+ /* ScratchBlocks is availiable when this is called */
+ const modal = await ScratchBlocks.customPrompt(
+ { title: "Newgrounds Audio" }, { content: { width: "500px" } },
+ [
+ {
+ name: "Add Track", role: "ok", callback: () => {
+ if (url && name && songURL) addTrack2Editor(songURL, `${name} -- ${author}`);
+ }
+ },
+ { name: "Cancel", role: "close", callback: () => {} }
+ ]
+ );
+
+ const okayButton = modal.parentNode.querySelector(`button[class^="prompt_ok-button"]`);
+ okayButton.style.filter = "brightness(70%)";
+ okayButton.style.pointerEvents = "none";
+
+ const label = document.createElement("div");
+ label.innerHTML = `Import Newgrounds audio directly into your Project.
Not all tracks are fully free-to-use, read the report after searching.`;
+ label.setAttribute("style", "text-align: center; font-size: .85rem;");
+
+ const idInputDiv = document.createElement("div");
+ idInputDiv.setAttribute("style", "width: 100%; margin: 15px 0; padding: 10px 20px; border-radius: 15px; border: dashed 2px grey; text-align: center; font-size: .85rem;");
+
+ const idLabel = document.createElement("b");
+ idLabel.textContent = "Track ID/URL: ";
+ const idInput = document.createElement("input");
+
+ idInput.setAttribute("style", "margin-left: 5px; width: 70%; height: 25px; text-align: center; background: #ffffff20; border-radius: 15px; border: solid grey 1px;");
+ idInput.type = "text";
+ idInput.placeholder = "https://www.newgrounds.com/audio/listen/1395716";
+ idInput.value = "1395716";
+ url = idInput.placeholder;
+ idInput.addEventListener("change", (e) => {
+ url = String(e.target.value);
+ if (!url.startsWith("https://www.newgrounds.com/audio/listen/")) url = "https://www.newgrounds.com/audio/listen/" + url;
+ e.stopPropagation();
+ });
+
+ const searchBtn = document.createElement("button");
+ searchBtn.setAttribute("style", "border: none; border-radius: 5px; padding: 10px 20px; margin: 10px 0 0; background: hsla(194, 100%, 50%, 1); cursor: pointer; font-weight: 600; font-size: 0.85rem; color: white;");
+ searchBtn.textContent = "Search";
+ searchBtn.addEventListener("click", async (e) => {
+ // unfortunately we have to scrape html here since the Newgrounds API is hidden
+ const htmlText = await safeFetch(url, "text");
+ if (!htmlText) {
+ alert("Failed to Fetch Track URL!");
+ return;
+ }
+
+ /* extract info */
+ author = htmlText.match(/"artist":"([^"]+)"/)?.[1] || "";
+ name = htmlText.match(/
\s*([\s\S]*?)\s*<\/p>/i)?.[1] || ""; + const type = isScouted ? ccLicense2Rating(ccLicense) : "unwhitelisted"; + + if (infoBox) infoBox.remove(); + infoBox = genCopyrightInfoBox(type, name, author); + if (type === "bad" || type === "unwhitelisted") { + name = undefined; + okayButton.style.filter = "brightness(70%)"; + okayButton.style.pointerEvents = "none"; + } else { + okayButton.style.filter = ""; + okayButton.style.pointerEvents = ""; + } + modal.appendChild(infoBox); + e.stopPropagation(); + }); + + idInputDiv.append(idLabel, idInput, searchBtn); + modal.append(label, idInputDiv); + } + + function ccLicense2Rating(licence) { + licence = String(licence).toLowerCase().trim(); + const goodTexts = [ + `you may only use this piece for commercial purposes if your work is a web-based game or animation,`, + ]; + for (const text of goodTexts) { + if (licence.startsWith(text)) return "good"; + } + + const badTexts = [ + `you may not use this work for any purposes`, + ]; + for (const text of badTexts) { + if (licence.startsWith(text)) return "bad"; + } + + const warnTexts = [ + `you are free to copy, distribute and transmit this work under the following conditions:`, + `please contact me if you would like to use this in a project. we can discuss the details.`, + ]; + for (const text of warnTexts) { + if (licence.startsWith(text)) return "warn"; + } + return "warn"; // warn is the default + } + + function genCopyrightInfoBox(type, name, author) { + const color = type === "good" ? "#00ff00" : type === "bad" || type === "unwhitelisted" ? "#ff0000" : "#ffc400"; + const box = document.createElement("div"); + box.setAttribute("style", `display: flex; width: 100%; margin: 15px 0; padding: 10px 20px 10px 30px; border-radius: 15px; border: solid 2px ${color}; background: ${color}30; text-align: center; font-size: .9rem; font-weight: bold;`); + + const img = document.createElement("img"); + img.setAttribute("style", "width:35px; margin-right: 5px;"); + img.src = type === "good" ? safeIcon : type === "bad" || type === "unwhitelisted" ? unsafeIcon : warnIcon; + + const label = document.createElement("span"); + if (type === "good") label.innerHTML = `The Track: ${name} by ${author}, can freely be used for web-based games`; + else if (type === "bad") label.innerHTML = `The Track: ${name} by ${author}, is not allowed for use!`; + else if (type === "unwhitelisted") label.innerHTML = `The Track: ${name} by ${author}, is not allowed for use. ${author} is not scouted on Newgrounds!`; + else label.innerHTML = `The Track: ${name} by ${author}, can only be used for non-profit web-based games WITH credit. Further use requires permission from ${author}`; + + box.append(img, label); + return box; + } + + function addButtonNG() { + // TODO add a tooltip maybe + const itemDiv = document.querySelector(`div[class^="action-menu_menu-container"] div[class^="action-menu_more-buttons-outer"] div[class^="action-menu_more-buttons"]`); + + ngButtonElement = itemDiv.children[0].cloneNode(true); + const innerButton = ngButtonElement.firstChild; + innerButton.setAttribute("data-tip", "Newgrounds Sound"); + innerButton.setAttribute("aria-label", "Newgrounds Sound"); + /* cleanup */ + for (var i = 1; i < innerButton.children.length; i++) { + const child = innerButton.children[i]; + if (child) child.remove(); + } + innerButton.firstChild.src = ngIcon; + ngButtonElement.addEventListener("click", openNewgroundsPopup); + itemDiv.insertBefore(ngButtonElement, itemDiv.children[0]); + } + + function startListenerWorker() { + ReduxStore.subscribe(() => queueMicrotask(() => { + const reduxState = ReduxStore.getState().scratchGui; + /* sound tab */ + if (!reduxState.mode.isPlayerOnly && reduxState.editorTab.activeTabIndex === 2) { + if (!ngButtonElement) addButtonNG(); + } else { + ngButtonElement = undefined; + } + })); + } + + if (typeof scaffolding === "undefined") startListenerWorker(); +} diff --git a/src/addons/generated/addon-entries.js b/src/addons/generated/addon-entries.js index f34d93101e9..83218495021 100644 --- a/src/addons/generated/addon-entries.js +++ b/src/addons/generated/addon-entries.js @@ -77,4 +77,5 @@ export default { "multi-tab-code": () => import(/* webpackChunkName: "addon-default-entry" */ "../addons/multi-tab-code/_runtime_entry.js"), "editor-animations": () => import(/* webpackChunkName: "addon-default-entry" */ "../addons/editor-animations/_runtime_entry.js"), "reorder-custom-inputs": () => import(/* webpackChunkName: "addon-default-entry" */ "../addons/reorder-custom-inputs/_runtime_entry.js"), + "sounds-newgrounds-button": () => import(/* webpackChunkName: "addon-default-entry" */ "../addons/sounds-newgrounds-button/_runtime_entry.js"), }; diff --git a/src/addons/generated/addon-manifests.js b/src/addons/generated/addon-manifests.js index a9317d5d657..5b54eec8885 100644 --- a/src/addons/generated/addon-manifests.js +++ b/src/addons/generated/addon-manifests.js @@ -75,6 +75,7 @@ import _tw_disable_cloud_variables from "../addons/tw-disable-cloud-variables/_m import _multi_tab_code from "../addons/multi-tab-code/_manifest_entry.js"; import _editor_animations from "../addons/editor-animations/_manifest_entry.js"; import _reorder_custom_inputs from "../addons/reorder-custom-inputs/_manifest_entry.js"; +import _sounds_newgrounds_button from "../addons/sounds-newgrounds-button/_manifest_entry.js"; export default { "cat-blocks": _cat_blocks, @@ -154,4 +155,5 @@ export default { "multi-tab-code": _multi_tab_code, "editor-animations": _editor_animations, "reorder-custom-inputs": _reorder_custom_inputs, + "sounds-newgrounds-button": _sounds_newgrounds_button, };