- Add a "Browse..." button to the plugin menu
- Remove the "Unload All" button from the plugin menu if there are no imported plugins to unload
- Add a new search filter: random:yes
- Resolution settings now affects the results screen assets
- Pixelate more assets with lowest resolution setting
- Fix loading error message not appearing sometimes
- Remove img.css from img assets, the background selectors have been moved to assets.js
- Separate the search logic from SongSelect to its own js file
- Load all image assets with crossorigin=anonymous, this could allow making assets low resolution or programatically taking screenshots at a later time
- If EditFunction in a plugin tries to edit something that is not a function, it will give a better error message
- Disallow search engine bots from indexing images and adding a translate link, which cannot load the game
This commit is contained in:
KatieFrogs 2022-03-16 09:55:25 +03:00
parent 84b4bf00a8
commit e231ad1fcf
17 changed files with 993 additions and 755 deletions

View File

@ -1,39 +0,0 @@
.pattern-bg{
background-image: url("bg-pattern-1.png");
}
#song-select{
background-image: url("bg_genre_0.png");
}
#title-screen{
background-image: url("title-screen.png");
}
#loading-don{
background-image: url("dancing-don.gif");
}
#touch-full-btn{
background-image: url("touch_fullscreen.png");
}
#touch-pause-btn{
background-image: url("touch_pause.png");
}
.settings-outer{
background-image: url("bg_settings.png");
}
#gamepad-bg,
#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: auto, 3.12em;
background-position: -1.2em;
}
.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");
}

View File

@ -127,3 +127,15 @@
#song-lyrics rt{
line-height: 1;
}
.pixelated #canvas,
.pixelated .donbg>div,
.pixelated #songbg>div,
.pixelated #song-stage,
.pixelated #touch-drum-img,
.pixelated #flowers1-in,
.pixelated #flowers2-in,
.pixelated #mikoshi-in,
.pixelated #tetsuo-in,
.pixelated #hana-in{
image-rendering: pixelated;
}

View File

