SongSel: Add browse for local songs button

This commit is contained in:
LoveEevee 2018-12-05 23:33:34 +03:00
parent 7f5b1e97c3
commit a435ed1a6d
9 changed files with 275 additions and 69 deletions

View File

@ -19,7 +19,8 @@ body{
background-size: 30vh; background-size: 30vh;
font-family: TnT, Meiryo, sans-serif; font-family: TnT, Meiryo, sans-serif;
} }
#assets{ #assets,
#browse{
display: none; display: none;
} }
.window{ .window{

View File

@ -51,8 +51,8 @@
ideographicComma: /[、。]/, ideographicComma: /[、。]/,
apostrophe: /[']/, apostrophe: /[']/,
degree: /[゚°]/, degree: /[゚°]/,
brackets: /[\(\)「」『』]/, brackets: /[\(\)\[\]「」『』【】]/,
tilde: /[\-~~〜]/, tilde: /[\-~~〜_]/,
tall: /[bdfghj-l-t♪]/, tall: /[bdfghj-l-t♪]/,
i: /[i]/, i: /[i]/,
uppercase: /[A-Z-]/, uppercase: /[A-Z-]/,
@ -68,7 +68,8 @@
em: /[mw]/, em: /[mw]/,
emCap: /[MW]/, emCap: /[MW]/,
rWidth: /[abdfIjo-rtv-]/, rWidth: /[abdfIjo-rtv-]/,
lWidth: /[il]/ lWidth: /[il]/,
ura: /\s*[\(]裏[\)]$/
} }
var numbersFull = "" var numbersFull = ""
@ -276,13 +277,18 @@
var ctx = config.ctx var ctx = config.ctx
var inputText = config.text var inputText = config.text
var mul = config.fontSize / 40 var mul = config.fontSize / 40
var ura = false
var r = this.regex
var matches = inputText.match(r.ura)
if(matches){
inputText = inputText.slice(0, matches.index)
ura = matches[0]
}
var string = inputText.split("") var string = inputText.split("")
var drawn = [] var drawn = []
var r = this.regex
var previousSymbol = ""
for(var i = 0; i < string.length; i++){ for(var i = 0; i < string.length; i++){
let symbol = string[i] let symbol = string[i]
if(symbol === " "){ if(symbol === " "){
@ -297,6 +303,8 @@
drawn.push({text: symbol, x: 0, y: 12, h: 45}) drawn.push({text: symbol, x: 0, y: 12, h: 45})
}else if(symbol === ""){ }else if(symbol === ""){
drawn.push({realText: symbol, text: ".", x: 13, y: -7, h: 15, scale: [1.2, 0.7]}) drawn.push({realText: symbol, text: ".", x: 13, y: -7, h: 15, scale: [1.2, 0.7]})
}else if(symbol === "…"){
drawn.push({text: symbol, x: 0, y: 5, h: 25, rotate: true})
}else if(r.comma.test(symbol)){ }else if(r.comma.test(symbol)){
// Comma, full stop // Comma, full stop
drawn.push({text: symbol, x: 13, y: -7, h: 15, scale: [1.2, 0.7]}) drawn.push({text: symbol, x: 13, y: -7, h: 15, scale: [1.2, 0.7]})
@ -408,22 +416,28 @@
} }
var scaling = 1 var scaling = 1
if(config.height && drawnHeight > config.height){ var height = config.height - (ura ? 52 * mul : 0)
if(height && drawnHeight > height){
if(config.align === "bottom"){ if(config.align === "bottom"){
scaling = Math.max(0.6, config.height / drawnHeight) scaling = Math.max(0.6, height / drawnHeight)
ctx.translate(40 * mul, 0) ctx.translate(40 * mul, 0)
ctx.scale(scaling, config.height / drawnHeight) ctx.scale(scaling, height / drawnHeight)
ctx.translate(-40 * mul, 0) ctx.translate(-40 * mul, 0)
}else{ }else{
scaling = config.height / drawnHeight scaling = height / drawnHeight
ctx.scale(1, scaling) ctx.scale(1, scaling)
} }
if(config.selectable){ if(config.selectable){
style.transform = "scale(1, " + scaling + ")" style.transform = "scale(1, " + scaling + ")"
style.top = (config.y + (config.height - drawnHeight) / 2 - 15 / 2 * scaling) * scale + "px" style.top = (config.y + (height - drawnHeight) / 2 - 15 / 2 * scaling) * scale + "px"
} }
} }
if(ura){
// Circled ura
drawn.push({realText: ura, text: "裏", x: 0, y: 18, h: 52, ura: true, scale: [1, 1 / scale]})
}
var actions = [] var actions = []
if(config.outline){ if(config.outline){
actions.push("stroke") actions.push("stroke")
@ -492,7 +506,7 @@
config.selectable.appendChild(div) config.selectable.appendChild(div)
continue continue
} }
if(symbol.rotate || symbol.scale || symbol.svg){ if(symbol.rotate || symbol.scale || symbol.svg || symbol.ura){
saved = true saved = true
ctx.save() ctx.save()
@ -517,7 +531,23 @@
}else{ }else{
ctx.textAlign = "center" ctx.textAlign = "center"
} }
ctx[action + "Text"](symbol.text, currentX, currentY) if(symbol.ura){
ctx.font = (30 * mul) + "px Meiryo, sans-serif"
ctx.textBaseline = "center"
ctx.beginPath()
ctx.arc(currentX, currentY + (21.5 * mul), (18 * mul), 0, Math.PI * 2)
if(action === "stroke"){
ctx.fillStyle = config.outline
ctx.fill()
}else if(action === "fill"){
ctx.strokeStyle = config.fill
ctx.lineWidth = 2.5 * mul
ctx.fillText(symbol.text, currentX, currentY)
}
ctx.stroke()
}else{
ctx[action + "Text"](symbol.text, currentX, currentY)
}
} }
if(saved){ if(saved){
ctx.restore() ctx.restore()

View File

@ -78,7 +78,8 @@ class Loader{
}) })
this.promises.push(this.ajax("/api/songs").then(songs => { this.promises.push(this.ajax("/api/songs").then(songs => {
assets.songs = JSON.parse(songs) assets.songsDefault = JSON.parse(songs)
assets.songs = assets.songsDefault
})) }))
assets.views.forEach(name => { assets.views.forEach(name => {

View File

@ -66,16 +66,17 @@ class loadSong{
} }
promises.push(this.loadSongBg(id)) promises.push(this.loadSongBg(id))
var songObj = assets.songs.find(song => song.id === id)
promises.push(new Promise((resolve, reject) => { promises.push(new Promise((resolve, reject) => {
var songObj
assets.songs.forEach(song => {
if(song.id == id){
songObj = song
}
})
if(songObj.sound){ if(songObj.sound){
songObj.sound.gain = snd.musicGain songObj.sound.gain = snd.musicGain
resolve() resolve()
}else if(songObj.music){
snd.musicGain.load(songObj.music, true).then(sound => {
songObj.sound = sound
resolve()
}, reject)
}else{ }else{
snd.musicGain.load(gameConfig.songs_baseurl + id + "/main.mp3").then(sound => { snd.musicGain.load(gameConfig.songs_baseurl + id + "/main.mp3").then(sound => {
songObj.sound = sound songObj.sound = sound
@ -83,9 +84,13 @@ class loadSong{
}, reject) }, reject)
} }
})) }))
promises.push(loader.ajax(this.getSongPath(song)).then(data => { if(songObj.chart){
this.songData = data.replace(/\0/g, "").split("\n") this.songData = songObj.chart
})) }else{
promises.push(loader.ajax(this.getSongPath(song)).then(data => {
this.songData = data.replace(/\0/g, "").split("\n")
}))
}
Promise.all(promises).then(() => { Promise.all(promises).then(() => {
this.setupMultiplayer() this.setupMultiplayer()
}, error => { }, error => {

View File

@ -1,5 +1,5 @@
class ParseOsu{ class ParseOsu{
constructor(fileContent, offset){ constructor(fileContent, offset, metaOnly){
this.osu = { this.osu = {
OFFSET: 0, OFFSET: 0,
MSPERBEAT: 1, MSPERBEAT: 1,
@ -52,9 +52,11 @@ class ParseOsu{
this.metadata = this.parseMetadata() this.metadata = this.parseMetadata()
this.editor = this.parseEditor() this.editor = this.parseEditor()
this.difficulty = this.parseDifficulty() this.difficulty = this.parseDifficulty()
this.timingPoints = this.parseTiming() if(!metaOnly){
this.circles = this.parseCircles() this.timingPoints = this.parseTiming()
this.measures = this.parseMeasures() this.circles = this.parseCircles()
this.measures = this.parseMeasures()
}
} }
getStartEndIndexes(type){ getStartEndIndexes(type){
var indexes = { var indexes = {
@ -186,40 +188,20 @@ class ParseOsu{
return measures return measures
} }
parseGeneralInfo(){ parseGeneralInfo(){
var generalInfo = { var generalInfo = {}
audioFilename: "",
audioWait: 0
}
var indexes = this.getStartEndIndexes("General") var indexes = this.getStartEndIndexes("General")
for(var i = indexes.start; i<= indexes.end; i++){ for(var i = indexes.start; i<= indexes.end; i++){
var [item, key] = this.data[i].split(":") var [item, key] = this.data[i].split(":")
switch(item){ generalInfo[item] = key.trim()
case "SliderMultiple":
generalInfo.audioFilename = key
break
case "AudioWait":
generalInfo.audioWait = parseInt(key)
break
}
} }
return generalInfo return generalInfo
} }
parseMetadata(){ parseMetadata(){
var metadata = { var metadata = {}
title: "",
artist: ""
}
var indexes = this.getStartEndIndexes("Metadata") var indexes = this.getStartEndIndexes("Metadata")
for(var i = indexes.start; i <= indexes.end; i++){ for(var i = indexes.start; i <= indexes.end; i++){
var [item, key] = this.data[i].split(":") var [item, key] = this.data[i].split(":")
switch(item){ metadata[item] = key.trim()
case "TitleUnicode":
metadata.title = key
break
case "ArtistUnicode":
metadata.artist = key
break
}
} }
return metadata return metadata
} }

View File

@ -1,5 +1,5 @@
class ParseTja{ class ParseTja{
constructor(file, difficulty, offset){ constructor(file, difficulty, offset, metaOnly){
this.data = [] this.data = []
for(let line of file){ for(let line of file){
line = line.replace(/\/\/.*/, "").trim() line = line.replace(/\/\/.*/, "").trim()
@ -34,10 +34,12 @@
this.metadata = this.parseMetadata() this.metadata = this.parseMetadata()
this.measures = [] this.measures = []
this.beatInfo = {} this.beatInfo = {}
this.circles = this.parseCircles() if(!metaOnly){
this.circles = this.parseCircles()
}
} }
parseMetadata(){ parseMetadata(){
var metaNumbers = ["bpm", "offset"] var metaNumbers = ["bpm", "offset", "demostart", "level"]
var inSong = false var inSong = false
var courses = {} var courses = {}
var currentCourse = {} var currentCourse = {}

View File

@ -35,6 +35,12 @@ class SongSelect{
border: ["#dff0ff", "#6890b2"], border: ["#dff0ff", "#6890b2"],
outline: "#217abb" outline: "#217abb"
}, },
"browse": {
sort: 7,
background: "#9791ff",
border: ["#e2dfff", "#6d68b2"],
outline: "#5350ba"
},
"J-POP": { "J-POP": {
sort: 0, sort: 0,
background: "#219fbb", background: "#219fbb",
@ -98,7 +104,8 @@ class SongSelect{
preview: song.preview || 0, preview: song.preview || 0,
type: song.type, type: song.type,
offset: song.offset, offset: song.offset,
songSkin: song.song_skin || {} songSkin: song.song_skin || {},
music: song.music
}) })
} }
this.songs.sort((a, b) => { this.songs.sort((a, b) => {
@ -139,6 +146,17 @@ class SongSelect{
action: "about", action: "about",
category: "ランダム" category: "ランダム"
}) })
if("webkitdirectory" in HTMLInputElement.prototype && !(/Android|iPhone|iPad/.test(navigator.userAgent))){
this.browse = document.getElementById("browse")
pageEvents.add(this.browse, "change", this.browseChange.bind(this))
this.songs.push({
title: assets.customSongs ? "デフォルト曲リスト" : "参照する…",
skin: this.songSkin.browse,
action: "browse",
category: "ランダム"
})
}
this.songs.push({ this.songs.push({
title: "もどる", title: "もどる",
skin: this.songSkin.back, skin: this.songSkin.back,
@ -204,8 +222,10 @@ class SongSelect{
this.selectedSong = this.songs.findIndex(song => song.action === fromTutorial) this.selectedSong = this.songs.findIndex(song => song.action === fromTutorial)
this.playBgm(true) this.playBgm(true)
}else{ }else{
if((!p2.session || fadeIn) && "selectedSong" in localStorage){ if(assets.customSongs){
this.selectedSong = Math.min(Math.max(0, localStorage["selectedSong"] |0), this.songs.length) this.selectedSong = assets.customSelected
}else if((!p2.session || fadeIn) && "selectedSong" in localStorage){
this.selectedSong = Math.min(Math.max(0, localStorage["selectedSong"] |0), this.songs.length - 1)
} }
assets.sounds["song-select"].play() assets.sounds["song-select"].play()
snd.musicGain.fadeOut() snd.musicGain.fadeOut()
@ -265,6 +285,7 @@ class SongSelect{
this.state.moveHover = null this.state.moveHover = null
}) })
pageEvents.add(loader.screen, ["mousedown", "touchstart"], this.mouseDown.bind(this)) pageEvents.add(loader.screen, ["mousedown", "touchstart"], this.mouseDown.bind(this))
pageEvents.add(this.canvas, "touchend", this.touchEnd.bind(this))
if(touchEnabled && fullScreenSupported){ if(touchEnabled && fullScreenSupported){
this.touchFullBtn = document.getElementById("touch-full-btn") this.touchFullBtn = document.getElementById("touch-full-btn")
this.touchFullBtn.style.display = "block" this.touchFullBtn.style.display = "block"
@ -404,6 +425,19 @@ class SongSelect{
} }
} }
} }
touchEnd(event){
event.preventDefault()
if(this.state.screen === "song"){
var currentSong = this.songs[this.selectedSong]
if(currentSong.action === "browse"){
var mouse = this.mouseOffset(event.changedTouches[0].pageX, event.changedTouches[0].pageY)
var moveBy = this.songSelMouse(mouse.x, mouse.y)
if(moveBy === 0){
this.toBrowse()
}
}
}
}
mouseMove(event){ mouseMove(event){
var mouse = this.mouseOffset(event.offsetX, event.offsetY) var mouse = this.mouseOffset(event.offsetX, event.offsetY)
var moveTo = null var moveTo = null
@ -521,6 +555,119 @@ class SongSelect{
assets.sounds["ka"].play() assets.sounds["ka"].play()
} }
} }
browseChange(event){
var files = event.target.files
var promises = []
var tjaFiles = []
var osuFiles = []
var otherFiles = {}
for(var i = 0; i < files.length; i++){
var file = files[i]
var name = file.name.toLowerCase()
if(name.endsWith(".tja")){
tjaFiles.push([file, i])
}else if(name.endsWith(".osu")){
osuFiles.push([file, i])
}else{
otherFiles[file.webkitRelativePath.toLowerCase()] = file
}
}
var songs = []
var courseTypes = {"easy": 0, "normal": 1, "hard": 2, "oni": 3, "ura": 4}
for(var i = 0; i < tjaFiles.length; i++){
let file = tjaFiles[i][0]
let index = tjaFiles[i][1]
var reader = new FileReader()
promises.push(pageEvents.load(reader).then(event => {
var data = event.target.result.replace(/\0/g, "").split("\n")
var tja = new ParseTja(data, "oni", 0, true)
var songObj = {
id: index + 1,
type: "tja",
chart: data,
stars: []
}
var dir = file.webkitRelativePath.toLowerCase()
dir = dir.slice(0, dir.lastIndexOf("/") + 1)
for(var diff in tja.metadata){
var meta = tja.metadata[diff]
songObj.title = songObj.title_en = meta.title || file.name.slice(0, file.name.lastIndexOf("."))
var subtitle = meta.subtitle || ""
if(subtitle.startsWith("--")){
subtitle = subtitle.slice(2)
}
songObj.subtitle = songObj.subtitle_en = subtitle
songObj.preview = meta.demostart ? Math.floor(meta.demostart * 1000) : 0
if(meta.level){
songObj.stars[courseTypes[diff]] = meta.level
}
if(meta.wave){
songObj.music = otherFiles[dir + meta.wave.toLowerCase()]
}
}
if(songObj.music && songObj.stars.filter(star => star).length !== 0){
songs[index] = songObj
}
}))
reader.readAsText(file, "sjis")
}
for(var i = 0; i < osuFiles.length; i++){
let file = osuFiles[i][0]
let index = osuFiles[i][1]
var reader = new FileReader()
promises.push(pageEvents.load(reader).then(event => {
var data = event.target.result.replace(/\0/g, "").split("\n")
var osu = new ParseOsu(data, 0, true)
var dir = file.webkitRelativePath.toLowerCase()
dir = dir.slice(0, dir.lastIndexOf("/") + 1)
var songObj = {
id: index + 1,
type: "osu",
chart: data,
subtitle: osu.metadata.ArtistUnicode || osu.metadata.Artist,
subtitle_en: osu.metadata.Artist || osu.metadata.ArtistUnicode,
preview: osu.generalInfo.PreviewTime,
stars: [null, null, null, parseInt(osu.difficulty.overallDifficulty) || 1],
music: otherFiles[dir + osu.generalInfo.AudioFilename.toLowerCase()]
}
var filename = file.name.slice(0, file.name.lastIndexOf("."))
var title = osu.metadata.TitleUnicode || osu.metadata.Title
if(title){
var suffix = ""
var matches = filename.match(/\[.+?\]$/)
if(matches){
suffix = " " + matches[0]
}
songObj.title = title + suffix
songObj.title_en = (osu.metadata.Title || osu.metadata.TitleUnicode) + suffix
}else{
songObj.title = filename
}
if(songObj.music){
songs[index] = songObj
}
}).catch(() => {}))
reader.readAsText(file)
}
Promise.all(promises).then(() => {
songs = songs.filter(song => typeof song !== "undefined")
if(songs.length){
assets.songs = songs
assets.customSongs = true
assets.customSelected = 0
assets.sounds["don"].play()
this.clean()
setTimeout(() => {
new SongSelect("browse", false, this.touchEnabled)
}, 500)
}else{
this.browse.parentNode.reset()
}
})
}
toSelectDifficulty(fromP2){ toSelectDifficulty(fromP2){
var currentSong = this.songs[this.selectedSong] var currentSong = this.songs[this.selectedSong]
if(p2.session && !fromP2 && currentSong.action !== "random"){ if(p2.session && !fromP2 && currentSong.action !== "random"){
@ -564,6 +711,8 @@ class SongSelect{
this.toTutorial() this.toTutorial()
}else if(currentSong.action === "about"){ }else if(currentSong.action === "about"){
this.toAbout() this.toAbout()
}else if(currentSong.action === "browse"){
this.toBrowse()
} }
} }
this.pointer(false) this.pointer(false)
@ -593,7 +742,11 @@ class SongSelect{
assets.sounds["don"].play() assets.sounds["don"].play()
try{ try{
localStorage["selectedSong"] = this.selectedSong if(assets.customSongs){
assets.customSelected = this.selectedSong
}else{
localStorage["selectedSong"] = this.selectedSong
}
localStorage["selectedDiff"] = difficulty + this.diffOptions.length localStorage["selectedDiff"] = difficulty + this.diffOptions.length
}catch(e){} }catch(e){}
@ -670,6 +823,19 @@ class SongSelect{
}, 500) }, 500)
} }
} }
toBrowse(){
if(assets.customSongs){
assets.customSongs = false
assets.songs = assets.songsDefault
assets.sounds["don"].play()
this.clean()
setTimeout(() => {
new SongSelect("browse", false, this.touchEnabled)
}, 500)
}else{
this.browse.click()
}
}
redraw(){ redraw(){
if(!this.redrawRunning){ if(!this.redrawRunning){
@ -1636,10 +1802,16 @@ class SongSelect{
return snd.previewGain.load(gameConfig.songs_baseurl + id + previewFilename) return snd.previewGain.load(gameConfig.songs_baseurl + id + previewFilename)
} }
songObj.preview_time = 0 new Promise((resolve, reject) => {
loadPreview(previewFilename).catch(() => { if(currentSong.music){
songObj.preview_time = prvTime snd.previewGain.load(currentSong.music, true).then(resolve, reject)
return loadPreview("/main.mp3") }else{
songObj.preview_time = 0
loadPreview(previewFilename).catch(() => {
songObj.preview_time = prvTime
return loadPreview("/main.mp3")
}).then(resolve, reject)
}
}).then(sound => { }).then(sound => {
if(currentId === this.previewId){ if(currentId === this.previewId){
songObj.preview_sound = sound songObj.preview_sound = sound
@ -1799,11 +1971,14 @@ class SongSelect{
}) })
pageEvents.keyRemove(this, "all") pageEvents.keyRemove(this, "all")
pageEvents.remove(loader.screen, ["mousemove", "mouseleave", "mousedown", "touchstart"]) pageEvents.remove(loader.screen, ["mousemove", "mouseleave", "mousedown", "touchstart"])
pageEvents.remove(this.canvas, "touchend")
pageEvents.remove(p2, "message") pageEvents.remove(p2, "message")
if(this.touchEnabled && fullScreenSupported){ if(this.touchEnabled && fullScreenSupported){
pageEvents.remove(this.touchFullBtn, "click") pageEvents.remove(this.touchFullBtn, "click")
delete this.touchFullBtn delete this.touchFullBtn
} }
pageEvents.remove(this.browse, "change")
delete this.browse
delete this.selectable delete this.selectable
delete this.ctx delete this.ctx
delete this.canvas delete this.canvas

View File

@ -4,10 +4,19 @@
this.context = new AudioContext() this.context = new AudioContext()
pageEvents.add(window, ["click", "touchend"], this.pageClicked.bind(this)) pageEvents.add(window, ["click", "touchend"], this.pageClicked.bind(this))
} }
load(url, gain){ load(url, local, gain){
return loader.ajax(url, request => { if(local){
request.responseType = "arraybuffer" var reader = new FileReader()
}).then(response => { var loadPromise = pageEvents.load(reader).then(event => {
return event.target.result
})
reader.readAsArrayBuffer(url)
}else{
var loadPromise = loader.ajax(url, request => {
request.responseType = "arraybuffer"
})
}
return loadPromise.then(response => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
return this.context.decodeAudioData(response, resolve, reject) return this.context.decodeAudioData(response, resolve, reject)
}).catch(error => { }).catch(error => {
@ -66,8 +75,8 @@ class SoundGain{
} }
this.setVolume(1) this.setVolume(1)
} }
load(url){ load(url, local){
return this.soundBuffer.load(url, this) return this.soundBuffer.load(url, local, this)
} }
convertTime(time, absolute){ convertTime(time, absolute){
return this.soundBuffer.convertTime(time, absolute) return this.soundBuffer.convertTime(time, absolute)

View File

@ -2,4 +2,5 @@
<canvas id="song-sel-canvas"></canvas> <canvas id="song-sel-canvas"></canvas>
<div id="song-sel-selectable" tabindex="1"></div> <div id="song-sel-selectable" tabindex="1"></div>
<div id="touch-full-btn"></div> <div id="touch-full-btn"></div>
<form><input id="browse" type="file" webkitdirectory multiple></form>
</div> </div>