Merge branch 'master' into normalize-search

This commit is contained in:
Bui 2022-03-22 00:36:33 +00:00
commit 7d3cff5cbe
22 changed files with 1130 additions and 821 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{ #song-lyrics rt{
line-height: 1; 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; padding: 1em 1em 0 1em;
z-index: 1; z-index: 1;
box-sizing: border-box; box-sizing: border-box;
background-size: auto, 3.12em;
background-position: 0%, -2%;
} }
#song-search-container.touch-enabled{ #song-search-container.touch-enabled{
@ -96,6 +98,7 @@
box-sizing: border-box; box-sizing: border-box;
-moz-box-sizing: border-box; -moz-box-sizing: border-box;
-webkit-box-sizing: border-box; -webkit-box-sizing: border-box;
border: 0.4em solid;
} }
.song-search-result:last-of-type { .song-search-result:last-of-type {
@ -133,6 +136,7 @@
content: attr(alt); content: attr(alt);
position: absolute; position: absolute;
z-index: -1; z-index: -1;
-webkit-text-stroke-width: 0.4em;
} }
.song-search-result-course { .song-search-result-course {

View File

@ -452,3 +452,21 @@ kbd{
#dropzone.dragover{ #dropzone.dragover{
opacity: 1; opacity: 1;
} }
.plugin-browse-button{
position: relative;
overflow: hidden;
}
#plugin-browse{
position: absolute;
font-size: inherit;
top: -0.1em;
left: -0.1em;
right: -0.1em;
bottom: -0.1em;
border-radius: 0.5em;
opacity: 0;
cursor: pointer;
}
#plugin-browse::-webkit-file-upload-button{
cursor: pointer;
}

View File

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

View File

