Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8d58bf683f |
@@ -0,0 +1,20 @@
|
||||
I will optimize the loading process by implementing a **Multi-Threaded Worker Loader**. This involves creating a pool of Web Workers to fetch assets (JavaScript, Audio, Images, Views) in parallel, offloading the network initiation and handling from the main thread.
|
||||
|
||||
### Plan:
|
||||
1. **Create `public/src/js/loader-worker.js`**:
|
||||
* This worker will handle `fetch` requests for different resource types (`text`, `blob`, `arraybuffer`).
|
||||
* It will transfer the data back to the main thread (using zero-copy transfer for `ArrayBuffer`).
|
||||
|
||||
2. **Modify `public/src/js/loader.js`**:
|
||||
* **Initialize Worker Pool**: Create a pool of workers (defaulting to 4) in the `Loader` class.
|
||||
* **Implement `workerFetch(url, type)`**: A method to distribute fetch tasks to the worker pool.
|
||||
* **Override `ajax(url, ...)`**: Intercept requests for static assets (`src/`, `assets/`, etc.) and route them through `workerFetch`. Keep API calls (`api/`) on the main thread to ensure session stability.
|
||||
* **Update `loadScript(url)`**: Change it to fetch the script content via `workerFetch` and inject it using a `<script>` tag with inline content. This ensures JS files are also loaded via the "multi-process" mechanism.
|
||||
* **Update `loadSound` and `RemoteFile` logic**: Since `RemoteFile` uses `loader.ajax`, routing `ajax` to workers will automatically parallelize audio loading.
|
||||
|
||||
### Technical Details:
|
||||
* **Concurrency**: 4 Workers will be used to maximize throughput without overloading the browser's connection limit per domain.
|
||||
* **Resource Types**:
|
||||
* **JS/Views**: Fetched as `text`.
|
||||
* **Images**: Fetched as `blob` -> `URL.createObjectURL`.
|
||||
* **Audio**: Fetched as `arraybuffer` -> `AudioContext.decodeAudioData`.
|
||||
155
app.py
155
app.py
@@ -105,9 +105,6 @@ db.users.create_index('username', unique=True)
|
||||
db.songs.create_index('id', unique=True)
|
||||
db.songs.create_index('song_type')
|
||||
db.scores.create_index('username')
|
||||
db.leaderboards.create_index([('song_id', 1), ('difficulty', 1), ('month', 1), ('score_value', -1)])
|
||||
db.leaderboards.create_index([('song_id', 1), ('difficulty', 1), ('username', 1), ('month', 1)], unique=True)
|
||||
db.leaderboards.create_index('month')
|
||||
|
||||
|
||||
class HashException(Exception):
|
||||
@@ -749,158 +746,6 @@ def route_api_scores_get():
|
||||
return jsonify({'status': 'ok', 'scores': scores, 'username': user['username'], 'display_name': user['display_name'], 'don': don})
|
||||
|
||||
|
||||
@app.route(basedir + 'api/leaderboard/submit', methods=['POST'])
|
||||
@login_required
|
||||
def route_api_leaderboard_submit():
|
||||
data = request.get_json()
|
||||
if not schema.validate(data, schema.leaderboard_submit):
|
||||
return abort(400)
|
||||
|
||||
username = session.get('username')
|
||||
user = db.users.find_one({'username': username})
|
||||
if not user:
|
||||
return api_error('user_not_found')
|
||||
|
||||
song_id = data.get('song_id')
|
||||
difficulty = data.get('difficulty')
|
||||
score_data = data.get('score')
|
||||
|
||||
# Validate difficulty
|
||||
valid_difficulties = ['easy', 'normal', 'hard', 'oni', 'ura']
|
||||
if difficulty not in valid_difficulties:
|
||||
return api_error('invalid_difficulty')
|
||||
|
||||
# Get current month (YYYY-MM format)
|
||||
current_month = time.strftime('%Y-%m', time.gmtime())
|
||||
|
||||
# Check if user already has a record for this song/difficulty/month
|
||||
existing = db.leaderboards.find_one({
|
||||
'song_id': song_id,
|
||||
'difficulty': difficulty,
|
||||
'username': username,
|
||||
'month': current_month
|
||||
})
|
||||
|
||||
# Parse score (assuming it's in the same format as the scores collection)
|
||||
try:
|
||||
if isinstance(score_data, str):
|
||||
import json as json_module
|
||||
score_obj = json_module.loads(score_data)
|
||||
else:
|
||||
score_obj = score_data
|
||||
|
||||
score_value = int(score_obj.get('score', 0))
|
||||
except:
|
||||
return api_error('invalid_score_format')
|
||||
|
||||
if existing:
|
||||
# Update only if new score is higher
|
||||
existing_score = existing.get('score_value', 0)
|
||||
if score_value > existing_score:
|
||||
db.leaderboards.update_one(
|
||||
{'_id': existing['_id']},
|
||||
{'$set': {
|
||||
'score': score_data,
|
||||
'score_value': score_value,
|
||||
'display_name': user['display_name'],
|
||||
'submitted_at': time.time()
|
||||
}}
|
||||
)
|
||||
return jsonify({'status': 'ok', 'message': 'score_updated'})
|
||||
else:
|
||||
return jsonify({'status': 'ok', 'message': 'score_not_higher'})
|
||||
else:
|
||||
# Check if this score would be in top 50
|
||||
count = db.leaderboards.count_documents({
|
||||
'song_id': song_id,
|
||||
'difficulty': difficulty,
|
||||
'month': current_month
|
||||
})
|
||||
|
||||
if count >= 50:
|
||||
# Find the 50th score
|
||||
leaderboard = list(db.leaderboards.find({
|
||||
'song_id': song_id,
|
||||
'difficulty': difficulty,
|
||||
'month': current_month
|
||||
}).sort('score_value', -1).limit(50))
|
||||
|
||||
if len(leaderboard) >= 50:
|
||||
last_score = leaderboard[49].get('score_value', 0)
|
||||
if score_value <= last_score:
|
||||
return jsonify({'status': 'ok', 'message': 'score_too_low'})
|
||||
|
||||
# Insert new record
|
||||
db.leaderboards.insert_one({
|
||||
'song_id': song_id,
|
||||
'difficulty': difficulty,
|
||||
'username': username,
|
||||
'display_name': user['display_name'],
|
||||
'score': score_data,
|
||||
'score_value': score_value,
|
||||
'submitted_at': time.time(),
|
||||
'month': current_month
|
||||
})
|
||||
|
||||
# Remove entries beyond 50th place
|
||||
if count >= 50:
|
||||
# Get all entries sorted by score
|
||||
all_entries = list(db.leaderboards.find({
|
||||
'song_id': song_id,
|
||||
'difficulty': difficulty,
|
||||
'month': current_month
|
||||
}).sort('score_value', -1))
|
||||
|
||||
# Delete entries beyond 50
|
||||
if len(all_entries) > 50:
|
||||
for entry in all_entries[50:]:
|
||||
db.leaderboards.delete_one({'_id': entry['_id']})
|
||||
|
||||
return jsonify({'status': 'ok', 'message': 'score_submitted'})
|
||||
|
||||
|
||||
@app.route(basedir + 'api/leaderboard/get')
|
||||
def route_api_leaderboard_get():
|
||||
song_id = request.args.get('song_id', None)
|
||||
difficulty = request.args.get('difficulty', None)
|
||||
|
||||
if not song_id or not difficulty:
|
||||
return abort(400)
|
||||
|
||||
try:
|
||||
song_id = int(song_id)
|
||||
except:
|
||||
return abort(400)
|
||||
|
||||
# Validate difficulty
|
||||
valid_difficulties = ['easy', 'normal', 'hard', 'oni', 'ura']
|
||||
if difficulty not in valid_difficulties:
|
||||
return abort(400)
|
||||
|
||||
# Get current month
|
||||
current_month = time.strftime('%Y-%m', time.gmtime())
|
||||
|
||||
# Get top 50 scores
|
||||
leaderboard = list(db.leaderboards.find({
|
||||
'song_id': song_id,
|
||||
'difficulty': difficulty,
|
||||
'month': current_month
|
||||
}, {
|
||||
'_id': False,
|
||||
'username': True,
|
||||
'display_name': True,
|
||||
'score': True,
|
||||
'score_value': True,
|
||||
'submitted_at': True
|
||||
}).sort('score_value', -1).limit(50))
|
||||
|
||||
# Add rank to each entry
|
||||
for i, entry in enumerate(leaderboard):
|
||||
entry['rank'] = i + 1
|
||||
|
||||
return jsonify({'status': 'ok', 'leaderboard': leaderboard, 'month': current_month})
|
||||
|
||||
|
||||
@app.route(basedir + 'privacy')
|
||||
def route_api_privacy():
|
||||
last_modified = time.strftime('%d %B %Y', time.gmtime(os.path.getmtime('templates/privacy.txt')))
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
#leaderboard {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
#leaderboard-canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
@@ -1,287 +0,0 @@
|
||||
class Leaderboard {
|
||||
constructor() {
|
||||
this.init()
|
||||
}
|
||||
|
||||
init() {
|
||||
this.canvas = document.getElementById("leaderboard-canvas")
|
||||
if (!this.canvas) {
|
||||
return
|
||||
}
|
||||
this.ctx = this.canvas.getContext("2d")
|
||||
|
||||
var resolution = settings.getItem("resolution")
|
||||
var noSmoothing = resolution === "low" || resolution === "lowest"
|
||||
if (noSmoothing) {
|
||||
this.ctx.imageSmoothingEnabled = false
|
||||
}
|
||||
|
||||
this.songId = null
|
||||
this.difficulty = null
|
||||
this.leaderboardData = []
|
||||
this.currentMonth = ""
|
||||
this.visible = false
|
||||
|
||||
this.draw = new CanvasDraw(noSmoothing)
|
||||
|
||||
// Keyboard controls
|
||||
this.keyboard = new Keyboard({
|
||||
confirm: ["enter", "escape", "don_l", "don_r"],
|
||||
back: ["escape"],
|
||||
left: ["left", "ka_l"],
|
||||
right: ["right", "ka_r"]
|
||||
}, this.keyPress.bind(this))
|
||||
|
||||
pageEvents.add(this.canvas, ["mousedown", "touchstart"], this.onClose.bind(this))
|
||||
}
|
||||
|
||||
keyPress(pressed, name) {
|
||||
if (!pressed || !this.visible) {
|
||||
return
|
||||
}
|
||||
if (name === "confirm" || name === "back") {
|
||||
this.close()
|
||||
} else if (name === "left") {
|
||||
this.changeDifficulty(-1)
|
||||
} else if (name === "right") {
|
||||
this.changeDifficulty(1)
|
||||
}
|
||||
}
|
||||
|
||||
async display(songId, difficulty) {
|
||||
this.songId = songId
|
||||
this.difficulty = difficulty
|
||||
this.visible = true
|
||||
|
||||
loader.changePage("leaderboard", false)
|
||||
|
||||
// Fetch leaderboard data
|
||||
await this.fetchLeaderboard()
|
||||
|
||||
// Start rendering
|
||||
this.redrawRunning = true
|
||||
this.redrawBind = this.redraw.bind(this)
|
||||
this.redraw()
|
||||
|
||||
assets.sounds["se_don"].play()
|
||||
}
|
||||
|
||||
async fetchLeaderboard() {
|
||||
try {
|
||||
var response = await loader.ajax(
|
||||
`${gameConfig.basedir || "/"}api/leaderboard/get?song_id=${this.songId}&difficulty=${this.difficulty}`
|
||||
)
|
||||
var data = JSON.parse(response)
|
||||
if (data.status === "ok") {
|
||||
this.leaderboardData = data.leaderboard || []
|
||||
this.currentMonth = data.month || ""
|
||||
} else {
|
||||
this.leaderboardData = []
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to fetch leaderboard:", e)
|
||||
this.leaderboardData = []
|
||||
}
|
||||
}
|
||||
|
||||
changeDifficulty(direction) {
|
||||
var difficulties = ["easy", "normal", "hard", "oni", "ura"]
|
||||
var currentIndex = difficulties.indexOf(this.difficulty)
|
||||
if (currentIndex === -1) {
|
||||
return
|
||||
}
|
||||
|
||||
var newIndex = (currentIndex + direction + difficulties.length) % difficulties.length
|
||||
this.difficulty = difficulties[newIndex]
|
||||
|
||||
this.fetchLeaderboard().then(() => {
|
||||
assets.sounds["se_ka"].play()
|
||||
})
|
||||
}
|
||||
|
||||
onClose(event) {
|
||||
if (!this.visible) {
|
||||
return
|
||||
}
|
||||
|
||||
var rect = this.canvas.getBoundingClientRect()
|
||||
var x = (event.offsetX || event.touches[0].pageX - rect.left)
|
||||
var y = (event.offsetY || event.touches[0].pageY - rect.top)
|
||||
|
||||
// Check if clicked outside modal (approximately)
|
||||
var centerX = this.canvas.width / 2
|
||||
var centerY = this.canvas.height / 2
|
||||
var modalWidth = 800
|
||||
var modalHeight = 600
|
||||
|
||||
if (x < centerX - modalWidth / 2 || x > centerX + modalWidth / 2 ||
|
||||
y < centerY - modalHeight / 2 || y > centerY + modalHeight / 2) {
|
||||
this.close()
|
||||
}
|
||||
}
|
||||
|
||||
close() {
|
||||
this.visible = false
|
||||
this.redrawRunning = false
|
||||
|
||||
if (this.keyboard) {
|
||||
this.keyboard.clean()
|
||||
}
|
||||
|
||||
assets.sounds["se_cancel"].play()
|
||||
|
||||
// Return to song select - get touchEnabled from global or default to false
|
||||
setTimeout(() => {
|
||||
var touchEnabled = typeof window.touchEnabled !== 'undefined' ? window.touchEnabled : false
|
||||
new SongSelect(false, false, touchEnabled)
|
||||
}, 100)
|
||||
}
|
||||
|
||||
redraw() {
|
||||
if (!this.redrawRunning) {
|
||||
return
|
||||
}
|
||||
|
||||
requestAnimationFrame(this.redrawBind)
|
||||
|
||||
var winW = innerWidth
|
||||
var winH = innerHeight
|
||||
var ratio = winH / 720
|
||||
|
||||
this.canvas.width = winW
|
||||
this.canvas.height = winH
|
||||
this.ctx.save()
|
||||
|
||||
// Draw semi-transparent background
|
||||
this.ctx.fillStyle = "rgba(0, 0, 0, 0.8)"
|
||||
this.ctx.fillRect(0, 0, winW, winH)
|
||||
|
||||
this.ctx.scale(ratio, ratio)
|
||||
|
||||
// Draw modal background
|
||||
var modalX = 240
|
||||
var modalY = 60
|
||||
var modalW = 800
|
||||
var modalH = 600
|
||||
|
||||
this.ctx.fillStyle = "#ffffff"
|
||||
this.ctx.fillRect(modalX, modalY, modalW, modalH)
|
||||
|
||||
this.ctx.strokeStyle = "#333333"
|
||||
this.ctx.lineWidth = 4
|
||||
this.ctx.strokeRect(modalX, modalY, modalW, modalH)
|
||||
|
||||
// Draw title
|
||||
this.ctx.fillStyle = "#000000"
|
||||
this.ctx.font = "bold 40px " + (strings.font || "sans-serif")
|
||||
this.ctx.textAlign = "center"
|
||||
this.ctx.fillText("🏆 排行榜 Leaderboard", 640, 110)
|
||||
|
||||
// Draw difficulty selector
|
||||
var diffX = 640
|
||||
var diffY = 160
|
||||
var difficulties = [
|
||||
{ id: "easy", name: "簡単", color: "#00a0e9" },
|
||||
{ id: "normal", name: "普通", color: "#00a040" },
|
||||
{ id: "hard", name: "難しい", color: "#ff8c00" },
|
||||
{ id: "oni", name: "鬼", color: "#dc143c" },
|
||||
{ id: "ura", name: "裏", color: "#9400d3" }
|
||||
]
|
||||
|
||||
this.ctx.font = "24px " + (strings.font || "sans-serif")
|
||||
for (var i = 0; i < difficulties.length; i++) {
|
||||
var diff = difficulties[i]
|
||||
var x = diffX - 200 + i * 100
|
||||
|
||||
if (diff.id === this.difficulty) {
|
||||
this.ctx.fillStyle = diff.color
|
||||
this.ctx.fillRect(x - 45, diffY - 25, 90, 40)
|
||||
this.ctx.fillStyle = "#ffffff"
|
||||
} else {
|
||||
this.ctx.strokeStyle = diff.color
|
||||
this.ctx.lineWidth = 2
|
||||
this.ctx.strokeRect(x - 45, diffY - 25, 90, 40)
|
||||
this.ctx.fillStyle = diff.color
|
||||
}
|
||||
|
||||
this.ctx.textAlign = "center"
|
||||
this.ctx.fillText(diff.name, x, diffY + 5)
|
||||
}
|
||||
|
||||
// Draw month info
|
||||
this.ctx.fillStyle = "#666666"
|
||||
this.ctx.font = "18px " + (strings.font || "sans-serif")
|
||||
this.ctx.textAlign = "center"
|
||||
this.ctx.fillText("当月排行 " + this.currentMonth, 640, 210)
|
||||
|
||||
// Draw leaderboard entries
|
||||
var startY = 240
|
||||
var rowHeight = 35
|
||||
|
||||
this.ctx.font = "20px " + (strings.font || "sans-serif")
|
||||
this.ctx.textAlign = "left"
|
||||
|
||||
if (this.leaderboardData.length === 0) {
|
||||
this.ctx.fillStyle = "#999999"
|
||||
this.ctx.textAlign = "center"
|
||||
this.ctx.fillText("暂无排行数据", 640, startY + 100)
|
||||
} else {
|
||||
for (var i = 0; i < Math.min(this.leaderboardData.length, 15); i++) {
|
||||
var entry = this.leaderboardData[i]
|
||||
var y = startY + i * rowHeight
|
||||
var rank = entry.rank
|
||||
|
||||
// Rank background color
|
||||
if (rank === 1) {
|
||||
this.ctx.fillStyle = "#ffd700" // Gold
|
||||
} else if (rank === 2) {
|
||||
this.ctx.fillStyle = "#c0c0c0" // Silver
|
||||
} else if (rank === 3) {
|
||||
this.ctx.fillStyle = "#cd7f32" // Bronze
|
||||
} else {
|
||||
this.ctx.fillStyle = "#f5f5f5"
|
||||
}
|
||||
this.ctx.fillRect(modalX + 20, y - 20, modalW - 40, 30)
|
||||
|
||||
// Rank
|
||||
this.ctx.fillStyle = rank <= 3 ? "#ffffff" : "#333333"
|
||||
this.ctx.font = "bold 20px " + (strings.font || "sans-serif")
|
||||
this.ctx.textAlign = "center"
|
||||
this.ctx.fillText(rank, modalX + 60, y)
|
||||
|
||||
// Display name
|
||||
this.ctx.fillStyle = "#000000"
|
||||
this.ctx.font = "20px " + (strings.font || "sans-serif")
|
||||
this.ctx.textAlign = "left"
|
||||
var displayName = entry.display_name || entry.username
|
||||
if (displayName.length > 15) {
|
||||
displayName = displayName.substring(0, 15) + "..."
|
||||
}
|
||||
this.ctx.fillText(displayName, modalX + 100, y)
|
||||
|
||||
// Score
|
||||
var score = entry.score && entry.score.score ? entry.score.score : 0
|
||||
this.ctx.textAlign = "right"
|
||||
this.ctx.fillText(score.toLocaleString(), modalX + modalW - 40, y)
|
||||
}
|
||||
}
|
||||
|
||||
// Draw close hint
|
||||
this.ctx.fillStyle = "#666666"
|
||||
this.ctx.font = "18px " + (strings.font || "sans-serif")
|
||||
this.ctx.textAlign = "center"
|
||||
this.ctx.fillText("按ESC或点击外部关闭 Press ESC or click outside to close", 640, 680)
|
||||
|
||||
this.ctx.restore()
|
||||
}
|
||||
|
||||
clean() {
|
||||
if (this.keyboard) {
|
||||
this.keyboard.clean()
|
||||
}
|
||||
if (this.redrawRunning) {
|
||||
this.redrawRunning = false
|
||||
}
|
||||
pageEvents.remove(this.canvas, ["mousedown", "touchstart"])
|
||||
}
|
||||
}
|
||||
26
public/src/js/loader-worker.js
Normal file
26
public/src/js/loader-worker.js
Normal file
@@ -0,0 +1,26 @@
|
||||
self.addEventListener('message', async e => {
|
||||
const { id, url, type } = e.data
|
||||
try{
|
||||
const response = await fetch(url)
|
||||
if(!response.ok){
|
||||
throw new Error(response.status + " " + response.statusText)
|
||||
}
|
||||
let data
|
||||
if(type === "arraybuffer"){
|
||||
data = await response.arrayBuffer()
|
||||
}else if(type === "blob"){
|
||||
data = await response.blob()
|
||||
}else{
|
||||
data = await response.text()
|
||||
}
|
||||
self.postMessage({
|
||||
id: id,
|
||||
data: data
|
||||
}, type === "arraybuffer" ? [data] : undefined)
|
||||
}catch(e){
|
||||
self.postMessage({
|
||||
id: id,
|
||||
error: e.toString()
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -11,6 +11,8 @@ class Loader{
|
||||
this.errorMessages = []
|
||||
this.songSearchGradient = "linear-gradient(to top, rgba(245, 246, 252, 0.08), #ff5963), "
|
||||
|
||||
this.initWorkers()
|
||||
|
||||
var promises = []
|
||||
|
||||
promises.push(this.ajax("src/views/loader.html").then(page => {
|
||||
@@ -23,6 +25,78 @@ class Loader{
|
||||
|
||||
Promise.all(promises).then(this.run.bind(this))
|
||||
}
|
||||
initWorkers(){
|
||||
this.workers = []
|
||||
this.workerQueue = []
|
||||
this.workerCallbacks = {}
|
||||
this.workerId = 0
|
||||
var concurrency = navigator.hardwareConcurrency || 4
|
||||
for(var i = 0; i < concurrency; i++){
|
||||
var worker = new Worker("src/js/loader-worker.js")
|
||||
worker.onmessage = this.onWorkerMessage.bind(this)
|
||||
this.workers.push({
|
||||
worker: worker,
|
||||
active: 0
|
||||
})
|
||||
}
|
||||
}
|
||||
onWorkerMessage(e){
|
||||
var data = e.data
|
||||
var callback = this.workerCallbacks[data.id]
|
||||
if(callback){
|
||||
delete this.workerCallbacks[data.id]
|
||||
if(data.error){
|
||||
callback.reject(data.error)
|
||||
}else{
|
||||
callback.resolve(data.data)
|
||||
}
|
||||
this.workerFree(callback.workerIndex)
|
||||
}
|
||||
}
|
||||
workerFetch(url, type){
|
||||
return new Promise((resolve, reject) => {
|
||||
var id = ++this.workerId
|
||||
this.workerQueue.push({
|
||||
id: id,
|
||||
url: new URL(url, location.href).href,
|
||||
type: type,
|
||||
resolve: resolve,
|
||||
reject: reject
|
||||
})
|
||||
this.workerRun()
|
||||
})
|
||||
}
|
||||
workerRun(){
|
||||
if(this.workerQueue.length === 0){
|
||||
return
|
||||
}
|
||||
var workerIndex = -1
|
||||
var minActive = Infinity
|
||||
for(var i = 0; i < this.workers.length; i++){
|
||||
if(this.workers[i].active < minActive){
|
||||
minActive = this.workers[i].active
|
||||
workerIndex = i
|
||||
}
|
||||
}
|
||||
if(workerIndex !== -1){
|
||||
var task = this.workerQueue.shift()
|
||||
var workerObj = this.workers[workerIndex]
|
||||
workerObj.active++
|
||||
this.workerCallbacks[task.id] = task
|
||||
task.workerIndex = workerIndex
|
||||
workerObj.worker.postMessage({
|
||||
id: task.id,
|
||||
url: task.url,
|
||||
type: task.type
|
||||
})
|
||||
}
|
||||
}
|
||||
workerFree(index){
|
||||
if(this.workers[index]){
|
||||
this.workers[index].active--
|
||||
this.workerRun()
|
||||
}
|
||||
}
|
||||
run(){
|
||||
this.promises = []
|
||||
this.loaderDiv = document.querySelector("#loader")
|
||||
@@ -538,6 +612,17 @@ class Loader{
|
||||
return css.join("\n")
|
||||
}
|
||||
ajax(url, customRequest, customResponse){
|
||||
if(!customResponse && (url.startsWith("src/") || url.startsWith("assets/") || url.indexOf("img/") !== -1 || url.indexOf("audio/") !== -1 || url.indexOf("fonts/") !== -1 || url.indexOf("views/") !== -1)){
|
||||
var type = "text"
|
||||
if(customRequest){
|
||||
var reqStub = {}
|
||||
customRequest(reqStub)
|
||||
if(reqStub.responseType){
|
||||
type = reqStub.responseType
|
||||
}
|
||||
}
|
||||
return this.workerFetch(url, type)
|
||||
}
|
||||
var request = new XMLHttpRequest()
|
||||
request.open("GET", url)
|
||||
var promise = pageEvents.load(request)
|
||||
@@ -557,12 +642,13 @@ class Loader{
|
||||
return promise
|
||||
}
|
||||
loadScript(url){
|
||||
var script = document.createElement("script")
|
||||
var url = url + this.queryString
|
||||
var promise = pageEvents.load(script)
|
||||
script.src = url
|
||||
document.head.appendChild(script)
|
||||
return promise
|
||||
return this.workerFetch(url, "text").then(code => {
|
||||
var script = document.createElement("script")
|
||||
code += "\n//# sourceURL=" + url
|
||||
script.text = code
|
||||
document.head.appendChild(script)
|
||||
})
|
||||
}
|
||||
getCsrfToken(){
|
||||
return this.ajax("api/csrftoken").then(response => {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,3 +0,0 @@
|
||||
<div id="leaderboard">
|
||||
<canvas id="leaderboard-canvas"></canvas>
|
||||
</div>
|
||||
11
schema.py
11
schema.py
@@ -80,14 +80,3 @@ scores_save = {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
leaderboard_submit = {
|
||||
'$schema': 'http://json-schema.org/schema#',
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'song_id': {'type': 'number'},
|
||||
'difficulty': {'type': 'string'},
|
||||
'score': {'type': 'object'}
|
||||
},
|
||||
'required': ['song_id', 'difficulty', 'score']
|
||||
}
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>太鼓ウェブ - Taiko Web | (゚∀゚)</title>
|
||||
<link rel="icon" href="{{config.assets_baseurl}}img/favicon.png" type="image/png">
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no">
|
||||
<meta name="description"
|
||||
content="2026年最新の無料オンラインゲームの パソコンとスマホのブラウザ向けの太鼓の達人シミュレータ 🥁 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="description" content="2025年最新の無料オンラインゲームの パソコンとスマホのブラウザ向けの太鼓の達人シミュレータ 🥁 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="robots" content="notranslate">
|
||||
<meta name="robots" content="noimageindex">
|
||||
<meta name="color-scheme" content="only light">
|
||||
|
||||
<link rel="canonical" href="https://taikoapp.uk/" />
|
||||
<link rel="canonical" href="https://taikoapp.uk/" />
|
||||
|
||||
<link rel="stylesheet" href="src/css/loader.css?{{version.commit_short}}">
|
||||
<link rel="manifest" href="manifest.json">
|
||||
@@ -27,17 +24,14 @@
|
||||
<script src="src/js/lib/jszip.js"></script>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="assets"></div>
|
||||
<div id="screen" class="pattern-bg"></div>
|
||||
<div data-nosnippet id="version">
|
||||
{% if version.version and version.commit_short and version.commit %}
|
||||
<a href="{{version.url}}commit/{{version.commit}}" target="_blank" rel="noopener" id="version-link"
|
||||
class="stroke-sub" alt="vLightNova 1.1.0">vLightNova 1.1.0</a>
|
||||
<a href="{{version.url}}commit/{{version.commit}}" target="_blank" rel="noopener" id="version-link" class="stroke-sub" alt="vLightNova 1.0.0">vLightNova 1.0.0</a>
|
||||
{% else %}
|
||||
<a target="_blank" rel="noopener" id="version-link" class="stroke-sub" alt="vLightNova 1.1.0">vLightNova
|
||||
1.1.0</a>
|
||||
<a target="_blank" rel="noopener" id="version-link" class="stroke-sub" alt="vLightNova 1.0.0">vLightNova 1.0.0</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<script src="src/js/browsersupport.js?{{version.commit_short}}"></script>
|
||||
@@ -49,5 +43,4 @@
|
||||
</div>
|
||||
</noscript>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user