mirror of
https://github.com/yuukiwww/taiko-web.git
synced 2024-10-22 17:05:49 +02:00
SongSel: Add browse for local songs button
This commit is contained in:
parent
7f5b1e97c3
commit
a435ed1a6d
@ -19,7 +19,8 @@ body{
|
||||
background-size: 30vh;
|
||||
font-family: TnT, Meiryo, sans-serif;
|
||||
}
|
||||
#assets{
|
||||
#assets,
|
||||
#browse{
|
||||
display: none;
|
||||
}
|
||||
.window{
|
||||
|
@ -51,8 +51,8 @@
|
||||
ideographicComma: /[、。]/,
|
||||
apostrophe: /['']/,
|
||||
degree: /[゚°]/,
|
||||
brackets: /[\((\))「」『』]/,
|
||||
tilde: /[\--~~〜]/,
|
||||
brackets: /[\((\))\[\]「」『』【】]/,
|
||||
tilde: /[\--~~〜_]/,
|
||||
tall: /[bbddffgghhj-lj-ltt♪]/,
|
||||
i: /[ii]/,
|
||||
uppercase: /[A-ZA-Z]/,
|
||||
@ -68,7 +68,8 @@
|
||||
em: /[mwmw]/,
|
||||
emCap: /[MWMW]/,
|
||||
rWidth: /[abdfIjo-rtvabdfIjo-rtv]/,
|
||||
lWidth: /[ilil]/
|
||||
lWidth: /[ilil]/,
|
||||
ura: /\s*[\((]裏[\))]$/
|
||||
}
|
||||
|
||||
var numbersFull = "0123456789"
|
||||
@ -276,13 +277,18 @@
|
||||
var ctx = config.ctx
|
||||
var inputText = config.text
|
||||
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 drawn = []
|
||||
|
||||
var r = this.regex
|
||||
var previousSymbol = ""
|
||||
|
||||
for(var i = 0; i < string.length; i++){
|
||||
let symbol = string[i]
|
||||
if(symbol === " "){
|
||||
@ -297,6 +303,8 @@
|
||||
drawn.push({text: symbol, x: 0, y: 12, h: 45})
|
||||
}else if(symbol === "."){
|
||||
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)){
|
||||
// Comma, full stop
|
||||
drawn.push({text: symbol, x: 13, y: -7, h: 15, scale: [1.2, 0.7]})
|
||||
@ -408,22 +416,28 @@
|
||||
}
|
||||
|
||||
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"){
|
||||
scaling = Math.max(0.6, config.height / drawnHeight)
|
||||
scaling = Math.max(0.6, height / drawnHeight)
|
||||
ctx.translate(40 * mul, 0)
|
||||
ctx.scale(scaling, config.height / drawnHeight)
|
||||
ctx.scale(scaling, height / drawnHeight)
|
||||
ctx.translate(-40 * mul, 0)
|
||||
}else{
|
||||
scaling = config.height / drawnHeight
|
||||
scaling = height / drawnHeight
|
||||
ctx.scale(1, scaling)
|
||||
}
|
||||
if(config.selectable){
|
||||
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 = []
|
||||
if(config.outline){
|
||||
actions.push("stroke")
|
||||
@ -492,7 +506,7 @@
|
||||
config.selectable.appendChild(div)
|
||||
continue
|
||||
}
|
||||
if(symbol.rotate || symbol.scale || symbol.svg){
|
||||
if(symbol.rotate || symbol.scale || symbol.svg || symbol.ura){
|
||||
saved = true
|
||||
ctx.save()
|
||||
|
||||
@ -517,8 +531,24 @@
|
||||
}else{
|
||||
ctx.textAlign = "center"
|
||||
}
|
||||
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){
|
||||
ctx.restore()
|
||||
}
|
||||
|
@ -78,7 +78,8 @@ class Loader{
|
||||
})
|
||||
|
||||
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 => {
|
||||
|
@ -66,16 +66,17 @@ class loadSong{
|
||||
}
|
||||
promises.push(this.loadSongBg(id))
|
||||
|
||||
var songObj = assets.songs.find(song => song.id === id)
|
||||
|
||||
promises.push(new Promise((resolve, reject) => {
|
||||
var songObj
|
||||
assets.songs.forEach(song => {
|
||||
if(song.id == id){
|
||||
songObj = song
|
||||
}
|
||||
})
|
||||
if(songObj.sound){
|
||||
songObj.sound.gain = snd.musicGain
|
||||
resolve()
|
||||
}else if(songObj.music){
|
||||
snd.musicGain.load(songObj.music, true).then(sound => {
|
||||
songObj.sound = sound
|
||||
resolve()
|
||||
}, reject)
|
||||
}else{
|
||||
snd.musicGain.load(gameConfig.songs_baseurl + id + "/main.mp3").then(sound => {
|
||||
songObj.sound = sound
|
||||
@ -83,9 +84,13 @@ class loadSong{
|
||||
}, reject)
|
||||
}
|
||||
}))
|
||||
if(songObj.chart){
|
||||
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(() => {
|
||||
this.setupMultiplayer()
|
||||
}, error => {
|
||||
|
@ -1,5 +1,5 @@
|
||||
class ParseOsu{
|
||||
constructor(fileContent, offset){
|
||||
constructor(fileContent, offset, metaOnly){
|
||||
this.osu = {
|
||||
OFFSET: 0,
|
||||
MSPERBEAT: 1,
|
||||
@ -52,10 +52,12 @@ class ParseOsu{
|
||||
this.metadata = this.parseMetadata()
|
||||
this.editor = this.parseEditor()
|
||||
this.difficulty = this.parseDifficulty()
|
||||
if(!metaOnly){
|
||||
this.timingPoints = this.parseTiming()
|
||||
this.circles = this.parseCircles()
|
||||
this.measures = this.parseMeasures()
|
||||
}
|
||||
}
|
||||
getStartEndIndexes(type){
|
||||
var indexes = {
|
||||
start: 0,
|
||||
@ -186,40 +188,20 @@ class ParseOsu{
|
||||
return measures
|
||||
}
|
||||
parseGeneralInfo(){
|
||||
var generalInfo = {
|
||||
audioFilename: "",
|
||||
audioWait: 0
|
||||
}
|
||||
var generalInfo = {}
|
||||
var indexes = this.getStartEndIndexes("General")
|
||||
for(var i = indexes.start; i<= indexes.end; i++){
|
||||
var [item, key] = this.data[i].split(":")
|
||||
switch(item){
|
||||
case "SliderMultiple":
|
||||
generalInfo.audioFilename = key
|
||||
break
|
||||
case "AudioWait":
|
||||
generalInfo.audioWait = parseInt(key)
|
||||
break
|
||||
}
|
||||
generalInfo[item] = key.trim()
|
||||
}
|
||||
return generalInfo
|
||||
}
|
||||
parseMetadata(){
|
||||
var metadata = {
|
||||
title: "",
|
||||
artist: ""
|
||||
}
|
||||
var metadata = {}
|
||||
var indexes = this.getStartEndIndexes("Metadata")
|
||||
for(var i = indexes.start; i <= indexes.end; i++){
|
||||
var [item, key] = this.data[i].split(":")
|
||||
switch(item){
|
||||
case "TitleUnicode":
|
||||
metadata.title = key
|
||||
break
|
||||
case "ArtistUnicode":
|
||||
metadata.artist = key
|
||||
break
|
||||
}
|
||||
metadata[item] = key.trim()
|
||||
}
|
||||
return metadata
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
class ParseTja{
|
||||
constructor(file, difficulty, offset){
|
||||
constructor(file, difficulty, offset, metaOnly){
|
||||
this.data = []
|
||||
for(let line of file){
|
||||
line = line.replace(/\/\/.*/, "").trim()
|
||||
@ -34,10 +34,12 @@
|
||||
this.metadata = this.parseMetadata()
|
||||
this.measures = []
|
||||
this.beatInfo = {}
|
||||
if(!metaOnly){
|
||||
this.circles = this.parseCircles()
|
||||
}
|
||||
}
|
||||
parseMetadata(){
|
||||
var metaNumbers = ["bpm", "offset"]
|
||||
var metaNumbers = ["bpm", "offset", "demostart", "level"]
|
||||
var inSong = false
|
||||
var courses = {}
|
||||
var currentCourse = {}
|
||||
|
@ -35,6 +35,12 @@ class SongSelect{
|
||||
border: ["#dff0ff", "#6890b2"],
|
||||
outline: "#217abb"
|
||||
},
|
||||
"browse": {
|
||||
sort: 7,
|
||||
background: "#9791ff",
|
||||
border: ["#e2dfff", "#6d68b2"],
|
||||
outline: "#5350ba"
|
||||
},
|
||||
"J-POP": {
|
||||
sort: 0,
|
||||
background: "#219fbb",
|
||||
@ -98,7 +104,8 @@ class SongSelect{
|
||||
preview: song.preview || 0,
|
||||
type: song.type,
|
||||
offset: song.offset,
|
||||
songSkin: song.song_skin || {}
|
||||
songSkin: song.song_skin || {},
|
||||
music: song.music
|
||||
})
|
||||
}
|
||||
this.songs.sort((a, b) => {
|
||||
@ -139,6 +146,17 @@ class SongSelect{
|
||||
action: "about",
|
||||
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({
|
||||
title: "もどる",
|
||||
skin: this.songSkin.back,
|
||||
@ -204,8 +222,10 @@ class SongSelect{
|
||||
this.selectedSong = this.songs.findIndex(song => song.action === fromTutorial)
|
||||
this.playBgm(true)
|
||||
}else{
|
||||
if((!p2.session || fadeIn) && "selectedSong" in localStorage){
|
||||
this.selectedSong = Math.min(Math.max(0, localStorage["selectedSong"] |0), this.songs.length)
|
||||
if(assets.customSongs){
|
||||
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()
|
||||
snd.musicGain.fadeOut()
|
||||
@ -265,6 +285,7 @@ class SongSelect{
|
||||
this.state.moveHover = null
|
||||
})
|
||||
pageEvents.add(loader.screen, ["mousedown", "touchstart"], this.mouseDown.bind(this))
|
||||
pageEvents.add(this.canvas, "touchend", this.touchEnd.bind(this))
|
||||
if(touchEnabled && fullScreenSupported){
|
||||
this.touchFullBtn = document.getElementById("touch-full-btn")
|
||||
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){
|
||||
var mouse = this.mouseOffset(event.offsetX, event.offsetY)
|
||||
var moveTo = null
|
||||
@ -521,6 +555,119 @@ class SongSelect{
|
||||
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){
|
||||
var currentSong = this.songs[this.selectedSong]
|
||||
if(p2.session && !fromP2 && currentSong.action !== "random"){
|
||||
@ -564,6 +711,8 @@ class SongSelect{
|
||||
this.toTutorial()
|
||||
}else if(currentSong.action === "about"){
|
||||
this.toAbout()
|
||||
}else if(currentSong.action === "browse"){
|
||||
this.toBrowse()
|
||||
}
|
||||
}
|
||||
this.pointer(false)
|
||||
@ -593,7 +742,11 @@ class SongSelect{
|
||||
assets.sounds["don"].play()
|
||||
|
||||
try{
|
||||
if(assets.customSongs){
|
||||
assets.customSelected = this.selectedSong
|
||||
}else{
|
||||
localStorage["selectedSong"] = this.selectedSong
|
||||
}
|
||||
localStorage["selectedDiff"] = difficulty + this.diffOptions.length
|
||||
}catch(e){}
|
||||
|
||||
@ -670,6 +823,19 @@ class SongSelect{
|
||||
}, 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(){
|
||||
if(!this.redrawRunning){
|
||||
@ -1636,10 +1802,16 @@ class SongSelect{
|
||||
return snd.previewGain.load(gameConfig.songs_baseurl + id + previewFilename)
|
||||
}
|
||||
|
||||
new Promise((resolve, reject) => {
|
||||
if(currentSong.music){
|
||||
snd.previewGain.load(currentSong.music, true).then(resolve, reject)
|
||||
}else{
|
||||
songObj.preview_time = 0
|
||||
loadPreview(previewFilename).catch(() => {
|
||||
songObj.preview_time = prvTime
|
||||
return loadPreview("/main.mp3")
|
||||
}).then(resolve, reject)
|
||||
}
|
||||
}).then(sound => {
|
||||
if(currentId === this.previewId){
|
||||
songObj.preview_sound = sound
|
||||
@ -1799,11 +1971,14 @@ class SongSelect{
|
||||
})
|
||||
pageEvents.keyRemove(this, "all")
|
||||
pageEvents.remove(loader.screen, ["mousemove", "mouseleave", "mousedown", "touchstart"])
|
||||
pageEvents.remove(this.canvas, "touchend")
|
||||
pageEvents.remove(p2, "message")
|
||||
if(this.touchEnabled && fullScreenSupported){
|
||||
pageEvents.remove(this.touchFullBtn, "click")
|
||||
delete this.touchFullBtn
|
||||
}
|
||||
pageEvents.remove(this.browse, "change")
|
||||
delete this.browse
|
||||
delete this.selectable
|
||||
delete this.ctx
|
||||
delete this.canvas
|
||||
|
@ -4,10 +4,19 @@
|
||||
this.context = new AudioContext()
|
||||
pageEvents.add(window, ["click", "touchend"], this.pageClicked.bind(this))
|
||||
}
|
||||
load(url, gain){
|
||||
return loader.ajax(url, request => {
|
||||
load(url, local, gain){
|
||||
if(local){
|
||||
var reader = new FileReader()
|
||||
var loadPromise = pageEvents.load(reader).then(event => {
|
||||
return event.target.result
|
||||
})
|
||||
reader.readAsArrayBuffer(url)
|
||||
}else{
|
||||
var loadPromise = loader.ajax(url, request => {
|
||||
request.responseType = "arraybuffer"
|
||||
}).then(response => {
|
||||
})
|
||||
}
|
||||
return loadPromise.then(response => {
|
||||
return new Promise((resolve, reject) => {
|
||||
return this.context.decodeAudioData(response, resolve, reject)
|
||||
}).catch(error => {
|
||||
@ -66,8 +75,8 @@ class SoundGain{
|
||||
}
|
||||
this.setVolume(1)
|
||||
}
|
||||
load(url){
|
||||
return this.soundBuffer.load(url, this)
|
||||
load(url, local){
|
||||
return this.soundBuffer.load(url, local, this)
|
||||
}
|
||||
convertTime(time, absolute){
|
||||
return this.soundBuffer.convertTime(time, absolute)
|
||||
|
@ -2,4 +2,5 @@
|
||||
<canvas id="song-sel-canvas"></canvas>
|
||||
<div id="song-sel-selectable" tabindex="1"></div>
|
||||
<div id="touch-full-btn"></div>
|
||||
<form><input id="browse" type="file" webkitdirectory multiple></form>
|
||||
</div>
|
||||
|
Loading…
Reference in New Issue
Block a user