@ -38,7 +38,8 @@ var assets = {
"customsongs.js", "customsongs.js",
"abstractfile.js", "abstractfile.js",
"idb.js", "idb.js",
"plugins.js" "plugins.js",
"search.js"
], ],
"css": [ "css": [
"main.css", "main.css",
@ -50,20 +51,13 @@ var assets = {
"view.css", "view.css",
"search.css" "search.css"
], ],
"assetsCss": [
"img/img.css"
],
"img": [ "img": [
"title-screen.png",
"notes.png", "notes.png",
"notes_drumroll.png", "notes_drumroll.png",
"notes_hit.png", "notes_hit.png",
"notes_explosion.png", "notes_explosion.png",
"balloon.png", "balloon.png",
"taiko.png", "taiko.png",
"dancing-don.gif",
"bg-pattern-1.png",
"difficulty.png",
"don_anim_normal_a.png", "don_anim_normal_a.png",
"don_anim_normal_b1.png", "don_anim_normal_b1.png",
"don_anim_normal_b2.png", "don_anim_normal_b2.png",
@ -81,24 +75,26 @@ var assets = {
"don_anim_clear_b2.png", "don_anim_clear_b2.png",
"fire_anim.png", "fire_anim.png",
"fireworks_anim.png", "fireworks_anim.png",
"bg_genre_def.png",
"bg_score_p1.png", "bg_score_p1.png",
"bg_score_p2.png", "bg_score_p2.png",
"bg_settings.png",
"bg_pause.png", "bg_pause.png",
"badge_auto.png", "badge_auto.png",
"touch_pause.png", "mimizu.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"
], ],
"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": [ "audioSfx": [
"se_pause.ogg", "se_pause.ogg",
"se_calibration.ogg", "se_calibration.ogg",

View File

@ -2,7 +2,7 @@ class CustomSongs{
constructor(...args){ constructor(...args){
this.init(...args) this.init(...args)
} }
init(touchEnabled, noPage){ init(touchEnabled, noPage, noLoading){
this.loaderDiv = document.createElement("div") this.loaderDiv = document.createElement("div")
this.loaderDiv.innerHTML = assets.pages["loadsong"] this.loaderDiv.innerHTML = assets.pages["loadsong"]
var loadingText = this.loaderDiv.querySelector("#loading-text") var loadingText = this.loaderDiv.querySelector("#loading-text")
@ -13,6 +13,7 @@ class CustomSongs{
if(noPage){ if(noPage){
this.noPage = true this.noPage = true
this.noLoading = noLoading
return return
} }
@ -262,11 +263,13 @@ class CustomSongs{
var importSongs = new ImportSongs() var importSongs = new ImportSongs()
return importSongs.load(files).then(this.songsLoaded.bind(this), e => { 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.locked = false
this.loading(false) this.loading(false)
if(e === "nosongs"){ if(e === "nosongs"){
this.showError(strings.customSongs.noSongs) this.showError(strings.customSongs.noSongs, "nosongs")
}else if(e !== "cancel"){ }else if(e !== "cancel"){
return Promise.reject(e) return Promise.reject(e)
} }
@ -308,13 +311,15 @@ class CustomSongs{
this.locked = false this.locked = false
this.loading(false) this.loading(false)
if(e === "nosongs"){ if(e === "nosongs"){
this.showError(strings.customSongs.noSongs) this.showError(strings.customSongs.noSongs, "nosongs")
}else if(e !== "cancel"){ }else if(e !== "cancel"){
return Promise.reject(e) return Promise.reject(e)
} }
}).finally(() => { }).finally(() => {
var addRemove = !gpicker || !gpicker.oauthToken ? "add" : "remove" if(this.linkGdriveAccount){
this.linkGdriveAccount.classList[addRemove]("hiddenbtn") var addRemove = !gpicker || !gpicker.oauthToken ? "add" : "remove"
this.linkGdriveAccount.classList[addRemove]("hiddenbtn")
}
}) })
} }
gdriveAccount(event){ gdriveAccount(event){
@ -369,7 +374,7 @@ class CustomSongs{
open("privacy") open("privacy")
} }
loading(show){ loading(show){
if(this.noPage){ if(this.noLoading){
return return
} }
if(show){ if(show){
@ -385,14 +390,16 @@ class CustomSongs{
assets.customSongs = true assets.customSongs = true
assets.customSelected = this.noPage ? +localStorage.getItem("customSelected") : 0 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() assets.sounds["se_don"].play()
setTimeout(() => {
new SongSelect("customSongs", false, this.touchEnabled)
pageEvents.send("import-songs", length)
}, 500)
} }
this.clean() this.clean()
setTimeout(() => {
new SongSelect("customSongs", false, this.touchEnabled)
pageEvents.send("import-songs", length)
}, 500)
return songs && songs.length return songs && songs.length
} }
keyPressed(pressed, name){ keyPressed(pressed, name){
@ -472,10 +479,14 @@ class CustomSongs{
resolve() resolve()
}, 500)) }, 500))
} }
showError(text){ showError(text, errorName){
this.locked = false this.locked = false
this.loading(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 return
} }
this.mode = "error" this.mode = "error"
@ -514,6 +525,9 @@ class CustomSongs{
pageEvents.remove(document, ["dragover", "dragleave", "drop"]) pageEvents.remove(document, ["dragover", "dragleave", "drop"])
delete this.dropzone delete this.dropzone
} }
if(gpicker){
gpicker.tokenResolve = null
}
delete this.browse delete this.browse
delete this.linkLocalFolder delete this.linkLocalFolder
delete this.linkGdriveFolder delete this.linkGdriveFolder

View File

@ -11,6 +11,7 @@ class Gpicker{
this.filesUrl = "https://www.googleapis.com/drive/v3/files/" this.filesUrl = "https://www.googleapis.com/drive/v3/files/"
this.resolveQueue = [] this.resolveQueue = []
this.queueActive = false this.queueActive = false
this.clientCallbackBind = this.clientCallback.bind(this)
} }
browse(lockedCallback, errorCallback){ browse(lockedCallback, errorCallback){
return this.loadApi() return this.loadApi()
@ -123,9 +124,12 @@ class Gpicker{
if(window.gapi && gapi.client && gapi.client.drive){ if(window.gapi && gapi.client && gapi.client.drive){
return Promise.resolve() return Promise.resolve()
} }
return loader.loadScript("https://apis.google.com/js/api.js") var promises = [
.then(() => new Promise((resolve, reject) => loader.loadScript("https://apis.google.com/js/api.js"),
gapi.load("auth2:picker:client", { loader.loadScript("https://accounts.google.com/gsi/client")
]
return Promise.all(promises).then(() => new Promise((resolve, reject) =>
gapi.load("picker:client", {
callback: resolve, callback: resolve,
onerror: reject onerror: reject
}) })
@ -134,58 +138,53 @@ class Gpicker{
gapi.client.load("drive", "v3").then(resolve, reject) gapi.client.load("drive", "v3").then(resolve, reject)
)) ))
} }
getAuth(errorCallback=()=>{}){ getClient(errorCallback=()=>{}, force){
if(!this.auth){ var obj = {
return new Promise((resolve, reject) => { client_id: this.oauthClientId,
gapi.auth2.init({ scope: this.scope,
clientId: this.oauthClientId, callback: this.clientCallbackBind
fetch_basic_profile: false, }
scope: this.scope if(force){
}).then(() => { if(!this.clientForce){
this.auth = gapi.auth2.getAuthInstance() obj.select_account = true
resolve(this.auth) this.clientForce = google.accounts.oauth2.initTokenClient(obj)
}, e => { }
if(e.details){ return this.clientForce
var errorStr = strings.gpicker.authError.replace("%s", e.details)
if(/cookie/i.test(e.details)){
errorStr += "\n\n" + strings.gpicker.cookieError
}
errorCallback(errorStr)
}
reject(e)
})
})
}else{ }else{
return Promise.resolve(this.auth) if(!this.client){
this.client = google.accounts.oauth2.initTokenClient(obj)
}
return this.client
}
}
clientCallback(tokenResponse){
this.tokenResponse = tokenResponse
this.oauthToken = tokenResponse && tokenResponse.access_token
if(this.oauthToken && this.tokenResolve){
this.tokenResolve()
} }
} }
getToken(lockedCallback=()=>{}, errorCallback=()=>{}, force){ getToken(lockedCallback=()=>{}, errorCallback=()=>{}, force){
if(this.oauthToken && !force){ if(this.oauthToken && !force){
return Promise.resolve() return Promise.resolve()
} }
return this.getAuth(errorCallback).then(auth => { var client = this.getClient(errorCallback, force)
var user = force || auth.currentUser.get() var promise = new Promise(resolve => {
if(force || !this.checkScope(user)){ this.tokenResolve = resolve
lockedCallback(false) })
return auth.signIn(force ? { lockedCallback(false)
prompt: "select_account" client.requestAccessToken()
} : undefined).then(user => { return promise.then(() => {
if(this.checkScope(user)){ this.tokenResolve = null
lockedCallback(true) if(this.checkScope()){
}else{ lockedCallback(true)
return Promise.reject("cancel") }else{
} return Promise.reject("cancel")
}, () => Promise.reject("cancel"))
} }
}) })
} }
checkScope(user){ checkScope(){
if(user.hasGrantedScopes(this.scope)){ return google.accounts.oauth2.hasGrantedAnyScope(this.tokenResponse, this.scope)
this.oauthToken = user.getAuthResponse(true).access_token
return this.oauthToken
}else{
return false
}
} }
switchAccounts(lockedCallback, errorCallback){ switchAccounts(lockedCallback, errorCallback){
return this.loadApi().then(() => this.getToken(lockedCallback, errorCallback, true)) return this.loadApi().then(() => this.getToken(lockedCallback, errorCallback, true))
@ -221,12 +220,12 @@ class Gpicker{
.build() .build()
.setVisible(true) .setVisible(true)
} }
downloadFile(id, arrayBuffer, retry){ downloadFile(id, responseType, retry){
var url = this.filesUrl + id + "?alt=media" var url = this.filesUrl + id + "?alt=media"
return this.queue().then(this.getToken.bind(this)).then(() => return this.queue().then(this.getToken.bind(this)).then(() =>
loader.ajax(url, request => { loader.ajax(url, request => {
if(arrayBuffer){ if(responseType){
request.responseType = "arraybuffer" request.responseType = responseType
} }
request.setRequestHeader("Authorization", "Bearer " + this.oauthToken) request.setRequestHeader("Authorization", "Bearer " + this.oauthToken)
}, true).then(event => { }, true).then(event => {
@ -239,7 +238,7 @@ class Gpicker{
var e = response.error var e = response.error
if(e && e.errors[0].reason === "authError"){ if(e && e.errors[0].reason === "authError"){
delete this.oauthToken delete this.oauthToken
return this.downloadFile(id, arrayBuffer, true) return this.downloadFile(id, responseType, true)
}else{ }else{
return reject() return reject()
} }

View File

@ -61,12 +61,6 @@ class Loader{
stylesheet.href = "/src/css/" + name + this.queryString stylesheet.href = "/src/css/" + name + this.queryString
document.head.appendChild(stylesheet) 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 = () => { var checkStyles = () => {
if(document.styleSheets.length >= cssCount){ if(document.styleSheets.length >= cssCount){
resolve() resolve()
@ -84,9 +78,10 @@ class Loader{
}), url) }), url)
} }
assets.img.forEach(name=>{ assets.img.forEach(name => {
var id = this.getFilename(name) var id = this.getFilename(name)
var image = document.createElement("img") var image = document.createElement("img")
image.crossOrigin = "anonymous"
var url = gameConfig.assets_baseurl + "img/" + name var url = gameConfig.assets_baseurl + "img/" + name
this.addPromise(pageEvents.load(image), url) this.addPromise(pageEvents.load(image), url)
image.id = name image.id = name
@ -95,6 +90,37 @@ class Loader{
assets.image[id] = image 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 => { assets.views.forEach(name => {
var id = this.getFilename(name) var id = this.getFilename(name)
var url = "/src/views/" + name + this.queryString var url = "/src/views/" + name + this.queryString
@ -147,6 +173,10 @@ class Loader{
return 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 => { this.addPromise(this.ajax("/api/songs").then(songs => {
songs = JSON.parse(songs) songs = JSON.parse(songs)
songs.forEach(song => { songs.forEach(song => {
@ -179,16 +209,22 @@ class Loader{
.filter(cat => cat.songSkin && cat.songSkin.bg_img) .filter(cat => cat.songSkin && cat.songSkin.bg_img)
.forEach(cat => { .forEach(cat => {
let name = cat.songSkin.bg_img let name = cat.songSkin.bg_img
var id = this.getFilename(name)
var image = document.createElement("img")
var url = gameConfig.assets_baseurl + "img/" + name var url = gameConfig.assets_baseurl + "img/" + name
categoryPromises.push(pageEvents.load(image).catch(response => { categoryPromises.push(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)
image.id = name
image.src = blobUrl
this.assetsDiv.appendChild(image)
assets.image[id] = image
return promise
}).catch(response => {
return this.errorMsg(response, url) return this.errorMsg(response, url)
})) }))
image.id = name
image.src = url
this.assetsDiv.appendChild(image)
assets.image[id] = image
}) })
this.addPromise(Promise.all(categoryPromises)) this.addPromise(Promise.all(categoryPromises))
@ -356,6 +392,7 @@ class Loader{
this.canvasTest.clean() this.canvasTest.clean()
this.clean() this.clean()
this.callback(songId) this.callback(songId)
this.ready = true
pageEvents.send("ready", readyEvent) pageEvents.send("ready", readyEvent)
}, () => this.errorMsg()) }, () => this.errorMsg())
}, () => this.errorMsg()) }, () => this.errorMsg())
@ -407,7 +444,7 @@ class Loader{
if(!lang){ if(!lang){
lang = "en" 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] var loaderError = loader.screen.getElementsByClassName("loader-error-div")[0]
loaderError.style.display = "flex" loaderError.style.display = "flex"
@ -472,6 +509,19 @@ class Loader{
this.screen.innerHTML = assets.pages[name] this.screen.innerHTML = assets.pages[name]
this.screen.classList[patternBg ? "add" : "remove"]("pattern-bg") 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){ ajax(url, customRequest, customResponse){
var request = new XMLHttpRequest() var request = new XMLHttpRequest()
request.open("GET", url) request.open("GET", url)

View File

@ -103,8 +103,8 @@ class LoadSong{
} }
let img = document.createElement("img") let img = document.createElement("img")
let force = imgLoad[i].type === "song" && this.touchEnabled let force = imgLoad[i].type === "song" && this.touchEnabled
if(!songObj.custom && (this.imgScale !== 1 || force)){ if(!songObj.custom){
img.crossOrigin = "Anonymous" img.crossOrigin = "anonymous"
} }
let promise = pageEvents.load(img) let promise = pageEvents.load(img)
this.addPromise(promise.then(() => { this.addPromise(promise.then(() => {
@ -147,15 +147,30 @@ class LoadSong{
} }
if(this.touchEnabled && !assets.image["touch_drum"]){ if(this.touchEnabled && !assets.image["touch_drum"]){
let img = document.createElement("img") let img = document.createElement("img")
if(this.imgScale !== 1){ img.crossOrigin = "anonymous"
img.crossOrigin = "Anonymous"
}
var url = gameConfig.assets_baseurl + "img/touch_drum.png" var url = gameConfig.assets_baseurl + "img/touch_drum.png"
this.addPromise(pageEvents.load(img).then(() => { this.addPromise(pageEvents.load(img).then(() => {
return this.scaleImg(img, "touch_drum", "") return this.scaleImg(img, "touch_drum", "")
}), url) }), url)
img.src = 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){ if(songObj.volume && songObj.volume !== 1){
this.promises.push(new Promise(resolve => setTimeout(resolve, 500))) this.promises.push(new Promise(resolve => setTimeout(resolve, 500)))
} }
@ -217,9 +232,7 @@ class LoadSong{
if(!(filenameAb in assets.image)){ if(!(filenameAb in assets.image)){
let img = document.createElement("img") let img = document.createElement("img")
let force = filenameAb.startsWith("bg_song_") && this.touchEnabled 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" var url = gameConfig.assets_baseurl + "img/" + filenameAb + ".png"
this.addPromise(pageEvents.load(img).then(() => { this.addPromise(pageEvents.load(img).then(() => {
return this.scaleImg(img, filenameAb, "", force) return this.scaleImg(img, filenameAb, "", force)
@ -235,32 +248,29 @@ class LoadSong{
if(force && scale > 0.5){ if(force && scale > 0.5){
scale = 0.5 scale = 0.5
} }
if(scale !== 1){ var canvas = document.createElement("canvas")
var canvas = document.createElement("canvas") var w = Math.floor(img.width * scale)
var w = Math.floor(img.width * scale) var h = Math.floor(img.height * scale)
var h = Math.floor(img.height * scale) canvas.width = Math.max(1, w)
canvas.width = Math.max(1, w) canvas.height = Math.max(1, h)
canvas.height = Math.max(1, h) var ctx = canvas.getContext("2d")
var ctx = canvas.getContext("2d") ctx.drawImage(img, 0, 0, w, h)
ctx.drawImage(img, 0, 0, w, h) var saveScaled = url => {
var saveScaled = url => { let img2 = document.createElement("img")
let img2 = document.createElement("img") pageEvents.load(img2).then(() => {
pageEvents.load(img2).then(() => { assets.image[prefix + filename] = img2
assets.image[prefix + filename] = img2 loader.assetsDiv.appendChild(img2)
resolve() resolve()
}, reject) }, reject)
img2.src = url img2.id = prefix + filename
} img2.src = url
if("toBlob" in canvas){ }
canvas.toBlob(blob => { if("toBlob" in canvas){
saveScaled(URL.createObjectURL(blob)) canvas.toBlob(blob => {
}) saveScaled(URL.createObjectURL(blob))
}else{ })
saveScaled(canvas.toDataURL())
}
}else{ }else{
assets.image[prefix + filename] = img saveScaled(canvas.toDataURL())
resolve()
} }
}) })
} }

View File

@ -104,7 +104,7 @@ var kanaPairs = [["っきゃ","ッキャ"],["っきゅ","ッキュ"],["っきょ
["ば","バ"],["び","ビ"],["ぶ","ブ"],["べ","ベ"],["ぼ","ボ"],["ぱ","パ"],["ぴ","パ"],["ぷ","プ"],["ぺ","ペ"],["ぽ","ポ"],["ゔ","ヴ"]] ["ば","バ"],["び","ビ"],["ぶ","ブ"],["べ","ベ"],["ぼ","ボ"],["ぱ","パ"],["ぴ","パ"],["ぷ","プ"],["ぺ","ペ"],["ぽ","ポ"],["ゔ","ヴ"]]
pageEvents.add(root, ["touchstart", "touchmove", "touchend"], event => { pageEvents.add(root, ["touchstart", "touchmove", "touchend"], event => {
if(event.cancelable && cancelTouch && event.target.tagName !== "SELECT"){ if(event.cancelable && cancelTouch && event.target.tagName !== "SELECT" && (event.target.tagName !== "INPUT" || event.target.type !== "file")){
event.preventDefault() event.preventDefault()
} }
}) })

View File

@ -67,13 +67,15 @@
if((name === "start" || name === "start p1") && !inSong){ if((name === "start" || name === "start p1") && !inSong){
inSong = true inSong = true
if(!hasSong){ if(!hasSong || name === "start" && courses[courseName] && courses[courseName].startName !== "start"){
hasSong = false
if(!(courseName in courses)){ if(!(courseName in courses)){
courses[courseName] = {} courses[courseName] = {}
} }
for(var name in currentCourse){ courses[courseName].startName = name
if(name !== "branch"){ for(var opt in currentCourse){
courses[courseName][name] = currentCourse[name] if(opt !== "branch"){
courses[courseName][opt] = currentCourse[opt]
} }
} }
courses[courseName].start = lineNum + 1 courses[courseName].start = lineNum + 1

View File

@ -582,6 +582,12 @@ class EditFunction extends EditValue{
if(this.name){ if(this.name){
this.original = this.name[0][this.name[1]] 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) var args = plugins.argsFromFunc(this.original)
try{ try{
var output = this.loadCallback(plugins.strFromFunc(this.original), args) var output = this.loadCallback(plugins.strFromFunc(this.original), args)
@ -618,8 +624,13 @@ class EditFunction extends EditValue{
} }
class Patch{ class Patch{
edits = [] constructor(...args){
addedLanguages = [] this.init(...args)
}
init(){
this.edits = []
this.addedLanguages = []
}
addEdits(...args){ addEdits(...args){
args.forEach(arg => this.edits.push(arg)) 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){ constructor(...args){
this.init(...args) this.init(...args)
} }
init(touchEnabled, tutorial, songId, toSetting, settingsItems){ init(touchEnabled, tutorial, songId, toSetting, settingsItems, noSoundStart){
this.touchEnabled = touchEnabled this.touchEnabled = touchEnabled
this.tutorial = tutorial this.tutorial = tutorial
this.songId = songId this.songId = songId
this.customSettings = !!settingsItems this.customSettings = !!settingsItems
this.settingsItems = settingsItems || settings.items this.settingsItems = settingsItems || settings.items
this.locked = false
loader.changePage("settings", tutorial) loader.changePage("settings", tutorial)
assets.sounds["bgm_settings"].playLoop(0.1, false, 0, 1.392, 26.992) if(!noSoundStart){
assets.sounds["bgm_settings"].playLoop(0.1, false, 0, 1.392, 26.992)
}
this.defaultButton = document.getElementById("settings-default") this.defaultButton = document.getElementById("settings-default")
this.viewOuter = this.getElement("view-outer") this.viewOuter = this.getElement("view-outer")
if(touchEnabled){ if(touchEnabled){
@ -377,16 +380,46 @@ class SettingsView{
this.items.push(outputObject) this.items.push(outputObject)
this.getValue(i, valueDiv) this.getValue(i, valueDiv)
} }
this.items.push({ var selectBack = this.items.length === 0
id: "default", if(this.customSettings){
settingBox: this.defaultButton var form = document.createElement("form")
}) this.browse = document.createElement("input")
this.addTouch(this.defaultButton, this.defaultSettings.bind(this)) this.browse.id = "plugin-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)
this.browseButton = document.createElement("div")
this.browseButton.classList.add("taibtn", "stroke-sub", "plugin-browse-button")
this.browseText = document.createTextNode("")
this.browseButton.appendChild(this.browseText)
this.browseButton.appendChild(form)
this.defaultButton.parentNode.insertBefore(this.browseButton, this.defaultButton)
this.items.push({
id: "browse",
settingBox: this.browseButton
})
}
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({ this.items.push({
id: "back", id: "back",
settingBox: this.endButton settingBox: this.endButton
}) })
this.addTouch(this.endButton, this.onEnd.bind(this)) this.addTouch(this.endButton, this.onEnd.bind(this))
if(selectBack){
this.selected = this.items.length - 1
this.endButton.classList.add("selected")
}
if(!this.customSettings){ if(!this.customSettings){
this.gamepadSettings = document.getElementById("settings-gamepad") this.gamepadSettings = document.getElementById("settings-gamepad")
@ -606,6 +639,9 @@ class SettingsView{
valueDiv.innerText = value valueDiv.innerText = value
} }
setValue(name){ setValue(name){
if(this.locked){
return
}
var promise var promise
var current = this.settingsItems[name] var current = this.settingsItems[name]
if(current.getItem){ if(current.getItem){
@ -674,6 +710,9 @@ class SettingsView{
}) })
} }
keyPressed(pressed, name, event, repeat){ keyPressed(pressed, name, event, repeat){
if(this.locked){
return
}
if(pressed){ if(pressed){
if(!this.pressedKeys[name]){ if(!this.pressedKeys[name]){
this.pressedKeys[name] = this.getMS() + 300 this.pressedKeys[name] = this.getMS() + 300
@ -693,6 +732,11 @@ class SettingsView{
this.onEnd() this.onEnd()
}else if(selected.id === "default"){ }else if(selected.id === "default"){
this.defaultSettings() this.defaultSettings()
}else if(selected.id === "browse"){
if(event){
this.playSound("se_don")
this.browse.click()
}
}else{ }else{
this.setValue(selected.id) this.setValue(selected.id)
} }
@ -700,7 +744,7 @@ class SettingsView{
selected.settingBox.classList.remove("selected") selected.settingBox.classList.remove("selected")
do{ do{
this.selected = this.mod(this.items.length, this.selected + ((name === "right" || name === "down") ? 1 : -1)) 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 = this.items[this.selected]
selected.settingBox.classList.add("selected") selected.settingBox.classList.add("selected")
this.scrollTo(selected.settingBox) this.scrollTo(selected.settingBox)
@ -1027,7 +1071,9 @@ class SettingsView{
defaultSettings(){ defaultSettings(){
if(this.customSettings){ if(this.customSettings){
plugins.unloadImported() plugins.unloadImported()
return this.onEnd() this.clean(true)
this.playSound("se_don")
return setTimeout(() => this.restart(), 500)
} }
if(this.mode === "keyboard"){ if(this.mode === "keyboard"){
this.keyboardBack(this.items[this.selected]) this.keyboardBack(this.items[this.selected])
@ -1046,6 +1092,31 @@ class SettingsView{
this.drumSounds = settings.getItem("latency").drumSounds this.drumSounds = settings.getItem("latency").drumSounds
this.playSound("se_don") 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(){ onEnd(){
if(this.mode === "number"){ if(this.mode === "number"){
this.numberBack(this.items[this.selected]) this.numberBack(this.items[this.selected])
@ -1063,6 +1134,12 @@ class SettingsView{
} }
}, 500) }, 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){ getLocalTitle(title, titleLang){
if(titleLang){ if(titleLang){
for(var id in titleLang){ for(var id in titleLang){
@ -1109,14 +1186,19 @@ class SettingsView{
setStrings(){ setStrings(){
this.setAltText(this.viewTitle, this.customSettings ? strings.plugins.title : strings.gameSettings) this.setAltText(this.viewTitle, this.customSettings ? strings.plugins.title : strings.gameSettings)
this.setAltText(this.endButton, strings.settings.ok) this.setAltText(this.endButton, strings.settings.ok)
if(!this.customSettings){ if(this.customSettings){
this.browseText.data = strings.plugins.browse
this.browseButton.setAttribute("alt", strings.plugins.browse)
}else{
this.setAltText(this.gamepadTitle, strings.settings.gamepadLayout.name) this.setAltText(this.gamepadTitle, strings.settings.gamepadLayout.name)
this.setAltText(this.gamepadEndButton, strings.settings.ok) this.setAltText(this.gamepadEndButton, strings.settings.ok)
this.setAltText(this.latencyTitle, strings.settings.latency.name) this.setAltText(this.latencyTitle, strings.settings.latency.name)
this.setAltText(this.latencyDefaultButton, strings.settings.default) this.setAltText(this.latencyDefaultButton, strings.settings.default)
this.setAltText(this.latencyEndButton, strings.settings.ok) this.setAltText(this.latencyEndButton, strings.settings.ok)
} }
this.setAltText(this.defaultButton, this.customSettings ? strings.plugins.unloadAll : strings.settings.default) if(this.showDefault){
this.setAltText(this.defaultButton, this.customSettings ? strings.plugins.unloadAll : strings.settings.default)
}
} }
setAltText(element, text){ setAltText(element, text){
element.innerText = text element.innerText = text
@ -1154,11 +1236,13 @@ class SettingsView{
getMS(){ getMS(){
return Date.now() return Date.now()
} }
clean(){ clean(noSoundStop){
this.redrawRunning = false this.redrawRunning = false
this.keyboard.clean() this.keyboard.clean()
this.gamepad.clean() this.gamepad.clean()
assets.sounds["bgm_settings"].stop() if(!noSoundStop){
assets.sounds["bgm_settings"].stop()
}
pageEvents.remove(window, ["mouseup", "touchstart", "touchmove", "touchend", "blur"], this.windowSymbol) pageEvents.remove(window, ["mouseup", "touchstart", "touchmove", "touchend", "blur"], this.windowSymbol)
if(this.customSettings){ if(this.customSettings){
pageEvents.remove(window, "language-change", this.windowSymbol) pageEvents.remove(window, "language-change", this.windowSymbol)
@ -1176,7 +1260,12 @@ class SettingsView{
if(this.defaultButton){ if(this.defaultButton){
delete this.defaultButton delete this.defaultButton
} }
if(!this.customSettings){ if(this.customSettings){
pageEvents.remove(this.browse, "change")
delete this.browse
delete this.browseButton
delete this.browseText
}else{
this.removeTouch(this.gamepadSettings) this.removeTouch(this.gamepadSettings)
this.removeTouch(this.gamepadEndButton) this.removeTouch(this.gamepadEndButton)
this.removeTouch(this.gamepadBox) this.removeTouch(this.gamepadBox)
@ -1204,8 +1293,12 @@ class SettingsView{
delete this.latencyEndButton delete this.latencyEndButton
if(this.resolution !== settings.getItem("resolution")){ if(this.resolution !== settings.getItem("resolution")){
for(var i in assets.image){ for(var i in assets.image){
if(i === "touch_drum" || i.startsWith("bg_song_") || i.startsWith("bg_stage_") || i.startsWith("bg_don_")){ if(i === "touch_drum" || i.startsWith("bg_song_") || i.startsWith("bg_stage_") || i.startsWith("bg_don_") || i.startsWith("results_")){
URL.revokeObjectURL(assets.image[i].src) var img = assets.image[i]
URL.revokeObjectURL(img.src)
if(img.parentNode){
img.parentNode.removeChild(img)
}
delete assets.image[i] delete assets.image[i]
} }
} }

View File

@ -92,22 +92,6 @@ class SongSelect{
} }
this.songSkin["default"].sort = songSkinLength + 1 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.font = strings.font
this.songs = [] this.songs = []
@ -194,14 +178,12 @@ class SongSelect{
category: strings.random category: strings.random
}) })
} }
if(plugins.hasSettings()){ this.songs.push({
this.songs.push({ title: strings.plugins.title,
title: strings.plugins.title, skin: this.songSkin.plugins,
skin: this.songSkin.plugins, action: "plugins",
action: "plugins", category: strings.random
category: strings.random })
})
}
this.songs.push({ this.songs.push({
title: strings.back, title: strings.back,
@ -246,6 +228,8 @@ class SongSelect{
this.currentSongCache = new CanvasCache(noSmoothing) this.currentSongCache = new CanvasCache(noSmoothing)
this.nameplateCache = new CanvasCache(noSmoothing) this.nameplateCache = new CanvasCache(noSmoothing)
this.search = new Search(this)
this.difficulty = [strings.easy, strings.normal, strings.hard, strings.oni] this.difficulty = [strings.easy, strings.normal, strings.hard, strings.oni]
this.difficultyId = ["easy", "normal", "hard", "oni", "ura"] this.difficultyId = ["easy", "normal", "hard", "oni", "ura"]
@ -257,7 +241,6 @@ class SongSelect{
this.selectedSong = 0 this.selectedSong = 0
this.selectedDiff = 0 this.selectedDiff = 0
this.lastCurrentSong = {} this.lastCurrentSong = {}
this.searchEnabled = true
this.lastRandom = false this.lastRandom = false
assets.sounds["bgm_songsel"].playLoop(0.1, false, 0, 1.442, 3.506) assets.sounds["bgm_songsel"].playLoop(0.1, false, 0, 1.442, 3.506)
@ -434,44 +417,14 @@ class SongSelect{
this.state.showWarning = false this.state.showWarning = false
this.showWarning = false this.showWarning = false
} }
}else if (this.search){ }else if(this.search.opened){
if(name === "back" || (event && event.keyCode && event.keyCode === 70 && ctrl)) { this.search.keyPress(pressed, name, event, repeat)
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.state.screen === "song"){ }else if(this.state.screen === "song"){
if(event && event.keyCode && event.keyCode === 70 && ctrl){ if(event && event.keyCode && event.keyCode === 70 && ctrl){
this.displaySearch() this.search.display()
if(event){ event.preventDefault() } if(event){
event.preventDefault()
}
}else if(name === "confirm"){ }else if(name === "confirm"){
this.toSelectDifficulty() this.toSelectDifficulty()
}else if(name === "back"){ }else if(name === "back"){
@ -504,8 +457,10 @@ class SongSelect{
} }
}else if(this.state.screen === "difficulty"){ }else if(this.state.screen === "difficulty"){
if(event && event.keyCode && event.keyCode === 70 && ctrl){ if(event && event.keyCode && event.keyCode === 70 && ctrl){
this.displaySearch() this.search.display()
if(event){ event.preventDefault() } if(event){
event.preventDefault()
}
}else if(name === "confirm"){ }else if(name === "confirm"){
if(this.selectedDiff === 0){ if(this.selectedDiff === 0){
this.toSongSelect() this.toSongSelect()
@ -528,8 +483,10 @@ class SongSelect{
} }
}else if(this.state.screen === "title" || this.state.screen === "titleFadeIn"){ }else if(this.state.screen === "title" || this.state.screen === "titleFadeIn"){
if(event && event.keyCode && event.keyCode === 70 && ctrl){ if(event && event.keyCode && event.keyCode === 70 && ctrl){
this.displaySearch() this.search.display()
if(event){ event.preventDefault() } if(event){
event.preventDefault()
}
} }
} }
} }
@ -627,7 +584,7 @@ class SongSelect{
if(408 < mouse.x && mouse.x < 872 && 470 < mouse.y && mouse.y < 550){ if(408 < mouse.x && mouse.x < 872 && 470 < mouse.y && mouse.y < 550){
moveTo = "showWarning" 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)){ 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" moveTo = mouse.x < 640 ? "categoryPrev" : "categoryNext"
}else if(!p2.session && 60 < mouse.x && mouse.x < 332 && 640 < mouse.y && mouse.y < 706 && gameConfig.accounts){ }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){ }else if(this.state.locked === 0 || fromP2){
this.removeSearch() this.search.remove()
if(currentSong.courses){ if(currentSong.courses){
if(currentSong.unloaded){ if(currentSong.unloaded){
return return
@ -815,7 +772,6 @@ class SongSelect{
} }
pageEvents.send("song-select-difficulty", currentSong) pageEvents.send("song-select-difficulty", currentSong)
}else if(currentSong.action === "back"){ }else if(currentSong.action === "back"){
this.clean()
this.toTitleScreen() this.toTitleScreen()
}else if(currentSong.action === "random"){ }else if(currentSong.action === "random"){
do{ do{
@ -827,7 +783,7 @@ class SongSelect{
this.toSelectDifficulty(false, playVoice=false) this.toSelectDifficulty(false, playVoice=false)
pageEvents.send("song-select-random") pageEvents.send("song-select-random")
}else if(currentSong.action === "search"){ }else if(currentSong.action === "search"){
this.displaySearch(true) this.search.display(true)
}else if(currentSong.action === "tutorial"){ }else if(currentSong.action === "tutorial"){
this.toTutorial() this.toTutorial()
}else if(currentSong.action === "about"){ }else if(currentSong.action === "about"){
@ -1116,8 +1072,8 @@ class SongSelect{
this.selectableText = "" this.selectableText = ""
if(this.search && this.searchContainer){ if(this.search.opened && this.search.container){
this.searchInput() this.search.onInput()
} }
}else if(!document.hasFocus() && !p2.session){ }else if(!document.hasFocus() && !p2.session){
if(this.state.focused){ if(this.state.focused){
@ -1146,15 +1102,7 @@ class SongSelect{
var screen = this.state.screen var screen = this.state.screen
var selectedWidth = this.songAsset.width var selectedWidth = this.songAsset.width
if(this.search && this.searchContainer){ this.search.redraw()
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
}
if(this.wheelScrolls !== 0 && !this.state.locked && ms >= this.wheelTimer + 20) { if(this.wheelScrolls !== 0 && !this.state.locked && ms >= this.wheelTimer + 20) {
if(p2.session){ if(p2.session){
@ -2726,531 +2674,6 @@ class SongSelect{
} }
return addedSong 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
}
}
normalizeSearch(string){
string = string
.replace('', '\'').replace('“', '"').replace('”', '"')
kanaPairs.forEach(pair => {
string = string.replace(pair[1], pair[0])
})
return string.normalize("NFKD").replace(/[\u0300-\u036f]/g, "")
}
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 = this.normalizeSearch(editedSplit.join(" ").trim())
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){ onusers(response){
var p2InSong = false var p2InSong = false
@ -3279,17 +2702,17 @@ class SongSelect{
if(this.state.screen !== "difficulty"){ if(this.state.screen !== "difficulty"){
this.toSelectDifficulty({player: response.value.player}) this.toSelectDifficulty({player: response.value.player})
} }
this.searchEnabled = false this.search.enabled = false
p2InSong = true p2InSong = true
this.removeSearch() this.search.remove()
} }
} }
} }
}) })
} }
if(!this.searchEnabled && !p2InSong){ if(!this.search.enabled && !p2InSong){
this.searchEnabled = true this.search.enabled = true
} }
} }
onsongsel(response){ onsongsel(response){
@ -3415,6 +2838,7 @@ class SongSelect{
this.sessionCache.clean() this.sessionCache.clean()
this.currentSongCache.clean() this.currentSongCache.clean()
this.nameplateCache.clean() this.nameplateCache.clean()
this.search.clean()
assets.sounds["bgm_songsel"].stop() assets.sounds["bgm_songsel"].stop()
if(!this.bgmEnabled){ if(!this.bgmEnabled){
snd.musicGain.fadeIn() snd.musicGain.fadeIn()
@ -3436,13 +2860,8 @@ class SongSelect{
pageEvents.remove(this.touchFullBtn, "click") pageEvents.remove(this.touchFullBtn, "click")
delete this.touchFullBtn delete this.touchFullBtn
} }
if(this.searchStyle){
loader.screen.removeChild(this.searchStyle)
}
delete this.selectable delete this.selectable
delete this.ctx delete this.ctx
delete this.canvas delete this.canvas
delete this.searchContainer
delete this.searchStyle
} }
} }

View File

@ -1331,6 +1331,17 @@ var translations = {
version: { version: {
ja: "Ver. %s", ja: "Ver. %s",
en: "Version %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: { search: {

View File

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

View File

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

View File

@ -7,6 +7,8 @@
<meta name="viewport" content="width=device-width, user-scalable=no"> <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="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="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"> <meta name="color-scheme" content="only light">
<link rel="stylesheet" href="/src/css/loader.css?{{version.commit_short}}"> <link rel="stylesheet" href="/src/css/loader.css?{{version.commit_short}}">
@ -22,7 +24,7 @@
<div id="screen" class="pattern-bg"></div> <div id="screen" class="pattern-bg"></div>
<div data-nosnippet id="version"> <div data-nosnippet id="version">
{% if version.version and version.commit_short and version.commit %} {% 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 %} {% 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> <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 %} {% endif %}

View File

@ -23,10 +23,7 @@ You can use the Google Drive integration to let Taiko Web make your Taiko chart
Applications that integrate with a Google account must declare their intent by requesting permissions. These permissions to your account must be granted in order for Taiko Web to integrate with Google accounts. Below is a list of these permissions and why they are required. At no time will Taiko Web request or have access to your Google account password. Applications that integrate with a Google account must declare their intent by requesting permissions. These permissions to your account must be granted in order for Taiko Web to integrate with Google accounts. Below is a list of these permissions and why they are required. At no time will Taiko Web request or have access to your Google account password.
3.1 "Associate you with your personal info on Google" Permission 3.1 "See and download all your Google Drive files" Permission
Required for Google Sign-In to provide Taiko Web with a non-identifiable user authentication token. No other information provided by this permission is used.
3.2 "See and download all your Google Drive files" Permission
When selecting a folder with the Google Drive file picker, Taiko Web instructs your Browser to recursively download all the files of that folder directly into your computer's memory. Limitation of Google Drive's permission model requires us to request access to all your Google Drive files, however, Taiko Web will only access the selected folder and its children, and only when requested. File parsing is handled locally; none of your Google Drive files is ever sent to our servers or third parties. When selecting a folder with the Google Drive file picker, Taiko Web instructs your Browser to recursively download all the files of that folder directly into your computer's memory. Limitation of Google Drive's permission model requires us to request access to all your Google Drive files, however, Taiko Web will only access the selected folder and its children, and only when requested. File parsing is handled locally; none of your Google Drive files is ever sent to our servers or third parties.
{% endif %}{% if config.email %} {% endif %}{% if config.email %}
{% if integration %}4{% else %}3{% endif %}. Contact Info {% if integration %}4{% else %}3{% endif %}. Contact Info

62
tools/setup.sh Normal file
View File

@ -0,0 +1,62 @@
#!/bin/bash
set -euo pipefail
sudo apt update
sudo apt install -y git python3-pip python3-virtualenv python3-venv nginx ffmpeg redis supervisor
if [[ -r /etc/os-release ]]; then
. /etc/os-release
if [[ $ID = ubuntu ]]; then
if [[ $VERSION_CODENAME = impish ]]; then
VERSION_CODENAME=focal # MongoDB does not provide packages for Ubuntu 21.10
fi
REPO="https://repo.mongodb.org/apt/ubuntu $VERSION_CODENAME/mongodb-org/5.0 multiverse"
elif [[ $ID = debian ]]; then
if [[ $VERSION_CODENAME = bullseye ]]; then
VERSION_CODENAME=buster # MongoDB does not provide packages for Debian 11 yet
fi
REPO="https://repo.mongodb.org/apt/debian $VERSION_CODENAME/mongodb-org/5.0 main"
else
echo "Unsupported distribution $ID"
exit 1
fi
else
echo "Not running a distribution with /etc/os-release available"
exit 1
fi
wget -qO - https://www.mongodb.org/static/pgp/server-5.0.asc | sudo apt-key add -
echo "deb [ arch=amd64,arm64 ] $REPO" | sudo tee /etc/apt/sources.list.d/mongodb-org-5.0.list
sudo apt update
sudo apt install -y mongodb-org
sudo mkdir -p /srv/taiko-web
sudo chown $USER /srv/taiko-web
git clone https://github.com/bui/taiko-web.git /srv/taiko-web
cd /srv/taiko-web
tools/get_version.sh
cp tools/hooks/* .git/hooks/
cp config.example.py config.py
sudo cp tools/nginx.conf /etc/nginx/conf.d/taiko-web.conf
sudo sed -i 's/^\(\s\{0,\}\)\(include \/etc\/nginx\/sites-enabled\/\*;\)$/\1#\2/g' /etc/nginx/nginx.conf
sudo sed -i 's/}/ application\/wasm wasm;\n}/g' /etc/nginx/mime.types
sudo nginx -s reload
python3 -m venv .venv
.venv/bin/pip install --upgrade pip wheel setuptools
.venv/bin/pip install -r requirements.txt
sudo mkdir -p /var/log/taiko-web
sudo cp tools/supervisor.conf /etc/supervisor/conf.d/taiko-web.conf
sudo service supervisor restart
sudo systemctl enable mongod.service
sudo service mongod start
IP=$(dig +short txt ch whoami.cloudflare @1.0.0.1 | tr -d '"')
echo
echo "Setup complete! You should be able to access your taiko-web instance at http://$IP"
echo