From 88941729df165e66af6eac606e1a4ee5f98ae096 Mon Sep 17 00:00:00 2001 From: MaterArc <105017592+MaterArc@users.noreply.github.com> Date: Thu, 1 Aug 2024 10:58:08 -0400 Subject: [PATCH 1/6] Search in Studio --- features/search-in-studio/data.json | 28 ++++ features/search-in-studio/script.js | 205 ++++++++++++++++++++++++++++ features/search-in-studio/style.css | 10 ++ 3 files changed, 243 insertions(+) create mode 100644 features/search-in-studio/data.json create mode 100644 features/search-in-studio/script.js create mode 100644 features/search-in-studio/style.css diff --git a/features/search-in-studio/data.json b/features/search-in-studio/data.json new file mode 100644 index 00000000..1e04f94c --- /dev/null +++ b/features/search-in-studio/data.json @@ -0,0 +1,28 @@ +{ + "title": "Search in Studio", + "description": "Allows users to search for projects in studios.", + "credits": [ + { "username": "MaterArc", "url": "https://scratch.mit.edu/users/MaterArc/" } + ], + "type": ["Website"], + "tags": ["New"], + "dynamic": true, + "scripts": [ + { + "file": "script.js", + "runOn": "/studios/*" + } + ], + "styles": [ + { + "file": "style.css", + "runOn": "/studios/*" + } + ], + "components": [ + { + "type": "info", + "content": "Searching can take a while to load depending on the studio size." + } + ] +} diff --git a/features/search-in-studio/script.js b/features/search-in-studio/script.js new file mode 100644 index 00000000..e8db036a --- /dev/null +++ b/features/search-in-studio/script.js @@ -0,0 +1,205 @@ +export default async function ({ feature, console }) { + let projects = []; + let projectDetailsMap = {}; + + function tokenize(text) { + return text + .toLowerCase() + .split(/\s+/) + .filter((word) => word.length > 0); + } + + function computeExactPhraseScore(title, searchTokens) { + const titleTokens = tokenize(title); + const phrase = searchTokens.join(" "); + const titleString = titleTokens.join(" "); + if (titleString.includes(phrase)) { + const startIndex = titleString.indexOf(phrase); + return startIndex === 0 ? 2 : 1; + } + return 0; + } + + function computeSingleWordScore(title, searchToken) { + const titleTokens = tokenize(title); + if (titleTokens[0] === searchToken) { + return 2; + } else if (titleTokens.includes(searchToken)) { + return 1; + } + return 0; + } + + function searchProject(searchText) { + const searchTokens = tokenize(searchText.trim()); + const exactMatchProjects = projects + .map((project) => ({ + project, + score: computeExactPhraseScore( + projectDetailsMap[project.id].title.toLowerCase(), + searchTokens + ), + })) + .filter(({ score }) => score > 0) + .sort((a, b) => b.score - a.score) + .map(({ project }) => project); + + if (searchTokens.length === 1) { + const singleWordProjects = projects + .map((project) => ({ + project, + score: computeSingleWordScore( + projectDetailsMap[project.id].title.toLowerCase(), + searchTokens[0] + ), + })) + .filter(({ score }) => score > 0) + .sort((a, b) => b.score - a.score) + .map(({ project }) => project); + + const combinedResults = [...exactMatchProjects, ...singleWordProjects]; + updateProjectContainer(combinedResults); + } else { + updateProjectContainer(exactMatchProjects); + } + } + + function injectSearchBar() { + ScratchTools.waitForElements(".studio-header-container", () => { + const headerContainer = document.querySelector( + ".studio-header-container" + ); + if (!headerContainer) return; + + const searchContainer = document.createElement("div"); + searchContainer.className = "search-container"; + + const searchInput = document.createElement("input"); + searchInput.type = "text"; + searchInput.className = "search-bar"; + searchInput.id = "projectSearch"; + searchInput.placeholder = "Search projects..."; + + searchContainer.appendChild(searchInput); + headerContainer.appendChild(searchContainer); + + searchInput.addEventListener("input", () => { + const searchText = searchInput.value; + if (searchText.trim() === "") { + updateProjectContainer(projects); + } else { + searchProject(searchText); + } + }); + }); + } + + async function fetchAllStudioProjects(studioId) { + let projects = []; + let offset = 0; + const limit = 40; + + while (true) { + const response = await fetch( + `https://api.scratch.mit.edu/studios/${studioId}/projects?limit=${limit}&offset=${offset}` + ); + const data = await response.json(); + + if (data.length === 0) break; + + projects = projects.concat(data); + offset += limit; + } + + return projects; + } + + async function getProjectDetails(projectId) { + const response = await fetch( + `https://api.scratch.mit.edu/projects/${projectId}` + ); + return response.json(); + } + + async function updateProjectContainer(filteredProjects) { + ScratchTools.waitForElements(".studio-projects-grid", async () => { + const container = document.querySelector(".studio-projects-grid"); + if (!container) return; + + container.innerHTML = ""; + + if (filteredProjects.length === 0) return; + + for (const project of filteredProjects) { + const projectDetails = await getProjectDetails(project.id); + + const projectTile = document.createElement("div"); + projectTile.className = "studio-project-tile"; + + const projectLink = document.createElement("a"); + projectLink.href = `/projects/${project.id}/`; + + const projectImage = document.createElement("img"); + projectImage.className = "studio-project-image"; + projectImage.src = projectDetails.image || ""; + + const projectBottom = document.createElement("div"); + projectBottom.className = "studio-project-bottom"; + + const userLink = document.createElement("a"); + userLink.href = `/users/${projectDetails.author.username}/`; + + const userImage = document.createElement("img"); + userImage.className = "studio-project-avatar"; + userImage.src = `https://cdn2.scratch.mit.edu/get_image/user/${projectDetails.author.id}_90x90.png`; + + const projectInfo = document.createElement("div"); + projectInfo.className = "studio-project-info"; + + const projectTitle = document.createElement("a"); + projectTitle.className = "studio-project-title"; + projectTitle.href = `/projects/${project.id}/`; + projectTitle.textContent = projectDetails.title; + + const projectUsername = document.createElement("div"); + projectUsername.className = "studio-project-username"; + projectUsername.textContent = projectDetails.author.username; + + projectLink.appendChild(projectImage); + projectTile.appendChild(projectLink); + + userLink.appendChild(userImage); + projectInfo.appendChild(projectTitle); + projectInfo.appendChild(projectUsername); + projectBottom.appendChild(userLink); + projectBottom.appendChild(projectInfo); + projectTile.appendChild(projectBottom); + + container.appendChild(projectTile); + } + }); + } + + async function searchAndDisplayProjects() { + const studioId = getStudioIdFromUrl(); + if (!studioId) return; + + projects = await fetchAllStudioProjects(studioId); + + projectDetailsMap = {}; + for (const project of projects) { + projectDetailsMap[project.id] = await getProjectDetails(project.id); + } + + injectSearchBar(); + updateProjectContainer(projects); + } + + function getStudioIdFromUrl() { + const url = window.location.href; + const matches = url.match(/studios\/(\d+)/); + return matches ? matches[1] : null; + } + + searchAndDisplayProjects(); +} diff --git a/features/search-in-studio/style.css b/features/search-in-studio/style.css new file mode 100644 index 00000000..f1248fa4 --- /dev/null +++ b/features/search-in-studio/style.css @@ -0,0 +1,10 @@ +.search-container { + display: flex; + align-items: center; + margin-left: 10px; +} +.search-bar { + padding: 7px; + border: 1px solid #ccc; + border-radius: 4px; +} From 2db8f33a8561f51b5f944a335937fdbb0c5f9c2d Mon Sep 17 00:00:00 2001 From: MaterArc <105017592+MaterArc@users.noreply.github.com> Date: Thu, 1 Aug 2024 10:59:27 -0400 Subject: [PATCH 2/6] Update features.json --- features/features.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/features/features.json b/features/features.json index 6e545b93..9b0a6f6e 100644 --- a/features/features.json +++ b/features/features.json @@ -1,4 +1,9 @@ [ + { + "version": 2, + "id": "search-in-studio", + "versionAdded": "v4.0.0" + }, { "version": 2, "id": "video-recorder", From 4bbc20c5c83b20f621300a48bed71eef5021d037 Mon Sep 17 00:00:00 2001 From: MaterArc <105017592+MaterArc@users.noreply.github.com> Date: Thu, 1 Aug 2024 11:06:14 -0400 Subject: [PATCH 3/6] Format + Add Credits --- features/search-in-studio/data.json | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/features/search-in-studio/data.json b/features/search-in-studio/data.json index 1e04f94c..d5bd8169 100644 --- a/features/search-in-studio/data.json +++ b/features/search-in-studio/data.json @@ -2,10 +2,21 @@ "title": "Search in Studio", "description": "Allows users to search for projects in studios.", "credits": [ - { "username": "MaterArc", "url": "https://scratch.mit.edu/users/MaterArc/" } + { + "username": "LoganMSM", + "url": "https://scratch.mit.edu/users/LoganMSM/" + }, + { + "username": "MaterArc", + "url": "https://scratch.mit.edu/users/MaterArc/" + } + ], + "type": [ + "Website" + ], + "tags": [ + "New" ], - "type": ["Website"], - "tags": ["New"], "dynamic": true, "scripts": [ { From f0a492f126c7e87724deb14cb70c1c3905909203 Mon Sep 17 00:00:00 2001 From: MaterArc <105017592+MaterArc@users.noreply.github.com> Date: Thu, 1 Aug 2024 11:21:14 -0400 Subject: [PATCH 4/6] Prevent Search --- features/search-in-studio/script.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/features/search-in-studio/script.js b/features/search-in-studio/script.js index e8db036a..78cfd94f 100644 --- a/features/search-in-studio/script.js +++ b/features/search-in-studio/script.js @@ -65,6 +65,11 @@ export default async function ({ feature, console }) { } function injectSearchBar() { + const url = window.location.href; + + if (!url.match(/^https:\/\/scratch\.mit\.edu\/studios\/\d+$/)) { + return; + } ScratchTools.waitForElements(".studio-header-container", () => { const headerContainer = document.querySelector( ".studio-header-container" From ef5a72997160c596f2f6d603f7e7fa0dae66164a Mon Sep 17 00:00:00 2001 From: MaterArc <105017592+MaterArc@users.noreply.github.com> Date: Thu, 1 Aug 2024 13:02:30 -0400 Subject: [PATCH 5/6] Remove query selector --- features/search-in-studio/script.js | 54 ++++++++++++++--------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/features/search-in-studio/script.js b/features/search-in-studio/script.js index 78cfd94f..b52548b2 100644 --- a/features/search-in-studio/script.js +++ b/features/search-in-studio/script.js @@ -70,33 +70,33 @@ export default async function ({ feature, console }) { if (!url.match(/^https:\/\/scratch\.mit\.edu\/studios\/\d+$/)) { return; } - ScratchTools.waitForElements(".studio-header-container", () => { - const headerContainer = document.querySelector( - ".studio-header-container" - ); - if (!headerContainer) return; - - const searchContainer = document.createElement("div"); - searchContainer.className = "search-container"; - - const searchInput = document.createElement("input"); - searchInput.type = "text"; - searchInput.className = "search-bar"; - searchInput.id = "projectSearch"; - searchInput.placeholder = "Search projects..."; - - searchContainer.appendChild(searchInput); - headerContainer.appendChild(searchContainer); - - searchInput.addEventListener("input", () => { - const searchText = searchInput.value; - if (searchText.trim() === "") { - updateProjectContainer(projects); - } else { - searchProject(searchText); - } - }); - }); + ScratchTools.waitForElements( + ".studio-header-container", + (headerContainer) => { + if (!headerContainer) return; + + const searchContainer = document.createElement("div"); + searchContainer.className = "search-container"; + + const searchInput = document.createElement("input"); + searchInput.type = "text"; + searchInput.className = "search-bar"; + searchInput.id = "projectSearch"; + searchInput.placeholder = "Search projects..."; + + searchContainer.appendChild(searchInput); + headerContainer.appendChild(searchContainer); + + searchInput.addEventListener("input", () => { + const searchText = searchInput.value; + if (searchText.trim() === "") { + updateProjectContainer(projects); + } else { + searchProject(searchText); + } + }); + } + ); } async function fetchAllStudioProjects(studioId) { From 7c5872135c258eb84c00b4ab3d2205d5634d0af9 Mon Sep 17 00:00:00 2001 From: MaterArc <105017592+MaterArc@users.noreply.github.com> Date: Thu, 1 Aug 2024 13:05:14 -0400 Subject: [PATCH 6/6] Update script.js --- features/search-in-studio/script.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/features/search-in-studio/script.js b/features/search-in-studio/script.js index b52548b2..4e42fedc 100644 --- a/features/search-in-studio/script.js +++ b/features/search-in-studio/script.js @@ -127,8 +127,7 @@ export default async function ({ feature, console }) { } async function updateProjectContainer(filteredProjects) { - ScratchTools.waitForElements(".studio-projects-grid", async () => { - const container = document.querySelector(".studio-projects-grid"); + ScratchTools.waitForElements(".studio-projects-grid", async (container) => { if (!container) return; container.innerHTML = "";