implement song search

This commit is contained in:
Bui 2022-02-25 18:16:11 +00:00
parent 73a95abf62
commit 6c8b635c2a
9 changed files with 707 additions and 6 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 598 B

BIN
public/assets/img/crown.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 584 B

View File

@ -23,3 +23,17 @@
#gamepad-buttons{
background-image: url("settings_gamepad.png");
}
#song-search{
background: linear-gradient(to top, rgb(245 246 252 / 8%), #ff5963), url("bg_search.png");
background-size: 3.12vmax;
background-position: -1.2vmax;
}
.song-search-result-course::before{
background-image: url("difficulty.png");
}
.song-search-result-crown{
background-image: url("crown.png");
}
.song-search-tip-error{
background-image: url("miss.png");
}

BIN
public/assets/img/miss.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 714 B

248
public/src/css/search.css Normal file
View File

@ -0,0 +1,248 @@
#song-search-container {
position: fixed;
width: 100%;
height: 100%;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0,0,0,0.5);
z-index: 2;
}
#song-search {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
margin: auto;
display: flex;
flex-direction: column;
width: 60vmax;
height: 80vmin;
border-radius: 0.8vmax;
border: 0.35vmax solid #8C0C42;
color: #fff;
padding: 1vmax 1vmax 0 1vmax;
z-index: 1;
}
#song-search-input {
width: 100%;
font-size: 2vmax;
padding: 0.8vmax 1.2vmax;
border-radius: 0.3vmax;
border: 0.25vmax black solid;
font-family: TnT;
box-sizing: border-box;
-webkit-box-sizing:border-box;
-moz-box-sizing: border-box;
outline: none;
}
#song-search-input:focus {
border: 0.25vmax #fff923 solid;
}
#song-search-results {
margin-top: 0.5vmax;
overflow-y: scroll;
-ms-overflow-style: none;
scrollbar-width: none;
scroll-behavior: smooth;
}
#song-search-results::-webkit-scrollbar {
display: none;
}
.song-search-result {
display: flex;
height: 3.2vmax;
margin: 0.2vmax;
padding: 0.7vmax;
flex-direction: row;
text-align: center;
align-items: center;
justify-content: center;
border: 0.3vmax black solid;
position: relative;
}
.song-search-result::before {
display: block;
position: absolute;
content: '';
height: 100%;
width: 100%;
box-sizing: border-box;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
}
.song-search-result:last-of-type {
margin-bottom: 1vmax;
}
.song-search-result-info {
flex: 10;
font-size: 1.2vmax;
margin: 0.3vmax;
text-align: left;
z-index: 0;
position: relative;
white-space: nowrap;
padding-left: 0.2vmax;
overflow-x: clip;
}
.song-search-result-subtitle {
display: block;
font-size: 0.8vmax;
margin-top: 0.5vmax;
}
.song-search-result-title::before,
.song-search-result-subtitle::before {
content: attr(alt);
position: absolute;
z-index: -1;
}
.song-search-result-course {
flex: 1;
width: 100%;
height: 100%;
margin: 0.2vmax;
font-size: 1.2vmax;
border-radius: 0.3vmax;
position: relative;
z-index: 1;
}
.song-search-result-hidden {
visibility: hidden;
}
.song-search-result:hover {
border-color: #fff923;
cursor: pointer;
}
.song-search-result-active {
border-color: #fff923;
}
.song-search-result-course::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0.5;
z-index: -1;
background-size: 6vmax;
border-radius: 0.3vmax;
}
.song-search-result-stars {
bottom: 0;
background: rgb(0 0 0 / 47%);
position: absolute;
width: 100%;
padding: 0.1vmax 0;
border-radius: 0 0 0.3vmax 0.3vmax;
}
.song-search-result-easy {
background-color: #D13215;
}
.song-search-result-easy::before {
background-position-x: -1.1vmax;
background-position-y: -0.7vmax;
}
.song-search-result-normal {
background-color: #799C22;
}
.song-search-result-normal::before {
background-position-x: -1.2vmax;
background-position-y: -6.4vmax;
}
.song-search-result-hard {
background-color: #31799B;
}
.song-search-result-hard::before {
background-position-x: -1.1vmax;
background-position-y: -11.4vmax;
}
.song-search-result-oni {
background-color: #AF2C7F;
}
.song-search-result-oni::before {
background-position-x: -1.2vmax;
background-position-y: -16.5vmax;
}
.song-search-result-ura {
background-color: #604AD5;
}
.song-search-result-ura::before {
background-position-x: -1.2vmax;
background-position-y: -21.6vmax;
}
.song-search-result-crown {
background-size: 1.9vmax;
background-position-x: 0.82vmax;
background-repeat: repeat-y;
width: 100%;
position: absolute;
height: 2vmax;
}
.song-search-result-gold {
background-position-y: 5.15vmax;
}
.song-search-result-silver {
background-position-y: 7.6vmax;
}
.song-search-result-noclear {
background-position-y: 0.15vmax;
}
#song-search-tip {
font-size: 1vmax;
margin-top: 1vmax;
text-align: center;
background-repeat: no-repeat;
background-position: top;
background-size: 10vmax;
background-color: #00000087;
border-radius: 0.5vmax;
padding: 1vmax;
}
#song-search-close {
position: absolute;
right: -0.77vmax;
top: -1.26vmax;
font-size: 2vmax;
font-family: TnT;
cursor: pointer;
}
.song-search-tip-error {
height: 8vmax;
}

