CustomSongs: Restore custom song list after reload

Uses the File System Access API supported in some browsers to keep the custom song list between sessions, restoring it back even when the page was closed.
This commit is contained in:
LoveEevee 2021-05-27 20:23:19 +03:00
parent 1fceaadc7d
commit b42b246a99
8 changed files with 230 additions and 64 deletions

View File

@ -4,6 +4,21 @@ function readFile(file, arrayBuffer, encoding){
reader[arrayBuffer ? "readAsArrayBuffer" : "readAsText"](file, encoding) reader[arrayBuffer ? "readAsArrayBuffer" : "readAsText"](file, encoding)
return promise return promise
} }
function filePermission(file){
return file.queryPermission().then(response => {
if(response === "granted"){
return file
}else{
return file.requestPermission().then(response => {
if(response === "granted"){
return file
}else{
return Promise.reject(file)
}
})
}
})
}
class RemoteFile{ class RemoteFile{
constructor(url){ constructor(url){
this.url = url this.url = url
@ -54,6 +69,23 @@ class LocalFile{
return Promise.resolve(this.file) return Promise.resolve(this.file)
} }
} }
class FilesystemFile{
constructor(file, path){
this.file = file
this.path = path
this.url = this.path
this.name = file.name
}
arrayBuffer(){
return this.blob().then(blob => blob.arrayBuffer())
}
read(encoding){
return this.blob().then(blob => readFile(blob, false, encoding))
}
blob(){
return filePermission(this.file).then(file => file.getFile())
}
}
class GdriveFile{ class GdriveFile{
constructor(fileObj){ constructor(fileObj){
this.path = fileObj.path this.path = fileObj.path

View File

@ -35,7 +35,8 @@ var assets = {
"account.js", "account.js",
"lyrics.js", "lyrics.js",
"customsongs.js", "customsongs.js",
"abstractfile.js" "abstractfile.js",
"idb.js"
], ],
"css": [ "css": [
"main.css", "main.css",

View File

@ -1,12 +1,23 @@
class CustomSongs{ class CustomSongs{
constructor(touchEnabled){ constructor(touchEnabled, noPage){
this.loaderDiv = document.createElement("div")
this.loaderDiv.innerHTML = assets.pages["loadsong"]
var loadingText = this.loaderDiv.querySelector("#loading-text")
this.setAltText(loadingText, strings.loading)
this.locked = false
this.mode = "main"
if(noPage){
this.noPage = true
return
}
this.touchEnabled = touchEnabled this.touchEnabled = touchEnabled
loader.changePage("customsongs", true) loader.changePage("customsongs", true)
if(touchEnabled){ if(touchEnabled){
this.getElement("view-outer").classList.add("touch-enabled") this.getElement("view-outer").classList.add("touch-enabled")
} }
this.locked = false
this.mode = "main"
var tutorialTitle = this.getElement("view-title") var tutorialTitle = this.getElement("view-title")
this.setAltText(tutorialTitle, strings.customSongs.title) this.setAltText(tutorialTitle, strings.customSongs.title)
@ -19,7 +30,7 @@ class CustomSongs{
this.items = [] this.items = []
this.linkLocalFolder = document.getElementById("link-localfolder") this.linkLocalFolder = document.getElementById("link-localfolder")
this.hasLocal = "webkitdirectory" in HTMLInputElement.prototype && !(/Android|iPhone|iPad/.test(navigator.userAgent)) this.hasLocal = (typeof showDirectoryPicker === "function" || "webkitdirectory" in HTMLInputElement.prototype) && !(/Android|iPhone|iPad/.test(navigator.userAgent))
this.selected = -1 this.selected = -1
if(this.hasLocal){ if(this.hasLocal){
@ -68,12 +79,7 @@ class CustomSongs{
this.selected = this.items.length - 1 this.selected = this.items.length - 1
} }
this.loaderDiv = document.createElement("div") if(DataTransferItem.prototype.getAsFileSystemHandle || DataTransferItem.prototype.webkitGetAsEntry){
this.loaderDiv.innerHTML = assets.pages["loadsong"]
var loadingText = this.loaderDiv.querySelector("#loading-text")
this.setAltText(loadingText, strings.loading)
if(DataTransferItem.prototype.webkitGetAsEntry){
this.dropzone = document.getElementById("dropzone") this.dropzone = document.getElementById("dropzone")
var dropContent = this.dropzone.getElementsByClassName("view-content")[0] var dropContent = this.dropzone.getElementsByClassName("view-content")[0]
dropContent.innerText = strings.customSongs.dropzone dropContent.innerText = strings.customSongs.dropzone
@ -142,7 +148,19 @@ class CustomSongs{
return return
} }
this.changeSelected(this.linkLocalFolder) this.changeSelected(this.linkLocalFolder)
this.browse.click() if(typeof showDirectoryPicker === "function"){
return showDirectoryPicker().then(file => {
this.walkFilesystem(file).then(files => this.importLocal(files)).then(e => {
db.setItem("customFolder", file)
}).catch(e => {
if(e !== "cancel"){
return Promise.reject(e)
}
})
}, () => {})
}else{
this.browse.click()
}
} }
browseChange(event){ browseChange(event){
var files = [] var files = []
@ -151,6 +169,24 @@ class CustomSongs{
} }
this.importLocal(files) this.importLocal(files)
} }
walkFilesystem(dir, path=dir.name + "/", output=[]){
return filePermission(dir).then(dir => {
var values = dir.values()
var walkValues = () => values.next().then(generator => {
if(generator.done){
return output
}
var file = generator.value
if(file.kind === "directory"){
return this.walkFilesystem(file, path + file.name + "/", output).then(() => walkValues())
}else{
output.push(new FilesystemFile(file, path + file.name))
return walkValues()
}
})
return walkValues()
}, () => Promise.resolve())
}
filesDropped(event){ filesDropped(event){
event.preventDefault() event.preventDefault()
this.dropzone.classList.remove("dragover") this.dropzone.classList.remove("dragover")
@ -158,46 +194,69 @@ class CustomSongs{
if(this.locked){ if(this.locked){
return return
} }
var files = [] var allFiles = []
var walk = (entry, path="") => {
return new Promise(resolve => {
if(entry.isFile){
entry.file(file => {
files.push(new LocalFile(file, path + file.name))
return resolve()
}, resolve)
}else if(entry.isDirectory){
var dirReader = entry.createReader()
dirReader.readEntries(entries => {
var dirPromises = []
for(var i = 0; i < entries.length; i++){
dirPromises.push(walk(entries[i], path + entry.name + "/"))
}
return Promise.all(dirPromises).then(resolve)
}, resolve)
}else{
return resolve()
}
})
}
var dropPromises = [] var dropPromises = []
for(var i = 0; i < event.dataTransfer.items.length; i++){ var dropLength = event.dataTransfer.items.length
var entry = event.dataTransfer.items[i].webkitGetAsEntry() for(var i = 0; i < dropLength; i++){
if(entry){ var item = event.dataTransfer.items[i]
dropPromises.push(walk(entry)) let promise
if(item.getAsFileSystemHandle){
promise = item.getAsFileSystemHandle().then(file => {
if(file.kind === "directory"){
return this.walkFilesystem(file).then(files => {
if(files.length && dropLength === 1){
db.setItem("customFolder", file)
}
return files
})
}else{
return [new FilesystemFile(file, file.name)]
}
})
}else{
var entry = item.webkitGetAsEntry()
if(entry){
promise = this.walkEntry(entry)
}
}
if(promise){
dropPromises.push(promise.then(files => {
allFiles = allFiles.concat(files)
}))
} }
} }
Promise.all(dropPromises).then(() => this.importLocal(files)) Promise.all(dropPromises).then(() => this.importLocal(allFiles))
}
walkEntry(entry, path="", output=[]){
return new Promise(resolve => {
if(entry.isFile){
entry.file(file => {
output.push(new LocalFile(file, path + file.name))
return resolve()
}, resolve)
}else if(entry.isDirectory){
var dirReader = entry.createReader()
dirReader.readEntries(entries => {
var dirPromises = []
for(var i = 0; i < entries.length; i++){
dirPromises.push(this.walkEntry(entries[i], path + entry.name + "/", output))
}
return Promise.all(dirPromises).then(resolve)
}, resolve)
}else{
return resolve()
}
}).then(() => output)
} }
importLocal(files){ importLocal(files){
if(!files.length){ if(!files.length){
return return Promise.resolve("cancel")
} }
this.locked = true this.locked = true
this.loading(true) this.loading(true)
var importSongs = new ImportSongs() var importSongs = new ImportSongs()
importSongs.load(files).then(this.songsLoaded.bind(this), e => { return importSongs.load(files).then(this.songsLoaded.bind(this), e => {
this.browse.parentNode.reset() this.browse.parentNode.reset()
this.locked = false this.locked = false
this.loading(false) this.loading(false)
@ -315,7 +374,7 @@ class CustomSongs{
var length = songs.length var length = songs.length
assets.songs = songs assets.songs = songs
assets.customSongs = true assets.customSongs = true
assets.customSelected = 0 assets.customSelected = this.noPage ? +localStorage.getItem("customSelected") : 0
} }
assets.sounds["se_don"].play() assets.sounds["se_don"].play()
this.clean() this.clean()
@ -393,15 +452,18 @@ class CustomSongs{
touched = this.touchEnabled touched = this.touchEnabled
} }
this.clean() this.clean()
assets.sounds[confirm ? "se_don" : "se_cancel"].play() if(!this.noPage){
setTimeout(() => { assets.sounds[confirm ? "se_don" : "se_cancel"].play()
}
return new Promise(resolve => setTimeout(() => {
new SongSelect("customSongs", false, touched) new SongSelect("customSongs", false, touched)
}, 500) resolve()
}, 500))
} }
showError(text){ showError(text){
this.locked = false this.locked = false
this.loading(false) this.loading(false)
if(this.mode === "error"){ if(this.noPage || this.mode === "error"){
return return
} }
this.mode = "error" this.mode = "error"
@ -418,6 +480,10 @@ class CustomSongs{
assets.sounds[confirm ? "se_don" : "se_cancel"].play() assets.sounds[confirm ? "se_don" : "se_cancel"].play()
} }
clean(){ clean(){
delete this.loaderDiv
if(this.noPage){
return
}
this.keyboard.clean() this.keyboard.clean()
this.gamepad.clean() this.gamepad.clean()
pageEvents.remove(this.browse, "change") pageEvents.remove(this.browse, "change")
@ -443,7 +509,6 @@ class CustomSongs{
delete this.linkPrivacy delete this.linkPrivacy
delete this.endButton delete this.endButton
delete this.items delete this.items
delete this.loaderDiv
delete this.errorDiv delete this.errorDiv
delete this.errorContent delete this.errorContent
delete this.errorEnd delete this.errorEnd

51
public/src/js/idb.js Normal file
View File

@ -0,0 +1,51 @@
class IDB{
constructor(name, store){
this.name = name
this.store = store
}
init(){
if(this.db){
return Promise.resolve(this.db)
}
var request = indexedDB.open(this.name)
request.onupgradeneeded = event => {
var db = event.target.result
db.createObjectStore(this.store)
}
return this.promise(request).then(result => {
this.db = result
return this.db
}, target =>
console.warn("DB error", target)
)
}
promise(request){
return new Promise((resolve, reject) => {
return pageEvents.race(request, "success", "error").then(response => {
if(response.type === "success"){
return resolve(event.target.result)
}else{
return reject(event.target)
}
})
})
}
transaction(method, ...args){
return this.init().then(db =>
db.transaction(this.store, "readwrite").objectStore(this.store)[method](...args)
).then(this.promise.bind(this))
}
getItem(name){
return this.transaction("get", name)
}
setItem(name, value){
return this.transaction("put", value, name)
}
removeItem(name){
return this.transaction("delete", name)
}
removeDB(){
delete this.db
return indexedDB.deleteDatabase(this.name)
}
}

View File

@ -252,6 +252,7 @@ class Loader{
settings = new Settings() settings = new Settings()
pageEvents.setKbd() pageEvents.setKbd()
scoreStorage = new ScoreStorage() scoreStorage = new ScoreStorage()
db = new IDB("taiko", "store")
Promise.all(this.promises).then(() => { Promise.all(this.promises).then(() => {
if(this.error){ if(this.error){

View File

@ -90,6 +90,7 @@ var settings
var scoreStorage var scoreStorage
var account = {} var account = {}
var gpicker var gpicker
var db
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"){

View File

@ -210,7 +210,7 @@ class SongSelect{
if(!assets.customSongs && !fromTutorial && !("selectedSong" in localStorage) && !songId){ if(!assets.customSongs && !fromTutorial && !("selectedSong" in localStorage) && !songId){
fromTutorial = touchEnabled ? "about" : "tutorial" fromTutorial = touchEnabled ? "about" : "tutorial"
} }
if(p2.session){ if(p2.session || assets.customSongs && "customSelected" in localStorage){
fromTutorial = false fromTutorial = false
} }
@ -231,7 +231,7 @@ class SongSelect{
if(songIdIndex !== -1){ if(songIdIndex !== -1){
this.selectedSong = songIdIndex this.selectedSong = songIdIndex
}else if(assets.customSongs){ }else if(assets.customSongs){
this.selectedSong = assets.customSelected this.selectedSong = Math.min(Math.max(0, assets.customSelected), this.songs.length - 1)
}else if((!p2.session || fadeIn) && "selectedSong" in localStorage){ }else if((!p2.session || fadeIn) && "selectedSong" in localStorage){
this.selectedSong = Math.min(Math.max(0, localStorage["selectedSong"] |0), this.songs.length - 1) this.selectedSong = Math.min(Math.max(0, localStorage["selectedSong"] |0), this.songs.length - 1)
} }
@ -508,7 +508,7 @@ class SongSelect{
moveTo = "account" moveTo = "account"
}else if(p2.session && 438 < mouse.x && mouse.x < 834 && mouse.y > 603){ }else if(p2.session && 438 < mouse.x && mouse.x < 834 && mouse.y > 603){
moveTo = "session" moveTo = "session"
}else if(!p2.session && mouse.x > 641 && mouse.y > 603 && p2.socket.readyState === 1 && !assets.customSongs){ }else if(!p2.session && mouse.x > 641 && mouse.y > 603 && p2.socket && p2.socket.readyState === 1 && !assets.customSongs){
moveTo = "session" moveTo = "session"
}else{ }else{
var moveTo = this.songSelMouse(mouse.x, mouse.y) var moveTo = this.songSelMouse(mouse.x, mouse.y)
@ -739,6 +739,7 @@ class SongSelect{
try{ try{
if(assets.customSongs){ if(assets.customSongs){
assets.customSelected = this.selectedSong assets.customSelected = this.selectedSong
localStorage["customSelected"] = this.selectedSong
}else{ }else{
localStorage["selectedSong"] = this.selectedSong localStorage["selectedSong"] = this.selectedSong
} }
@ -850,6 +851,8 @@ class SongSelect{
setTimeout(() => { setTimeout(() => {
new SongSelect("customSongs", false, this.touchEnabled) new SongSelect("customSongs", false, this.touchEnabled)
}, 500) }, 500)
localStorage.removeItem("customSelected")
db.removeItem("customFolder")
pageEvents.send("import-songs-default") pageEvents.send("import-songs-default")
}else{ }else{
localStorage["selectedSong"] = this.selectedSong localStorage["selectedSong"] = this.selectedSong
@ -1174,6 +1177,7 @@ class SongSelect{
this.state.locked = 2 this.state.locked = 2
if(assets.customSongs){ if(assets.customSongs){
assets.customSelected = this.selectedSong assets.customSelected = this.selectedSong
localStorage["customSelected"] = this.selectedSong
}else if(!p2.session){ }else if(!p2.session){
try{ try{
localStorage["selectedSong"] = this.selectedSong localStorage["selectedSong"] = this.selectedSong
@ -2097,7 +2101,7 @@ class SongSelect{
ctx.lineTo(x + 4, y + 4) ctx.lineTo(x + 4, y + 4)
ctx.lineTo(x + 4, y + h) ctx.lineTo(x + 4, y + h)
ctx.fill() ctx.fill()
if(screen !== "difficulty" && p2.socket.readyState === 1 && !assets.customSongs){ if(screen !== "difficulty" && p2.socket && p2.socket.readyState === 1 && !assets.customSongs){
var elapsed = (ms - this.state.screenMS) % 3100 var elapsed = (ms - this.state.screenMS) % 3100
var fade = 1 var fade = 1
if(!p2.session && screen === "song"){ if(!p2.session && screen === "song"){

View File

@ -1,6 +1,7 @@
class Titlescreen{ class Titlescreen{
constructor(songId){ constructor(songId){
this.songId = songId this.songId = songId
db.getItem("customFolder").then(folder => this.customFolder = folder)
if(!songId){ if(!songId){
loader.changePage("titlescreen", false) loader.changePage("titlescreen", false)
@ -50,7 +51,7 @@ class Titlescreen{
onPressed(pressed, name){ onPressed(pressed, name){
if(pressed){ if(pressed){
if(name === "gamepadConfirm" && snd.buffer.context.state === "suspended"){ if(name === "gamepadConfirm" && (snd.buffer.context.state === "suspended" || this.customFolder)){
return return
} }
this.titleScreen.style.cursor = "auto" this.titleScreen.style.cursor = "auto"
@ -62,18 +63,28 @@ class Titlescreen{
goNext(fromP2){ goNext(fromP2){
if(p2.session && !fromP2){ if(p2.session && !fromP2){
p2.send("songsel") p2.send("songsel")
}else if(fromP2 || localStorage.getItem("tutorial") === "true"){
if(this.touched){
localStorage.setItem("tutorial", "true")
}
pageEvents.remove(p2, "message")
setTimeout(() => {
new SongSelect(false, false, this.touched, this.songId)
}, 500)
}else{ }else{
setTimeout(() => { if(fromP2 || this.customFolder || localStorage.getItem("tutorial") === "true"){
new SettingsView(this.touched, true, this.songId) if(this.touched){
}, 500) localStorage.setItem("tutorial", "true")
}
pageEvents.remove(p2, "message")
if(this.customFolder && !fromP2 && !assets.customSongs){
var customSongs = new CustomSongs(this.touched, true)
customSongs.walkFilesystem(this.customFolder).then(files => customSongs.importLocal(files)).catch(() => {
db.removeItem("customFolder")
new SongSelect(false, false, this.touched, this.songId)
})
}else{
setTimeout(() => {
new SongSelect(false, false, this.touched, this.songId)
}, 500)
}
}else{
setTimeout(() => {
new SettingsView(this.touched, true, this.songId)
}, 500)
}
} }
} }
setLang(lang, noEvent){ setLang(lang, noEvent){