diff --git a/new_server_7003/api/account.py b/new_server_7003/api/account.py
index 660732f..3e6baf8 100644
--- a/new_server_7003/api/account.py
+++ b/new_server_7003/api/account.py
@@ -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: 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)
diff --git a/new_server_7003/api/admin.py b/new_server_7003/api/admin.py
index b884559..9ce9d2d 100644
--- a/new_server_7003/api/admin.py
+++ b/new_server_7003/api/admin.py
@@ -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"]),
}
diff --git a/new_server_7003/api/batch.py b/new_server_7003/api/batch.py
index 077ffb4..e7ba6c4 100644
--- a/new_server_7003/api/batch.py
+++ b/new_server_7003/api/batch.py
@@ -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)
diff --git a/new_server_7003/api/database.py b/new_server_7003/api/database.py
index 2b3d952..64b0837 100644
--- a/new_server_7003/api/database.py
+++ b/new_server_7003/api/database.py
@@ -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
\ No newline at end of file
+ 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}")
\ No newline at end of file
diff --git a/new_server_7003/api/file.py b/new_server_7003/api/file.py
index e6fb3e2..4cce9a3 100644
--- a/new_server_7003/api/file.py
+++ b/new_server_7003/api/file.py
@@ -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"]),
diff --git a/new_server_7003/api/misc.py b/new_server_7003/api/misc.py
index 2a7b636..06739df 100644
--- a/new_server_7003/api/misc.py
+++ b/new_server_7003/api/misc.py
@@ -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:
diff --git a/new_server_7003/api/play.py b/new_server_7003/api/play.py
index e081ea8..7ea24cb 100644
--- a/new_server_7003/api/play.py
+++ b/new_server_7003/api/play.py
@@ -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
diff --git a/new_server_7003/api/ranking.py b/new_server_7003/api/ranking.py
index fdc7f85..8877131 100644
--- a/new_server_7003/api/ranking.py
+++ b/new_server_7003/api/ranking.py
@@ -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"],
diff --git a/new_server_7003/api/web.py b/new_server_7003/api/web.py
index 59c82ff..feca4f1 100644
--- a/new_server_7003/api/web.py
+++ b/new_server_7003/api/web.py
@@ -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"""
- Admin Panel
+ Admin Panel
"""
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"]),
]
\ No newline at end of file
diff --git a/new_server_7003/db-conv.py b/new_server_7003/db-conv.py
new file mode 100644
index 0000000..f89131e
--- /dev/null
+++ b/new_server_7003/db-conv.py
@@ -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())
\ No newline at end of file
diff --git a/new_server_7003/files/web/ranking.js b/new_server_7003/files/web/ranking.js
index 69d94a5..3d721cb 100644
--- a/new_server_7003/files/web/ranking.js
+++ b/new_server_7003/files/web/ranking.js
@@ -168,9 +168,11 @@ function showDetailTotal(mode, page) {
// Mode boxes
- html = `${songName}
`;
+ html = `
+
${songName}
+ `;
- rowStart = '
'
+ rowStart = '
'
rowEnd = '
'
rowContent = []
@@ -182,7 +184,7 @@ function showDetailTotal(mode, page) {
rowContent.push(`
${label}
`);
} else {
rowContent.push(
- `
${label} `
+ `
${label} `
);
}
}
@@ -210,25 +212,29 @@ function showDetailTotal(mode, page) {
`;
// generate pagination buttons
- if (generatePrevButton) {
- html += `
-
- Prev Page
-
- `;
- }
+ html += `
+
+ `;
// Loop leaderboard ranks
+ html += `
`;
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 += `
-
- Prev Page
-
- `;
- }
-
- if (generateNextButton) {
- html += `
-
- Next Page
-
- `;
- }
-
- html += `
-
- Back
-
- `;
+ html += `
`;
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 += `
-
- Prev Page
-
- `;
- }
+ html += `
+
+ `;
// Loop leaderboard ranks
+ html += `
`;
for (let i = 0; i < rankingList.length; i++) {
userData = rankingList[i];
@@ -448,33 +435,9 @@ function showDetailIndividual(songId, mode, page = 0) {
`;
}
+
+ html += `
`;
- // Generate pagination buttons again
-
- if (generatePrevButton) {
- html += `
-
- Prev Page
-
- `;
- }
-
- if (generateNextButton) {
- html += `
-
- Next Page
-
- `;
- }
-
- html += `
-
- Back
-
- `;
-
document.getElementById('ranking_box').innerHTML = html;
} else {
diff --git a/new_server_7003/files/web/style.css b/new_server_7003/files/web/style.css
index 1e4bc26..77021bf 100644
--- a/new_server_7003/files/web/style.css
+++ b/new_server_7003/files/web/style.css
@@ -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;
}
\ No newline at end of file
diff --git a/new_server_7003/files/web/web_shop.js b/new_server_7003/files/web/web_shop.js
index 27adeb8..77f7b60 100644
--- a/new_server_7003/files/web/web_shop.js
+++ b/new_server_7003/files/web/web_shop.js
@@ -101,17 +101,17 @@ function generateMenuContent() {
if (shopMode === 0 && stagePage === 0) {
html += `
-
+
-
+
`;
}
if (generatePrevButton) {
html += `
-
@@ -120,7 +120,7 @@ function generateMenuContent() {
if (generateNextButton) {
html += `
-
@@ -144,7 +144,7 @@ function generateMenuContent() {
if (generatePrevButton) {
html += `
-
@@ -153,7 +153,7 @@ function generateMenuContent() {
if (generateNextButton) {
html += `
-
@@ -231,10 +231,10 @@ function showItemDetail(mode, itemId) {
-
Would you like to purchase this song?
+
Would you like to purchase this song?
-
${payload.data.property_first} - ${payload.data.property_second}
-
Difficulty Levels: ${payload.data.property_third}
+
${payload.data.property_first} - ${payload.data.property_second}
+
Difficulty Levels: ${payload.data.property_third}
@@ -247,10 +247,10 @@ function showItemDetail(mode, itemId) {
-
Would you like to purchase this avatar?
+
Would you like to purchase this avatar?
-
${payload.data.property_first}
-
Effect: ${payload.data.property_second}
+
${payload.data.property_first}
+
Effect: ${payload.data.property_second}
@@ -263,10 +263,10 @@ function showItemDetail(mode, itemId) {
-
Would you like to purchase this item?
+
Would you like to purchase this item?
-
${payload.data.property_first}
-
Effect: ${payload.data.property_second}
+
${payload.data.property_first}
+
Effect: ${payload.data.property_second}
@@ -329,19 +329,17 @@ function purchaseItem(mode, itemId) {
if (payload.state === 1) {
contentElement.innerHTML = `
${payload.message}
-
- Back
-
+
`;
document.getElementById('coinCounter').innerText = payload.data.coin;
} else {
contentElement.innerHTML = `
Purchase failed: ${payload.message}
-
- Back
-
+
`;
}
})
@@ -349,10 +347,9 @@ function purchaseItem(mode, itemId) {
console.error('Error processing purchase:', error);
contentElement.innerHTML = `
Failed to process your purchase. Please try again later.
-
- Back
-
+
`;
});
}
@@ -373,7 +370,7 @@ function restoreBaseStructure() {