diff --git a/extensions/SharkPool/Community-Spotlight.js b/extensions/SharkPool/Community-Spotlight.js new file mode 100644 index 0000000000..6dd3609446 --- /dev/null +++ b/extensions/SharkPool/Community-Spotlight.js @@ -0,0 +1,357 @@ +// Name: Community Spotlight +// ID: SPspotlight +// Description: Display and Advertise Promotional Content for Free. +// By: SharkPool +// Licence: MIT + +// Version V.1.0.1 + +(function (Scratch) { + "use strict"; + if (!Scratch.extensions.unsandboxed) throw new Error("Community Spotlight must run unsandboxed!"); + + const menuIconURI = +"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI3Ny4yMDUiIGhlaWdodD0iNzcuMjA1IiB2aWV3Qm94PSIwIDAgNzcuMjA1IDc3LjIwNSI+PGRlZnM+PGxpbmVhckdyYWRpZW50IHgxPSIyMTIuNzA0IiB5MT0iMTUyLjcwNCIgeDI9IjI2Ny4yOTYiIHkyPSIyMDcuMjk2IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgaWQ9ImEiPjxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzAwOTk0ZiIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzAwNzg5OSIvPjwvbGluZWFyR3JhZGllbnQ+PGxpbmVhckdyYWRpZW50IHgxPSIyMTUuODk0IiB5MT0iMTU1Ljg5NCIgeDI9IjI2NC4xMDYiIHkyPSIyMDQuMTA2IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgaWQ9ImIiPjxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzAwZmY4NCIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzAwYzlmZiIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxnIHN0cm9rZS1taXRlcmxpbWl0PSIxMCI+PHBhdGggZD0iTTIxMi43MDQgMjA3LjI5NmMtMTUuMDc1LTE1LjA3NS0xNS4wNzUtMzkuNTE3IDAtNTQuNTkyczM5LjUxNy0xNS4wNzUgNTQuNTkyIDAgMTUuMDc1IDM5LjUxNyAwIDU0LjU5Mi0zOS41MTcgMTUuMDc1LTU0LjU5MiAwIiBmaWxsPSJ1cmwoI2EpIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMjAxLjM5OCAtMTQxLjM5OCkiLz48cGF0aCBkPSJNMjE1Ljg5NCAyMDQuMTA2Yy0xMy4zMTMtMTMuMzE0LTEzLjMxMy0zNC44OTggMC00OC4yMTJzMzQuODk4LTEzLjMxMyA0OC4yMTIgMCAxMy4zMTMgMzQuODk4IDAgNDguMjEyLTM0Ljg5OCAxMy4zMTMtNDguMjEyIDAiIGZpbGw9InVybCgjYikiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMDEuMzk4IC0xNDEuMzk4KSIvPjxwYXRoIGQ9Im00Ny42MzQgMzcuMzI0LTctNS42OTQgMTQuNTI3LTEyLjc2NXMxLjM1LTEuNDI0IDIuMzE2LTEuMDFjMS41NjEuNjY4Ljg2NSAyLjI4OC44NjUgMi4yODh6TTE3LjQyNSA1MS45ODljLjQ4NS0uNDg4IDEuMDMzLS45MDYgMS41NS0xLjM2bDQuOTE3LTQuMzEzIDQuODk2LTQuMjk1IDcuOTc5IDEyLjcxM3MtLjAzMS4wOTItLjI0Ni40MzdjLS40NTcuNzM1LS43NDcgMS41MDMtMS40MTIgMi4xMDQtMS40NTkgMS4zMTUtMy41MiAxLjkxLTUuNDEzIDIuMjI5LTIuNDgyLjQyLTUuMDg4LjQwMS03LjU2LS4wNzYtMS44MTktLjM1Mi0zLjgxLS45NzQtNS4xNjMtMi4zMDctLjQ4Mi0uNDc1LS44Ny0xLjA2My0xLjAwMS0xLjczNmEyLjY0IDIuNjQgMCAwIDEgLjA1NC0xLjIzNGMuMTMzLS40NjMuNjI5LTEuMzkgMS4zOTktMi4xNjJtOC44Mi0uMTRjLTQuNDA3IDAtNy45OCAxLjM2OC03Ljk4IDMuMDU2czMuNTczIDMuMDU2IDcuOTggMy4wNTYgNy45OC0xLjM2OCA3Ljk4LTMuMDU2LTMuNTczLTMuMDU2LTcuOTgtMy4wNTZtMzEuODE2LTEuMzY5IDEuNDg0IDEuMzAyYy41NC40NzMuOTY5IDEuMDUxIDEuMzMgMS42N3EuMDAxLjAwNi4wMDUuMDFjLjI1LjQyOS40LjkxNC40IDEuNDEyIDAgMy4yNzMtNC44NzYgNC41MDItNy4zMzYgNC43MzItMy43MDYuMzQ3LTguMTM2LjI2NC0xMS4yMS0xLjgzNS0xLjAwOC0uNjg5LTEuNDM0LTEuNjEzLTIuMDQyLTIuNTlsLTEuNjM3LTIuNjNRMzEuOTkgNDEuMjA4IDI0LjkyOCAyOS44NjNsLTYuNTM0LTEwLjQ5NWMtLjYyMy0uOTk4IDEuMTY3LTIuNjYgMi4zMTEtMS42NTdsOC43MDMgNy42MzQgMjEuNzE1IDE5LjA0OHptLTYuOTkgMS4zNjljLTQuNDA3IDAtNy45OCAxLjM2OC03Ljk4IDMuMDU2czMuNTczIDMuMDU2IDcuOTggMy4wNTYgNy45OC0xLjM2OCA3Ljk4LTMuMDU2LTMuNTczLTMuMDU2LTcuOTgtMy4wNTYiIGZpbGw9IiNmZmYiLz48L2c+PC9zdmc+Cg=="; + + const vm = Scratch.vm; + const runtime = vm.runtime; + + const acceptableRatios = ["1:1", "4:3", "4:5", "16:9", "9:16"]; + const acceptableImgs = [ + "250x250 Square", "300x250 Rectangle", "480x270 Widescreen", + "300x50 Horizontal Banner", "50x300 Vertical Banner", + "360x120 Large Horizontal Banner", "120x360 Large Vertical Banner" + ]; + + let tags = []; + let initialized = false; + + // Community Spotlight Exports + // Uses MIT Licence (https://github.com/Community-Spotlight) + // eslint-disable-next-line + window.CommunitySpotlight={b:"https://raw.githubusercontent.com/Community-Spotlight/",u:{},c:{}};window.CommunitySpotlight.u["fetchIndex"]=async function(){const r=await fetch(`${window.CommunitySpotlight.b}promotion-index/main/index.json`);if(!r.ok)throw new Error("Couldn't fetch promotions!");return await r.json()};window.CommunitySpotlight.u["filterPromos"]=function(j,t,p){const r=(a)=>a[Math.floor(Math.random()*a.length)];t=t==="video"?"video":t==="html"?"html":"image";p=typeof p==="object"?p:{};if(p.tags){if(p.tags.constructor.name==="Array"){p.tags=p.tags.map((e)=>{return e.toLowerCase()})}else{console.warn("CS -- 'tags' parameter must be an Array");return{}}}const{aspectRatio,videoLength}=p;let c=Object.values(j).filter(q=>{return q.media[t+"s"].length>0});if(c.length===0){console.warn("CS -- No promotions found with given type");return {}}if(p.tags&&p.tags.length>0)c=c.filter((q)=>q.tags.some(tag=>p.tags.includes(tag)));if(aspectRatio)c=c.filter(q=>{return q.media[t+"s"].some((i)=>{return i.size===aspectRatio})});if(t==="video"&&videoLength)c=c.filter(q=>{return q.media.videos.some((i)=>{return i.length===videoLength})});if(c.length===0){console.warn("CS -- No promotions found with given parameters");return{}}const g=r(c);let m,l,f;switch(t){case"video":f=g.media.videos,m=aspectRatio&&videoLength?f.find(e=>e.size===aspectRatio&&e.length===videoLength):!aspectRatio&&videoLength?f.find(e=>e.length===videoLength):aspectRatio&&!videoLength?f.find(e=>e.size===aspectRatio):r(f),l=`sz${m.size.replace(":","x")}leng${m.length}.${m.type}`;break;case"html":f=g.media.htmls,l=`sz${(m=aspectRatio?f.find(e=>e.size===aspectRatio):r(f)).size.replace(":","x")}.html`;break;default:f=g.media.images,m=aspectRatio?f.find(e=>e.size===aspectRatio):r(f),l=`${m.size}.${m.type}`}delete g.media;g.url=`${window.CommunitySpotlight.b}promotion-media/main/${encodeURIComponent(g.id)}/${l}`;return g};async function refreshPromoCacheCS(){try{window.CommunitySpotlight.c = await window.CommunitySpotlight.u["fetchIndex"]()}catch (e){console.error(e)}}async function getOnlinePromoCS(t,p){return window.CommunitySpotlight.u["filterPromos"](await window.CommunitySpotlight.u["fetchIndex"](),t,p)}function getCachedPromoCS(t,p){return window.CommunitySpotlight.u["filterPromos"](structuredClone(window.CommunitySpotlight.c),t,p)} + + class SPspotlight { + constructor() { + // Initialize the Promotions + this.refresh(); + this.promoSpaceInfo = { pos: [0, 0], sz: [1, 1] }; + this.promoSpace = document.createElement("div"); + vm.renderer.addOverlay(this.promoSpace, "scale-centered"); + + runtime.on("PROJECT_STOP_ALL", () => { this.deletePromo() }); + runtime.on("PROJECT_START", () => { this.deletePromo() }); + runtime.on("RUNTIME_PAUSED", () => { + const video = this.promoSpace.querySelector("video"); + if (video) video.pause(); + }); + runtime.on("RUNTIME_UNPAUSED", () => { + const video = this.promoSpace.querySelector("video"); + if (video) video.play(); + }); + } + getInfo() { + return { + id: "SPspotlight", + name: "Community Spotlight", + color1: "#00a1e6", + color2: "#0085e6", + color3: "#006fbf", + menuIconURI, + blocks: [ + { + func: "promoDisclaim", + blockType: Scratch.BlockType.BUTTON, + text: "Promotion Disclaimer" + }, + { + func: "addPromo", + blockType: Scratch.BlockType.BUTTON, + text: "Add My Promotion" + }, + "---", + { + opcode: "activeTags", + blockType: Scratch.BlockType.REPORTER, + text: "active tags" + }, + { + opcode: "filterTags", + blockType: Scratch.BlockType.COMMAND, + text: "filter promos with tags [TAGS]", + arguments: { + TAGS: { type: Scratch.ArgumentType.STRING, defaultValue: "[\"Gaming\", \"Art\"]" } + }, + }, + { + opcode: "refresh", + blockType: Scratch.BlockType.COMMAND, + text: "refresh promo cache" + }, + { blockType: Scratch.BlockType.LABEL, text: "Manual Display" }, + { + opcode: "getImgPromo", + blockType: Scratch.BlockType.REPORTER, + text: "get promo image with size [SIZE]", + arguments: { + SIZE: { type: Scratch.ArgumentType.STRING, menu: "IMGS" } + }, + }, + { + opcode: "getVidPromo", + blockType: Scratch.BlockType.REPORTER, + text: "get [LENGTH] sec promo video with size [SIZE]", + arguments: { + SIZE: { type: Scratch.ArgumentType.STRING, menu: "VID_RATIO" }, + LENGTH: { type: Scratch.ArgumentType.STRING, menu: "VID_LENGTH" } + }, + }, + { + opcode: "getHTMLPromo", + blockType: Scratch.BlockType.REPORTER, + text: "get promo html with size [SIZE]", + arguments: { + SIZE: { type: Scratch.ArgumentType.STRING, menu: "VID_RATIO" }, + }, + }, + { blockType: Scratch.BlockType.LABEL, text: "Auto Display" }, + { + opcode: "showImgPromo", + blockType: Scratch.BlockType.COMMAND, + text: "display promo image with size [SIZE]", + arguments: { + SIZE: { type: Scratch.ArgumentType.STRING, menu: "IMGS" } + }, + }, + { + opcode: "showVidPromo", + blockType: Scratch.BlockType.COMMAND, + text: "display [LENGTH] sec promo video with size [SIZE]", + arguments: { + SIZE: { type: Scratch.ArgumentType.STRING, menu: "VID_RATIO" }, + LENGTH: { type: Scratch.ArgumentType.STRING, menu: "VID_LENGTH" } + }, + }, + { + opcode: "showHTMLPromo", + blockType: Scratch.BlockType.COMMAND, + text: "display promo HTML with size [SIZE]", + arguments: { + SIZE: { type: Scratch.ArgumentType.STRING, menu: "VID_RATIO" } + }, + }, + "---", + { + opcode: "visiblePromo", + blockType: Scratch.BlockType.COMMAND, + text: "[TYPE] displayed promo", + arguments: { + TYPE: { type: Scratch.ArgumentType.STRING, menu: "VISIBLE" } + }, + }, + { + opcode: "deletePromo", + blockType: Scratch.BlockType.COMMAND, + text: "delete displayed promo" + }, + "---", + { + opcode: "setPromoPos", + blockType: Scratch.BlockType.COMMAND, + text: "set position of promo to x [x] y [y]", + arguments: { + x: { type: Scratch.ArgumentType.NUMBER, defaultValue: 0 }, + y: { type: Scratch.ArgumentType.NUMBER, defaultValue: 0 } + }, + }, + { + opcode: "setPromoScale", + blockType: Scratch.BlockType.COMMAND, + text: "set scale of promo to x [x] y [y]", + arguments: { + x: { type: Scratch.ArgumentType.NUMBER, defaultValue: 200 }, + y: { type: Scratch.ArgumentType.NUMBER, defaultValue: 100 } + }, + } + ], + menus: { + VISIBLE: { acceptReporters: true, items: ["show", "hide"] }, + IMGS: { acceptReporters: true, items: acceptableImgs }, + VID_RATIO: { acceptReporters: true, items: acceptableRatios }, + VID_LENGTH: { + acceptReporters: true, + items: ["any", "5", "10", "15", "30"] + } + } + }; + } + + // Helper Funcs + promoDisclaim() { + alert(`WARNING: Community Spotlight will NOT earn you any Money.\nScratch projects have numerous Security Issues that make earning Money Impractical.\n\nAll Promotions are Moderated, Free and Acessible to Everyone.`); + } + + addPromo() { Scratch.openWindow("https://community-spotlight.github.io/uploader-site/") } + + displayPromo(promo, scale) { + if (promo.id === undefined) return; + const isVideo = promo.url.endsWith(".mp4"); + const isHTML = promo.url.endsWith(".html"); + + const div = document.createElement("div"); + div.setAttribute("style", "width: 100%; height: 100%; position: absolute; top: -50%; left: -50%;"); + + const newSpace = document.createElement(isVideo ? "video" : isHTML ? "iframe" : "img"); + newSpace.setAttribute("title", promo.promoter); + newSpace.src = promo.url; + newSpace.draggable = false; + newSpace.crossOrigin = "anonymous"; + newSpace.setAttribute("style", "pointer-events: auto; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);"); + if (!isVideo && !isHTML) newSpace.style.cursor = "pointer"; + + // even though HTML is Moderated, its good to have some sandboxes + if (isHTML) newSpace.setAttribute("sandbox", "allow-scripts allow-forms allow-same-origin"); + + let sz = scale.split(" ")[0]; + if (isVideo || isHTML) { + const scaleAmt = sz === "1:1" ? 240 : 120; + newSpace.style.width = `${parseInt(sz.split(":")[0]) * scaleAmt}px`; + newSpace.style.height = `${parseInt(sz.split(":")[1]) * scaleAmt}px`; + } else { + newSpace.style.width = `${sz.split("x")[0]}px`; + newSpace.style.height = `${sz.split("x")[1]}px`; + } + div.appendChild(newSpace); + + this.promoSpace.firstChild?.remove(); + this.promoSpace.appendChild(div); + this.updatePromo(); + + newSpace.addEventListener("click", (e) => { + if (isVideo && e.target !== newSpace) return; + // TODO add proper click detection for iframes. Maybe make a content holder? + Scratch.openWindow(promo["promoter-url"]); + }); + if (isVideo) { + newSpace.controls = true; + newSpace.play(); + } + } + + updatePromo() { + if (!this.promoSpace.firstChild) return; + const { pos, sz } = this.promoSpaceInfo; + this.promoSpace.firstChild.style.transform = `translate(${pos[0]}px, ${pos[1]}px) scale(${sz[0]},${sz[1]})`; + } + + // Block Funcs + activeTags() { return JSON.stringify(tags) } + + filterTags(args) { + try { tags = JSON.parse(Scratch.Cast.toString(args.TAGS).toLowerCase()) } + catch { tags = [] } + } + + async refresh() { + await refreshPromoCacheCS(); + initialized = true; + } + + getImgPromo(args) { + if (!initialized) return "{}"; + if (acceptableImgs.indexOf(args.SIZE) === -1) return "{}"; + const aspectRatio = args.SIZE.split(" ")[0]; + return JSON.stringify( + getCachedPromoCS("image", { tags, aspectRatio }) + ); + } + showImgPromo(args) { + if (!initialized) return; + this.displayPromo( + JSON.parse(this.getImgPromo(args)), args.SIZE + ); + } + + getVidPromo(args) { + if (!initialized) return "{}"; + if (acceptableRatios.indexOf(args.SIZE) === -1) return "{}"; + const videoLength = args.LENGTH === "any" ? undefined : Scratch.Cast.toNumber(args.LENGTH); + return JSON.stringify( + getCachedPromoCS("video", { + tags, videoLength, aspectRatio: args.SIZE + }) + ); + } + showVidPromo(args) { + if (!initialized) return; + this.displayPromo( + JSON.parse(this.getVidPromo(args)), args.SIZE + ); + } + + getHTMLPromo(args) { + if (!initialized) return "{}"; + if (acceptableRatios.indexOf(args.SIZE) === -1) return "{}"; + return JSON.stringify( + getCachedPromoCS("html", { + tags, aspectRatio: args.SIZE + }) + ); + } + showHTMLPromo(args) { + if (!initialized) return; + this.displayPromo( + JSON.parse(this.getHTMLPromo(args)), args.SIZE + ); + } + + visiblePromo(args) { + this.promoSpace.style.display = args.TYPE === "show" ? "" : "none"; + } + + deletePromo() { this.promoSpace.firstChild?.remove() } + + setPromoPos(args) { + this.promoSpaceInfo.pos = [ + Scratch.Cast.toNumber(args.x), Scratch.Cast.toNumber(args.y) * -1 + ]; + this.updatePromo(); + } + + setPromoScale(args) { + this.promoSpaceInfo.sz = [ + Scratch.Cast.toNumber(args.x) / 100, Scratch.Cast.toNumber(args.y) / 100 + ]; + this.updatePromo(); + } + } + + function addGradientToBody() { + var grad = document.createElement("div"); + grad.innerHTML = ` + + + `; + document.body.appendChild(grad); + } + if (Scratch.gui) Scratch.gui.getBlockly().then((ScratchBlocks) => { + addGradientToBody(); + if (!ScratchBlocks?.SPgradients?.patched) { // Gradient Patch by 0znzw & SharkPool + ScratchBlocks.SPgradients = {gradientUrls: {}, patched: false}; + const BSP = ScratchBlocks.BlockSvg.prototype, BSPR = BSP.render; + BSP.render = function(...args) { + const res = BSPR.apply(this, args); + let category; + if (this?.svgPath_ && this?.category_ && (category = this.type.slice(0, this.type.indexOf("_"))) && ScratchBlocks.SPgradients.gradientUrls[category]) { + const urls = ScratchBlocks.SPgradients.gradientUrls[category]; + if (urls) this.svgPath_.setAttribute("fill", urls[0]); + } + return res; + } + ScratchBlocks.SPgradients.patched = true; + } + ScratchBlocks.SPgradients.gradientUrls["SPspotlight"] = ["url(#SPspotlight-GRAD)", "url(#SPspotlight-GRAD)"]; + }); + + Scratch.extensions.register(new SPspotlight()); +})(Scratch); diff --git a/images/SharkPool/Community-Spotlight.svg b/images/SharkPool/Community-Spotlight.svg new file mode 100644 index 0000000000..e212bb2243 --- /dev/null +++ b/images/SharkPool/Community-Spotlight.svg @@ -0,0 +1,236 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +