Merge pull request #369 from bui/customsongs-filesystem

CustomSongs: Restore custom song list after reload
This commit is contained in:
Bui 2021-06-06 13:39:18 +01:00 committed by GitHub
commit 8fbb45db4f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 255 additions and 66 deletions

View File

@ -4,6 +4,21 @@ function readFile(file, arrayBuffer, encoding){
reader[arrayBuffer ? "readAsArrayBuffer" : "readAsText"](file, encoding)
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{
constructor(url){
this.url = url
@ -54,6 +69,23 @@ class LocalFile{
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{
constructor(fileObj){
this.path = fileObj.path

View File

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

View File

@ -1,12 +1,23 @@
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
loader.changePage("customsongs", true)
if(touchEnabled){
this.getElement("view-outer").classList.add("touch-enabled")
}
this.locked = false
this.mode = "main"
var tutorialTitle = this.getElement("view-title")
this.setAltText(tutorialTitle, strings.customSongs.title)
@ -19,7 +30,7 @@ class CustomSongs{
this.items = []
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
if(this.hasLocal){
@ -68,12 +79,8 @@ class CustomSongs{
this.selected = this.items.length - 1
}
this.loaderDiv = document.createElement("div")
this.loaderDiv.innerHTML = assets.pages["loadsong"]
var loadingText = this.loaderDiv.querySelector("#loading-text")
this.setAltText(loadingText, strings.loading)
if(DataTransferItem.prototype.webkitGetAsEntry){
this.fileSystem = location.protocol === "https:" && DataTransferItem.prototype.getAsFileSystemHandle
if(this.fileSystem || DataTransferItem.prototype.webkitGetAsEntry){
this.dropzone = document.getElementById("dropzone")
var dropContent = this.dropzone.getElementsByClassName("view-content")[0]
dropContent.innerText = strings.customSongs.dropzone
@ -142,7 +149,19 @@ class CustomSongs{
return
}
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){
var files = []
@ -151,6 +170,23 @@ class CustomSongs{
}
this.importLocal(files)
}
walkFilesystem(file, path="", output=[]){
if(file.kind === "directory"){
return filePermission(file).then(file => {
var values = file.values()
var walkValues = () => values.next().then(generator => {
if(generator.done){
return output
}
return this.walkFilesystem(generator.value, path + file.name + "/", output).then(() => walkValues())
})
return walkValues()
}, () => Promise.resolve())
}else{
output.push(new FilesystemFile(file, path + file.name))
return Promise.resolve(output)
}
}
filesDropped(event){
event.preventDefault()
this.dropzone.classList.remove("dragover")
@ -158,46 +194,69 @@ class CustomSongs{
if(this.locked){
return
}
var files = []
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 allFiles = []
var dropPromises = []
var dbItems = []
for(var i = 0; i < event.dataTransfer.items.length; i++){
var entry = event.dataTransfer.items[i].webkitGetAsEntry()
if(entry){
dropPromises.push(walk(entry))
var item = event.dataTransfer.items[i]
let promise
if(this.fileSystem){
promise = item.getAsFileSystemHandle().then(file => {
dbItems.push(file)
return this.walkFilesystem(file)
})
}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)).then(() => {
if(dbItems.length){
db.setItem("customFolder", dbItems)
}
})
}
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){
if(!files.length){
return
if(this.noPage){
return Promise.reject("cancel")
}else{
return Promise.resolve("cancel")
}
}
this.locked = true
this.loading(true)
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.locked = false
this.loading(false)
@ -304,6 +363,9 @@ class CustomSongs{
open("privacy")
}
loading(show){
if(this.noPage){
return
}
if(show){
loader.screen.appendChild(this.loaderDiv)
}else if(this.loaderDiv.parentNode){
@ -315,9 +377,11 @@ class CustomSongs{
var length = songs.length
assets.songs = songs
assets.customSongs = true
assets.customSelected = 0
assets.customSelected = this.noPage ? +localStorage.getItem("customSelected") : 0
}
if(!this.noPage){
assets.sounds["se_don"].play()
}
assets.sounds["se_don"].play()
this.clean()
setTimeout(() => {
new SongSelect("customSongs", false, this.touchEnabled)
@ -393,15 +457,18 @@ class CustomSongs{
touched = this.touchEnabled
}
this.clean()
assets.sounds[confirm ? "se_don" : "se_cancel"].play()
setTimeout(() => {
if(!this.noPage){
assets.sounds[confirm ? "se_don" : "se_cancel"].play()
}
return new Promise(resolve => setTimeout(() => {
new SongSelect("customSongs", false, touched)
}, 500)
resolve()
}, 500))
}
showError(text){
this.locked = false
this.loading(false)
if(this.mode === "error"){
if(this.noPage || this.mode === "error"){
return
}
this.mode = "error"
@ -418,6 +485,10 @@ class CustomSongs{
assets.sounds[confirm ? "se_don" : "se_cancel"].play()
}
clean(){
delete this.loaderDiv
if(this.noPage){
return
}
this.keyboard.clean()
this.gamepad.clean()
pageEvents.remove(this.browse, "change")
@ -443,7 +514,6 @@ class CustomSongs{
delete this.linkPrivacy
delete this.endButton
delete this.items
delete this.loaderDiv
delete this.errorDiv
delete this.errorContent
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()
pageEvents.setKbd()
scoreStorage = new ScoreStorage()
db = new IDB("taiko", "store")
Promise.all(this.promises).then(() => {
if(this.error){

View File

@ -90,6 +90,7 @@ var settings
var scoreStorage
var account = {}
var gpicker
var db
pageEvents.add(root, ["touchstart", "touchmove", "touchend"], event => {
if(event.cancelable && cancelTouch && event.target.tagName !== "SELECT"){

View File

@ -210,7 +210,7 @@ class SongSelect{
if(!assets.customSongs && !fromTutorial && !("selectedSong" in localStorage) && !songId){
fromTutorial = touchEnabled ? "about" : "tutorial"
}
if(p2.session){
if(p2.session || assets.customSongs && "customSelected" in localStorage){
fromTutorial = false
}
@ -231,7 +231,7 @@ class SongSelect{
if(songIdIndex !== -1){
this.selectedSong = songIdIndex
}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){
this.selectedSong = Math.min(Math.max(0, localStorage["selectedSong"] |0), this.songs.length - 1)
}
@ -508,7 +508,7 @@ class SongSelect{
moveTo = "account"
}else if(p2.session && 438 < mouse.x && mouse.x < 834 && mouse.y > 603){
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"
}else{
var moveTo = this.songSelMouse(mouse.x, mouse.y)
@ -739,6 +739,7 @@ class SongSelect{
try{
if(assets.customSongs){
assets.customSelected = this.selectedSong
localStorage["customSelected"] = this.selectedSong
}else{
localStorage["selectedSong"] = this.selectedSong
}
@ -756,7 +757,7 @@ class SongSelect{
autoplay = true
}else if(shift){
autoplay = shift
}else if(p2.socket.readyState === 1 && !assets.customSongs){
}else if(p2.socket && p2.socket.readyState === 1 && !assets.customSongs){
multiplayer = ctrl
}
var diff = this.difficultyId[difficulty]
@ -832,7 +833,7 @@ class SongSelect{
this.state.moveHover = null
}else{
localStorage["selectedSong"] = this.selectedSong
this.playSound("se_don")
this.clean()
setTimeout(() => {
@ -850,6 +851,8 @@ class SongSelect{
setTimeout(() => {
new SongSelect("customSongs", false, this.touchEnabled)
}, 500)
localStorage.removeItem("customSelected")
db.removeItem("customFolder")
pageEvents.send("import-songs-default")
}else{
localStorage["selectedSong"] = this.selectedSong
@ -1174,6 +1177,7 @@ class SongSelect{
this.state.locked = 2
if(assets.customSongs){
assets.customSelected = this.selectedSong
localStorage["customSelected"] = this.selectedSong
}else if(!p2.session){
try{
localStorage["selectedSong"] = this.selectedSong
@ -2097,7 +2101,7 @@ class SongSelect{
ctx.lineTo(x + 4, y + 4)
ctx.lineTo(x + 4, y + h)
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 fade = 1
if(!p2.session && screen === "song"){

View File

@ -1,6 +1,7 @@
class Titlescreen{
constructor(songId){
this.songId = songId
db.getItem("customFolder").then(folder => this.customFolder = folder)
if(!songId){
loader.changePage("titlescreen", false)
@ -50,30 +51,58 @@ class Titlescreen{
onPressed(pressed, name){
if(pressed){
if(name === "gamepadConfirm" && snd.buffer.context.state === "suspended"){
if(name === "gamepadConfirm" && (snd.buffer.context.state === "suspended" || this.customFolder)){
return
}
this.titleScreen.style.cursor = "auto"
this.clean()
assets.sounds["se_don"].play()
if(!this.customFolder || assets.customSongs){
assets.sounds["se_don"].play()
}
this.goNext()
}
}
goNext(fromP2){
if(p2.session && !fromP2){
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{
setTimeout(() => {
new SettingsView(this.touched, true, this.songId)
}, 500)
if(fromP2 || this.customFolder || localStorage.getItem("tutorial") === "true"){
if(this.touched){
localStorage.setItem("tutorial", "true")
}
pageEvents.remove(p2, "message")
if(this.customFolder && !fromP2 && !assets.customSongs){
var customSongs = new CustomSongs(this.touched, true)
var soundPlayed = false
var promises = []
var allFiles = []
this.customFolder.forEach(file => {
promises.push(customSongs.walkFilesystem(file, undefined, allFiles))
})
Promise.all(promises).then(() => {
assets.sounds["se_don"].play()
soundPlayed = true
return customSongs.importLocal(allFiles)
}).catch(() => {
localStorage.removeItem("customSelected")
db.removeItem("customFolder")
if(!soundPlayed){
assets.sounds["se_don"].play()
}
setTimeout(() => {
new SongSelect(false, false, this.touched, this.songId)
}, 500)
})
}else{
setTimeout(() => {
new SongSelect(false, false, this.touched, this.songId)
}, 500)
}
}else{
setTimeout(() => {
new SettingsView(this.touched, true, this.songId)
}, 500)
}
}
}
setLang(lang, noEvent){