@ -26,6 +26,8 @@
padding: 1em 1em 0 1em;
z-index: 1;
box-sizing: border-box;
background-size: auto, 3.12em;
background-position: 0%, -2%;
}
#song-search-container.touch-enabled{
@ -96,6 +98,7 @@
box-sizing: border-box;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
border: 0.4em solid;
}
.song-search-result:last-of-type {
@ -133,6 +136,7 @@
content: attr(alt);
position: absolute;
z-index: -1;
-webkit-text-stroke-width: 0.4em;
}
.song-search-result-course {

View File

@ -59,7 +59,9 @@ class RemoteFile{
}
}
blob(){
return this.arrayBuffer().then(response => new Blob([response]))
return loader.ajax(this.url, request => {
request.responseType = "blob"
})
}
}
class LocalFile{
@ -113,7 +115,7 @@ class GdriveFile{
this.url = gpicker.filesUrl + this.id + "?alt=media"
}
arrayBuffer(){
return gpicker.downloadFile(this.id, true)
return gpicker.downloadFile(this.id, "arraybuffer")
}
read(encoding){
if(encoding){
@ -123,7 +125,7 @@ class GdriveFile{
}
}
blob(){
return this.arrayBuffer().then(response => new Blob([response]))
return gpicker.downloadFile(this.id, "blob")
}
}
class CachedFile{
@ -144,6 +146,6 @@ class CachedFile{
return this.arrayBuffer()
}
blob(){
return this.arrayBuffer().then(response => new Blob([response]))
return this.arrayBuffer()
}
}

View File

@ -38,7 +38,8 @@ var assets = {
"customsongs.js",
"abstractfile.js",
"idb.js",
"plugins.js"
"plugins.js",
"search.js"
],
"css": [
"main.css",
@ -50,20 +51,13 @@ var assets = {
"view.css",
"search.css"
],
"assetsCss": [
"img/img.css"
],
"img": [
"title-screen.png",
"notes.png",
"notes_drumroll.png",
"notes_hit.png",
"notes_explosion.png",
"balloon.png",
"taiko.png",
"dancing-don.gif",
"bg-pattern-1.png",
"difficulty.png",
"don_anim_normal_a.png",
"don_anim_normal_b1.png",
"don_anim_normal_b2.png",
@ -81,24 +75,26 @@ var assets = {
"don_anim_clear_b2.png",
"fire_anim.png",
"fireworks_anim.png",
"bg_genre_def.png",
"bg_score_p1.png",
"bg_score_p2.png",
"bg_settings.png",
"bg_pause.png",
"badge_auto.png",
"touch_pause.png",
"touch_fullscreen.png",
"mimizu.png",
"results_flowers.png",
"results_mikoshi.png",
"results_tetsuohana.png",
"results_tetsuohana2.png",
"settings_gamepad.png",
"crown.png",
"miss.png",
"bg_search.png"
"mimizu.png"
],
"cssBackground": {
"#title-screen": "title-screen.png",
"#loading-don": "dancing-don.gif",
".pattern-bg": "bg-pattern-1.png",
".song-search-result-course::before": "difficulty.png",
"#song-select": "bg_genre_def.png",
".settings-outer": "bg_settings.png",
"#touch-pause-btn": "touch_pause.png",
"#touch-full-btn": "touch_fullscreen.png",
"#gamepad-bg, #gamepad-buttons": "settings_gamepad.png",
".song-search-result-crown": "crown.png",
".song-search-tip-error": "miss.png",
"#song-search": "bg_search.png"
},
"audioSfx": [
"se_pause.ogg",
"se_calibration.ogg",

View File

@ -2,7 +2,7 @@ class CustomSongs{
constructor(...args){
this.init(...args)
}
init(touchEnabled, noPage){
init(touchEnabled, noPage, noLoading){
this.loaderDiv = document.createElement("div")
this.loaderDiv.innerHTML = assets.pages["loadsong"]
var loadingText = this.loaderDiv.querySelector("#loading-text")
@ -13,6 +13,7 @@ class CustomSongs{
if(noPage){
this.noPage = true
this.noLoading = noLoading
return
}
@ -262,11 +263,13 @@ class CustomSongs{
var importSongs = new ImportSongs()
return importSongs.load(files).then(this.songsLoaded.bind(this), e => {
this.browse.parentNode.reset()
if(!this.noPage){
this.browse.form.reset()
}
this.locked = false
this.loading(false)
if(e === "nosongs"){
this.showError(strings.customSongs.noSongs)
this.showError(strings.customSongs.noSongs, "nosongs")
}else if(e !== "cancel"){
return Promise.reject(e)
}
@ -308,7 +311,7 @@ class CustomSongs{
this.locked = false
this.loading(false)
if(e === "nosongs"){
this.showError(strings.customSongs.noSongs)
this.showError(strings.customSongs.noSongs, "nosongs")
}else if(e !== "cancel"){
return Promise.reject(e)
}
@ -371,7 +374,7 @@ class CustomSongs{
open("privacy")
}
loading(show){
if(this.noPage){
if(this.noLoading){
return
}
if(show){
@ -387,14 +390,16 @@ class CustomSongs{
assets.customSongs = true
assets.customSelected = this.noPage ? +localStorage.getItem("customSelected") : 0
}
if(!this.noPage){
if(this.noPage){
pageEvents.send("import-songs", length)
}else{
assets.sounds["se_don"].play()
}
this.clean()
setTimeout(() => {
new SongSelect("customSongs", false, this.touchEnabled)
pageEvents.send("import-songs", length)
}, 500)
}
this.clean()
return songs && songs.length
}
keyPressed(pressed, name){
@ -474,10 +479,14 @@ class CustomSongs{
resolve()
}, 500))
}
showError(text){
showError(text, errorName){
this.locked = false
this.loading(false)
if(this.noPage || this.mode === "error"){
if(this.noPage){
var error = new Error(text)
error.name = errorName
throw error
}else if(this.mode === "error"){
return
}
this.mode = "error"

View File

@ -159,7 +159,7 @@ class Gpicker{
}
clientCallback(tokenResponse){
this.tokenResponse = tokenResponse
this.oauthToken = tokenResponse.access_token
this.oauthToken = tokenResponse && tokenResponse.access_token
if(this.oauthToken && this.tokenResolve){
this.tokenResolve()
}
@ -220,12 +220,12 @@ class Gpicker{
.build()
.setVisible(true)
}
downloadFile(id, arrayBuffer, retry){
downloadFile(id, responseType, retry){
var url = this.filesUrl + id + "?alt=media"
return this.queue().then(this.getToken.bind(this)).then(() =>
loader.ajax(url, request => {
if(arrayBuffer){
request.responseType = "arraybuffer"
if(responseType){
request.responseType = responseType
}
request.setRequestHeader("Authorization", "Bearer " + this.oauthToken)
}, true).then(event => {
@ -238,7 +238,7 @@ class Gpicker{
var e = response.error
if(e && e.errors[0].reason === "authError"){
delete this.oauthToken
return this.downloadFile(id, arrayBuffer, true)
return this.downloadFile(id, responseType, true)
}else{
return reject()
}

View File

@ -61,12 +61,6 @@ class Loader{
stylesheet.href = "/src/css/" + name + this.queryString
document.head.appendChild(stylesheet)
})
assets.assetsCss.forEach(name => {
var stylesheet = document.createElement("link")
stylesheet.rel = "stylesheet"
stylesheet.href = gameConfig.assets_baseurl + name + this.queryString
document.head.appendChild(stylesheet)
})
var checkStyles = () => {
if(document.styleSheets.length >= cssCount){
resolve()
@ -87,6 +81,7 @@ class Loader{
assets.img.forEach(name => {
var id = this.getFilename(name)
var image = document.createElement("img")
image.crossOrigin = "anonymous"
var url = gameConfig.assets_baseurl + "img/" + name
this.addPromise(pageEvents.load(image), url)
image.id = name
@ -95,6 +90,37 @@ class Loader{
assets.image[id] = image
})
var css = []
for(let selector in assets.cssBackground){
let name = assets.cssBackground[selector]
var url = gameConfig.assets_baseurl + "img/" + name
this.addPromise(loader.ajax(url, request => {
request.responseType = "blob"
}).then(blob => {
var id = this.getFilename(name)
var image = document.createElement("img")
let blobUrl = URL.createObjectURL(blob)
var promise = pageEvents.load(image).then(() => {
var gradient = ""
if(selector === ".pattern-bg"){
loader.screen.style.backgroundImage = "url(\"" + blobUrl + "\")"
}else if(selector === "#song-search"){
gradient = "linear-gradient(to top, rgba(245, 246, 252, 0.08), #ff5963), "
}
css.push(this.cssRuleset({
[selector]: {
"background-image": gradient + "url(\"" + blobUrl + "\")"
}
}))
})
image.id = name
image.src = blobUrl
this.assetsDiv.appendChild(image)
assets.image[id] = image
return promise
}), url)
}
assets.views.forEach(name => {
var id = this.getFilename(name)
var url = "/src/views/" + name + this.queryString
@ -147,6 +173,10 @@ class Loader{
return
}
var style = document.createElement("style")
style.appendChild(document.createTextNode(css.join("\n")))
document.head.appendChild(style)
this.addPromise(this.ajax("/api/songs").then(songs => {
songs = JSON.parse(songs)
songs.forEach(song => {
@ -179,16 +209,22 @@ class Loader{
.filter(cat => cat.songSkin && cat.songSkin.bg_img)
.forEach(cat => {
let name = cat.songSkin.bg_img
var url = gameConfig.assets_baseurl + "img/" + name
categoryPromises.push(loader.ajax(url, request => {
request.responseType = "blob"
}).then(blob => {
var id = this.getFilename(name)
var image = document.createElement("img")
var url = gameConfig.assets_baseurl + "img/" + name
categoryPromises.push(pageEvents.load(image).catch(response => {
return this.errorMsg(response, url)
}))
let blobUrl = URL.createObjectURL(blob)
var promise = pageEvents.load(image)
image.id = name
image.src = url
image.src = blobUrl
this.assetsDiv.appendChild(image)
assets.image[id] = image
return promise
}).catch(response => {
return this.errorMsg(response, url)
}))
})
this.addPromise(Promise.all(categoryPromises))
@ -356,6 +392,7 @@ class Loader{
this.canvasTest.clean()
this.clean()
this.callback(songId)
this.ready = true
pageEvents.send("ready", readyEvent)
}, () => this.errorMsg())
}, () => this.errorMsg())
@ -407,7 +444,7 @@ class Loader{
if(!lang){
lang = "en"
}
loader.screen.getElementsByClassName("view-content")[0].innerText = allStrings[lang].errorOccured
loader.screen.getElementsByClassName("view-content")[0].innerText = allStrings[lang] && allStrings[lang].errorOccured || allStrings.en.errorOccured
}
var loaderError = loader.screen.getElementsByClassName("loader-error-div")[0]
loaderError.style.display = "flex"
@ -472,6 +509,19 @@ class Loader{
this.screen.innerHTML = assets.pages[name]
this.screen.classList[patternBg ? "add" : "remove"]("pattern-bg")
}
cssRuleset(rulesets){
var css = []
for(var selector in rulesets){
var declarationsObj = rulesets[selector]
var declarations = []
for(var property in declarationsObj){
var value = declarationsObj[property]
declarations.push("\t" + property + ": " + value + ";")
}
css.push(selector + "{\n" + declarations.join("\n") + "\n}")
}
return css.join("\n")
}
ajax(url, customRequest, customResponse){
var request = new XMLHttpRequest()
request.open("GET", url)

View File

@ -103,8 +103,8 @@ class LoadSong{
}
let img = document.createElement("img")
let force = imgLoad[i].type === "song" && this.touchEnabled
if(!songObj.custom && (this.imgScale !== 1 || force)){
img.crossOrigin = "Anonymous"
if(!songObj.custom){
img.crossOrigin = "anonymous"
}
let promise = pageEvents.load(img)
this.addPromise(promise.then(() => {
@ -147,15 +147,30 @@ class LoadSong{
}
if(this.touchEnabled && !assets.image["touch_drum"]){
let img = document.createElement("img")
if(this.imgScale !== 1){
img.crossOrigin = "Anonymous"
}
img.crossOrigin = "anonymous"
var url = gameConfig.assets_baseurl + "img/touch_drum.png"
this.addPromise(pageEvents.load(img).then(() => {
return this.scaleImg(img, "touch_drum", "")
}), url)
img.src = url
}
var resultsImg = [
"results_flowers",
"results_mikoshi",
"results_tetsuohana",
"results_tetsuohana2"
]
resultsImg.forEach(id => {
if(!assets.image[id]){
var img = document.createElement("img")
img.crossOrigin = "anonymous"
var url = gameConfig.assets_baseurl + "img/" + id + ".png"
this.addPromise(pageEvents.load(img).then(() => {
return this.scaleImg(img, id, "")
}), url)
img.src = url
}
})
if(songObj.volume && songObj.volume !== 1){
this.promises.push(new Promise(resolve => setTimeout(resolve, 500)))
}
@ -217,9 +232,7 @@ class LoadSong{
if(!(filenameAb in assets.image)){
let img = document.createElement("img")
let force = filenameAb.startsWith("bg_song_") && this.touchEnabled
if(this.imgScale !== 1 || force){
img.crossOrigin = "Anonymous"
}
img.crossOrigin = "anonymous"
var url = gameConfig.assets_baseurl + "img/" + filenameAb + ".png"
this.addPromise(pageEvents.load(img).then(() => {
return this.scaleImg(img, filenameAb, "", force)
@ -235,7 +248,6 @@ class LoadSong{
if(force && scale > 0.5){
scale = 0.5
}
if(scale !== 1){
var canvas = document.createElement("canvas")
var w = Math.floor(img.width * scale)
var h = Math.floor(img.height * scale)
@ -247,8 +259,10 @@ class LoadSong{
let img2 = document.createElement("img")
pageEvents.load(img2).then(() => {
assets.image[prefix + filename] = img2
loader.assetsDiv.appendChild(img2)
resolve()
}, reject)
img2.id = prefix + filename
img2.src = url
}
if("toBlob" in canvas){
@ -258,10 +272,6 @@ class LoadSong{
}else{
saveScaled(canvas.toDataURL())
}
}else{
assets.image[prefix + filename] = img
resolve()
}
})
}
randInt(min, max){

View File

@ -582,6 +582,12 @@ class EditFunction extends EditValue{
if(this.name){
this.original = this.name[0][this.name[1]]
}
if(typeof this.original !== "function"){
console.error(this.loadCallback)
var error = new Error()
error.stack = "Error editing the function value of " + this.getName() + ": Original value is not a function"
throw error
}
var args = plugins.argsFromFunc(this.original)
try{
var output = this.loadCallback(plugins.strFromFunc(this.original), args)
@ -618,8 +624,13 @@ class EditFunction extends EditValue{
}
class Patch{
edits = []
addedLanguages = []
constructor(...args){
this.init(...args)
}
init(){
this.edits = []
this.addedLanguages = []
}
addEdits(...args){
args.forEach(arg => this.edits.push(arg))
}

637
public/src/js/search.js Normal file
View File

@ -0,0 +1,637 @@
class Search{
constructor(...args){
this.init(...args)
}
init(songSelect){
this.songSelect = songSelect
this.opened = false
this.enabled = true
this.style = document.createElement("style")
var css = []
for(var i in this.songSelect.songSkin){
var skin = this.songSelect.songSkin[i]
if("id" in skin || i === "default"){
var id = "id" in skin ? ("cat" + skin.id) : i
css.push(loader.cssRuleset({
[".song-search-" + id]: {
"background-color": skin.background
},
[".song-search-" + id + "::before"]: {
"border-color": skin.border[0],
"border-bottom-color": skin.border[1],
"border-right-color": skin.border[1]
},
[".song-search-" + id + " .song-search-result-title::before, .song-search-" + id + " .song-search-result-subtitle::before"]: {
"-webkit-text-stroke-color": skin.outline
}
}))
}
}
this.style.appendChild(document.createTextNode(css.join("\n")))
loader.screen.appendChild(this.style)
}
perform(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":
var range = this.parseRange(parts[1])
if(range){
filters[parts[0]] = range
}
break
case "extreme":
var range = this.parseRange(parts[1])
if(range){
filters.oni = this.parseRange(parts[1])
}
break
case "clear":
case "silver":
case "gold":
case "genre":
case "lyrics":
case "creative":
case "played":
case "maker":
case "diverge":
case "random":
filters[parts[0]] = parts[1]
break
}
editedSplit.splice(editedSplit.indexOf(word), 1)
}
}
})
query = editedSplit.join(" ").trim().normalize("NFD").replace(/[\u0300-\u036f]/g, "")
var totalFilters = Object.keys(filters).length
var random = false
for(var i = 0; i < assets.songs.length; i++){
var song = assets.songs[i]
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 "maker":
if(song.maker && song.maker.name.toLowerCase().includes(value.toLowerCase())){
passedFilters++
}
break
case "genre":
var cat = assets.categories.find(cat => cat.id === song.category_id)
var aliases = cat.aliases ? cat.aliases.concat([cat.title]) : [cat.title]
if(aliases.find(alias => alias.toLowerCase() === value.toLowerCase())){
passedFilters++
}
break
case "diverge":
var branch = Object.values(song.courses).find(course => course && course.branch)
if((value === "yes" && branch) || (value === "no" && !branch)){
passedFilters++
}
break
case "random":
if(value === "yes" || value === "no"){
random = value === "yes"
passedFilters++
}
break
}
})
if(passedFilters === totalFilters){
results.push(song)
}
}
var maxResults = totalFilters > 0 && !query ? 100 : 50
if(query){
results = fuzzysort.go(query, results, {
keys: ["titlePrepared", "subtitlePrepared"],
allowTypo: true,
limit: maxResults,
scoreFn: a => {
if(a[0]){
var score0 = a[0].score
a[0].ranges = this.indexesToRanges(a[0].indexes)
if(a[0].indexes.length > 1){
var rangeAmount = a[0].ranges.length
var lastIdx = -3
a[0].ranges.forEach(range => {
if(range[0] - lastIdx <= 2){
rangeAmount--
score0 -= 1000
}
lastIdx = range[1]
})
var index = a[0].target.toLowerCase().indexOf(query)
if(index !== -1){
a[0].ranges = [[index, index + query.length - 1]]
}else if(rangeAmount > a[0].indexes.length / 2){
score0 = -Infinity
a[0].ranges = null
}else if(rangeAmount !== 1){
score0 -= 9000
}
}
}
if(a[1]){
var score1 = a[1].score - 1000
a[1].ranges = this.indexesToRanges(a[1].indexes)
if(a[1].indexes.length > 1){
var rangeAmount = a[1].ranges.length
var lastIdx = -3
a[1].ranges.forEach(range => {
if(range[0] - lastIdx <= 2){
rangeAmount--
score1 -= 1000
}
lastIdx = range[1]
})
var index = a[1].target.indexOf(query)
if(index !== -1){
a[1].ranges = [[index, index + query.length - 1]]
}else if(rangeAmount > a[1].indexes.length / 2){
score1 = -Infinity
a[1].ranges = null
}else if(rangeAmount !== 1){
score1 -= 9000
}
}
}
if(a[0]){
return a[1] ? Math.max(score0, score1) : score0
}else{
return a[1] ? score1 : -Infinity
}
}
})
}else{
results = results.slice(0, maxResults).map(result => {
return {obj: result}
})
}
if(random){
for(var i = results.length - 1; i > 0; i--){
var j = Math.floor(Math.random() * (i + 1))
var temp = results[i]
results[i] = results[j]
results[j] = temp
}
}
return results
}
createResult(result, resultWidth, fontSize){
var song = result.obj
var title = this.songSelect.getLocalTitle(song.title, song.title_lang)
var subtitle = this.songSelect.getLocalTitle(title === song.title ? song.subtitle : "", song.subtitle_lang)
var id = "default"
if(song.category_id){
var cat = assets.categories.find(cat => cat.id === song.category_id)
if(cat && "id" in cat){
id = "cat" + cat.id
}
}
var resultDiv = document.createElement("div")
resultDiv.classList.add("song-search-result", "song-search-" + id)
resultDiv.dataset.songId = 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")
resultInfoTitle.appendChild(this.highlightResult(title, result[0]))
resultInfoTitle.setAttribute("alt", title)
resultInfoDiv.appendChild(resultInfoTitle)
if(subtitle){
resultInfoDiv.appendChild(document.createElement("br"))
var resultInfoSubtitle = document.createElement("span")
resultInfoSubtitle.classList.add("song-search-result-subtitle")
resultInfoSubtitle.appendChild(this.highlightResult(subtitle, result[1]))
resultInfoSubtitle.setAttribute("alt", 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 + "\u2605"
courseDiv.appendChild(courseCrown)
courseDiv.appendChild(courseStars)
} else {
courseDiv.classList.add("song-search-result-hidden")
}
resultDiv.appendChild(courseDiv)
})
this.songSelect.ctx.font = (1.2 * fontSize) + "px " + strings.font
var titleWidth = this.songSelect.ctx.measureText(title).width
var titleRatio = resultWidth / titleWidth
if(titleRatio < 1){
resultInfoTitle.style.transform = "scale(" + titleRatio + ", 1)"
}
if(subtitle){
this.songSelect.ctx.font = (0.8 * 1.2 * fontSize) + "px " + strings.font
var subtitleWidth = this.songSelect.ctx.measureText(subtitle).width
var subtitleRatio = resultWidth / subtitleWidth
if(subtitleRatio < 1){
resultInfoSubtitle.style.transform = "scale(" + subtitleRatio + ", 1)"
}
}
return resultDiv
}
highlightResult(text, result){
var fragment = document.createDocumentFragment()
var ranges = (result ? result.ranges : null) || []
var lastIdx = 0
ranges.forEach(range => {
if(lastIdx !== range[0]){
fragment.appendChild(document.createTextNode(text.slice(lastIdx, range[0])))
}
var span = document.createElement("span")
span.classList.add("highlighted-text")
span.innerText = text.slice(range[0], range[1] + 1)
fragment.appendChild(span)
lastIdx = range[1] + 1
})
if(text.length !== lastIdx){
fragment.appendChild(document.createTextNode(text.slice(lastIdx)))
}
return fragment
}
setActive(idx){
this.songSelect.playSound("se_ka")
var active = this.div.querySelector(":scope .song-search-result-active")
if(active){
active.classList.remove("song-search-result-active")
}
if(idx === null){
this.active = null
return
}
var el = this.results[idx]
this.input.blur()
el.classList.add("song-search-result-active")
this.scrollTo(el)
this.active = idx
}
display(fromButton=false){
if(!this.enabled){
return
}
if(this.opened){
return this.remove(true)
}
this.opened = true
this.results = []
this.div = document.createElement("div")
this.div.innerHTML = assets.pages["search"]
this.container = this.div.querySelector(":scope #song-search-container")
if(this.touchEnabled){
this.container.classList.add("touch-enabled")
}
pageEvents.add(this.container, ["mousedown", "touchstart"], this.onClick.bind(this))
this.input = this.div.querySelector(":scope #song-search-input")
this.input.setAttribute("placeholder", strings.search.searchInput)
pageEvents.add(this.input, ["input"], this.onInput.bind(this))
this.songSelect.playSound("se_pause")
loader.screen.appendChild(this.div)
this.setTip()
cancelTouch = false
noResizeRoot = true
if(this.songSelect.songs[this.songSelect.selectedSong].courses){
snd.previewGain.setVolumeMul(0.5)
}else if(this.songSelect.bgmEnabled){
snd.musicGain.setVolumeMul(0.5)
}
setTimeout(() => {
this.input.focus()
this.input.setSelectionRange(0, this.input.value.length)
}, 10)
var lastQuery = localStorage.getItem("lastSearchQuery")
if(lastQuery){
this.input.value = lastQuery
this.input.dispatchEvent(new Event("input", {
value: lastQuery
}))
}
}
remove(byUser=false){
if(this.opened){
this.opened = false
if(byUser){
this.songSelect.playSound("se_cancel")
}
pageEvents.remove(this.div.querySelector(":scope #song-search-container"),
["mousedown", "touchstart"])
pageEvents.remove(this.input, ["input"])
this.div.remove()
delete this.results
delete this.div
delete this.input
delete this.tip
delete this.active
cancelTouch = true
noResizeRoot = false
if(this.songSelect.songs[this.songSelect.selectedSong].courses){
snd.previewGain.setVolumeMul(1)
}else if(this.songSelect.bgmEnabled){
snd.musicGain.setVolumeMul(1)
}
}
}
setTip(tip, error=false){
if(this.tip){
this.tip.remove()
delete this.tip
}
if(!tip){
tip = strings.search.tip + " " + strings.search.tips[Math.floor(Math.random() * strings.search.tips.length)]
}
var resultsDiv = this.div.querySelector(":scope #song-search-results")
resultsDiv.innerHTML = ""
this.results = []
this.tip = document.createElement("div")
this.tip.id = "song-search-tip"
this.tip.innerText = tip
this.div.querySelector(":scope #song-search").appendChild(this.tip)
if(error){
this.tip.classList.add("song-search-tip-error")
}
}
proceed(songId){
var song = this.songSelect.songs.find(song => song.id === songId)
this.remove()
this.songSelect.playBgm(false)
var songIndex = this.songSelect.songs.findIndex(song => song.id === songId)
this.songSelect.setSelectedSong(songIndex)
this.songSelect.toSelectDifficulty()
}
scrollTo(element){
var parentNode = element.parentNode
var selected = element.getBoundingClientRect()
var parent = parentNode.getBoundingClientRect()
var scrollY = parentNode.scrollTop
var selectedPosTop = selected.top - selected.height / 2
if(Math.floor(selectedPosTop) < Math.floor(parent.top)){
parentNode.scrollTop += selectedPosTop - parent.top
}else{
var selectedPosBottom = selected.top + selected.height * 1.5 - parent.top
if(Math.floor(selectedPosBottom) > Math.floor(parent.height)){
parentNode.scrollTop += selectedPosBottom - parent.height
}
}
}
parseRange(string){
var range = string.split("-")
if(range.length == 1){
var min = parseInt(range[0]) || 0
return min > 0 ? {min: min, max: min} : false
} else if(range.length == 2){
var min = parseInt(range[0]) || 0
var max = parseInt(range[1]) || 0
return min > 0 && max > 0 ? {min: min, max: max} : false
}
}
indexesToRanges(indexes){
var ranges = []
var range
indexes.forEach(idx => {
if(range && range[1] === idx - 1){
range[1] = idx
}else{
range = [idx, idx]
ranges.push(range)
}
})
return ranges
}
onInput(){
var text = this.input.value
localStorage.setItem("lastSearchQuery", text)
text = text.toLowerCase()
if(text.length === 0){
this.setTip()
return
}
var new_results = this.perform(text)
if(new_results.length === 0){
this.setTip(strings.search.noResults, true)
return
}else if(this.tip){
this.tip.remove()
delete this.tip
}
var resultsDiv = this.div.querySelector(":scope #song-search-results")
resultsDiv.innerHTML = ""
this.results = []
var fontSize = parseFloat(getComputedStyle(this.div.querySelector(":scope #song-search")).fontSize.slice(0, -2))
var resultsWidth = parseFloat(getComputedStyle(resultsDiv).width.slice(0, -2))
var vmin = Math.min(innerWidth, lastHeight) / 100
var courseWidth = Math.min(3 * fontSize * 1.2, 7 * vmin)
var resultWidth = resultsWidth - 1.8 * fontSize - 0.8 * fontSize - (courseWidth + 0.4 * fontSize * 1.2) * 5 - 0.6 * fontSize
this.songSelect.ctx.save()
var fragment = document.createDocumentFragment()
new_results.forEach(result => {
var result = this.createResult(result, resultWidth, fontSize)
fragment.appendChild(result)
this.results.push(result)
})
resultsDiv.appendChild(fragment)
this.songSelect.ctx.restore()
}
onClick(e){
if((e.target.id === "song-search-container" || e.target.id === "song-search-close") && e.which === 1){
this.remove(true)
}else if(e.which === 1){
var songEl = e.target.closest(".song-search-result")
if(songEl){
var songId = parseInt(songEl.dataset.songId)
this.proceed(songId)
}
}
}
keyPress(pressed, name, event, repeat){
if(name === "back" || (event && event.keyCode && event.keyCode === 70 && ctrl)) {
this.remove(true)
if(event){
event.preventDefault()
}
}else if(name === "down" && this.results.length){
if(this.input == document.activeElement && this.results){
this.setActive(0)
}else if(this.active === this.results.length - 1){
this.setActive(null)
this.input.focus()
}else if(Number.isInteger(this.active)){
this.setActive(this.active + 1)
}else{
this.setActive(0)
}
}else if(name === "up" && this.results.length){
if(this.input == document.activeElement && this.results){
this.setActive(this.results.length - 1)
}else if(this.active === 0){
this.setActive(null)
this.input.focus()
setTimeout(() => {
this.input.setSelectionRange(this.input.value.length, this.input.value.length)
}, 0)
}else if(Number.isInteger(this.active)){
this.setActive(this.active - 1)
}else{
this.setActive(this.results.length - 1)
}
}else if(name === "confirm"){
if(Number.isInteger(this.active)){
this.proceed(parseInt(this.results[this.active].dataset.songId))
}else{
this.onInput()
}
}
}
redraw(){
if(this.opened && this.container){
var vmin = Math.min(innerWidth, lastHeight) / 100
if(this.vmin !== vmin){
this.container.style.setProperty("--vmin", vmin + "px")
this.vmin = vmin
}
}else{
this.vmin = null
}
}
clean(){
loader.screen.removeChild(this.style)
fuzzysort.cleanup()
delete this.container
delete this.style
delete this.songSelect
}
}

View File

@ -217,15 +217,18 @@ class SettingsView{
constructor(...args){
this.init(...args)
}
init(touchEnabled, tutorial, songId, toSetting, settingsItems){
init(touchEnabled, tutorial, songId, toSetting, settingsItems, noSoundStart){
this.touchEnabled = touchEnabled
this.tutorial = tutorial
this.songId = songId
this.customSettings = !!settingsItems
this.settingsItems = settingsItems || settings.items
this.locked = false
loader.changePage("settings", tutorial)
if(!noSoundStart){
assets.sounds["bgm_settings"].playLoop(0.1, false, 0, 1.392, 26.992)
}
this.defaultButton = document.getElementById("settings-default")
this.viewOuter = this.getElement("view-outer")
if(touchEnabled){
@ -377,16 +380,48 @@ class SettingsView{
this.items.push(outputObject)
this.getValue(i, valueDiv)
}
var selectBack = this.items.length === 0
if(this.customSettings){
var form = document.createElement("form")
this.browse = document.createElement("input")
this.browse.id = "browse"
this.browse.type = "file"
this.browse.multiple = true
this.browse.accept = ".taikoweb.js"
pageEvents.add(this.browse, "change", this.browseChange.bind(this))
form.appendChild(this.browse)
loader.screen.appendChild(form)
this.browseButton = document.createElement("div")
this.browseButton.classList.add("taibtn", "stroke-sub")
this.defaultButton.parentNode.insertBefore(this.browseButton, this.defaultButton)
this.items.push({
id: "browse",
settingBox: this.browseButton
})
this.addTouch(this.browseButton, () => {
this.playSound("se_don")
this.browse.click()
})
}
this.showDefault = !this.customSettings || plugins.allPlugins.filter(obj => obj.plugin.imported).length
if(this.showDefault){
this.items.push({
id: "default",
settingBox: this.defaultButton
})
this.addTouch(this.defaultButton, this.defaultSettings.bind(this))
}else{
this.defaultButton.parentNode.removeChild(this.defaultButton)
}
this.items.push({
id: "back",
settingBox: this.endButton
})
this.addTouch(this.endButton, this.onEnd.bind(this))
if(selectBack){
this.selected = this.items.length - 1
this.endButton.classList.add("selected")
}
if(!this.customSettings){
this.gamepadSettings = document.getElementById("settings-gamepad")
@ -606,6 +641,9 @@ class SettingsView{
valueDiv.innerText = value
}
setValue(name){
if(this.locked){
return
}
var promise
var current = this.settingsItems[name]
if(current.getItem){
@ -674,6 +712,9 @@ class SettingsView{
})
}
keyPressed(pressed, name, event, repeat){
if(this.locked){
return
}
if(pressed){
if(!this.pressedKeys[name]){
this.pressedKeys[name] = this.getMS() + 300
@ -693,6 +734,11 @@ class SettingsView{
this.onEnd()
}else if(selected.id === "default"){
this.defaultSettings()
}else if(selected.id === "browse"){
if(event){
this.playSound("se_don")
this.browse.click()
}
}else{
this.setValue(selected.id)
}
@ -700,7 +746,7 @@ class SettingsView{
selected.settingBox.classList.remove("selected")
do{
this.selected = this.mod(this.items.length, this.selected + ((name === "right" || name === "down") ? 1 : -1))
}while(this.items[this.selected].id === "default" && name !== "left")
}while((this.items[this.selected].id === "default" || this.items[this.selected].id === "browse") && name !== "left")
selected = this.items[this.selected]
selected.settingBox.classList.add("selected")
this.scrollTo(selected.settingBox)
@ -1027,7 +1073,9 @@ class SettingsView{
defaultSettings(){
if(this.customSettings){
plugins.unloadImported()
return this.onEnd()
this.clean(true)
this.playSound("se_don")
return setTimeout(() => this.restart(), 500)
}
if(this.mode === "keyboard"){
this.keyboardBack(this.items[this.selected])
@ -1046,6 +1094,31 @@ class SettingsView{
this.drumSounds = settings.getItem("latency").drumSounds
this.playSound("se_don")
}
browseChange(event){
this.locked = true
var files = []
for(var i = 0; i < event.target.files.length; i++){
files.push(new LocalFile(event.target.files[i]))
}
var customSongs = new CustomSongs(this.touchEnabled, true)
customSongs.importLocal(files).then(() => {
this.clean(true)
return this.restart()
}).catch(e => {
if(e){
var message = e.message
if(e.name === "nosongs"){
message = strings.plugins.noPlugins
}
if(message){
alert(message)
}
}
this.locked = false
this.browse.form.reset()
return Promise.resolve()
})
}
onEnd(){
if(this.mode === "number"){
this.numberBack(this.items[this.selected])
@ -1063,6 +1136,12 @@ class SettingsView{
}
}, 500)
}
restart(){
if(this.mode === "number"){
this.numberBack(this.items[this.selected])
}
return new SettingsView(this.touchEnabled, this.tutorial, this.songId, undefined, this.customSettings ? plugins.getSettings() : undefined, true)
}
getLocalTitle(title, titleLang){
if(titleLang){
for(var id in titleLang){
@ -1109,15 +1188,19 @@ class SettingsView{
setStrings(){
this.setAltText(this.viewTitle, this.customSettings ? strings.plugins.title : strings.gameSettings)
this.setAltText(this.endButton, strings.settings.ok)
if(!this.customSettings){
if(this.customSettings){
this.setAltText(this.browseButton, strings.plugins.browse)
}else{
this.setAltText(this.gamepadTitle, strings.settings.gamepadLayout.name)
this.setAltText(this.gamepadEndButton, strings.settings.ok)
this.setAltText(this.latencyTitle, strings.settings.latency.name)
this.setAltText(this.latencyDefaultButton, strings.settings.default)
this.setAltText(this.latencyEndButton, strings.settings.ok)
}
if(this.showDefault){
this.setAltText(this.defaultButton, this.customSettings ? strings.plugins.unloadAll : strings.settings.default)
}
}
setAltText(element, text){
element.innerText = text
element.setAttribute("alt", text)
@ -1154,11 +1237,13 @@ class SettingsView{
getMS(){
return Date.now()
}
clean(){
clean(noSoundStop){
this.redrawRunning = false
this.keyboard.clean()
this.gamepad.clean()
if(!noSoundStop){
assets.sounds["bgm_settings"].stop()
}
pageEvents.remove(window, ["mouseup", "touchstart", "touchmove", "touchend", "blur"], this.windowSymbol)
if(this.customSettings){
pageEvents.remove(window, "language-change", this.windowSymbol)
@ -1176,7 +1261,12 @@ class SettingsView{
if(this.defaultButton){
delete this.defaultButton
}
if(!this.customSettings){
if(this.customSettings){
pageEvents.remove(this.browse, "change")
this.removeTouch(this.browseButton)
delete this.browse
delete this.browseButton
}else{
this.removeTouch(this.gamepadSettings)
this.removeTouch(this.gamepadEndButton)
this.removeTouch(this.gamepadBox)
@ -1204,8 +1294,12 @@ class SettingsView{
delete this.latencyEndButton
if(this.resolution !== settings.getItem("resolution")){
for(var i in assets.image){
if(i === "touch_drum" || i.startsWith("bg_song_") || i.startsWith("bg_stage_") || i.startsWith("bg_don_")){
URL.revokeObjectURL(assets.image[i].src)
if(i === "touch_drum" || i.startsWith("bg_song_") || i.startsWith("bg_stage_") || i.startsWith("bg_don_") || i.startsWith("results_")){
var img = assets.image[i]
URL.revokeObjectURL(img.src)
if(img.parentNode){
img.parentNode.removeChild(img)
}
delete assets.image[i]
}
}

View File

@ -92,22 +92,6 @@ class SongSelect{
}
this.songSkin["default"].sort = songSkinLength + 1
this.searchStyle = document.createElement("style")
var searchCss = []
Object.keys(this.songSkin).forEach(key => {
var skin = this.songSkin[key]
if("id" in skin || key === "default"){
var id = "id" in skin ? ("cat" + skin.id) : key
searchCss.push('.song-search-' + id + ' { background-color: ' + skin.background + ' }')
searchCss.push('.song-search-' + id + '::before { border: 0.4em solid ' + skin.border[0] + ' ; border-bottom-color: ' + skin.border[1] + ' ; border-right-color: ' + skin.border[1] + ' }')
searchCss.push('.song-search-' + id + ' .song-search-result-title::before { -webkit-text-stroke: 0.4em ' + skin.outline + ' }')
searchCss.push('.song-search-' + id + ' .song-search-result-subtitle::before { -webkit-text-stroke: 0.4em ' + skin.outline + ' }')
}
})
this.searchStyle.appendChild(document.createTextNode(searchCss.join("\n")))
loader.screen.appendChild(this.searchStyle)
this.font = strings.font
this.songs = []
@ -194,14 +178,12 @@ class SongSelect{
category: strings.random
})
}
if(plugins.hasSettings()){
this.songs.push({
title: strings.plugins.title,
skin: this.songSkin.plugins,
action: "plugins",
category: strings.random
})
}
this.songs.push({
title: strings.back,
@ -246,6 +228,8 @@ class SongSelect{
this.currentSongCache = new CanvasCache(noSmoothing)
this.nameplateCache = new CanvasCache(noSmoothing)
this.search = new Search(this)
this.difficulty = [strings.easy, strings.normal, strings.hard, strings.oni]
this.difficultyId = ["easy", "normal", "hard", "oni", "ura"]
@ -257,7 +241,6 @@ class SongSelect{
this.selectedSong = 0
this.selectedDiff = 0
this.lastCurrentSong = {}
this.searchEnabled = true
this.lastRandom = false
assets.sounds["bgm_songsel"].playLoop(0.1, false, 0, 1.442, 3.506)
@ -434,44 +417,14 @@ class SongSelect{
this.state.showWarning = false
this.showWarning = false
}
}else if (this.search){
if(name === "back" || (event && event.keyCode && event.keyCode === 70 && ctrl)) {
this.removeSearch(true)
if(event){ event.preventDefault() }
}else if(name === "down" && this.search.results.length){
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(Number.isInteger(this.search.active)){
this.searchSetActive(this.search.active+1)
}else{
this.searchSetActive(0)
}
}else if(name === "up" && this.search.results.length){
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()
setTimeout(() => {
this.search.input.setSelectionRange(this.search.input.value.length, this.search.input.value.length)
}, 0)
}else if(Number.isInteger(this.search.active)){
this.searchSetActive(this.search.active-1)
}else{
this.searchSetActive(this.search.results.length-1)
}
}else if(name === "confirm"){
if(Number.isInteger(this.search.active)){
this.searchProceed(parseInt(this.search.results[this.search.active].dataset.songId))
}
}
}else if(this.search.opened){
this.search.keyPress(pressed, name, event, repeat)
}else if(this.state.screen === "song"){
if(event && event.keyCode && event.keyCode === 70 && ctrl){
this.displaySearch()
if(event){ event.preventDefault() }
this.search.display()
if(event){
event.preventDefault()
}
}else if(name === "confirm"){
this.toSelectDifficulty()
}else if(name === "back"){
@ -504,8 +457,10 @@ class SongSelect{
}
}else if(this.state.screen === "difficulty"){
if(event && event.keyCode && event.keyCode === 70 && ctrl){
this.displaySearch()
if(event){ event.preventDefault() }
this.search.display()
if(event){
event.preventDefault()
}
}else if(name === "confirm"){
if(this.selectedDiff === 0){
this.toSongSelect()
@ -528,8 +483,10 @@ class SongSelect{
}
}else if(this.state.screen === "title" || this.state.screen === "titleFadeIn"){
if(event && event.keyCode && event.keyCode === 70 && ctrl){
this.displaySearch()
if(event){ event.preventDefault() }
this.search.display()
if(event){
event.preventDefault()
}
}
}
}
@ -627,7 +584,7 @@ class SongSelect{
if(408 < mouse.x && mouse.x < 872 && 470 < mouse.y && mouse.y < 550){
moveTo = "showWarning"
}
}else if(this.state.screen === "song" && !this.search){
}else if(this.state.screen === "song" && !this.search.opened){
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){
@ -792,7 +749,7 @@ class SongSelect{
}
}
}else if(this.state.locked === 0 || fromP2){
this.removeSearch()
this.search.remove()
if(currentSong.courses){
if(currentSong.unloaded){
return
@ -815,7 +772,6 @@ class SongSelect{
}
pageEvents.send("song-select-difficulty", currentSong)
}else if(currentSong.action === "back"){
this.clean()
this.toTitleScreen()
}else if(currentSong.action === "random"){
do{
@ -827,7 +783,7 @@ class SongSelect{
this.toSelectDifficulty(false, playVoice=false)
pageEvents.send("song-select-random")
}else if(currentSong.action === "search"){
this.displaySearch(true)
this.search.display(true)
}else if(currentSong.action === "tutorial"){
this.toTutorial()
}else if(currentSong.action === "about"){
@ -1116,8 +1072,8 @@ class SongSelect{
this.selectableText = ""
if(this.search && this.searchContainer){
this.searchInput()
if(this.search.opened && this.search.container){
this.search.onInput()
}
}else if(!document.hasFocus() && !p2.session){
if(this.state.focused){
@ -1146,15 +1102,7 @@ class SongSelect{
var screen = this.state.screen
var selectedWidth = this.songAsset.width
if(this.search && this.searchContainer){
var vmin = Math.min(innerWidth, lastHeight) / 100
if(this.vmin !== vmin){
this.searchContainer.style.setProperty("--vmin", vmin + "px")
this.vmin = vmin
}
}else{
this.vmin = null
}
this.search.redraw()
if(this.wheelScrolls !== 0 && !this.state.locked && ms >= this.wheelTimer + 20) {
if(p2.session){
@ -2727,520 +2675,6 @@ class SongSelect{
return addedSong
}
createSearchResult(result, resultWidth, fontSize){
var song = result.obj
var title = this.getLocalTitle(song.title, song.title_lang)
var subtitle = this.getLocalTitle(title === song.title ? song.subtitle : "", song.subtitle_lang)
var id = "default"
if(song.category_id){
var cat = assets.categories.find(cat => cat.id === song.category_id)
if(cat && "id" in cat){
id = "cat" + cat.id
}
}
var resultDiv = document.createElement("div")
resultDiv.classList.add("song-search-result", "song-search-" + id)
resultDiv.dataset.songId = 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")
resultInfoTitle.appendChild(this.highlightResult(title, result[0]))
resultInfoTitle.setAttribute("alt", title)
resultInfoDiv.appendChild(resultInfoTitle)
if(subtitle){
resultInfoDiv.appendChild(document.createElement("br"))
var resultInfoSubtitle = document.createElement("span")
resultInfoSubtitle.classList.add("song-search-result-subtitle")
resultInfoSubtitle.appendChild(this.highlightResult(subtitle, result[1]))
resultInfoSubtitle.setAttribute("alt", 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)
})
this.ctx.font = (1.2 * fontSize) + "px " + strings.font
var titleWidth = this.ctx.measureText(title).width
var titleRatio = resultWidth / titleWidth
if(titleRatio < 1){
resultInfoTitle.style.transform = "scale(" + titleRatio + ", 1)"
}
if(subtitle){
this.ctx.font = (0.8 * 1.2 * fontSize) + "px " + strings.font
var subtitleWidth = this.ctx.measureText(subtitle).width
var subtitleRatio = resultWidth / subtitleWidth
if(subtitleRatio < 1){
resultInfoSubtitle.style.transform = "scale(" + subtitleRatio + ", 1)"
}
}
return resultDiv
}
highlightResult(text, result){
var fragment = document.createDocumentFragment()
var ranges = (result ? result.ranges : null) || []
var lastIdx = 0
ranges.forEach(range => {
if(lastIdx !== range[0]){
fragment.appendChild(document.createTextNode(text.slice(lastIdx, range[0])))
}
var span = document.createElement("span")
span.classList.add("highlighted-text")
span.innerText = text.slice(range[0], range[1] + 1)
fragment.appendChild(span)
lastIdx = range[1] + 1
})
if(text.length !== lastIdx){
fragment.appendChild(document.createTextNode(text.slice(lastIdx)))
}
return fragment
}
searchSetActive(idx){
this.playSound("se_ka")
var active = this.search.div.querySelector(":scope .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")
this.scrollTo(el)
this.search.active = idx
}
scrollTo(element){
var parentNode = element.parentNode
var selected = element.getBoundingClientRect()
var parent = parentNode.getBoundingClientRect()
var scrollY = parentNode.scrollTop
var selectedPosTop = selected.top - selected.height / 2
if(Math.floor(selectedPosTop) < Math.floor(parent.top)){
parentNode.scrollTop += selectedPosTop - parent.top
}else{
var selectedPosBottom = selected.top + selected.height * 1.5 - parent.top
if(Math.floor(selectedPosBottom) > Math.floor(parent.height)){
parentNode.scrollTop += selectedPosBottom - parent.height
}
}
}
displaySearch(fromButton=false){
if(!this.searchEnabled){
return
}
if(this.search){
return this.removeSearch(true)
}
this.search = {results: []}
this.search.div = document.createElement("div")
this.search.div.innerHTML = assets.pages["search"]
this.searchContainer = this.search.div.querySelector(":scope #song-search-container")
if(this.touchEnabled){
this.searchContainer.classList.add("touch-enabled")
}
pageEvents.add(this.searchContainer, ["mousedown", "touchstart"], this.searchClick.bind(this))
this.search.input = this.search.div.querySelector(":scope #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()
cancelTouch = false
noResizeRoot = true
if(this.songs[this.selectedSong].courses){
snd.previewGain.setVolumeMul(0.5)
}else if(this.bgmEnabled){
snd.musicGain.setVolumeMul(0.5)
}
setTimeout(() => {
this.search.input.focus()
this.search.input.setSelectionRange(0, this.search.input.value.length)
}, 10)
var lastQuery = localStorage.getItem("lastSearchQuery")
if(lastQuery){
this.search.input.value = lastQuery
this.search.input.dispatchEvent(new Event('input', {value: lastQuery}))
}
}
removeSearch(byUser=false){
if(this.search){
if(byUser){
this.playSound("se_cancel")
}
pageEvents.remove(this.search.div.querySelector(":scope #song-search-container"),
["mousedown", "touchstart"])
pageEvents.remove(this.search.input, ["input"])
this.search.div.remove()
delete this.search
cancelTouch = true
noResizeRoot = false
if(this.songs[this.selectedSong].courses){
snd.previewGain.setVolumeMul(1)
}else if(this.bgmEnabled){
snd.musicGain.setVolumeMul(1)
}
}
}
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(":scope #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(":scope #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){
var min = parseInt(range[0]) || 0
return min > 0 ? {min: min, max: min} : false
} else if(range.length == 2){
var min = parseInt(range[0]) || 0
var max = parseInt(range[1]) || 0
return min > 0 && max > 0 ? {min: min, max: max} : false
}
}
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":
var range = this.parseRange(parts[1])
if (range) { filters[parts[0]] = range }
break
case "extreme":
var range = this.parseRange(parts[1])
if (range) { filters.oni = this.parseRange(parts[1]) }
break
case "clear":
case "silver":
case "gold":
case "genre":
case "lyrics":
case "creative":
case "played":
case "maker":
case "diverge":
filters[parts[0]] = parts[1]
break
}
editedSplit.splice(editedSplit.indexOf(word), 1)
}
}
})
query = editedSplit.join(" ").trim().normalize("NFD").replace(/[\u0300-\u036f]/g, "")
var totalFilters = Object.keys(filters).length
for(var i = 0; i < assets.songs.length; i++){
var song = assets.songs[i]
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 "maker":
if(song.maker && song.maker.name.toLowerCase().includes(value.toLowerCase())){
passedFilters++
}
break
case "genre":
var cat = assets.categories.find(cat => cat.id === song.category_id)
var aliases = cat.aliases ? cat.aliases.concat([cat.title]) : [cat.title]
if(aliases.find(alias => alias.toLowerCase() === value.toLowerCase())){
passedFilters++
}
break
case "diverge":
var branch = Object.values(song.courses).find(course => course && course.branch)
if((value === "yes" && branch) || (value === "no" && !branch)){
passedFilters++
}
break
}
})
if(passedFilters === totalFilters){
results.push(song)
}
}
var maxResults = totalFilters > 0 && !query ? 100 : 50
if(query){
results = fuzzysort.go(query, results, {
keys: ["titlePrepared", "subtitlePrepared"],
allowTypo: true,
limit: maxResults,
scoreFn: a => {
if(a[0]){
var score0 = a[0].score
a[0].ranges = this.indexesToRanges(a[0].indexes)
if(a[0].indexes.length > 1){
var rangeAmount = a[0].ranges.length
var lastIdx = -3
a[0].ranges.forEach(range => {
if(range[0] - lastIdx <= 2){
rangeAmount--
score0 -= 1000
}
lastIdx = range[1]
})
var index = a[0].target.toLowerCase().indexOf(query)
if(index !== -1){
a[0].ranges = [[index, index + query.length - 1]]
}else if(rangeAmount > a[0].indexes.length / 2){
score0 = -Infinity
a[0].ranges = null
}else if(rangeAmount !== 1){
score0 -= 9000
}
}
}
if(a[1]){
var score1 = a[1].score - 1000
a[1].ranges = this.indexesToRanges(a[1].indexes)
if(a[1].indexes.length > 1){
var rangeAmount = a[1].ranges.length
var lastIdx = -3
a[1].ranges.forEach(range => {
if(range[0] - lastIdx <= 2){
rangeAmount--
score1 -= 1000
}
lastIdx = range[1]
})
var index = a[1].target.indexOf(query)
if(index !== -1){
a[1].ranges = [[index, index + query.length - 1]]
}else if(rangeAmount > a[1].indexes.length / 2){
score1 = -Infinity
a[1].ranges = null
}else if(rangeAmount !== 1){
score1 -= 9000
}
}
}
if(a[0]){
return a[1] ? Math.max(score0, score1) : score0
}else{
return a[1] ? score1 : -Infinity
}
}
})
}else{
results = results.map(result => {
return {obj: result}
}).slice(0, maxResults)
}
return results
}
indexesToRanges(indexes){
var ranges = []
var range
indexes.forEach(idx => {
if(range && range[1] === idx - 1){
range[1] = idx
}else{
range = [idx, idx]
ranges.push(range)
}
})
return ranges
}
searchInput(){
var text = this.search.input.value
localStorage.setItem("lastSearchQuery", text)
text = text.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(":scope #song-search-results")
resultsDiv.innerHTML = ""
this.search.results = []
var fontSize = parseFloat(getComputedStyle(this.search.div.querySelector(":scope #song-search")).fontSize.slice(0, -2))
var resultsWidth = parseFloat(getComputedStyle(resultsDiv).width.slice(0, -2))
var vmin = Math.min(innerWidth, lastHeight) / 100
var courseWidth = Math.min(3 * fontSize * 1.2, 7 * vmin)
var resultWidth = resultsWidth - 1.8 * fontSize - 0.8 * fontSize - (courseWidth + 0.4 * fontSize * 1.2) * 5 - 0.6 * fontSize
this.ctx.save()
var fragment = document.createDocumentFragment()
new_results.forEach(result => {
var result = this.createSearchResult(result, resultWidth, fontSize)
fragment.appendChild(result)
this.search.results.push(result)
})
resultsDiv.appendChild(fragment)
this.ctx.restore()
}
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.songId)
this.searchProceed(songId)
}
}
}
searchProceed(songId){
var song = this.songs.find(song => song.id === songId)
this.removeSearch()
this.playBgm(false)
var songIndex = this.songs.findIndex(song => song.id === songId)
this.setSelectedSong(songIndex)
this.toSelectDifficulty()
}
onusers(response){
var p2InSong = false
this.songs.forEach(song => {
@ -3268,17 +2702,17 @@ class SongSelect{
if(this.state.screen !== "difficulty"){
this.toSelectDifficulty({player: response.value.player})
}
this.searchEnabled = false
this.search.enabled = false
p2InSong = true
this.removeSearch()
this.search.remove()
}
}
}
})
}
if(!this.searchEnabled && !p2InSong){
this.searchEnabled = true
if(!this.search.enabled && !p2InSong){
this.search.enabled = true
}
}
onsongsel(response){
@ -3404,6 +2838,7 @@ class SongSelect{
this.sessionCache.clean()
this.currentSongCache.clean()
this.nameplateCache.clean()
this.search.clean()
assets.sounds["bgm_songsel"].stop()
if(!this.bgmEnabled){
snd.musicGain.fadeIn()
@ -3425,13 +2860,8 @@ class SongSelect{
pageEvents.remove(this.touchFullBtn, "click")
delete this.touchFullBtn
}
if(this.searchStyle){
loader.screen.removeChild(this.searchStyle)
}
delete this.selectable
delete this.ctx
delete this.canvas
delete this.searchContainer
delete this.searchStyle
}
}

View File

@ -1331,6 +1331,17 @@ var translations = {
version: {
ja: "Ver. %s",
en: "Version %s"
},
browse: {
ja: "参照する…",
en: "Browse...",
cn: "浏览…",
tw: "開啟檔案…",
ko: "찾아보기…"
},
noPlugins: {
ja: null,
en: "No .taikoweb.js plugin files have been found in the provided file list."
}
},
search: {

View File

@ -8,6 +8,7 @@ class Titlescreen{
if(!songId){
loader.changePage("titlescreen", false)
loader.screen.style.backgroundImage = ""
this.titleScreen = document.getElementById("title-screen")
this.proceed = document.getElementById("title-proceed")
@ -75,8 +76,9 @@ class Titlescreen{
}
pageEvents.remove(p2, "message")
if(this.customFolder && !fromP2 && !assets.customSongs){
var customSongs = new CustomSongs(this.touched, true)
var customSongs = new CustomSongs(this.touched, true, true)
var soundPlayed = false
var noError = true
var promises = []
var allFiles = []
this.customFolder.forEach(file => {
@ -95,6 +97,13 @@ class Titlescreen{
setTimeout(() => {
new SongSelect(false, false, this.touched, this.songId)
}, 500)
noError = false
}).then(() => {
if(noError){
setTimeout(() => {
new SongSelect("customSongs", false, this.touchEnabled)
}, 500)
}
})
}else{
setTimeout(() => {

View File

@ -12,8 +12,9 @@
if(noSmoothing){
this.ctx.imageSmoothingEnabled = false
}
if(resolution === "lowest"){
this.canvas.style.imageRendering = "pixelated"
this.multiplayer = this.controller.multiplayer
if(this.multiplayer !== 2 && resolution === "lowest"){
document.getElementById("game").classList.add("pixelated")
}
this.gameDiv = document.getElementById("game")
@ -97,7 +98,6 @@
this.branchCache = new CanvasCache(noSmoothing)
this.nameplateCache = new CanvasCache(noSmoothing)
this.multiplayer = this.controller.multiplayer
if(this.multiplayer === 2){
this.player = p2.player === 2 ? 1 : 2
}else{

View File

@ -7,6 +7,8 @@
<meta name="viewport" content="width=device-width, user-scalable=no">
<meta name="description" content="パソコンとスマホのブラウザ向けの太鼓の達人シミュレータ 🥁 Taiko no Tatsujin rhythm game simulator for desktop and mobile browsers">
<meta name="keywords" content="taiko no tatsujin, taiko, don chan, online, rhythm, browser, html5, game, for browsers, pc, arcade, emulator, free, download, website, 太鼓の達人, 太鼓ウェブ, 太鼓之達人, 太鼓達人, 太鼓网页, 网页版, 太鼓網頁, 網頁版, 태고의 달인, 태고 웹">
<meta name="robots" content="notranslate">
<meta name="robots" content="noimageindex">
<meta name="color-scheme" content="only light">
<link rel="stylesheet" href="/src/css/loader.css?{{version.commit_short}}">
@ -22,7 +24,7 @@
<div id="screen" class="pattern-bg"></div>
<div data-nosnippet id="version">
{% if version.version and version.commit_short and version.commit %}
<a href="{{version.url}}commit/{{version.commit}}" target="_blank" id="version-link" class="stroke-sub" alt="taiko-web ver.{{version.version}} ({{version.commit_short}})">taiko-web ver.{{version.version}} ({{version.commit_short}})</a>
<a href="{{version.url}}commit/{{version.commit}}" target="_blank" rel="noopener" id="version-link" class="stroke-sub" alt="taiko-web ver.{{version.version}} ({{version.commit_short}})">taiko-web ver.{{version.version}} ({{version.commit_short}})</a>
{% else %}
<a href="{{version.url}}" target="_blank" rel="noopener" id="version-link" class="stroke-sub" alt="taiko-web (unknown version)">taiko-web (unknown version)</a>
{% endif %}