View File

@ -46,7 +46,8 @@ var assets = {
"game.css",
"debug.css",
"songbg.css",
"view.css"
"view.css",
"search.css"
],
"assetsCss": [
"img/img.css"
@ -92,7 +93,10 @@ var assets = {
"results_mikoshi.png",
"results_tetsuohana.png",
"results_tetsuohana2.png",
"settings_gamepad.png"
"settings_gamepad.png",
"crown.png",
"miss.png",
"bg_search.png"
],
"audioSfx": [
"se_pause.ogg",
@ -149,7 +153,8 @@ var assets = {
"settings.html",
"account.html",
"login.html",
"customsongs.html"
"customsongs.html",
"search.html"
],
"songs": [],

View File

@ -34,6 +34,12 @@ class SongSelect{
border: ["#ffdfff", "#b068b2"],
outline: "#b221bb"
},
"search": {
sort: 0,
background: "#FF5266",
border: ["#FF9FB7", "#BE1432"],
outline: "#A50B15"
},
"tutorial": {
sort: 0,
background: "#29e8aa",
@ -85,6 +91,16 @@ class SongSelect{
}
this.songSkin["default"].sort = songSkinLength + 1
Object.keys(this.songSkin).forEach(key => {
var skin = this.songSkin[key]
var stripped = key.replace(/\W/g, '')
document.styleSheets[0].insertRule('.song-search-' + stripped + ' { background-color: ' + skin.background + ' }')
document.styleSheets[0].insertRule('.song-search-' + stripped + '::before { border: 0.4vmax solid ' + skin.border[0] + ' ; border-bottom-color: ' + skin.border[1] + ' ; border-right-color: ' + skin.border[1] + ' }')
document.styleSheets[0].insertRule('.song-search-' + stripped + ' .song-search-result-title::before { -webkit-text-stroke: 0.4em ' + skin.outline + ' }')
document.styleSheets[0].insertRule('.song-search-' + stripped + ' .song-search-result-subtitle::before { -webkit-text-stroke: 0.4em ' + skin.outline + ' }')
})
this.font = strings.font
this.songs = []
@ -117,6 +133,12 @@ class SongSelect{
category: strings.random,
canJump: true
})
this.songs.push({
title: strings.search.search,
skin: this.songSkin.search,
action: "search",
category: strings.random,
})
}
if(touchEnabled){
if(fromTutorial === "tutorial"){
@ -359,7 +381,12 @@ class SongSelect{
pageEvents.send("song-select-difficulty", this.songs[this.selectedSong])
}
}
setAltText(element, text){
element.innerText = text
element.setAttribute("alt", text)
}
keyPress(pressed, name, event, repeat){
if(pressed){
if(!this.pressedKeys[name]){
@ -380,8 +407,41 @@ class SongSelect{
this.state.showWarning = false
this.showWarning = false
}
}else if (this.search){
if(name === "back" || (event && event.code === "KeyF" && ctrl)) {
this.removeSearch(true)
}else if(name === "down"){
if(this.search.input == document.activeElement && this.search.results){
this.searchSetActive(0)
}else if(this.search.active === this.search.results.length-1){
this.searchSetActive(null)
this.search.input.focus()
}else if(this.search.active !== null){
this.searchSetActive(this.search.active+1)
}else{
this.searchSetActive(0)
}
}else if(name === "up"){
if(this.search.input == document.activeElement && this.search.results){
this.searchSetActive(this.search.results.length-1)
}else if(this.search.active === 0){
this.searchSetActive(null)
this.search.input.focus()
//this.search.input.setSelectionRange(this.search.input.value.length, this.search.input.value.length)
}else if(this.search.active !== null){
this.searchSetActive(this.search.active-1)
}else{
this.searchSetActive(this.search.results.length-1)
}
}else if(name === "confirm"){
if(this.search.active !== null){
this.searchProceed(parseInt(this.search.results[this.search.active].dataset.song_id))
}
}
}else if(this.state.screen === "song"){
if(name === "confirm"){
if(event && event.code === "KeyF" && ctrl){
this.displaySearch()
}else if(name === "confirm"){
this.toSelectDifficulty()
}else if(name === "back"){
this.toTitleScreen()
@ -528,7 +588,7 @@ class SongSelect{
if(408 < mouse.x && mouse.x < 872 && 470 < mouse.y && mouse.y < 550){
moveTo = "showWarning"
}
}else if(this.state.screen === "song"){
}else if(this.state.screen === "song" && !this.search){
if(20 < mouse.y && mouse.y < 90 && 410 < mouse.x && mouse.x < 880 && (mouse.x < 540 || mouse.x > 750)){
moveTo = mouse.x < 640 ? "categoryPrev" : "categoryNext"
}else if(!p2.session && 60 < mouse.x && mouse.x < 332 && 640 < mouse.y && mouse.y < 706 && gameConfig.accounts){
@ -692,6 +752,7 @@ class SongSelect{
}
}
}else if(this.state.locked === 0 || fromP2){
this.removeSearch()
if(currentSong.courses){
if(currentSong.unloaded){
return
@ -725,6 +786,8 @@ class SongSelect{
this.moveToSong(moveBy, fromP2)
}, 200)
pageEvents.send("song-select-random")
}else if(currentSong.action === "search"){
this.displaySearch(true)
}else if(currentSong.action === "tutorial"){
this.toTutorial()
}else if(currentSong.action === "about"){
@ -2607,6 +2670,333 @@ class SongSelect{
}
return addedSong
}
createSearchResult(song){
var title = this.getLocalTitle(song.title, song.title_lang)
var subtitle = this.getLocalTitle(title === song.title ? song.subtitle : "", song.subtitle_lang)
var strippedCat = "default"
if(song.category_id){
var cat = assets.categories.find(cat => cat.id === song.category_id)
strippedCat = cat.title.replace(/\W/g, '')
}
var resultDiv = document.createElement("div")
resultDiv.classList.add("song-search-result", "song-search-" + strippedCat)
resultDiv.dataset.song_id = song.id
var resultInfoDiv = document.createElement("div")
resultInfoDiv.classList.add("song-search-result-info")
var resultInfoTitle = document.createElement("span")
resultInfoTitle.classList.add("song-search-result-title")
this.setAltText(resultInfoTitle, title)
resultInfoDiv.appendChild(resultInfoTitle)
if(subtitle){
var resultInfoSubtitle = document.createElement("span")
resultInfoSubtitle.classList.add("song-search-result-subtitle")
this.setAltText(resultInfoSubtitle, subtitle)
resultInfoDiv.appendChild(resultInfoSubtitle)
}
resultDiv.appendChild(resultInfoDiv)
var courses = ["easy", "normal", "hard", "oni", "ura"]
courses.forEach(course => {
var courseDiv = document.createElement("div")
courseDiv.classList.add("song-search-result-course", "song-search-result-" + course)
if (song.courses[course]) {
var crown = "noclear"
if (scoreStorage.scores[song.hash]) {
if (scoreStorage.scores[song.hash][course]) {
crown = scoreStorage.scores[song.hash][course].crown || "noclear"
}
}
var courseCrown = document.createElement("div")
courseCrown.classList.add("song-search-result-crown", "song-search-result-" + crown)
var courseStars = document.createElement("div")
courseStars.classList.add("song-search-result-stars")
courseStars.innerText = song.courses[course].stars + '★'
courseDiv.appendChild(courseCrown)
courseDiv.appendChild(courseStars)
} else {
courseDiv.classList.add("song-search-result-hidden")
}
resultDiv.appendChild(courseDiv)
})
return resultDiv
}
searchSetActive(idx){
this.playSound("se_ka")
var active = this.search.div.querySelector(".song-search-result-active")
if(active){
active.classList.remove("song-search-result-active")
}
if(idx === null){
this.search.active = null
return
}
var el = this.search.results[idx]
this.search.input.blur()
el.classList.add("song-search-result-active")
el.scrollIntoView();
this.search.active = idx
}
displaySearch(fromButton=false){
if(this.search){
return this.removeSearch(true)
}
this.search = {results: []}
this.search.div = document.createElement("div")
this.search.div.innerHTML = assets.pages["search"]
pageEvents.add(this.search.div.querySelector("#song-search-container"),
["mousedown", "touchstart"], this.searchClick.bind(this))
this.search.input = this.search.div.querySelector("#song-search-input")
this.search.input.setAttribute("placeholder", strings.search.searchInput)
pageEvents.add(this.search.input, ["input"], this.searchInput.bind(this))
this.playSound("se_pause")
loader.screen.appendChild(this.search.div)
this.setSearchTip()
this.search.input.focus()
}
removeSearch(byUser=false){
if(this.search){
if(byUser){
this.playSound("se_cancel")
}
pageEvents.remove(this.search.div.querySelector("#song-search-container"),
["mousedown", "touchstart"])
pageEvents.remove(this.search.input, ["input"])
this.search.div.remove()
delete this.search
}
}
setSearchTip(tip, error=false){
if(this.search.tip){
this.search.tip.remove()
delete this.search.tip
}
if(!tip){
tip = strings.search.tip + " " + strings.search.tips[Math.floor(Math.random() * strings.search.tips.length)]
}
var resultsDiv = this.search.div.querySelector("#song-search-results")
resultsDiv.innerHTML = ""
this.search.results = []
this.search.tip = document.createElement("div")
this.search.tip.setAttribute("id", "song-search-tip")
this.search.tip.innerText = tip
this.search.div.querySelector("#song-search").appendChild(this.search.tip)
if(error){
this.search.tip.classList.add("song-search-tip-error")
}
}
parseRange(string){
var range = string.split("-")
if(range.length == 1){
return {min: parseInt(range[0]), max: parseInt(range[0])}
} else if(range.length == 2){
return {min: parseInt(range[0]), max: parseInt(range[1])}
}
}
performSearch(query){
var results = []
var filters = {}
var querySplit = query.split(" ")
var editedSplit = query.split(" ")
querySplit.forEach(word => {
if(word.length > 0){
var parts = word.toLowerCase().split(":")
if(parts.length > 1){
switch(parts[0]){
case "easy":
case "normal":
case "hard":
case "oni":
case "ura":
filters[parts[0]] = this.parseRange(parts[1])
break
case "extreme":
filters.oni = this.parseRange(parts[1])
break
case "clear":
case "silver":
case "gold":
case "genre":
case "lyrics":
case "creative":
case "played":
filters[parts[0]] = parts[1]
break
}
editedSplit.splice(editedSplit.indexOf(word), 1)
}
}
})
query = editedSplit.join(" ")
var songs = assets.songs
// TODO: fix this so it doesn't suck
songs.sort((a, b) => {
var aScore = 0
var bScore = 0
var aTitle = a.title.replace(query, "").length
var bTitle = b.title.replace(query, "").length
var aLength = aTitle - query.length
var bLength = bTitle - query.length
aScore += aLength - bLength
bScore += bLength - aLength
return aScore - bScore
})
assets.songs.forEach(song => {
var passedFilters = 0
Object.keys(filters).forEach(filter => {
var value = filters[filter]
switch(filter){
case "easy":
case "normal":
case "hard":
case "oni":
case "ura":
if(song.courses[filter] && song.courses[filter].stars >= value.min && song.courses[filter].stars <= value.max){
passedFilters++
}
break
case "clear":
case "silver":
case "gold":
if(value === "any"){
var score = scoreStorage.scores[song.hash]
scoreStorage.difficulty.forEach(difficulty => {
if(score && score[difficulty] && score[difficulty].crown && (filter === "clear" || score[difficulty].crown === filter)){
passedFilters++
}
})
} else {
var score = scoreStorage.scores[song.hash]
if(score && score[value] && score[value].crown && (filter === "clear" || score[value].crown === filter)){
passedFilters++
}
}
break
case "played":
var score = scoreStorage.scores[song.hash]
if((value === "yes" && score) || (value === "no" && !score)){
passedFilters++
}
break
case "lyrics":
if((value === "yes" && song.lyrics) || (value === "no" && !song.lyrics)){
passedFilters++
}
break
case "creative":
if((value === "yes" && song.maker) || (value === "no" && !song.maker)){
passedFilters++
}
break
case "genre":
var cat = assets.categories.find(cat => cat.id === song.category_id)
var aliases = cat.aliases ? cat.aliases.concat([song.category]) : [song.category]
if(aliases.find(alias => alias.toLowerCase() === value.toLowerCase())){
passedFilters++
}
break
}
})
if(passedFilters === Object.keys(filters).length){
var title = this.getLocalTitle(song.title, song.title_lang)
var subtitle = this.getLocalTitle(title === song.title ? song.subtitle : "", song.subtitle_lang)
if(title.toLowerCase().includes(query) || (subtitle && subtitle.toLowerCase().includes(query))){
results.push(song)
}
}
})
results = results.slice(0, 100)
return results
}
searchInput(e){
var text = e.target.value.toLowerCase()
if(text.length === 0){
this.setSearchTip()
return
}
var new_results = this.performSearch(text)
if (new_results.length === 0) {
this.setSearchTip(strings.search.noResults, true)
return
} else if (this.search.tip) {
this.search.tip.remove()
delete this.search.tip
}
var resultsDiv = this.search.div.querySelector("#song-search-results")
resultsDiv.innerHTML = ""
this.search.results = []
new_results.forEach(song => {
var result = this.createSearchResult(song)
resultsDiv.appendChild(result)
this.search.results.push(result)
})
}
searchClick(e){
if((e.target.id === "song-search-container" || e.target.id === "song-search-close") && e.which === 1){
this.removeSearch(true)
}else if(e.which === 1){
var songEl = e.target.closest(".song-search-result")
if(songEl){
var songId = parseInt(songEl.dataset.song_id)
this.searchProceed(songId)
}
}
}
searchProceed(songId){
var song = this.songs.find(song => song.id === songId)
this.removeSearch()
this.drawBackground(song.originalCategory)
var songIndex = this.songs.findIndex(song => song.id === songId)
this.selectedSong = songIndex
this.toSelectDifficulty()
}
onusers(response){
this.songs.forEach(song => {

View File

@ -1332,6 +1332,41 @@ var translations = {
ja: "Ver. %s",
en: "Version %s"
}
},
search: {
search: {
ja: "曲を検索",
en: "Search Songs"
},
searchInput: {
ja: "曲を検索...",
en: "Search for songs..."
},
noResults: {
ja: "結果は見つかりませんでした。",
en: "No results found."
},
tip: {
ja: "ヒント:",
en: "Tip:"
},
tips: {
ja: [
"CTRL+Fで検索窓を開く!"
],
en: [
"Open the search window by pressing CTRL+F!",
"Mix and match as many search filters as you want!",
"Only the 100 most relevant search results are shown.",
"Filter by genre by using the \"genre:\" keyword! (e.g. \"genre:variety\", \"genre:namco\")",
"Use filters like \"oni:10\" to search for songs with a particular difficulty!",
"Difficulty filters support ranges, too! Try \"ura:1-5\"!",
"Want to see your full combos? Try \"gold:any\", \"gold:oni\", etc.!",
"Only want to see creative songs? Use the \"creative:yes\" filter!",
"Find songs with lyrics enabled with the \"lyrics:yes\" filter!",
"Feel like trying something new? Use the \"played:no\" filter to only see songs you haven't played yet!"
]
}
}
}
var allStrings = {}

View File

@ -0,0 +1,9 @@
<div id="song-search-container">
<div id="song-search">
<div id="song-search-close" class="stroke-sub" alt="x">x</div>
<div id="song-search-bar">
<input type="search" id="song-search-input" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false">
</div>
<div id="song-search-results"></div>
</div>
</div>