mirror of
https://github.com/qwerfd2/Groove_Coaster_2_Server.git
synced 2025-12-22 03:30:18 +00:00
mid-push
This commit is contained in:
@@ -4,8 +4,8 @@ from starlette.routing import Route
|
||||
from datetime import datetime
|
||||
import secrets
|
||||
|
||||
from api.misc import is_alphanumeric, inform_page, verify_password, hash_password, crc32_decimal, read_user_save_file, write_user_save_file, should_serve, generate_salt
|
||||
from api.database import check_blacklist, user_name_to_user_info, decrypt_fields_to_user_info, set_user_data_using_decrypted_fields, get_user_from_save_id, create_user, logout_user, login_user, get_bind
|
||||
from api.misc import is_alphanumeric, inform_page, verify_password, hash_password, crc32_decimal, should_serve, generate_salt
|
||||
from api.database import check_blacklist, user_name_to_user_info, decrypt_fields_to_user_info, set_user_data_using_decrypted_fields, get_user_from_save_id, create_user, logout_user, login_user, get_bind, read_user_save_file, write_user_save_file
|
||||
from api.crypt import decrypt_fields
|
||||
from config import AUTHORIZATION_MODE
|
||||
|
||||
@@ -76,7 +76,6 @@ async def password_reset(request: Request):
|
||||
return inform_page("FAILED:<br>Password must have 6 or more characters.", 0)
|
||||
|
||||
old_hash = user_info['password_hash']
|
||||
print("hash type", type(old_hash))
|
||||
if old_hash:
|
||||
if verify_password(old_password, old_hash):
|
||||
hashed_new_password = hash_password(new_password)
|
||||
@@ -228,7 +227,7 @@ async def login(request: Request):
|
||||
user_id = user_record['id']
|
||||
|
||||
password_hash_record = user_record['password_hash']
|
||||
if password_hash_record and verify_password(password, password_hash_record[0]):
|
||||
if password_hash_record and verify_password(password, password_hash_record):
|
||||
device_id = decrypted_fields[b'vid'][0].decode()
|
||||
await login_user(user_id, device_id)
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ import sqlalchemy
|
||||
import json
|
||||
import datetime
|
||||
|
||||
from api.database import player_database, accounts, results, devices, whitelists, blacklists, batch_tokens, binds, webs, logs, is_admin
|
||||
from api.misc import crc32_decimal, read_user_save_file, write_user_save_file
|
||||
from api.database import player_database, accounts, results, devices, whitelists, blacklists, batch_tokens, binds, webs, logs, is_admin, read_user_save_file, write_user_save_file
|
||||
from api.misc import crc32_decimal
|
||||
|
||||
TABLE_MAP = {
|
||||
"accounts": (accounts, ["id", "username", "password_hash", "save_crc", "save_timestamp", "save_id", "coin_mp", "title", "avatar", "created_at", "updated_at"]),
|
||||
@@ -14,9 +14,9 @@ TABLE_MAP = {
|
||||
"devices": (devices, ["device_id", "user_id", "my_stage", "my_avatar", "item", "daily_day", "coin", "lvl", "title", "avatar", "mobile_sum", "arcade_sum", "total_sum", "created_at", "updated_at", "last_login_at"]),
|
||||
"whitelist": (whitelists, ["id", "device_id"]),
|
||||
"blacklist": (blacklists, ["id", "ban_terms", "reason"]),
|
||||
"batch_tokens": (batch_tokens, ["id", "bind_token", "expire_at", "uses_left", "auth_id", "created_at", "updated_at"]),
|
||||
"binds": (binds, ["id", "user_id", "bind_account", "bind_code", "is_verified", "bind_token", "created_at", "updated_at"]),
|
||||
"webs": (webs, ["id", "user_id", "permission", "web_token", "created_at", "updated_at"]),
|
||||
"batch_tokens": (batch_tokens, ["id", "batch_token", "expire_at", "uses_left", "auth_id", "created_at", "updated_at"]),
|
||||
"binds": (binds, ["id", "user_id", "bind_account", "bind_code", "is_verified", "auth_token", "created_at", "updated_at"]),
|
||||
"webs": (webs, ["id", "user_id", "permission", "web_token", "last_save_export", "created_at", "updated_at"]),
|
||||
"logs": (logs, ["id", "user_id", "filename", "filesize", "timestamp"]),
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from starlette.responses import HTMLResponse
|
||||
from starlette.requests import Request
|
||||
from starlette.routing import Route
|
||||
from datetime import datetime
|
||||
import os
|
||||
import json
|
||||
import time
|
||||
@@ -27,6 +28,15 @@ async def batch_handler(request: Request):
|
||||
if result['expire_at'] < int(time.time()):
|
||||
return HTMLResponse(content="Token expired", status_code=400)
|
||||
|
||||
if result['uses_left'] <= 0:
|
||||
return HTMLResponse(content="No uses left for this token", status_code=400)
|
||||
|
||||
update_query = batch_tokens.update().where(batch_tokens.c.token == token).values(
|
||||
uses_left=result['uses_left'] - 1,
|
||||
updated_at=datetime.utcnow()
|
||||
)
|
||||
await player_database.execute(update_query)
|
||||
|
||||
with open(os.path.join('api/config/', 'download_manifest.json'), 'r', encoding='utf-8') as f:
|
||||
stage_manifest = json.load(f)
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import sqlalchemy
|
||||
from sqlalchemy import Table, Column, Integer, String, DateTime, JSON, ForeignKey, Index
|
||||
from sqlalchemy.ext.asyncio import create_async_engine
|
||||
from sqlalchemy import select, update, insert
|
||||
from sqlalchemy import select, update
|
||||
import base64
|
||||
import aiofiles
|
||||
import json
|
||||
|
||||
from config import START_COIN, SIMULTANEOUS_LOGINS
|
||||
from api.template import START_AVATARS, START_STAGES
|
||||
@@ -82,8 +84,7 @@ results = Table(
|
||||
Column("os", String(8), nullable=False),
|
||||
Column("os_ver", String(16), nullable=False),
|
||||
Column("ver", String(8), nullable=False),
|
||||
Column("created_at", DateTime, default=datetime.utcnow),
|
||||
Column("updated_at", DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
Column("created_at", DateTime, default=datetime.utcnow)
|
||||
)
|
||||
|
||||
Index(
|
||||
@@ -100,6 +101,7 @@ webs = Table(
|
||||
Column("user_id", Integer, ForeignKey("accounts.id")),
|
||||
Column("permission", Integer, default=1),
|
||||
Column("web_token", String(128), unique=True, nullable=False),
|
||||
Column("last_save_export", Integer, nullable=True),
|
||||
Column("created_at", DateTime, default=datetime.utcnow),
|
||||
Column("updated_at", DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
)
|
||||
@@ -108,7 +110,7 @@ batch_tokens = Table(
|
||||
"batch_tokens",
|
||||
player_metadata,
|
||||
Column("id", Integer, primary_key=True, autoincrement=True),
|
||||
Column("bind_token", String(128), unique=True, nullable=False),
|
||||
Column("batch_token", String(128), unique=True, nullable=False),
|
||||
Column("expire_at", DateTime, nullable=False),
|
||||
Column("uses_left", Integer, default=1),
|
||||
Column("auth_id", String(64), nullable=False),
|
||||
@@ -139,7 +141,7 @@ binds = Table(
|
||||
Column("bind_account", String(128), unique=True, nullable=False),
|
||||
Column("bind_code", String(6), nullable=False),
|
||||
Column("is_verified", Integer, default=0),
|
||||
Column("bind_token", String(64), unique=True, nullable=False),
|
||||
Column("auth_token", String(64), unique=True),
|
||||
Column("bind_date", DateTime, default=datetime.utcnow)
|
||||
)
|
||||
|
||||
@@ -215,7 +217,7 @@ async def refresh_bind(user_id):
|
||||
if existing_bind and existing_bind['is_verified'] == 1:
|
||||
new_auth_token = base64.urlsafe_b64encode(os.urandom(64)).decode("utf-8")
|
||||
update_query = update(binds).where(binds.c.id == existing_bind['id']).values(
|
||||
bind_token=new_auth_token
|
||||
auth_token=new_auth_token
|
||||
)
|
||||
await player_database.execute(update_query)
|
||||
|
||||
@@ -425,7 +427,7 @@ async def create_device(device_id, current_time):
|
||||
async def is_admin(token):
|
||||
if not token:
|
||||
return False
|
||||
query = webs.select().where(webs.c.token == token)
|
||||
query = webs.select().where(webs.c.web_token == token)
|
||||
web_data = await player_database.fetch_one(query)
|
||||
if not web_data:
|
||||
return False
|
||||
@@ -467,4 +469,60 @@ async def get_rank_cache(key):
|
||||
await clear_rank_cache(key)
|
||||
return None
|
||||
return dict(result)['value']
|
||||
return None
|
||||
return None
|
||||
|
||||
async def get_user_export_data(user_id):
|
||||
user_data = {}
|
||||
user_info, device_list = await user_id_to_user_info(user_id)
|
||||
|
||||
user_save = await read_user_save_file(user_id)
|
||||
user_info['save_data'] = user_save
|
||||
|
||||
user_data['account'] = [user_info]
|
||||
user_data['devices'] = device_list
|
||||
|
||||
all_results_query = results.select().where(results.c.user_id == user_id)
|
||||
all_results = await player_database.fetch_all(all_results_query)
|
||||
user_data['results'] = [dict(result) for result in all_results]
|
||||
|
||||
user_binds_query = binds.select().where(binds.c.user_id == user_id)
|
||||
user_binds = await player_database.fetch_all(user_binds_query)
|
||||
|
||||
user_data['binds'] = [dict(bind) for bind in user_binds]
|
||||
# Convert JSON fields to strings
|
||||
for key, value in user_data.items():
|
||||
if isinstance(value, list):
|
||||
for item in value:
|
||||
if isinstance(item, dict):
|
||||
for field, field_value in item.items():
|
||||
if isinstance(field_value, (dict, list)):
|
||||
item[field] = json.dumps(field_value)
|
||||
|
||||
return user_data
|
||||
|
||||
async def read_user_save_file(user_id):
|
||||
if user_id is None:
|
||||
return ""
|
||||
elif type(user_id) != int:
|
||||
return ""
|
||||
else:
|
||||
try:
|
||||
async with aiofiles.open(f"./save/{user_id}.dat", "rb") as file:
|
||||
result = await file.read()
|
||||
result = result.decode("utf-8")
|
||||
return result
|
||||
|
||||
except FileNotFoundError:
|
||||
return ""
|
||||
|
||||
async def write_user_save_file(user_id, data):
|
||||
if user_id is None:
|
||||
return
|
||||
elif type(user_id) != int:
|
||||
return
|
||||
else:
|
||||
try:
|
||||
async with aiofiles.open(f"./save/{user_id}.dat", "wb") as file:
|
||||
await file.write(data.encode("utf-8"))
|
||||
except Exception as e:
|
||||
print(f"An error occurred while writing the file: {e}")
|
||||
@@ -2,6 +2,8 @@ from starlette.responses import Response, FileResponse
|
||||
from starlette.requests import Request
|
||||
from starlette.routing import Route
|
||||
from sqlalchemy import select
|
||||
import openpyxl
|
||||
from io import BytesIO
|
||||
import os
|
||||
|
||||
from api.database import player_database, devices, binds, batch_tokens, log_download, get_downloaded_bytes
|
||||
@@ -18,7 +20,7 @@ async def serve_file(request: Request):
|
||||
if not filename.endswith(".zip") and not filename.endswith(".pak"):
|
||||
return Response("Unauthorized", status_code=403)
|
||||
|
||||
existing_batch_token = select(batch_tokens).where(batch_tokens.c.bind_token == auth_token)
|
||||
existing_batch_token = select(batch_tokens).where(batch_tokens.c.batch_token == auth_token)
|
||||
batch_result = await player_database.fetch_one(existing_batch_token)
|
||||
if batch_result:
|
||||
pass
|
||||
@@ -71,7 +73,38 @@ async def serve_public_file(request: Request):
|
||||
return FileResponse(safe_filename)
|
||||
else:
|
||||
return Response("File not found", status_code=404)
|
||||
|
||||
async def convert_user_export_data(data):
|
||||
wb = openpyxl.Workbook()
|
||||
|
||||
# Remove default sheet
|
||||
default_sheet = wb.active
|
||||
wb.remove(default_sheet)
|
||||
|
||||
# Create sheet for each top-level key
|
||||
for sheet_name, rows in data.items():
|
||||
ws = wb.create_sheet(title=sheet_name)
|
||||
|
||||
# rows expected to be list[dict]
|
||||
if isinstance(rows, list):
|
||||
await write_dict_list_to_sheet(ws, rows)
|
||||
|
||||
stream = BytesIO()
|
||||
wb.save(stream)
|
||||
stream.seek(0)
|
||||
return stream
|
||||
|
||||
async def write_dict_list_to_sheet(ws, rows):
|
||||
if not rows:
|
||||
return
|
||||
|
||||
# headers
|
||||
headers = list(rows[0].keys())
|
||||
ws.append(headers)
|
||||
|
||||
# rows
|
||||
for row in rows:
|
||||
ws.append([row.get(h, "") for h in headers])
|
||||
|
||||
routes = [
|
||||
Route("/files/gc2/{auth_token}/{folder}/{filename}", serve_file, methods=["GET"]),
|
||||
|
||||
@@ -65,6 +65,8 @@ def hash_password(password):
|
||||
def verify_password(password, hashed_password):
|
||||
if type(hashed_password) == str:
|
||||
hashed_password = hashed_password.encode('utf-8')
|
||||
|
||||
print("hashed_password:", hashed_password)
|
||||
return bcrypt.checkpw(password.encode('utf-8'), hashed_password)
|
||||
|
||||
def is_alphanumeric(username):
|
||||
@@ -246,33 +248,6 @@ def check_email(email):
|
||||
STRICT_EMAIL_REGEX = r"^[A-Za-z0-9]+(?:[._-][A-Za-z0-9]+)*@[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*(?:\.[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*)*\.[A-Za-z]{2,}$"
|
||||
return re.match(STRICT_EMAIL_REGEX, email) is not None
|
||||
|
||||
async def read_user_save_file(user_id):
|
||||
if user_id is None:
|
||||
return ""
|
||||
elif type(user_id) != int:
|
||||
return ""
|
||||
else:
|
||||
try:
|
||||
async with aiofiles.open(f"./save/{user_id}.dat", "rb") as file:
|
||||
result = await file.read()
|
||||
result = result.decode("utf-8")
|
||||
return result
|
||||
|
||||
except FileNotFoundError:
|
||||
return ""
|
||||
|
||||
async def write_user_save_file(user_id, data):
|
||||
if user_id is None:
|
||||
return
|
||||
elif type(user_id) != int:
|
||||
return
|
||||
else:
|
||||
try:
|
||||
async with aiofiles.open(f"./save/{user_id}.dat", "wb") as file:
|
||||
await file.write(data.encode("utf-8"))
|
||||
except Exception as e:
|
||||
print(f"An error occurred while writing the file: {e}")
|
||||
|
||||
async def should_serve(decrypted_fields):
|
||||
should_serve = True
|
||||
if AUTHORIZATION_NEEDED:
|
||||
|
||||
@@ -101,7 +101,8 @@ async def result_request(request: Request):
|
||||
item=item,
|
||||
os=device_os,
|
||||
os_ver=os_ver,
|
||||
ver=ver
|
||||
ver=ver,
|
||||
created_at=datetime.utcnow()
|
||||
)
|
||||
await player_database.execute(update_query)
|
||||
|
||||
@@ -123,8 +124,7 @@ async def result_request(request: Request):
|
||||
os=device_os,
|
||||
os_ver=os_ver,
|
||||
ver=ver,
|
||||
created_at=datetime.utcnow(),
|
||||
updated_at=datetime.utcnow()
|
||||
created_at=datetime.utcnow()
|
||||
)
|
||||
result = await player_database.execute(insert_query)
|
||||
target_row_id = result
|
||||
|
||||
@@ -215,7 +215,7 @@ async def user_ranking_individual(request: Request):
|
||||
ranking_list = []
|
||||
player_ranking = {"username": user_info["username"] if user_info else "Guest (Not Ranked)", "score": 0, "position": -1, "title": user_info["title"] if user_info else device_info['title'], "avatar": device_info["avatar"]}
|
||||
|
||||
cache_key = f"{song_id}-{mode}"
|
||||
cache_key = f"{str(song_id)}-{str(mode)}"
|
||||
cached_data = await get_rank_cache(cache_key)
|
||||
|
||||
if cached_data:
|
||||
@@ -232,13 +232,14 @@ async def user_ranking_individual(request: Request):
|
||||
for index, record in enumerate(records):
|
||||
if index >= page_number * page_count and index < (page_number + 1) * page_count:
|
||||
rank_user = await user_id_to_user_info_simple(record["user_id"])
|
||||
ranking_list.append({
|
||||
"position": index + 1,
|
||||
"username": rank_user["username"],
|
||||
"score": record["score"],
|
||||
"title": rank_user["title"],
|
||||
"avatar": record["avatar"]
|
||||
})
|
||||
if rank_user:
|
||||
ranking_list.append({
|
||||
"position": index + 1,
|
||||
"username": rank_user["username"],
|
||||
"score": record["score"],
|
||||
"title": rank_user["title"],
|
||||
"avatar": record["avatar"]
|
||||
})
|
||||
if user_id and record["user_id"] == user_id:
|
||||
player_ranking = {
|
||||
"username": user_info["username"],
|
||||
@@ -258,6 +259,8 @@ async def user_ranking_individual(request: Request):
|
||||
}
|
||||
}
|
||||
|
||||
print("toital_count: " + str(total_count))
|
||||
|
||||
return JSONResponse(payload)
|
||||
|
||||
async def user_ranking_total(request: Request):
|
||||
@@ -290,7 +293,7 @@ async def user_ranking_total(request: Request):
|
||||
|
||||
score_obj = ["total_delta", "mobile_delta", "arcade_delta"]
|
||||
|
||||
cache_key = f"0-{mode}"
|
||||
cache_key = f"0-{str(mode)}"
|
||||
cached_data = await get_rank_cache(cache_key)
|
||||
if cached_data:
|
||||
records = cached_data
|
||||
@@ -302,24 +305,29 @@ async def user_ranking_total(request: Request):
|
||||
accounts.c[score_obj[mode]],
|
||||
accounts.c.title,
|
||||
accounts.c.avatar,
|
||||
).order_by(accounts.c[score_obj[mode]].desc())
|
||||
).where(
|
||||
accounts.c[score_obj[mode]] > 0
|
||||
).order_by(
|
||||
accounts.c[score_obj[mode]].desc()
|
||||
)
|
||||
|
||||
records = await player_database.fetch_all(query)
|
||||
records = [dict(record) for record in records]
|
||||
await write_rank_cache(cache_key, records, expire_seconds=120)
|
||||
|
||||
total_count = len(records)
|
||||
print("total_count fetched: " + str(total_count))
|
||||
for index, record in enumerate(records):
|
||||
print(record)
|
||||
if index >= page_number * page_count and index < (page_number + 1) * page_count:
|
||||
rank_user = await user_id_to_user_info_simple(record["id"])
|
||||
ranking_list.append({
|
||||
"position": index + 1,
|
||||
"username": rank_user["username"],
|
||||
"score": record[score_obj[mode]],
|
||||
"title": rank_user["title"],
|
||||
"avatar": record["avatar"]
|
||||
})
|
||||
if rank_user:
|
||||
ranking_list.append({
|
||||
"position": index + 1,
|
||||
"username": rank_user["username"],
|
||||
"score": record[score_obj[mode]],
|
||||
"title": rank_user["title"],
|
||||
"avatar": record["avatar"]
|
||||
})
|
||||
if user_id and record["id"] == user_id:
|
||||
player_ranking = {
|
||||
"username": user_info["username"],
|
||||
|
||||
@@ -1,28 +1,33 @@
|
||||
from starlette.requests import Request
|
||||
from starlette.routing import Route
|
||||
from starlette.responses import HTMLResponse, JSONResponse, RedirectResponse
|
||||
from starlette.responses import HTMLResponse, JSONResponse, RedirectResponse, StreamingResponse
|
||||
import secrets
|
||||
from datetime import datetime
|
||||
import time
|
||||
|
||||
from api.database import player_database, webs, is_admin, user_name_to_user_info, user_id_to_user_info_simple
|
||||
from api.misc import read_user_save_file, verify_password, should_serve_web
|
||||
from config import AUTHORIZATION_MODE
|
||||
from api.database import player_database, webs, is_admin, user_name_to_user_info, user_id_to_user_info_simple, get_user_export_data
|
||||
from api.misc import verify_password, should_serve_web
|
||||
from api.file import convert_user_export_data
|
||||
from config import AUTHORIZATION_MODE, SAVE_EXPORT_COOLDOWN
|
||||
|
||||
async def is_user(request: Request):
|
||||
token = request.cookies.get("token")
|
||||
if not token:
|
||||
return False
|
||||
query = webs.select().where(webs.c.token == token)
|
||||
return False, None
|
||||
query = webs.select().where(webs.c.web_token == token)
|
||||
web_data = await player_database.fetch_one(query)
|
||||
if not web_data:
|
||||
return False
|
||||
return False, None
|
||||
if web_data['permission'] < 1:
|
||||
return False
|
||||
return False, None
|
||||
|
||||
if AUTHORIZATION_MODE > 0:
|
||||
return await should_serve_web(web_data['user_id'])
|
||||
result = await should_serve_web(web_data['user_id'])
|
||||
if not result:
|
||||
return False, None
|
||||
|
||||
return True
|
||||
|
||||
return True, web_data
|
||||
|
||||
async def web_login_page(request: Request):
|
||||
with open("web/login.html", "r", encoding="utf-8") as file:
|
||||
@@ -53,13 +58,15 @@ async def web_login_login(request: Request):
|
||||
return JSONResponse({"status": "failed", "message": "Access denied."}, status_code=403)
|
||||
|
||||
query = webs.update().where(webs.c.user_id == user_info['id']).values(
|
||||
token=token
|
||||
web_token=token,
|
||||
updated_at=datetime.utcnow()
|
||||
)
|
||||
else:
|
||||
query = webs.insert().values(
|
||||
user_id=user_info['id'],
|
||||
permission=1,
|
||||
web_token=token,
|
||||
last_save_export=0,
|
||||
created_at=datetime.utcnow(),
|
||||
updated_at=datetime.utcnow()
|
||||
)
|
||||
@@ -75,7 +82,7 @@ async def user_center_api(request: Request):
|
||||
if not token:
|
||||
return JSONResponse({"status": "failed", "message": "Token is required."}, status_code=400)
|
||||
|
||||
query = webs.select().where(webs.c.token == token)
|
||||
query = webs.select().where(webs.c.web_token == token)
|
||||
web_record = await player_database.fetch_one(query)
|
||||
if not web_record:
|
||||
return JSONResponse({"status": "failed", "message": "Invalid token."}, status_code=403)
|
||||
@@ -96,7 +103,7 @@ async def user_center_api(request: Request):
|
||||
|
||||
response_data = {
|
||||
"username": user_info['username'],
|
||||
"last_save_export": web_record['last_save_export'].isoformat() if web_record['last_save_export'] else "None",
|
||||
"next_save_export": web_record['last_save_export'] + SAVE_EXPORT_COOLDOWN,
|
||||
}
|
||||
return JSONResponse({"status": "success", "data": response_data})
|
||||
|
||||
@@ -105,28 +112,64 @@ async def user_center_api(request: Request):
|
||||
return JSONResponse({"status": "failed", "message": "Invalid action."}, status_code=400)
|
||||
|
||||
async def user_center_page(request: Request):
|
||||
usr = await is_user(request)
|
||||
if not usr:
|
||||
serve, web_data = await is_user(request)
|
||||
if not serve:
|
||||
response = RedirectResponse(url="/login")
|
||||
response.delete_cookie(key="token")
|
||||
return response
|
||||
|
||||
with open("web/user.html", "r", encoding="utf-8") as file:
|
||||
html_template = file.read()
|
||||
is_adm = await is_admin(request)
|
||||
is_adm = await is_admin(request.cookies.get("token"))
|
||||
if is_adm:
|
||||
admin_button = f"""
|
||||
<div class="container mt-4">
|
||||
<button class="btn btn-light" onclick="window.location.href='/admin'">Admin Panel</button>
|
||||
<button class="btn btn-light" onclick="window.location.href='/admin'">Admin Panel</button>
|
||||
</div>
|
||||
"""
|
||||
html_template += admin_button
|
||||
|
||||
return HTMLResponse(content=html_template)
|
||||
|
||||
async def user_center_export_data(request: Request):
|
||||
serve, web_data = await is_user(request)
|
||||
if not serve:
|
||||
response = RedirectResponse(url="/login")
|
||||
response.delete_cookie(key="token")
|
||||
return response
|
||||
|
||||
user_id = web_data['user_id']
|
||||
last_save_export = web_data['last_save_export']
|
||||
current_time = time.time()
|
||||
|
||||
if current_time - last_save_export < SAVE_EXPORT_COOLDOWN:
|
||||
wait_time = int(SAVE_EXPORT_COOLDOWN - (current_time - last_save_export))
|
||||
return JSONResponse({"status": "failed", "message": f"Please wait {wait_time} seconds before exporting again."}, status_code=429)
|
||||
|
||||
user_json_data_set = await get_user_export_data(user_id)
|
||||
user_xlsx_stream = await convert_user_export_data(user_json_data_set)
|
||||
|
||||
headers = {
|
||||
"Content-Disposition": 'attachment; filename="export.xlsx"'
|
||||
}
|
||||
|
||||
update_query = webs.update().where(webs.c.user_id == user_id).values(
|
||||
last_save_export=int(current_time),
|
||||
updated_at=datetime.utcnow()
|
||||
)
|
||||
await player_database.execute(update_query)
|
||||
|
||||
return StreamingResponse(
|
||||
user_xlsx_stream,
|
||||
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
routes = [
|
||||
Route("/login", web_login_page, methods=["GET"]),
|
||||
Route("/login/", web_login_page, methods=["GET"]),
|
||||
Route("/login/login", web_login_login, methods=["POST"]),
|
||||
Route("/usercenter", user_center_page, methods=["GET"]),
|
||||
Route("/usercenter/api", user_center_api, methods=["POST"])
|
||||
Route("/usercenter/api", user_center_api, methods=["POST"]),
|
||||
Route("/usercenter/export_data", user_center_export_data, methods=["GET"]),
|
||||
]
|
||||
492
new_server_7003/db-conv.py
Normal file
492
new_server_7003/db-conv.py
Normal file
@@ -0,0 +1,492 @@
|
||||
import sqlalchemy
|
||||
from sqlalchemy import Table, Column, Integer, String, DateTime, JSON, ForeignKey, Index
|
||||
from sqlalchemy.ext.asyncio import create_async_engine
|
||||
from sqlalchemy import select, update, insert
|
||||
import json
|
||||
import os
|
||||
|
||||
START_COIN = 10
|
||||
|
||||
import os
|
||||
import databases
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
DB_NAME = "player.db"
|
||||
DB_PATH = os.path.join(os.getcwd(), DB_NAME)
|
||||
DATABASE_URL = f"sqlite+aiosqlite:///{DB_PATH}"
|
||||
|
||||
OLD_DB_NAME = "player_d.db"
|
||||
OLD_DB_PATH = os.path.join(os.getcwd(), OLD_DB_NAME)
|
||||
OLD_DATABASE_URL = f"sqlite+aiosqlite:///{OLD_DB_PATH}"
|
||||
|
||||
old_database = databases.Database(OLD_DATABASE_URL)
|
||||
old_metadata = sqlalchemy.MetaData()
|
||||
|
||||
player_database = databases.Database(DATABASE_URL)
|
||||
player_metadata = sqlalchemy.MetaData()
|
||||
|
||||
#----------------------- Old Table definitions -----------------------#
|
||||
|
||||
user = Table(
|
||||
"user",
|
||||
old_metadata,
|
||||
Column("id", Integer, primary_key=True, autoincrement=True),
|
||||
Column("username", String(20), unique=True, nullable=False),
|
||||
Column("password_hash", String(255), nullable=False),
|
||||
Column("device_id", String(512)),
|
||||
Column("data", String, nullable=True),
|
||||
Column("save_id", String, nullable=True),
|
||||
Column("crc", Integer, nullable=True),
|
||||
Column("timestamp", DateTime, default=None),
|
||||
Column("coin_mp", Integer, default=1)
|
||||
)
|
||||
|
||||
daily_reward = Table(
|
||||
"daily_reward",
|
||||
old_metadata,
|
||||
Column("id", Integer, primary_key=True, autoincrement=True),
|
||||
Column("device_id", String(512)),
|
||||
Column("timestamp", DateTime, default=datetime.utcnow),
|
||||
Column("my_stage", JSON),
|
||||
Column("my_avatar", JSON),
|
||||
Column("item", JSON),
|
||||
Column("day", Integer),
|
||||
Column("coin", Integer),
|
||||
Column("lvl", Integer),
|
||||
Column("title", Integer),
|
||||
Column("avatar", Integer),
|
||||
)
|
||||
|
||||
result = Table(
|
||||
"result",
|
||||
old_metadata,
|
||||
Column("rid", Integer, primary_key=True, autoincrement=True),
|
||||
Column("vid", String(512), nullable=False),
|
||||
Column("tid", String(512), nullable=False),
|
||||
Column("sid", Integer, nullable=False),
|
||||
Column("stts", String(64)),
|
||||
Column("id", Integer),
|
||||
Column("mode", Integer),
|
||||
Column("avatar", Integer),
|
||||
Column("score", Integer),
|
||||
Column("high_score", String(128)),
|
||||
Column("play_rslt", String(128)),
|
||||
Column("item", Integer),
|
||||
Column("os", String(16)),
|
||||
Column("os_ver", String(16)),
|
||||
Column("ver", String(16)),
|
||||
Column("mike", Integer),
|
||||
)
|
||||
|
||||
whitelist = Table(
|
||||
"whitelist",
|
||||
old_metadata,
|
||||
Column("id", String(512), primary_key=True)
|
||||
)
|
||||
blacklist = Table(
|
||||
"blacklist",
|
||||
old_metadata,
|
||||
Column("id", String(512), primary_key=True),
|
||||
Column("reason", String(256))
|
||||
)
|
||||
|
||||
bind = Table(
|
||||
"bind",
|
||||
old_metadata,
|
||||
Column("id", Integer, primary_key=True, autoincrement=True),
|
||||
Column("user_id", Integer, sqlalchemy.ForeignKey("user.id")),
|
||||
Column("bind_acc", String(256), unique=True, nullable=False),
|
||||
Column("bind_code", String(16), nullable=False),
|
||||
Column("is_verified", Integer, default=0),
|
||||
Column("auth_token", String(256), nullable=True),
|
||||
Column("bind_date", DateTime, default=datetime.utcnow)
|
||||
)
|
||||
|
||||
logs = Table(
|
||||
"logs",
|
||||
old_metadata,
|
||||
Column("id", Integer, primary_key=True, autoincrement=True),
|
||||
Column("user_id", Integer, sqlalchemy.ForeignKey("user.id")),
|
||||
Column("filename", String(256)),
|
||||
Column("filesize", Integer),
|
||||
Column("timestamp", DateTime, default=datetime.utcnow)
|
||||
)
|
||||
|
||||
device_list = Table(
|
||||
"device_list",
|
||||
old_metadata,
|
||||
Column("id", Integer, primary_key=True, autoincrement=True),
|
||||
Column("user_id", Integer, sqlalchemy.ForeignKey("user.id")),
|
||||
Column("device_id", String(512), unique=True, nullable=False),
|
||||
Column("last_login", DateTime, default=datetime.utcnow)
|
||||
)
|
||||
|
||||
#----------------------- End of old Table definitions -----------------------#
|
||||
|
||||
#----------------------- New Table definitions -----------------------#
|
||||
|
||||
accounts = Table(
|
||||
"accounts",
|
||||
player_metadata,
|
||||
Column("id", Integer, primary_key=True, autoincrement=True),
|
||||
Column("username", String(20), unique=True, nullable=False),
|
||||
Column("password_hash", String(255), nullable=False),
|
||||
Column("save_crc", String(24), nullable=True),
|
||||
Column("save_timestamp", DateTime, nullable=True),
|
||||
Column("save_id", String(24), nullable=True),
|
||||
Column("coin_mp", Integer, default=0),
|
||||
Column("title", Integer, default=1),
|
||||
Column("avatar", Integer, default=1),
|
||||
Column("mobile_delta", Integer, default=0),
|
||||
Column("arcade_delta", Integer, default=0),
|
||||
Column("total_delta", Integer, default=0),
|
||||
Column("created_at", DateTime, default=datetime.utcnow),
|
||||
Column("updated_at", DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
)
|
||||
|
||||
devices = Table(
|
||||
"devices",
|
||||
player_metadata,
|
||||
Column("device_id", String(64), primary_key=True),
|
||||
Column("user_id", Integer, ForeignKey("accounts.id")),
|
||||
Column("my_stage", JSON, default=[]),
|
||||
Column("my_avatar", JSON, default=[]),
|
||||
Column("item", JSON, default=[]),
|
||||
Column("daily_day", Integer, default=0),
|
||||
Column("daily_timestamp", DateTime, default=datetime.min),
|
||||
Column("coin", Integer, default=START_COIN),
|
||||
Column("lvl", Integer, default=1),
|
||||
Column("title", Integer, default=1),
|
||||
Column("avatar", Integer, default=1),
|
||||
Column("created_at", DateTime, default=datetime.utcnow),
|
||||
Column("updated_at", DateTime, default=datetime.utcnow, onupdate=datetime.utcnow),
|
||||
Column("last_login_at", DateTime, default=None)
|
||||
)
|
||||
|
||||
results = Table(
|
||||
"results",
|
||||
player_metadata,
|
||||
Column("id", Integer, primary_key=True, autoincrement=True),
|
||||
Column("device_id", String(64), ForeignKey("devices.device_id")),
|
||||
Column("user_id", Integer, ForeignKey("accounts.id")),
|
||||
Column("stts", JSON, nullable=False),
|
||||
Column("song_id", Integer, nullable=False),
|
||||
Column("mode", Integer, nullable=False),
|
||||
Column("avatar", Integer, nullable=False),
|
||||
Column("score", Integer, nullable=False),
|
||||
Column("high_score", JSON, nullable=False),
|
||||
Column("play_rslt", JSON, nullable=False),
|
||||
Column("item", Integer, nullable=False),
|
||||
Column("os", String(8), nullable=False),
|
||||
Column("os_ver", String(16), nullable=False),
|
||||
Column("ver", String(8), nullable=False),
|
||||
Column("created_at", DateTime, default=datetime.utcnow)
|
||||
)
|
||||
|
||||
Index(
|
||||
"idx_results_song_mode_score",
|
||||
results.c.song_id,
|
||||
results.c.mode,
|
||||
results.c.score.desc(),
|
||||
)
|
||||
|
||||
webs = Table(
|
||||
"webs",
|
||||
player_metadata,
|
||||
Column("id", Integer, primary_key=True, autoincrement=True),
|
||||
Column("user_id", Integer, ForeignKey("accounts.id")),
|
||||
Column("permission", Integer, default=1),
|
||||
Column("web_token", String(128), unique=True, nullable=False),
|
||||
Column("last_save_export", Integer, nullable=True),
|
||||
Column("created_at", DateTime, default=datetime.utcnow),
|
||||
Column("updated_at", DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
)
|
||||
|
||||
batch_tokens = Table(
|
||||
"batch_tokens",
|
||||
player_metadata,
|
||||
Column("id", Integer, primary_key=True, autoincrement=True),
|
||||
Column("batch_token", String(128), unique=True, nullable=False),
|
||||
Column("expire_at", DateTime, nullable=False),
|
||||
Column("uses_left", Integer, default=1),
|
||||
Column("auth_id", String(64), nullable=False),
|
||||
Column("created_at", DateTime, default=datetime.utcnow),
|
||||
Column("updated_at", DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
)
|
||||
|
||||
whitelists = Table(
|
||||
"whitelists",
|
||||
player_metadata,
|
||||
Column("id", Integer, primary_key=True, autoincrement=True),
|
||||
Column("device_id", String(64), ForeignKey("devices.device_id")),
|
||||
)
|
||||
|
||||
blacklists = Table(
|
||||
"blacklists",
|
||||
player_metadata,
|
||||
Column("id", Integer, primary_key=True, autoincrement=True),
|
||||
Column("ban_terms", String(128), unique=True, nullable=False),
|
||||
Column("reason", String(255), nullable=True)
|
||||
)
|
||||
|
||||
binds = Table(
|
||||
"binds",
|
||||
player_metadata,
|
||||
Column("id", Integer, primary_key=True, autoincrement=True),
|
||||
Column("user_id", Integer, ForeignKey("accounts.id")),
|
||||
Column("bind_account", String(128), unique=True, nullable=False),
|
||||
Column("bind_code", String(6), nullable=False),
|
||||
Column("is_verified", Integer, default=0),
|
||||
Column("auth_token", String(64), unique=True),
|
||||
Column("bind_date", DateTime, default=datetime.utcnow)
|
||||
)
|
||||
|
||||
logs = Table(
|
||||
"logs",
|
||||
player_metadata,
|
||||
Column("id", Integer, primary_key=True, autoincrement=True),
|
||||
Column("user_id", Integer, ForeignKey("accounts.id")),
|
||||
Column("filename", String(255), nullable=False),
|
||||
Column("filesize", Integer, nullable=False),
|
||||
Column("timestamp", DateTime, default=datetime.utcnow)
|
||||
)
|
||||
|
||||
#----------------------- End of new Table definitions -----------------------#
|
||||
|
||||
async def init_db():
|
||||
if not os.path.exists(DB_PATH):
|
||||
print("[DB] Creating new database:", DB_PATH)
|
||||
|
||||
if not os.path.exists(OLD_DB_PATH):
|
||||
print("[DB] Old db must exist as player_d.db")
|
||||
|
||||
old_engine = create_async_engine(OLD_DATABASE_URL, echo=False)
|
||||
async with old_engine.begin() as conn:
|
||||
await conn.run_sync(old_metadata.create_all)
|
||||
await old_engine.dispose()
|
||||
|
||||
engine = create_async_engine(DATABASE_URL, echo=False)
|
||||
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(player_metadata.create_all)
|
||||
|
||||
await engine.dispose()
|
||||
print("[DB] Database initialized successfully.")
|
||||
|
||||
async def save_user_data(user_id, user_data):
|
||||
save_dir = "save"
|
||||
save_file = os.path.join(save_dir, f"{user_id}.dat")
|
||||
|
||||
if not os.path.exists(save_dir):
|
||||
os.makedirs(save_dir)
|
||||
|
||||
with open(save_file, "w") as file:
|
||||
file.write(user_data)
|
||||
|
||||
async def convert_db():
|
||||
await old_database.connect()
|
||||
await player_database.connect()
|
||||
|
||||
# Conversion logic goes here
|
||||
print("[DB] Starting database conversion...")
|
||||
|
||||
# Convert user -> accounts
|
||||
print("[DB] Converting users to accounts...")
|
||||
query = select(old_metadata.tables["user"])
|
||||
all_users = await old_database.fetch_all(query)
|
||||
all_users = [dict(u) for u in all_users]
|
||||
for user in all_users:
|
||||
user_device_id = user['device_id']
|
||||
|
||||
user_data = user.get('data') if 'data' in user else None
|
||||
if user_data:
|
||||
user_id = user['id']
|
||||
await save_user_data(user_id, user_data)
|
||||
|
||||
|
||||
user_old_device = await old_database.fetch_one(
|
||||
select(old_metadata.tables["daily_reward"]).where(old_metadata.tables["daily_reward"].c.device_id == user_device_id)
|
||||
)
|
||||
user_old_device = dict(user_old_device) if user_old_device else None
|
||||
|
||||
insert_query = insert(accounts).values(
|
||||
username=user['username'],
|
||||
password_hash=user['password_hash'],
|
||||
save_crc=user['crc'],
|
||||
save_timestamp=user['timestamp'],
|
||||
save_id=user['save_id'],
|
||||
coin_mp=user['coin_mp'],
|
||||
title=user_old_device['title'] if user_old_device else 1,
|
||||
avatar=user_old_device['avatar'] if user_old_device else 1,
|
||||
mobile_delta=0,
|
||||
arcade_delta=0,
|
||||
total_delta=0,
|
||||
created_at=datetime.utcnow(),
|
||||
updated_at=datetime.utcnow()
|
||||
)
|
||||
await player_database.execute(insert_query)
|
||||
|
||||
# Compute score deltas
|
||||
print("[DB] Computing score deltas for accounts...")
|
||||
all_new_users = await player_database.fetch_all(select(accounts))
|
||||
all_new_users = [dict(u) for u in all_new_users]
|
||||
for new_user in all_new_users:
|
||||
username = new_user['username']
|
||||
old_user_object = await old_database.fetch_one(
|
||||
select(old_metadata.tables["user"]).where(old_metadata.tables["user"].c.username == username)
|
||||
)
|
||||
old_user_id = old_user_object['id']
|
||||
old_user_results_mobile = await old_database.fetch_all(
|
||||
select(old_metadata.tables["result"]).where(
|
||||
(old_metadata.tables["result"].c.sid == old_user_id) &
|
||||
(old_metadata.tables["result"].c.mode.in_([1, 2, 3]))
|
||||
)
|
||||
)
|
||||
old_user_mobile_sum = sum([int(r['score']) for r in old_user_results_mobile])
|
||||
old_user_results_arcade = await old_database.fetch_all(
|
||||
select(old_metadata.tables["result"]).where(
|
||||
(old_metadata.tables["result"].c.sid == old_user_id) &
|
||||
(old_metadata.tables["result"].c.mode.in_([11, 12, 13]))
|
||||
)
|
||||
)
|
||||
old_user_arcade_sum = sum([int(r['score']) for r in old_user_results_arcade])
|
||||
old_user_results_total = old_user_mobile_sum + old_user_arcade_sum
|
||||
|
||||
update_query = (
|
||||
update(accounts).where(accounts.c.id == new_user['id']).values(
|
||||
mobile_delta=old_user_mobile_sum,
|
||||
arcade_delta=old_user_arcade_sum,
|
||||
total_delta=old_user_results_total
|
||||
)
|
||||
)
|
||||
await player_database.execute(update_query)
|
||||
|
||||
# Convert daily_reward -> devices
|
||||
print("[DB] Converting daily rewards to devices...")
|
||||
all_devices = await old_database.fetch_all(select(old_metadata.tables["daily_reward"]))
|
||||
all_devices = [dict(d) for d in all_devices]
|
||||
for device in all_devices:
|
||||
connected_user = await old_database.fetch_one(
|
||||
select(old_metadata.tables["user"]).where(old_metadata.tables["user"].c.device_id == device['device_id'])
|
||||
)
|
||||
insert_query = insert(devices).values(
|
||||
device_id=device['device_id'],
|
||||
user_id=connected_user['id'] if connected_user else None,
|
||||
my_stage=device['my_stage'],
|
||||
my_avatar=device['my_avatar'],
|
||||
item=device['item'],
|
||||
daily_day=device['day'],
|
||||
daily_timestamp=device['timestamp'],
|
||||
coin=device['coin'],
|
||||
lvl=device['lvl'],
|
||||
title=device['title'],
|
||||
avatar=device['avatar'],
|
||||
created_at=datetime.utcnow(),
|
||||
updated_at=datetime.utcnow(),
|
||||
last_login_at=datetime.utcnow()
|
||||
)
|
||||
await player_database.execute(insert_query)
|
||||
|
||||
# convert result -> results
|
||||
print("[DB] Converting results to results...")
|
||||
all_results = await old_database.fetch_all(select(old_metadata.tables["result"]))
|
||||
all_results = [dict(r) for r in all_results]
|
||||
for result in all_results:
|
||||
if result['sid'] is not None and result['sid'] != 0:
|
||||
# Skip guests, they are no longer ranked
|
||||
old_user_object = await old_database.fetch_one(
|
||||
select(old_metadata.tables["user"]).where(old_metadata.tables["user"].c.id == result['sid'])
|
||||
)
|
||||
old_user_username = old_user_object['username'] if old_user_object else None
|
||||
if old_user_username:
|
||||
new_user_object = await player_database.fetch_one(
|
||||
select(accounts).where(accounts.c.username == old_user_username)
|
||||
)
|
||||
new_user_id = new_user_object['id']
|
||||
insert_query = insert(results).values(
|
||||
device_id=result['vid'],
|
||||
user_id=new_user_id,
|
||||
stts=convert_array(result['stts']),
|
||||
song_id=result['id'],
|
||||
mode=result['mode'],
|
||||
avatar=result['avatar'],
|
||||
score=result['score'],
|
||||
high_score=convert_array(result['high_score']),
|
||||
play_rslt=convert_array(result['play_rslt']),
|
||||
item=result['item'],
|
||||
os=result['os'],
|
||||
os_ver=result['os_ver'],
|
||||
ver=result['ver'],
|
||||
created_at=datetime.utcnow(),
|
||||
)
|
||||
await player_database.execute(insert_query)
|
||||
|
||||
# convert bwlist
|
||||
print("[DB] Converting blacklists and whitelists...")
|
||||
old_blacklists = await old_database.fetch_all(select(old_metadata.tables["blacklist"]))
|
||||
old_blacklists = [dict(b) for b in old_blacklists]
|
||||
|
||||
for bl in old_blacklists:
|
||||
insert_query = insert(blacklists).values(
|
||||
ban_terms=bl['id'],
|
||||
reason=bl['reason']
|
||||
)
|
||||
await player_database.execute(insert_query)
|
||||
|
||||
old_whitelists = await old_database.fetch_all(select(old_metadata.tables["whitelist"]))
|
||||
old_whitelists = [dict(w) for w in old_whitelists]
|
||||
for wl in old_whitelists:
|
||||
insert_query = insert(whitelists).values(
|
||||
device_id=wl['id']
|
||||
)
|
||||
await player_database.execute(insert_query)
|
||||
|
||||
# convert binds (optional)
|
||||
|
||||
if 'bind' in old_metadata.tables:
|
||||
print("[DB] Converting binds...")
|
||||
old_binds = await old_database.fetch_all(select(old_metadata.tables["bind"]))
|
||||
old_binds = [dict(b) for b in old_binds]
|
||||
for b in old_binds:
|
||||
insert_query = insert(binds).values(
|
||||
user_id=b['user_id'],
|
||||
bind_account=b['bind_acc'],
|
||||
bind_code=b['bind_code'],
|
||||
is_verified=b['is_verified'],
|
||||
auth_token=b['auth_token'],
|
||||
bind_date=b['bind_date']
|
||||
)
|
||||
await player_database.execute(insert_query)
|
||||
|
||||
# convert logs (optional)
|
||||
|
||||
if 'logs' in old_metadata.tables:
|
||||
print("[DB] Converting logs...")
|
||||
old_logs = await old_database.fetch_all(select(old_metadata.tables["logs"]))
|
||||
old_logs = [dict(l) for l in old_logs]
|
||||
for l in old_logs:
|
||||
insert_query = insert(logs).values(
|
||||
user_id=l['user_id'],
|
||||
filename=l['filename'],
|
||||
filesize=l['filesize'],
|
||||
timestamp=l['timestamp']
|
||||
)
|
||||
await player_database.execute(insert_query)
|
||||
|
||||
# tables not converted: batch_tokens (new field added), webs (auto created)
|
||||
|
||||
await old_database.disconnect()
|
||||
await player_database.disconnect()
|
||||
print("[DB] Database conversion completed successfully.")
|
||||
|
||||
def convert_array(old_array):
|
||||
old_array = "[" + old_array + "]"
|
||||
old_array = json.loads(old_array)
|
||||
return old_array
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import asyncio
|
||||
asyncio.run(init_db())
|
||||
asyncio.run(convert_db())
|
||||
@@ -168,9 +168,11 @@ function showDetailTotal(mode, page) {
|
||||
|
||||
// Mode boxes
|
||||
|
||||
html = `<div>${songName}</div>`;
|
||||
html = `<div id="ranking_content">
|
||||
<div class="song-name">${songName}</div>
|
||||
`;
|
||||
|
||||
rowStart = '<div class="button-row">'
|
||||
rowStart = '<div class="button-row mode-buttons">'
|
||||
rowEnd = '</div>'
|
||||
rowContent = []
|
||||
|
||||
@@ -182,7 +184,7 @@ function showDetailTotal(mode, page) {
|
||||
rowContent.push(`<div class="bt_bg01_ac">${label}</div>`);
|
||||
} else {
|
||||
rowContent.push(
|
||||
`<a onclick="showDetailTotal(${modeValue});" class="bt_bg01_xnarrow">${label}</a>`
|
||||
`<a onclick="showDetailTotal(${modeValue}, 0);" class="bt_bg01_xnarrow">${label}</a>`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -210,25 +212,29 @@ function showDetailTotal(mode, page) {
|
||||
`;
|
||||
|
||||
// generate pagination buttons
|
||||
if (generatePrevButton) {
|
||||
html += `
|
||||
<button style="width: 170px; height: 40px; margin: 10px; background-color: #000000; color: #FFFFFF;"
|
||||
onclick="showDetailTotal(${mode}, ${page - 1})">
|
||||
Prev Page
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
html += `
|
||||
<div class="pagination-container">
|
||||
${generatePrevButton ? `
|
||||
<button class="pagination-button prev-button"
|
||||
onclick="showDetailTotal(${mode}, ${page - 1})">
|
||||
Prev Page
|
||||
</button>
|
||||
` : '<div class="placeholder"></div>'} <!-- Placeholder for alignment -->
|
||||
|
||||
if (generateNextButton) {
|
||||
html += `
|
||||
<button style="width: 170px; height: 40px; margin: 10px; background-color: #000000; color: #FFFFFF;"
|
||||
onclick="showDetailTotal(${mode}, ${page + 1})">
|
||||
Next Page
|
||||
<button onclick='restoreBaseStructure();loadUI();filterList();loadList();' class="pagination-button">
|
||||
Back
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
${generateNextButton ? `
|
||||
<button class="pagination-button next-button"
|
||||
onclick="showDetailTotal(${mode}, ${page + 1})">
|
||||
Next Page
|
||||
</button>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Loop leaderboard ranks
|
||||
html += `<div class="leaderboard-players">`;
|
||||
|
||||
for (let i = 0; i < rankingList.length; i++) {
|
||||
userData = rankingList[i];
|
||||
@@ -245,32 +251,8 @@ function showDetailTotal(mode, page) {
|
||||
`;
|
||||
|
||||
}
|
||||
|
||||
// Generate pagination buttons again
|
||||
|
||||
if (generatePrevButton) {
|
||||
html += `
|
||||
<button style="width: 170px; height: 40px; margin: 10px; background-color: #000000; color: #FFFFFF;"
|
||||
onclick="showDetailTotal(${mode}, ${page - 1})">
|
||||
Prev Page
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
if (generateNextButton) {
|
||||
html += `
|
||||
<button style="width: 170px; height: 40px; margin: 10px; background-color: #000000; color: #FFFFFF;"
|
||||
onclick="showDetailTotal(${mode}, ${page + 1})">
|
||||
Next Page
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
html += `
|
||||
<button onclick='restoreBaseStructure();loadUI();filterList();loadList();' style="margin-top: 20px;" class="bt_bg01">
|
||||
Back
|
||||
</button>
|
||||
`;
|
||||
html += `</div>`;
|
||||
|
||||
document.getElementById('ranking_box').innerHTML = html;
|
||||
|
||||
@@ -358,6 +340,7 @@ function showDetailIndividual(songId, mode, page = 0) {
|
||||
if (page > 0) {
|
||||
generatePrevButton = true;
|
||||
}
|
||||
|
||||
if ((page + 1) * pageCount < totalCount) {
|
||||
generateNextButton = true;
|
||||
}
|
||||
@@ -413,25 +396,29 @@ function showDetailIndividual(songId, mode, page = 0) {
|
||||
`;
|
||||
|
||||
// generate pagination buttons
|
||||
if (generatePrevButton) {
|
||||
html += `
|
||||
<button style="width: 170px; height: 40px; margin: 10px; background-color: #000000; color: #FFFFFF;"
|
||||
onclick="showDetailIndividual(${songId}, ${mode}, ${page - 1})">
|
||||
Prev Page
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
html += `
|
||||
<div class="pagination-container">
|
||||
${generatePrevButton ? `
|
||||
<button class="pagination-button prev-button"
|
||||
onclick="showDetailIndividual(${songId}, ${mode}, ${page - 1})">
|
||||
Prev Page
|
||||
</button>
|
||||
` : '<div class="placeholder"></div>'}
|
||||
|
||||
if (generateNextButton) {
|
||||
html += `
|
||||
<button style="width: 170px; height: 40px; margin: 10px; background-color: #000000; color: #FFFFFF;"
|
||||
onclick="showDetailIndividual(${songId}, ${mode}, ${page + 1})">
|
||||
Next Page
|
||||
<button onclick='restoreBaseStructure();loadUI();filterList();loadList();' class="pagination-button">
|
||||
Back
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
${generateNextButton ? `
|
||||
<button class="pagination-button next-button"
|
||||
onclick="showDetailIndividual(${songId}, ${mode}, ${page + 1})">
|
||||
Next Page
|
||||
</button>
|
||||
` : '<div class="placeholder"></div>'}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Loop leaderboard ranks
|
||||
html += `<div class="leaderboard-players">`;
|
||||
|
||||
for (let i = 0; i < rankingList.length; i++) {
|
||||
userData = rankingList[i];
|
||||
@@ -448,33 +435,9 @@ function showDetailIndividual(songId, mode, page = 0) {
|
||||
`;
|
||||
|
||||
}
|
||||
|
||||
html += `</div>`;
|
||||
|
||||
// Generate pagination buttons again
|
||||
|
||||
if (generatePrevButton) {
|
||||
html += `
|
||||
<button style="width: 170px; height: 40px; margin: 10px; background-color: #000000; color: #FFFFFF;"
|
||||
onclick="showDetailIndividual(${songId}, ${mode}, ${page - 1})">
|
||||
Prev Page
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
if (generateNextButton) {
|
||||
html += `
|
||||
<button style="width: 170px; height: 40px; margin: 10px; background-color: #000000; color: #FFFFFF;"
|
||||
onclick="showDetailIndividual(${songId}, ${mode}, ${page + 1})">
|
||||
Next Page
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
html += `
|
||||
<button onclick='restoreBaseStructure();loadUI();filterList();loadList();' style="margin-top: 20px;" class="bt_bg01">
|
||||
Back
|
||||
</button>
|
||||
`;
|
||||
|
||||
document.getElementById('ranking_box').innerHTML = html;
|
||||
|
||||
} else {
|
||||
|
||||
@@ -20,8 +20,8 @@
|
||||
padding: 20px;
|
||||
}
|
||||
.dlc_logo {
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
width: 70vw;
|
||||
max-width: 1000px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.dlc_extra_body {
|
||||
@@ -34,19 +34,19 @@
|
||||
padding: 0;
|
||||
}
|
||||
.text-content {
|
||||
font-size: 18px;
|
||||
font-size: 36px;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.buy-button {
|
||||
display: inline-block;
|
||||
width: 160px;
|
||||
height: 110px;
|
||||
width: 30vw;
|
||||
height: 20vw;
|
||||
background: url('/files/web/frame_buy.png') no-repeat center center;
|
||||
background-size: contain;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 22px;
|
||||
font-size: 30px;
|
||||
font-weight: bold;
|
||||
padding: 15px;
|
||||
cursor: pointer;
|
||||
@@ -54,26 +54,26 @@
|
||||
}
|
||||
.quit-button {
|
||||
display: inline-block;
|
||||
width: 150px;
|
||||
height: 30px;
|
||||
width: 33vw;
|
||||
height: 10vw;
|
||||
background: url('/files/web/quit_button.png') no-repeat center center;
|
||||
background-size: contain;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 20px;
|
||||
font-size: 30px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
.buy-button-extra {
|
||||
display: inline-block;
|
||||
width: 160px;
|
||||
height: 110px;
|
||||
width: 30vw;
|
||||
height: 20vw;
|
||||
background: url('/files/web/frame_buy_extra.png') no-repeat center center;
|
||||
background-size: contain;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 22px;
|
||||
font-size: 30px;
|
||||
font-weight: bold;
|
||||
padding: 15px;
|
||||
cursor: pointer;
|
||||
@@ -81,13 +81,13 @@
|
||||
}
|
||||
.quit-button-extra {
|
||||
display: inline-block;
|
||||
width: 150px;
|
||||
height: 30px;
|
||||
width: 33vw;
|
||||
height: 10vw;
|
||||
background: url('/files/web/quit_button_extra.png') no-repeat center center;
|
||||
background-size: contain;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 20px;
|
||||
font-size: 30px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
@@ -99,8 +99,13 @@
|
||||
margin-top: 10px;
|
||||
}
|
||||
.coin-icon {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
width: 7vw;
|
||||
height: 7vw;
|
||||
margin-left: 5px;
|
||||
}
|
||||
.coin-price {
|
||||
font-size: 30px;
|
||||
font-weight: bold;
|
||||
margin-left: 5px;
|
||||
}
|
||||
body {
|
||||
@@ -131,7 +136,12 @@ html {
|
||||
.d_ib{display:inline-block;}
|
||||
.mb10p{margin-bottom:10%;}
|
||||
.f90{font-size:90%;}
|
||||
.f80{font-size:70%;}
|
||||
.pt50{padding-top:50px;}
|
||||
.f20{font-size:20%;}
|
||||
.f30{font-size:30%;}
|
||||
.f40{font-size:40%;}
|
||||
.f50{font-size:50%;}
|
||||
.f60{font-size:60%;}
|
||||
.f70{font-size:70%;}
|
||||
.plr25{padding-left:25px;padding-right:25px;}
|
||||
@@ -156,7 +166,6 @@ div#wrapper{
|
||||
.wrapper_box{
|
||||
text-align:center;
|
||||
margin-top:5%;
|
||||
margin-bottom:-10%;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
@@ -261,6 +270,12 @@ div#wrapper{
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
flex-grow: 1;
|
||||
overflow-y: auto;
|
||||
height: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.song-item {
|
||||
@@ -295,6 +310,11 @@ div#wrapper{
|
||||
|
||||
.song-name {
|
||||
font-size: 26px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mode-buttons {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.composer-name-owned {
|
||||
@@ -319,7 +339,6 @@ div#wrapper{
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.player-element {
|
||||
display: flex;
|
||||
@@ -333,6 +352,7 @@ div#wrapper{
|
||||
height: 100px;
|
||||
width: 98%;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.player-info {
|
||||
@@ -376,14 +396,6 @@ div#wrapper{
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
}
|
||||
.leaderboard-container {
|
||||
height: calc(53vh);
|
||||
overflow-y: auto;
|
||||
border: 1px solid #444;
|
||||
margin: 20px auto;
|
||||
padding: 10px;
|
||||
background-color: #111;
|
||||
}
|
||||
|
||||
.leaderboard-player {
|
||||
display: flex;
|
||||
@@ -459,4 +471,77 @@ div#wrapper{
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
|
||||
#ranking_content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.song-list {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
#ranking_searchbar,
|
||||
#ranking_sort {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
#ranking_box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 90vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.leaderboard-players {
|
||||
flex-grow: 1;
|
||||
height: 90%;
|
||||
overflow-y: auto;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.pagination-button {
|
||||
width: 170px;
|
||||
height: 40px;
|
||||
background-color: #000000;
|
||||
color: #FFFFFF;
|
||||
border: 1px solid #808080;
|
||||
cursor: pointer;
|
||||
font-size: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.pagination-button:hover {
|
||||
background-color: #333333;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
flex: 0.25;
|
||||
}
|
||||
|
||||
.pagination-button-shop {
|
||||
width: 170px;
|
||||
height: 40px;
|
||||
margin: 10px;
|
||||
background-color: #000000;
|
||||
color: #FFFFFF;
|
||||
font-size: 24px;
|
||||
}
|
||||
@@ -101,17 +101,17 @@ function generateMenuContent() {
|
||||
if (shopMode === 0 && stagePage === 0) {
|
||||
html += `
|
||||
<a onclick="showFMax()">
|
||||
<img src="/files/web/dlc_4max.jpg" style="width: 90%; margin-bottom: 110px; margin-top: -100px;" />
|
||||
<img src="/files/web/dlc_4max.jpg" style="width: 90%; margin-bottom: 110px; margin-top: -80px;" />
|
||||
</a><br>
|
||||
<a onclick="showExtra()">
|
||||
<img src="/files/web/dlc_extra.jpg" style="width: 90%; margin-bottom: 20px; margin-top: -100px;" />
|
||||
<img src="/files/web/dlc_extra.jpg" style="width: 90%; margin-bottom: 20px; margin-top: -80px;" />
|
||||
</a><br>
|
||||
`;
|
||||
}
|
||||
|
||||
if (generatePrevButton) {
|
||||
html += `
|
||||
<button style="width: 170px; height: 40px; margin: 10px; background-color: #000000; color: #FFFFFF;"
|
||||
<button class="pagination-button-shop"
|
||||
onclick="prevPage(${shopMode})">
|
||||
Prev Page
|
||||
</button>
|
||||
@@ -120,7 +120,7 @@ function generateMenuContent() {
|
||||
|
||||
if (generateNextButton) {
|
||||
html += `
|
||||
<button style="width: 170px; height: 40px; margin: 10px; background-color: #000000; color: #FFFFFF;"
|
||||
<button class="pagination-button-shop"
|
||||
onclick="nextPage(${shopMode})">
|
||||
Next Page
|
||||
</button>
|
||||
@@ -144,7 +144,7 @@ function generateMenuContent() {
|
||||
|
||||
if (generatePrevButton) {
|
||||
html += `
|
||||
<button style="width: 170px; height: 40px; margin: 10px; background-color: #000000; color: #FFFFFF;"
|
||||
<button class="pagination-button-shop"
|
||||
onclick="prevPage(${shopMode})">
|
||||
Prev Page
|
||||
</button>
|
||||
@@ -153,7 +153,7 @@ function generateMenuContent() {
|
||||
|
||||
if (generateNextButton) {
|
||||
html += `
|
||||
<button style="width: 170px; height: 40px; margin: 10px; background-color: #000000; color: #FFFFFF;"
|
||||
<button class="pagination-button-shop"
|
||||
onclick="nextPage(${shopMode})">
|
||||
Next Page
|
||||
</button>
|
||||
@@ -231,10 +231,10 @@ function showItemDetail(mode, itemId) {
|
||||
<div class="image-container">
|
||||
<img src="/files/image/icon/shop/${itemId}.jpg" alt="Item Image" style="width: 180px; height: 180px;" />
|
||||
</div>
|
||||
<p>Would you like to purchase this song?</p>
|
||||
<p class="f80">Would you like to purchase this song?</p>
|
||||
<div>
|
||||
<p>${payload.data.property_first} - ${payload.data.property_second}</p>
|
||||
<p>Difficulty Levels: ${payload.data.property_third}</p>
|
||||
<p class="f80">${payload.data.property_first} - ${payload.data.property_second}</p>
|
||||
<p class="f80">Difficulty Levels: ${payload.data.property_third}</p>
|
||||
</div>
|
||||
<div>
|
||||
<img src="/files/web/coin_icon.png" class="coin-icon" style="width: 40px; height: 40px;" alt="Coin Icon" />
|
||||
@@ -247,10 +247,10 @@ function showItemDetail(mode, itemId) {
|
||||
<div class="image-container">
|
||||
<img src="/files/image/icon/avatar/${itemId}.png" alt="Item Image" style="width: 180px; height: 180px; background-color: black; object-fit: contain;" />
|
||||
</div>
|
||||
<p>Would you like to purchase this avatar?</p>
|
||||
<p class="f80">Would you like to purchase this avatar?</p>
|
||||
<div>
|
||||
<p>${payload.data.property_first}</p>
|
||||
<p>Effect: ${payload.data.property_second}</p>
|
||||
<p class="f80">${payload.data.property_first}</p>
|
||||
<p class="f80">Effect: ${payload.data.property_second}</p>
|
||||
</div>
|
||||
<div>
|
||||
<img src="/files/web/coin_icon.png" class="coin-icon" style="width: 40px; height: 40px;" alt="Coin Icon" />
|
||||
@@ -263,10 +263,10 @@ function showItemDetail(mode, itemId) {
|
||||
<div class="image-container">
|
||||
<img src="/files/image/icon/item/${itemId}.png" alt="Item Image" style="width: 180px; height: 180px;" />
|
||||
</div>
|
||||
<p>Would you like to purchase this item?</p>
|
||||
<p class="f80">Would you like to purchase this item?</p>
|
||||
<div>
|
||||
<p>${payload.data.property_first}</p>
|
||||
<p>Effect: ${payload.data.property_second}</p>
|
||||
<p class="f80">${payload.data.property_first}</p>
|
||||
<p class="f80">Effect: ${payload.data.property_second}</p>
|
||||
</div>
|
||||
<div>
|
||||
<img src="/files/web/coin_icon.png" class="coin-icon" style="width: 40px; height: 40px;" alt="Coin Icon" />
|
||||
@@ -329,19 +329,17 @@ function purchaseItem(mode, itemId) {
|
||||
if (payload.state === 1) {
|
||||
contentElement.innerHTML = `
|
||||
<p>${payload.message}</p>
|
||||
<button style="margin-top: 20px;" class="bt_bg01"
|
||||
onclick="on_initialize();">
|
||||
Back
|
||||
</button>
|
||||
<div style="margin-top: 20px;">
|
||||
<a onclick="on_initialize();" class="bt_bg01">Back</a><br>
|
||||
</div>
|
||||
`;
|
||||
document.getElementById('coinCounter').innerText = payload.data.coin;
|
||||
} else {
|
||||
contentElement.innerHTML = `
|
||||
<p>Purchase failed: ${payload.message}</p>
|
||||
<button style="margin-top: 20px;" class="bt_bg01"
|
||||
onclick="on_initialize();">
|
||||
Back
|
||||
</button>
|
||||
<div style="margin-top: 20px;">
|
||||
<a onclick="on_initialize();" class="bt_bg01">Back</a><br>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
})
|
||||
@@ -349,10 +347,9 @@ function purchaseItem(mode, itemId) {
|
||||
console.error('Error processing purchase:', error);
|
||||
contentElement.innerHTML = `
|
||||
<p>Failed to process your purchase. Please try again later.</p>
|
||||
<button style="margin-top: 20px;" class="bt_bg01"
|
||||
onclick="on_initialize();">
|
||||
Back
|
||||
</button>
|
||||
<div style="margin-top: 20px;">
|
||||
<a onclick="on_initialize();" class="bt_bg01">Back</a><br>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
}
|
||||
@@ -373,7 +370,7 @@ function restoreBaseStructure() {
|
||||
<div id="wrapper">
|
||||
<div class="wrapper_box">
|
||||
<div class="a_left w100 d_ib mb10p">
|
||||
<ul style="list-style-type: none; padding: 0; margin-top: 100px; text-align: center;" id="menuList">
|
||||
<ul style="list-style-type: none; padding: 0; margin-top: 60px; text-align: center;" id="menuList">
|
||||
</ul>
|
||||
<div class="f90 a_center pt50" id="content">
|
||||
Loading...
|
||||
@@ -446,13 +443,13 @@ function showFMax() {
|
||||
<div class="text-content">
|
||||
<p>Experience the arcade with the GC4MAX expansion! This DLC unlocks 320+ exclusive songs for your 2OS experience.</p>
|
||||
<p>Note that these songs don't have mobile difficulties. A short placeholder is used, and GCoin reward is not available for playing them. You must clear the Normal difficulty to unlock AC content.</p>
|
||||
<p>Due to technical limitations, Extra level charts cannot be ported as of now. After purchasing, you will have access to support information and update logs.</p>
|
||||
<p>After purchasing, you will have access to support information and update logs.</p>
|
||||
</div>
|
||||
<button class="buy-button" onclick="purchaseItem(3, 1);">
|
||||
Buy
|
||||
<div class="coin-container">
|
||||
<img src="/files/web/coin_icon.png" alt="Coin Icon" class="coin-icon">
|
||||
<span style="font-size: 22px; font-weight: bold;">${payload.data.price}</span>
|
||||
<span class="coin-price">${payload.data.price}</span>
|
||||
</div>
|
||||
</button>
|
||||
<br><br>
|
||||
@@ -537,7 +534,7 @@ function showExtra() {
|
||||
let html = '';
|
||||
if (extraPurchased) {
|
||||
html = `
|
||||
<br><br><br><br><br><br><br>
|
||||
<br><br><br><br><br><br><br><br><br>
|
||||
<div class="text-content">
|
||||
<p>You have unlocked the EXTRA Challenge!</p>
|
||||
<p>Please report bugs/missing tracks to Discord: #AnTcfgss, or QQ 3421587952.</p>
|
||||
@@ -548,7 +545,7 @@ function showExtra() {
|
||||
`;
|
||||
} else {
|
||||
html = `
|
||||
<br><br><br><br><br><br><br>
|
||||
<div style="margin-top: 30vh;"></div>
|
||||
<div class="text-content">
|
||||
<p>Are you looking for a bad time?</p>
|
||||
<p>If so, this is the Ultimate - Extra - Challenge.</p>
|
||||
@@ -560,7 +557,7 @@ function showExtra() {
|
||||
Buy
|
||||
<div class="coin-container">
|
||||
<img src="/files/web/coin_icon.png" alt="Coin Icon" class="coin-icon">
|
||||
<span style="font-size: 22px; font-weight: bold;">${payload.data.price}</span>
|
||||
<span class="coin-price">${payload.data.price}</span>
|
||||
</div>
|
||||
</button>
|
||||
<br><br>
|
||||
@@ -570,7 +567,12 @@ function showExtra() {
|
||||
`;
|
||||
}
|
||||
|
||||
document.body.innerHTML = html;
|
||||
|
||||
document.body.innerHTML = `
|
||||
<div class="dlc_container">
|
||||
${html}
|
||||
</div>
|
||||
`;;
|
||||
} else {
|
||||
document.body.innerHTML = `
|
||||
<div class="dlc_container">
|
||||
|
||||
@@ -13,18 +13,18 @@
|
||||
<div id="wrapper">
|
||||
<div class="wrapper_box">
|
||||
<div class="a_left w100 d_ib mb10p">
|
||||
<div class="f90 a_center pt50">This is a Private Server</div>
|
||||
<div class="f50 a_center pt50">This is a Private Server</div>
|
||||
<hr />
|
||||
<div class="f70 plr25">Your text here</div>
|
||||
<div class="f30 plr25">Your text here</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="wrapper">
|
||||
<div class="wrapper_box">
|
||||
<div class="a_left w100 d_ib mb10p">
|
||||
<div class="f90 a_center pt50">About Coins</div>
|
||||
<div class="f50 a_center pt50">About Coins</div>
|
||||
<hr />
|
||||
<div class="f70 plr25">Coins are an in-game currency used within GROOVE COASTER ZERO.
|
||||
<div class="f30 plr25">Coins are an in-game currency used within GROOVE COASTER ZERO.
|
||||
They may be used to purchase special tracks, Avatars, and Items unavailable through regular game play.
|
||||
Spent Coins will not be refunded under any circumstances.
|
||||
Please note that Coin-related records are stored online, so a network connection is required for Coin use.
|
||||
@@ -35,9 +35,9 @@
|
||||
<div id="wrapper">
|
||||
<div class="wrapper_box">
|
||||
<div class="a_left w100 d_ib mb10p">
|
||||
<div class="f90 a_center pt50">Regarding Data Backup</div>
|
||||
<div class="f50 a_center pt50">Regarding Data Backup</div>
|
||||
<hr />
|
||||
<div class="f70 plr25">Please note that while the Backup/TAITO ID Registration option may be used to save play data.
|
||||
<div class="f30 plr25">Please note that while the Backup/TAITO ID Registration option may be used to save play data.
|
||||
Item stock information and replays are not saved.
|
||||
Contrary to the official server, you can log out of the TAITO ID, change username, and password.
|
||||
However, each account can only be linked to one device at a time.
|
||||
@@ -50,9 +50,9 @@
|
||||
<div id="wrapper">
|
||||
<div class="wrapper_box">
|
||||
<div class="a_left w100 d_ib mb10p">
|
||||
<div class="f90 a_center pt50">Regarding Online Use</div>
|
||||
<div class="f50 a_center pt50">Regarding Online Use</div>
|
||||
<hr />
|
||||
<div class="f70 plr25">Once the application's tutorial is complete the game may be played offline (without a network connection).
|
||||
<div class="f30 plr25">Once the application's tutorial is complete the game may be played offline (without a network connection).
|
||||
However, please note that a network connection is required for certain operations, including track data downloads, SHOP use, save backup/restore, etc.
|
||||
</div>
|
||||
</div>
|
||||
@@ -61,36 +61,36 @@
|
||||
<div id="wrapper">
|
||||
<div class="wrapper_box">
|
||||
<div class="a_left w100 d_ib mb10p">
|
||||
<div class="f90 a_center pt50">About HELP and OPTIONS</div>
|
||||
<div class="f4 0 a_center pt50 f60">About HELP and OPTIONS</div>
|
||||
<hr />
|
||||
<div class="f70 plr25">Tapping on the question mark (?) in the top left of the screen will bring up the application's help and settings.</div>
|
||||
<div class="f30 plr25">Tapping on the question mark (?) in the top left of the screen will bring up the application's help and settings.</div>
|
||||
<br />
|
||||
<div class="f70 plr25">There are many different settings under Options in the Help menu for you to customize to your liking.</div>
|
||||
<div class="f30 plr25">There are many different settings under Options in the Help menu for you to customize to your liking.</div>
|
||||
<br />
|
||||
<div class="f70 plr25">- SCREEN SETTINGS</div>
|
||||
<div class="f60 plr50">This menu is where you may adjust the visuals if you feel like the frame rate is unstable, or you want to disable AD-LIBS or Hit SFX when there's no TARGET to hit.</div>
|
||||
<div class="f30 plr25">- SCREEN SETTINGS</div>
|
||||
<div class="f30 plr50">This menu is where you may adjust the visuals if you feel like the frame rate is unstable, or you want to disable AD-LIBS or Hit SFX when there's no TARGET to hit.</div>
|
||||
<br />
|
||||
<div class="f70 plr25">- SOUND AND VISUAL TIMING ADJUSTMENT</div>
|
||||
<div class="f60 plr50">Adjust the settings here when you feel like the visuals or audio is not in sync.</div>
|
||||
<div class="f60 plr50">If you feel the visuals are ahead of the sound press the - button, if they're behind then use the + button. These buttons will adjust the visual display timing with the audio.</div>
|
||||
<div class="f30 plr25">- SOUND AND VISUAL TIMING ADJUSTMENT</div>
|
||||
<div class="f30 plr50">Adjust the settings here when you feel like the visuals or audio is not in sync.</div>
|
||||
<div class="f30 plr50">If you feel the visuals are ahead of the sound press the - button, if they're behind then use the + button. These buttons will adjust the visual display timing with the audio.</div>
|
||||
<br />
|
||||
<div class="f70 plr25">- INPUT REGISTRY ADJUSTMENT</div>
|
||||
<div class="f60 plr50">Adjust these settings if even though you feel like you're right in time with the beat, you still don't get a GREAT rating.</div>
|
||||
<div class="f60 plr50">
|
||||
<div class="f30 plr25">- INPUT REGISTRY ADJUSTMENT</div>
|
||||
<div class="f30 plr50">Adjust these settings if even though you feel like you're right in time with the beat, you still don't get a GREAT rating.</div>
|
||||
<div class="f30 plr50">
|
||||
Press the - button if you feel like you have to press earlier than you should for a GREAT rating, or press the + button if you feel like you have to press later than you should for a GREAT rating.
|
||||
</div>
|
||||
<br />
|
||||
<div class="f70 plr25">■MICROPHONE INPUT SENSITIVITY</div>
|
||||
<div class="f60 plr50">This setting will help to make playing ORIGINAL STYLE more smooth.</div>
|
||||
<div class="f30 plr25">■MICROPHONE INPUT SENSITIVITY</div>
|
||||
<div class="f30 plr50">This setting will help to make playing ORIGINAL STYLE more smooth.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="wrapper">
|
||||
<div class="wrapper_box">
|
||||
<div class="a_left w100 d_ib mb10p">
|
||||
<p class="a_center">About Arcade Mode</p>
|
||||
<p class="a_center f60">About Arcade Mode</p>
|
||||
<hr />
|
||||
<p class="f60 plr50">
|
||||
<p class="f30 plr50">
|
||||
Now you can play Arcade Mode which lets you enjoy a the same experience you'd have on a Groove Coaster arcade machine!<br />
|
||||
<br />
|
||||
*Please be aware that you will not be able to play Arcade Mode, even if you own it, if you have not yet cleared the NORMAL difficulty on the specified track.
|
||||
@@ -101,9 +101,9 @@
|
||||
<div id="wrapper">
|
||||
<div class="wrapper_box">
|
||||
<div class="a_left w100 d_ib mb10p">
|
||||
<p class="a_center">Play the game by making sounds!</p>
|
||||
<p class="a_center f60">Play the game by making sounds!</p>
|
||||
<hr />
|
||||
<p class="f60 plr50">
|
||||
<p class="f30 plr50">
|
||||
★What's Original Style?<br />
|
||||
<img src="./files/web/Info1.gif" class="w90 d_bl m_auto" /><br />
|
||||
It gives you a brand new way to play the game by making sounds!<br />
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
<body>
|
||||
<div class="login-card">
|
||||
<div class="d-flex justify-content-center align-items-center mb-4">
|
||||
<h3 class="text-center mb-0">Admin Login</h3>
|
||||
<h3 class="text-center mb-0">GC2OS Login</h3>
|
||||
<button type="button" class="btn btn-link ms-2 p-0" id="infoBtn" aria-label="Info" style="font-size:1.5rem;">
|
||||
<span class="bi bi-info-circle" style="vertical-align:middle;"></span>
|
||||
</button>
|
||||
@@ -95,7 +95,12 @@
|
||||
document.getElementById('infoBtn').addEventListener('click', function() {
|
||||
document.getElementById('loginModalLabel').innerText = "User Center Login";
|
||||
document.getElementById('loginModalBody').innerHTML =
|
||||
`<p>Don't do anything funny! Don't even think about it!</p>`;
|
||||
`
|
||||
<p>Don't do anything funny! Don't even think about it!</p>
|
||||
<p>Jokes aside, this login is for accessing your user center where you can view your stats and manage your account.</p>
|
||||
<p>If you don't have an account yet, please register in-game first.</p>
|
||||
<p>Depending on binding settings, you might need to bind your account to email/discord before the user center becomes accessible.</p>
|
||||
`;
|
||||
const modal = new bootstrap.Modal(document.getElementById('loginModal'));
|
||||
modal.show();
|
||||
});
|
||||
|
||||
@@ -20,60 +20,150 @@
|
||||
<body>
|
||||
<div class="container mt-5">
|
||||
<div class="text-center">
|
||||
<!-- Title -->
|
||||
<h1 id="welcome-title">Hello, {username}</h1>
|
||||
<!-- Export Data Button -->
|
||||
<button id="export-button" class="btn btn-primary mt-3">Export Data</button>
|
||||
<button id="export-button" class="btn btn-primary mt-3" onclick="openExportModal()">Export Data</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Function to get a cookie value by name
|
||||
function getCookie(name) {
|
||||
const value = `; ${document.cookie}`;
|
||||
const parts = value.split(`; ${name}=`);
|
||||
if (parts.length === 2) return parts.pop().split(';').shift();
|
||||
<!-- Export Save Data Modal -->
|
||||
<div class="modal fade" id="exportModal" tabindex="-1" aria-labelledby="exportModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="exportModalLabel">Export Save Data</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>You can export your save data to back it up or transfer it to another server.</p>
|
||||
<p id="next-export-time"></p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-primary" id="export-data-button" onclick="exportSaveData()">Export Data</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<script>
|
||||
|
||||
let nextSaveExportTimestamp = 0;
|
||||
|
||||
function getCookie(name) {
|
||||
const value = `; ${document.cookie}`;
|
||||
const parts = value.split(`; ${name}=`);
|
||||
if (parts.length === 2) return parts.pop().split(';').shift();
|
||||
}
|
||||
|
||||
async function fetchUserData() {
|
||||
const token = getCookie("token");
|
||||
if (!token) {
|
||||
console.error("Token not found in cookies.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch user data from the API
|
||||
async function fetchUserData() {
|
||||
const token = getCookie("token"); // Read the token from the cookie
|
||||
if (!token) {
|
||||
console.error("Token not found in cookies.");
|
||||
try {
|
||||
const response = await fetch("/usercenter/api", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
token: token,
|
||||
action: "basic"
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error("Failed to fetch user data:", response.statusText);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Query the API
|
||||
const response = await fetch("/usercenter/api", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
token: token,
|
||||
action: "basic"
|
||||
})
|
||||
});
|
||||
// Parse the response data
|
||||
const response_data = await response.json();
|
||||
const username = response_data.data.username || "User";
|
||||
nextSaveExportTimestamp = response_data.data.next_save_export || 0;
|
||||
|
||||
|
||||
if (!response.ok) {
|
||||
console.error("Failed to fetch user data:", response.statusText);
|
||||
return;
|
||||
}
|
||||
document.getElementById("welcome-title").textContent = `Hello, ${username}`;
|
||||
} catch (error) {
|
||||
console.error("Error fetching user data:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse the response data
|
||||
const response_data = await response.json();
|
||||
const username = response_data.data.username || "User";
|
||||
async function openExportModal() {
|
||||
const nextExportTime = new Date(nextSaveExportTimestamp * 1000); // Convert UNIX timestamp to milliseconds
|
||||
const now = new Date();
|
||||
|
||||
// Update the username in the title
|
||||
document.getElementById("welcome-title").textContent = `Hello, ${username}`;
|
||||
} catch (error) {
|
||||
console.error("Error fetching user data:", error);
|
||||
}
|
||||
// Check if the export is available
|
||||
if (nextSaveExportTimestamp > Math.floor(now.getTime() / 1000)) {
|
||||
const timeRemaining = Math.ceil((nextSaveExportTimestamp - now.getTime() / 1000) / 60);
|
||||
document.getElementById("next-export-time").textContent = `You can export your data again in approximately ${timeRemaining} minutes.`;
|
||||
document.getElementById("export-data-button").disabled = true; // Disable the button if export is not available
|
||||
} else {
|
||||
document.getElementById("next-export-time").textContent = "You can export your data now.";
|
||||
document.getElementById("export-data-button").disabled = false; // Enable the button
|
||||
}
|
||||
|
||||
// Call the function to fetch user data when the page loads
|
||||
document.addEventListener("DOMContentLoaded", fetchUserData);
|
||||
</script>
|
||||
// Show the modal
|
||||
const exportModal = new bootstrap.Modal(document.getElementById("exportModal"));
|
||||
exportModal.show();
|
||||
}
|
||||
|
||||
async function exportSaveData() {
|
||||
const token = getCookie("token");
|
||||
if (!token) {
|
||||
console.error("Token not found in cookies.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch("/usercenter/export_data", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error("Failed to export data:", response.statusText);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the response is JSON or a file stream
|
||||
const contentType = response.headers.get("Content-Type");
|
||||
if (contentType && contentType.includes("application/json")) {
|
||||
const responseData = await response.json();
|
||||
if (responseData.status === "failed") {
|
||||
alert(responseData.message); // Show the failure message
|
||||
}
|
||||
} else {
|
||||
// Handle file download
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = "save_export.xlsx"; // Default filename
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
const exportModal = bootstrap.Modal.getInstance(document.getElementById("exportModal"));
|
||||
if (exportModal) {
|
||||
exportModal.hide();
|
||||
}
|
||||
fetchUserData();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error exporting save data:", error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
document.addEventListener("DOMContentLoaded", fetchUserData);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user