userbox, avatar, mapicon, and voice ui configuration
This commit is contained in:
parent
e49c70b738
commit
626ce6bd96
@ -11,7 +11,7 @@ from core.frontend import FE_Base, UserSession
|
|||||||
from core.config import CoreConfig
|
from core.config import CoreConfig
|
||||||
from .database import ChuniData
|
from .database import ChuniData
|
||||||
from .config import ChuniConfig
|
from .config import ChuniConfig
|
||||||
from .const import ChuniConstants
|
from .const import ChuniConstants, AvatarCategory, ItemKind
|
||||||
|
|
||||||
|
|
||||||
def pairwise(iterable):
|
def pairwise(iterable):
|
||||||
@ -99,6 +99,12 @@ class ChuniFrontend(FE_Base):
|
|||||||
Route("/{index}", self.render_GET_playlog, methods=['GET']),
|
Route("/{index}", self.render_GET_playlog, methods=['GET']),
|
||||||
]),
|
]),
|
||||||
Route("/favorites", self.render_GET_favorites, methods=['GET']),
|
Route("/favorites", self.render_GET_favorites, methods=['GET']),
|
||||||
|
Route("/userbox", self.render_GET_userbox, methods=['GET']),
|
||||||
|
Route("/avatar", self.render_GET_avatar, methods=['GET']),
|
||||||
|
Route("/update.map-icon", self.update_map_icon, methods=['POST']),
|
||||||
|
Route("/update.system-voice", self.update_system_voice, methods=['POST']),
|
||||||
|
Route("/update.userbox", self.update_userbox, methods=['POST']),
|
||||||
|
Route("/update.avatar", self.update_avatar, methods=['POST']),
|
||||||
Route("/update.name", self.update_name, methods=['POST']),
|
Route("/update.name", self.update_name, methods=['POST']),
|
||||||
Route("/update.favorite_music_playlog", self.update_favorite_music_playlog, methods=['POST']),
|
Route("/update.favorite_music_playlog", self.update_favorite_music_playlog, methods=['POST']),
|
||||||
Route("/update.favorite_music_favorites", self.update_favorite_music_favorites, methods=['POST']),
|
Route("/update.favorite_music_favorites", self.update_favorite_music_favorites, methods=['POST']),
|
||||||
@ -123,15 +129,33 @@ class ChuniFrontend(FE_Base):
|
|||||||
usr_sesh.chunithm_version = versions[0]
|
usr_sesh.chunithm_version = versions[0]
|
||||||
profile = await self.data.profile.get_profile_data(usr_sesh.user_id, usr_sesh.chunithm_version)
|
profile = await self.data.profile.get_profile_data(usr_sesh.user_id, usr_sesh.chunithm_version)
|
||||||
|
|
||||||
|
user_id = usr_sesh.user_id
|
||||||
|
version = usr_sesh.chunithm_version
|
||||||
|
|
||||||
|
# While map icons and system voices weren't present prior to AMAZON, we don't need to bother checking
|
||||||
|
# version here - it'll just end up being empty sets and the jinja will ignore the variables anyway.
|
||||||
|
user_map_icons = await self.data.item.get_items(user_id, ItemKind.MAP_ICON.value)
|
||||||
|
user_map_icons = [icon["itemId"] for icon in user_map_icons] + [profile.mapIconId]
|
||||||
|
user_system_voices = await self.data.item.get_items(user_id, ItemKind.SYSTEM_VOICE.value)
|
||||||
|
user_system_voices = [icon["itemId"] for icon in user_system_voices] + [profile.voiceId]
|
||||||
|
|
||||||
|
map_icons, total_map_icons = await self.get_available_map_icons(version, user_map_icons)
|
||||||
|
system_voices, total_system_voices = await self.get_available_system_voices(version, user_system_voices)
|
||||||
|
|
||||||
resp = Response(template.render(
|
resp = Response(template.render(
|
||||||
title=f"{self.core_config.server.name} | {self.nav_name}",
|
title=f"{self.core_config.server.name} | {self.nav_name}",
|
||||||
game_list=self.environment.globals["game_list"],
|
game_list=self.environment.globals["game_list"],
|
||||||
sesh=vars(usr_sesh),
|
sesh=vars(usr_sesh),
|
||||||
user_id=usr_sesh.user_id,
|
user_id=user_id,
|
||||||
profile=profile,
|
profile=profile,
|
||||||
version_list=ChuniConstants.VERSION_NAMES,
|
version_list=ChuniConstants.VERSION_NAMES,
|
||||||
versions=versions,
|
versions=versions,
|
||||||
cur_version=usr_sesh.chunithm_version
|
cur_version=version,
|
||||||
|
cur_version_name=ChuniConstants.game_ver_to_string(version),
|
||||||
|
map_icons=map_icons,
|
||||||
|
system_voices=system_voices,
|
||||||
|
total_map_icons=total_map_icons,
|
||||||
|
total_system_voices=total_system_voices
|
||||||
), media_type="text/html; charset=utf-8")
|
), media_type="text/html; charset=utf-8")
|
||||||
|
|
||||||
if usr_sesh.chunithm_version >= 0:
|
if usr_sesh.chunithm_version >= 0:
|
||||||
@ -189,6 +213,8 @@ class ChuniFrontend(FE_Base):
|
|||||||
profile=profile,
|
profile=profile,
|
||||||
hot_list=hot_list,
|
hot_list=hot_list,
|
||||||
base_list=base_list,
|
base_list=base_list,
|
||||||
|
cur_version=usr_sesh.chunithm_version,
|
||||||
|
cur_version_name=ChuniConstants.game_ver_to_string(usr_sesh.chunithm_version)
|
||||||
), media_type="text/html; charset=utf-8")
|
), media_type="text/html; charset=utf-8")
|
||||||
else:
|
else:
|
||||||
return RedirectResponse("/gate/", 303)
|
return RedirectResponse("/gate/", 303)
|
||||||
@ -217,7 +243,9 @@ class ChuniFrontend(FE_Base):
|
|||||||
title=f"{self.core_config.server.name} | {self.nav_name}",
|
title=f"{self.core_config.server.name} | {self.nav_name}",
|
||||||
game_list=self.environment.globals["game_list"],
|
game_list=self.environment.globals["game_list"],
|
||||||
sesh=vars(usr_sesh),
|
sesh=vars(usr_sesh),
|
||||||
playlog_count=0
|
playlog_count=0,
|
||||||
|
cur_version=version,
|
||||||
|
cur_version_name=ChuniConstants.game_ver_to_string(version)
|
||||||
), media_type="text/html; charset=utf-8")
|
), media_type="text/html; charset=utf-8")
|
||||||
playlog = await self.data.score.get_playlogs_limited(user_id, version, index, 20)
|
playlog = await self.data.score.get_playlogs_limited(user_id, version, index, 20)
|
||||||
playlog_with_title = []
|
playlog_with_title = []
|
||||||
@ -257,6 +285,7 @@ class ChuniFrontend(FE_Base):
|
|||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
playlog=playlog_with_title,
|
playlog=playlog_with_title,
|
||||||
playlog_count=playlog_count,
|
playlog_count=playlog_count,
|
||||||
|
cur_version=version,
|
||||||
cur_version_name=ChuniConstants.game_ver_to_string(version)
|
cur_version_name=ChuniConstants.game_ver_to_string(version)
|
||||||
), media_type="text/html; charset=utf-8")
|
), media_type="text/html; charset=utf-8")
|
||||||
else:
|
else:
|
||||||
@ -319,11 +348,318 @@ class ChuniFrontend(FE_Base):
|
|||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
favorites_by_genre=favorites_by_genre,
|
favorites_by_genre=favorites_by_genre,
|
||||||
favorites_count=favorites_count,
|
favorites_count=favorites_count,
|
||||||
|
cur_version=version,
|
||||||
cur_version_name=ChuniConstants.game_ver_to_string(version)
|
cur_version_name=ChuniConstants.game_ver_to_string(version)
|
||||||
), media_type="text/html; charset=utf-8")
|
), media_type="text/html; charset=utf-8")
|
||||||
else:
|
else:
|
||||||
return RedirectResponse("/gate/", 303)
|
return RedirectResponse("/gate/", 303)
|
||||||
|
|
||||||
|
async def get_available_map_icons(self, version: int, user_unlocked_items: List[int]) -> (List[dict], int):
|
||||||
|
items = dict()
|
||||||
|
rows = await self.data.static.get_map_icons(version)
|
||||||
|
if rows:
|
||||||
|
for row in rows:
|
||||||
|
# Only include items that are either available by default or in the user unlocked list
|
||||||
|
if row["defaultHave"] or row["mapIconId"] in user_unlocked_items:
|
||||||
|
item = dict()
|
||||||
|
item["id"] = row["mapIconId"]
|
||||||
|
item["name"] = row["name"]
|
||||||
|
item["iconPath"] = path.splitext(row["iconPath"])[0] + ".png"
|
||||||
|
items[row["mapIconId"]] = item
|
||||||
|
|
||||||
|
return (items, len(rows))
|
||||||
|
|
||||||
|
async def get_available_system_voices(self, version: int, user_unlocked_items: List[int]) -> (List[dict], int):
|
||||||
|
items = dict()
|
||||||
|
rows = await self.data.static.get_system_voices(version)
|
||||||
|
if rows:
|
||||||
|
for row in rows:
|
||||||
|
# Only include items that are either available by default or in the user unlocked list
|
||||||
|
if row["defaultHave"] or row["voiceId"] in user_unlocked_items:
|
||||||
|
item = dict()
|
||||||
|
item["id"] = row["voiceId"]
|
||||||
|
item["name"] = row["name"]
|
||||||
|
item["imagePath"] = path.splitext(row["imagePath"])[0] + ".png"
|
||||||
|
items[row["voiceId"]] = item
|
||||||
|
|
||||||
|
return (items, len(rows))
|
||||||
|
|
||||||
|
async def get_available_nameplates(self, version: int, user_unlocked_items: List[int]) -> (List[dict], int):
|
||||||
|
items = dict()
|
||||||
|
rows = await self.data.static.get_nameplates(version)
|
||||||
|
if rows:
|
||||||
|
for row in rows:
|
||||||
|
# Only include items that are either available by default or in the user unlocked list
|
||||||
|
if row["defaultHave"] or row["nameplateId"] in user_unlocked_items:
|
||||||
|
item = dict()
|
||||||
|
item["id"] = row["nameplateId"]
|
||||||
|
item["name"] = row["name"]
|
||||||
|
item["texturePath"] = path.splitext(row["texturePath"])[0] + ".png"
|
||||||
|
items[row["nameplateId"]] = item
|
||||||
|
|
||||||
|
return (items, len(rows))
|
||||||
|
|
||||||
|
async def get_available_trophies(self, version: int, user_unlocked_items: List[int]) -> (List[dict], int):
|
||||||
|
items = dict()
|
||||||
|
rows = await self.data.static.get_trophies(version)
|
||||||
|
if rows:
|
||||||
|
for row in rows:
|
||||||
|
# Only include items that are either available by default or in the user unlocked list
|
||||||
|
if row["defaultHave"] or row["trophyId"] in user_unlocked_items:
|
||||||
|
item = dict()
|
||||||
|
item["id"] = row["trophyId"]
|
||||||
|
item["name"] = row["name"]
|
||||||
|
item["rarity"] = row["rareType"]
|
||||||
|
items[row["trophyId"]] = item
|
||||||
|
|
||||||
|
return (items, len(rows))
|
||||||
|
|
||||||
|
async def get_available_characters(self, version: int, user_unlocked_items: List[int]) -> (List[dict], int):
|
||||||
|
items = dict()
|
||||||
|
rows = await self.data.static.get_characters(version)
|
||||||
|
if rows:
|
||||||
|
for row in rows:
|
||||||
|
# Only include items that are either available by default or in the user unlocked list
|
||||||
|
if row["defaultHave"] or row["characterId"] in user_unlocked_items:
|
||||||
|
item = dict()
|
||||||
|
item["id"] = row["characterId"]
|
||||||
|
item["name"] = row["name"]
|
||||||
|
item["iconPath"] = path.splitext(row["imagePath3"])[0] + ".png"
|
||||||
|
items[row["characterId"]] = item
|
||||||
|
|
||||||
|
return (items, len(rows))
|
||||||
|
|
||||||
|
async def get_available_avatar_items(self, version: int, category: AvatarCategory, user_unlocked_items: List[int]) -> (List[dict], int):
|
||||||
|
items = dict()
|
||||||
|
rows = await self.data.static.get_avatar_items(version, category.value)
|
||||||
|
if rows:
|
||||||
|
for row in rows:
|
||||||
|
# Only include items that are either available by default or in the user unlocked list
|
||||||
|
if row["defaultHave"] or row["avatarAccessoryId"] in user_unlocked_items:
|
||||||
|
item = dict()
|
||||||
|
item["id"] = row["avatarAccessoryId"]
|
||||||
|
item["name"] = row["name"]
|
||||||
|
item["iconPath"] = path.splitext(row["iconPath"])[0] + ".png"
|
||||||
|
item["texturePath"] = path.splitext(row["texturePath"])[0] + ".png"
|
||||||
|
items[row["avatarAccessoryId"]] = item
|
||||||
|
|
||||||
|
return (items, len(rows))
|
||||||
|
|
||||||
|
async def render_GET_userbox(self, request: Request) -> bytes:
|
||||||
|
template = self.environment.get_template(
|
||||||
|
"titles/chuni/templates/chuni_userbox.jinja"
|
||||||
|
)
|
||||||
|
usr_sesh = self.validate_session(request)
|
||||||
|
if not usr_sesh:
|
||||||
|
usr_sesh = UserSession()
|
||||||
|
|
||||||
|
if usr_sesh.user_id > 0:
|
||||||
|
if usr_sesh.chunithm_version < 0:
|
||||||
|
return RedirectResponse("/game/chuni/", 303)
|
||||||
|
|
||||||
|
user_id = usr_sesh.user_id
|
||||||
|
version = usr_sesh.chunithm_version
|
||||||
|
|
||||||
|
# Get the user profile so we know how the userbox is currently configured
|
||||||
|
profile = await self.data.profile.get_profile_data(user_id, version)
|
||||||
|
# Get all the user unlocked components so we know what to populate as options
|
||||||
|
user_nameplates = await self.data.item.get_items(user_id, ItemKind.NAMEPLATE.value)
|
||||||
|
user_nameplates = [item["itemId"] for item in user_nameplates] + [profile.nameplateId]
|
||||||
|
user_trophies = await self.data.item.get_items(user_id, ItemKind.TROPHY.value)
|
||||||
|
user_trophies = [item["itemId"] for item in user_trophies] + [profile.trophyId]
|
||||||
|
user_characters = await self.data.item.get_characters(user_id)
|
||||||
|
user_characters = [chara["characterId"] for chara in user_characters] + [profile.charaIllustId]
|
||||||
|
|
||||||
|
# Build up available list of components
|
||||||
|
nameplates, total_nameplates = await self.get_available_nameplates(version, user_nameplates)
|
||||||
|
trophies, total_trophies = await self.get_available_trophies(version, user_trophies)
|
||||||
|
characters, total_characters = await self.get_available_characters(version, user_characters)
|
||||||
|
|
||||||
|
# Get the user's team
|
||||||
|
team_name = "ARTEMiS"
|
||||||
|
if profile["teamId"]:
|
||||||
|
team = await self.data.profile.get_team_by_id(profile["teamId"])
|
||||||
|
team_name = team["teamName"]
|
||||||
|
# Figure out the rating color we should use (rank maps to the stylesheet)
|
||||||
|
rating = profile.playerRating / 100;
|
||||||
|
rating_rank = 0
|
||||||
|
if rating >= 16:
|
||||||
|
rating_rank = 8
|
||||||
|
elif rating >= 15.25:
|
||||||
|
rating_rank = 7
|
||||||
|
elif rating >= 14.5:
|
||||||
|
rating_rank = 6
|
||||||
|
elif rating >= 13.25:
|
||||||
|
rating_rank = 5
|
||||||
|
elif rating >= 12:
|
||||||
|
rating_rank = 4
|
||||||
|
elif rating >= 10:
|
||||||
|
rating_rank = 3
|
||||||
|
elif rating >= 7:
|
||||||
|
rating_rank = 2
|
||||||
|
elif rating >= 4:
|
||||||
|
rating_rank = 1
|
||||||
|
|
||||||
|
return Response(template.render(
|
||||||
|
title=f"{self.core_config.server.name} | {self.nav_name}",
|
||||||
|
game_list=self.environment.globals["game_list"],
|
||||||
|
sesh=vars(usr_sesh),
|
||||||
|
user_id=user_id,
|
||||||
|
cur_version=version,
|
||||||
|
cur_version_name=ChuniConstants.game_ver_to_string(version),
|
||||||
|
profile=profile,
|
||||||
|
team_name=team_name,
|
||||||
|
rating_rank=rating_rank,
|
||||||
|
nameplates=nameplates,
|
||||||
|
trophies=trophies,
|
||||||
|
characters=characters,
|
||||||
|
total_nameplates=total_nameplates,
|
||||||
|
total_trophies=total_trophies,
|
||||||
|
total_characters=total_characters
|
||||||
|
), media_type="text/html; charset=utf-8")
|
||||||
|
else:
|
||||||
|
return RedirectResponse("/gate/", 303)
|
||||||
|
|
||||||
|
async def render_GET_avatar(self, request: Request) -> bytes:
|
||||||
|
template = self.environment.get_template(
|
||||||
|
"titles/chuni/templates/chuni_avatar.jinja"
|
||||||
|
)
|
||||||
|
usr_sesh = self.validate_session(request)
|
||||||
|
if not usr_sesh:
|
||||||
|
usr_sesh = UserSession()
|
||||||
|
|
||||||
|
if usr_sesh.user_id > 0:
|
||||||
|
if usr_sesh.chunithm_version < 11:
|
||||||
|
# Avatar configuration only for NEW!! and newer
|
||||||
|
return RedirectResponse("/game/chuni/", 303)
|
||||||
|
|
||||||
|
user_id = usr_sesh.user_id
|
||||||
|
version = usr_sesh.chunithm_version
|
||||||
|
|
||||||
|
# Get the user profile so we know what avatar items are currently in use
|
||||||
|
profile = await self.data.profile.get_profile_data(user_id, version)
|
||||||
|
# Get all the user avatar accessories so we know what to populate
|
||||||
|
user_accessories = await self.data.item.get_items(user_id, ItemKind.AVATAR_ACCESSORY.value)
|
||||||
|
user_accessories = [item["itemId"] for item in user_accessories] + \
|
||||||
|
[profile.avatarBack, profile.avatarItem, profile.avatarWear, \
|
||||||
|
profile.avatarFront, profile.avatarSkin, profile.avatarHead, profile.avatarFace]
|
||||||
|
|
||||||
|
# Build up available list of items for each avatar category
|
||||||
|
wears, total_wears = await self.get_available_avatar_items(version, AvatarCategory.WEAR, user_accessories)
|
||||||
|
faces, total_faces = await self.get_available_avatar_items(version, AvatarCategory.FACE, user_accessories)
|
||||||
|
heads, total_heads = await self.get_available_avatar_items(version, AvatarCategory.HEAD, user_accessories)
|
||||||
|
skins, total_skins = await self.get_available_avatar_items(version, AvatarCategory.SKIN, user_accessories)
|
||||||
|
items, total_items = await self.get_available_avatar_items(version, AvatarCategory.ITEM, user_accessories)
|
||||||
|
fronts, total_fronts = await self.get_available_avatar_items(version, AvatarCategory.FRONT, user_accessories)
|
||||||
|
backs, total_backs = await self.get_available_avatar_items(version, AvatarCategory.BACK, user_accessories)
|
||||||
|
|
||||||
|
return Response(template.render(
|
||||||
|
title=f"{self.core_config.server.name} | {self.nav_name}",
|
||||||
|
game_list=self.environment.globals["game_list"],
|
||||||
|
sesh=vars(usr_sesh),
|
||||||
|
user_id=user_id,
|
||||||
|
cur_version=version,
|
||||||
|
cur_version_name=ChuniConstants.game_ver_to_string(version),
|
||||||
|
profile=profile,
|
||||||
|
wears=wears,
|
||||||
|
faces=faces,
|
||||||
|
heads=heads,
|
||||||
|
skins=skins,
|
||||||
|
items=items,
|
||||||
|
fronts=fronts,
|
||||||
|
backs=backs,
|
||||||
|
total_wears=total_wears,
|
||||||
|
total_faces=total_faces,
|
||||||
|
total_heads=total_heads,
|
||||||
|
total_skins=total_skins,
|
||||||
|
total_items=total_items,
|
||||||
|
total_fronts=total_fronts,
|
||||||
|
total_backs=total_backs
|
||||||
|
), media_type="text/html; charset=utf-8")
|
||||||
|
else:
|
||||||
|
return RedirectResponse("/gate/", 303)
|
||||||
|
|
||||||
|
async def update_map_icon(self, request: Request) -> bytes:
|
||||||
|
usr_sesh = self.validate_session(request)
|
||||||
|
if not usr_sesh:
|
||||||
|
return RedirectResponse("/gate/", 303)
|
||||||
|
|
||||||
|
form_data = await request.form()
|
||||||
|
new_map_icon: str = form_data.get("id")
|
||||||
|
|
||||||
|
if not new_map_icon:
|
||||||
|
return RedirectResponse("/gate/?e=4", 303)
|
||||||
|
|
||||||
|
if not await self.data.profile.update_map_icon(usr_sesh.user_id, usr_sesh.chunithm_version, new_map_icon):
|
||||||
|
return RedirectResponse("/gate/?e=999", 303)
|
||||||
|
|
||||||
|
return RedirectResponse("/game/chuni/", 303)
|
||||||
|
|
||||||
|
async def update_system_voice(self, request: Request) -> bytes:
|
||||||
|
usr_sesh = self.validate_session(request)
|
||||||
|
if not usr_sesh:
|
||||||
|
return RedirectResponse("/gate/", 303)
|
||||||
|
|
||||||
|
form_data = await request.form()
|
||||||
|
new_system_voice: str = form_data.get("id")
|
||||||
|
|
||||||
|
if not new_system_voice:
|
||||||
|
return RedirectResponse("/gate/?e=4", 303)
|
||||||
|
|
||||||
|
if not await self.data.profile.update_system_voice(usr_sesh.user_id, usr_sesh.chunithm_version, new_system_voice):
|
||||||
|
return RedirectResponse("/gate/?e=999", 303)
|
||||||
|
|
||||||
|
return RedirectResponse("/game/chuni/", 303)
|
||||||
|
|
||||||
|
async def update_userbox(self, request: Request) -> bytes:
|
||||||
|
usr_sesh = self.validate_session(request)
|
||||||
|
if not usr_sesh:
|
||||||
|
return RedirectResponse("/gate/", 303)
|
||||||
|
|
||||||
|
form_data = await request.form()
|
||||||
|
new_nameplate: str = form_data.get("nameplate")
|
||||||
|
new_trophy: str = form_data.get("trophy")
|
||||||
|
new_character: str = form_data.get("character")
|
||||||
|
|
||||||
|
if not new_nameplate or \
|
||||||
|
not new_trophy or \
|
||||||
|
not new_character:
|
||||||
|
return RedirectResponse("/game/chuni/userbox?e=4", 303)
|
||||||
|
|
||||||
|
if not await self.data.profile.update_userbox(usr_sesh.user_id, usr_sesh.chunithm_version, new_nameplate, new_trophy, new_character):
|
||||||
|
return RedirectResponse("/gate/?e=999", 303)
|
||||||
|
|
||||||
|
return RedirectResponse("/game/chuni/userbox", 303)
|
||||||
|
|
||||||
|
async def update_avatar(self, request: Request) -> bytes:
|
||||||
|
usr_sesh = self.validate_session(request)
|
||||||
|
if not usr_sesh:
|
||||||
|
return RedirectResponse("/gate/", 303)
|
||||||
|
|
||||||
|
form_data = await request.form()
|
||||||
|
new_wear: str = form_data.get("wear")
|
||||||
|
new_face: str = form_data.get("face")
|
||||||
|
new_head: str = form_data.get("head")
|
||||||
|
new_skin: str = form_data.get("skin")
|
||||||
|
new_item: str = form_data.get("item")
|
||||||
|
new_front: str = form_data.get("front")
|
||||||
|
new_back: str = form_data.get("back")
|
||||||
|
|
||||||
|
if not new_wear or \
|
||||||
|
not new_face or \
|
||||||
|
not new_head or \
|
||||||
|
not new_skin or \
|
||||||
|
not new_item or \
|
||||||
|
not new_front or \
|
||||||
|
not new_back:
|
||||||
|
return RedirectResponse("/game/chuni/avatar?e=4", 303)
|
||||||
|
|
||||||
|
if not await self.data.profile.update_avatar(usr_sesh.user_id, usr_sesh.chunithm_version, new_wear, new_face, new_head, new_skin, new_item, new_front, new_back):
|
||||||
|
return RedirectResponse("/gate/?e=999", 303)
|
||||||
|
|
||||||
|
return RedirectResponse("/game/chuni/avatar", 303)
|
||||||
|
|
||||||
|
|
||||||
async def update_name(self, request: Request) -> bytes:
|
async def update_name(self, request: Request) -> bytes:
|
||||||
usr_sesh = self.validate_session(request)
|
usr_sesh = self.validate_session(request)
|
||||||
if not usr_sesh:
|
if not usr_sesh:
|
||||||
|
302
titles/chuni/templates/chuni_avatar.jinja
Normal file
302
titles/chuni/templates/chuni_avatar.jinja
Normal file
@ -0,0 +1,302 @@
|
|||||||
|
{% extends "core/templates/index.jinja" %}
|
||||||
|
{% block content %}
|
||||||
|
<style>
|
||||||
|
{% include 'titles/chuni/templates/css/chuni_style.css' %}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
{% include 'titles/chuni/templates/chuni_header.jinja' %}
|
||||||
|
|
||||||
|
<!-- AVATAR PREVIEW -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-8 m-auto mt-3">
|
||||||
|
<div class="card bg-card rounded">
|
||||||
|
<table class="table-large table-rowdistinct">
|
||||||
|
<caption align="top">AVATAR</caption>
|
||||||
|
<tr><td style="height:340px; width:50%" rowspan=8>
|
||||||
|
<img class="avatar-preview avatar-preview-platform" src="img/avatar-platform.png">
|
||||||
|
<img id="preview1_back" class="avatar-preview avatar-preview-back" src="">
|
||||||
|
<img id="preview1_skin" class="avatar-preview avatar-preview-skin-rightfoot" src="">
|
||||||
|
<img id="preview2_skin" class="avatar-preview avatar-preview-skin-leftfoot" src="">
|
||||||
|
<img id="preview3_skin" class="avatar-preview avatar-preview-skin-body" src="">
|
||||||
|
<img id="preview1_wear" class="avatar-preview avatar-preview-wear" src="">
|
||||||
|
<img class="avatar-preview avatar-preview-common" src="img/avatar-common.png">
|
||||||
|
<img id="preview1_head" class="avatar-preview avatar-preview-head" src="">
|
||||||
|
<img id="preview1_face" class="avatar-preview avatar-preview-face" src="">
|
||||||
|
<img id="preview1_item" class="avatar-preview avatar-preview-item-righthand" src="">
|
||||||
|
<img id="preview2_item" class="avatar-preview avatar-preview-item-lefthand" src="">
|
||||||
|
<img id="preview1_front" class="avatar-preview avatar-preview-front" src="">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr><td>Wear:</td><td><div id="name_wear"></div></td></tr>
|
||||||
|
<tr><td>Face:</td><td><div id="name_face"></div></td></tr>
|
||||||
|
<tr><td>Head:</td><td><div id="name_head"></div></td></tr>
|
||||||
|
<tr><td>Skin:</td><td><div id="name_skin"></div></td></tr>
|
||||||
|
<tr><td>Item:</td><td><div id="name_item"></div></td></tr>
|
||||||
|
<tr><td>Front:</td><td><div id="name_front"></div></td></tr>
|
||||||
|
<tr><td>Back:</td><td><div id="name_back"></div></td></tr>
|
||||||
|
|
||||||
|
<tr><td colspan=3 style="padding:8px 0px; text-align: center;">
|
||||||
|
<button id="save-btn" class="btn btn-primary" style="width:140px;" onClick="saveAvatar()">SAVE</button>
|
||||||
|
<button id="reset-btn" class="btn btn-danger" style="width:140px;" onClick="resetAvatar()">RESET</button>
|
||||||
|
</td></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ACCESSORY SELECTION -->
|
||||||
|
<div class="row col-lg-8 m-auto mt-3 scrolling-lists-lg card bg-card rounded">
|
||||||
|
|
||||||
|
<!-- WEAR ACCESSORIES -->
|
||||||
|
<button class="collapsible">Wear: {{ wears|length }}/{{ total_wears }} {{ "items" if total_wears > 1 else "item" }}</button>
|
||||||
|
<div id="scrollable-wear" class="collapsible-content">
|
||||||
|
{% for item in wears.values() %}
|
||||||
|
<img id="{{ item["id"] }}" onclick="changeAccessory('wear', '{{ item["id"] }}', '{{ item["name"] }}', '{{ item["texturePath"] }}')" src="img/avatar/{{ item["iconPath"] }}" alt="{{ item["name"] }}">
|
||||||
|
<span id="wear-br-{{ loop.index }}"></span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<!-- FACE ACCESSORIES -->
|
||||||
|
<button class="collapsible">Face: {{ faces|length }}/{{ total_faces }} {{ "items" if total_faces > 1 else "item" }}</button>
|
||||||
|
<div id="scrollable-face" class="collapsible-content">
|
||||||
|
{% for item in faces.values() %}
|
||||||
|
<img id="{{ item["id"] }}" onclick="changeAccessory('face', '{{ item["id"] }}', '{{ item["name"] }}', '{{ item["texturePath"] }}')" src="img/avatar/{{ item["iconPath"] }}" alt="{{ item["name"] }}">
|
||||||
|
<span id="face-br-{{ loop.index }}"></span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<!-- HEAD ACCESSORIES -->
|
||||||
|
<button class="collapsible">Head: {{ heads|length }}/{{ total_heads }} {{ "items" if total_heads > 1 else "item" }}</button>
|
||||||
|
<div id="scrollable-head" class="collapsible-content">
|
||||||
|
{% for item in heads.values() %}
|
||||||
|
<img id="{{ item["id"] }}" onclick="changeAccessory('head', '{{ item["id"] }}', '{{ item["name"] }}', '{{ item["texturePath"] }}')" src="img/avatar/{{ item["iconPath"] }}" alt="{{ item["name"] }}">
|
||||||
|
<span id="head-br-{{ loop.index }}"></span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<!-- SKIN ACCESSORIES -->
|
||||||
|
<button class="collapsible">Skin: {{ skins|length }}/{{ total_skins }} {{ "items" if total_skins > 1 else "item" }}</button>
|
||||||
|
<div id="scrollable-skin" class="collapsible-content">
|
||||||
|
{% for item in skins.values() %}
|
||||||
|
<img id="{{ item["id"] }}" onclick="changeAccessory('skin', '{{ item["id"] }}', '{{ item["name"] }}', '{{ item["texturePath"] }}')" src="img/avatar/{{ item["iconPath"] }}" alt="{{ item["name"] }}">
|
||||||
|
<span id="skin-br-{{ loop.index }}"></span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<!-- ITEM ACCESSORIES -->
|
||||||
|
<button class="collapsible">Item: {{ items|length }}/{{ total_items }} {{ "items" if total_items > 1 else "item" }}</button>
|
||||||
|
<div id="scrollable-item" class="collapsible-content">
|
||||||
|
{% for item in items.values() %}
|
||||||
|
<img id="{{ item["id"] }}" onclick="changeAccessory('item', '{{ item["id"] }}', '{{ item["name"] }}', '{{ item["texturePath"] }}')" src="img/avatar/{{ item["iconPath"] }}" alt="{{ item["name"] }}">
|
||||||
|
<span id="item-br-{{ loop.index }}"></span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<!-- FRONT ACCESSORIES -->
|
||||||
|
<button class="collapsible">Front: {{ fronts|length }}/{{ total_fronts }} {{ "items" if total_fronts > 1 else "item" }}</button>
|
||||||
|
<div id="scrollable-front" class="collapsible-content">
|
||||||
|
{% for item in fronts.values() %}
|
||||||
|
<img id="{{ item["id"] }}" onclick="changeAccessory('front', '{{ item["id"] }}', '{{ item["name"] }}', '{{ item["texturePath"] }}')" src="img/avatar/{{ item["iconPath"] }}" alt="{{ item["name"] }}">
|
||||||
|
<span id="front-br-{{ loop.index }}"></span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<!-- BACK ACCESSORIES -->
|
||||||
|
<button class="collapsible">Back: {{ backs|length }}/{{ total_backs }} {{ "items" if total_backs > 1 else "item" }}</button>
|
||||||
|
<div id="scrollable-back" class="collapsible-content">
|
||||||
|
{% for item in backs.values() %}
|
||||||
|
<img id="{{ item["id"] }}" onclick="changeAccessory('back', '{{ item["id"] }}', '{{ item["name"] }}', '{{ item["texturePath"] }}')" src="img/avatar/{{ item["iconPath"] }}" alt="{{ item["name"] }}">
|
||||||
|
<span id="back-br-{{ loop.index }}"></span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if error is defined %}
|
||||||
|
{% include "core/templates/widgets/err_banner.jinja" %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
{% include 'titles/chuni/templates/scripts/collapsibles.js' %}
|
||||||
|
|
||||||
|
///
|
||||||
|
/// This script handles all updates to the avatar
|
||||||
|
///
|
||||||
|
total_items = 0;
|
||||||
|
orig_id = 1;
|
||||||
|
orig_name = 2;
|
||||||
|
orig_img = 3;
|
||||||
|
curr_id = 4;
|
||||||
|
curr_name = 5;
|
||||||
|
curr_img = 6;
|
||||||
|
accessories = {
|
||||||
|
// [total_items, orig_id, orig_name, orig_img, curr_id, curr_name, curr_img]
|
||||||
|
"wear":["{{ wears|length }}",
|
||||||
|
"{{ profile.avatarWear }}",
|
||||||
|
"{{ wears[profile.avatarWear]["name"] }}",
|
||||||
|
"{{ wears[profile.avatarWear]["texturePath"] }}", "", "", "" ],
|
||||||
|
|
||||||
|
"face":["{{ faces|length }}",
|
||||||
|
"{{ profile.avatarFace }}",
|
||||||
|
"{{ faces[profile.avatarFace]["name"] }}",
|
||||||
|
"{{ faces[profile.avatarFace]["texturePath"] }}", "", "", "" ],
|
||||||
|
|
||||||
|
"head":["{{ heads|length }}",
|
||||||
|
"{{ profile.avatarHead }}",
|
||||||
|
"{{ heads[profile.avatarHead]["name"] }}",
|
||||||
|
"{{ heads[profile.avatarHead]["texturePath"] }}", "", "", "" ],
|
||||||
|
|
||||||
|
"skin":["{{ skins|length }}",
|
||||||
|
"{{ profile.avatarSkin }}",
|
||||||
|
"{{ skins[profile.avatarSkin]["name"] }}",
|
||||||
|
"{{ skins[profile.avatarSkin]["texturePath"] }}", "", "", "" ],
|
||||||
|
|
||||||
|
"item":["{{ items|length }}",
|
||||||
|
"{{ profile.avatarItem }}",
|
||||||
|
"{{ items[profile.avatarItem]["name"] }}",
|
||||||
|
"{{ items[profile.avatarItem]["texturePath"] }}", "", "", "" ],
|
||||||
|
|
||||||
|
"front":["{{ fronts|length }}",
|
||||||
|
"{{ profile.avatarFront }}",
|
||||||
|
"{{ fronts[profile.avatarFront]["name"] }}",
|
||||||
|
"{{ fronts[profile.avatarFront]["texturePath"] }}", "", "", "" ],
|
||||||
|
|
||||||
|
"back":["{{ backs|length }}",
|
||||||
|
"{{ profile.avatarBack }}",
|
||||||
|
"{{ backs[profile.avatarBack]["name"] }}",
|
||||||
|
"{{ backs[profile.avatarBack]["texturePath"] }}", "", "", "" ]
|
||||||
|
};
|
||||||
|
types = Object.keys(accessories);
|
||||||
|
|
||||||
|
function enableButtons(enabled) {
|
||||||
|
document.getElementById("reset-btn").disabled = !enabled;
|
||||||
|
document.getElementById("save-btn").disabled = !enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeAccessory(type, id, name, img) {
|
||||||
|
// clear select style for old accessory
|
||||||
|
var element = document.getElementById(accessories[type][curr_id]);
|
||||||
|
if (element) {
|
||||||
|
element.style.backgroundColor="inherit";
|
||||||
|
}
|
||||||
|
|
||||||
|
// set new accessory
|
||||||
|
accessories[type][curr_id] = id;
|
||||||
|
accessories[type][curr_name] = name;
|
||||||
|
accessories[type][curr_img] = img;
|
||||||
|
|
||||||
|
// update select style for new accessory
|
||||||
|
element = document.getElementById(id);
|
||||||
|
if (element) {
|
||||||
|
element.style.backgroundColor="#5F5";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the avatar preview and enable buttons
|
||||||
|
updatePreview();
|
||||||
|
if (id != accessories[type][orig_id]) {
|
||||||
|
enableButtons(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetAvatar() {
|
||||||
|
for (const type of types) {
|
||||||
|
changeAccessory(type, accessories[type][orig_id], accessories[type][orig_name], accessories[type][orig_img]);
|
||||||
|
}
|
||||||
|
// disable the save/reset buttons until something changes
|
||||||
|
enableButtons(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRandomInt(min, max) {
|
||||||
|
min = Math.ceil(min);
|
||||||
|
max = Math.floor(max);
|
||||||
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePreview() {
|
||||||
|
for (const type of types) {
|
||||||
|
for (let i = 1; i <= 3; i++) {
|
||||||
|
var img = document.getElementById("preview" + i + "_" + type);
|
||||||
|
if (img) {
|
||||||
|
img.src = "img/avatar/" + accessories[type][curr_img];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.getElementById("name_" + type).innerHTML = accessories[type][curr_name];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveAvatar() {
|
||||||
|
$.post("/game/chuni/update.avatar", { wear: accessories["wear"][curr_id],
|
||||||
|
face: accessories["face"][curr_id],
|
||||||
|
head: accessories["head"][curr_id],
|
||||||
|
skin: accessories["skin"][curr_id],
|
||||||
|
item: accessories["item"][curr_id],
|
||||||
|
front: accessories["front"][curr_id],
|
||||||
|
back: accessories["back"][curr_id] })
|
||||||
|
.done(function (data) {
|
||||||
|
// set the current as the original and disable buttons
|
||||||
|
for (const type of types) {
|
||||||
|
accessories[type][orig_id] = accessories[type][curr_id];
|
||||||
|
accessories[type][orig_name] = accessories[type][curr_name];
|
||||||
|
accessories[type][orig_img] = accessories[type][curr_img];
|
||||||
|
}
|
||||||
|
enableButtons(false);
|
||||||
|
})
|
||||||
|
.fail(function () {
|
||||||
|
alert("Failed to save avatar.");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function resizePage() {
|
||||||
|
//
|
||||||
|
// Handles item organization in the collapsible scrollables to try to keep the items-per-row presentable
|
||||||
|
//
|
||||||
|
// @note Yes, we could simply let the div overflow like usual. This could however get really nasty looking
|
||||||
|
// when dealing with something like userbox characters where there are 1000s of possible items to
|
||||||
|
// display. This approach gives us full control over where items in the div wrap, allowing us to try
|
||||||
|
// to keep things presentable.
|
||||||
|
//
|
||||||
|
for (const type of types) {
|
||||||
|
var numPerRow = Math.floor(document.getElementById("scrollable-" + type).offsetWidth / 132);
|
||||||
|
|
||||||
|
// Dont put fewer than 4 per row
|
||||||
|
numPerRow = Math.max(numPerRow, 4);
|
||||||
|
|
||||||
|
// Dont populate more than 6 rows
|
||||||
|
numPerRow = Math.max(numPerRow, Math.ceil(accessories[type][total_items] / 8));
|
||||||
|
|
||||||
|
// update the locations of the <br>
|
||||||
|
for (var i = 1; document.getElementById(type + "-br-" + i) != null; i++) {
|
||||||
|
var spanBr = document.getElementById(type + "-br-" + i);
|
||||||
|
if ( i % numPerRow == 0 ) {
|
||||||
|
spanBr.innerHTML = "<br>";
|
||||||
|
} else {
|
||||||
|
spanBr.innerHTML = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// update the max height for any currently visible containers
|
||||||
|
Collapsibles.updateAllHeights();
|
||||||
|
}
|
||||||
|
resizePage();
|
||||||
|
window.addEventListener('resize', resizePage);
|
||||||
|
|
||||||
|
// Set initial preview for current avatar
|
||||||
|
resetAvatar();
|
||||||
|
// Initialize scroll on all current accessories so we can see the selected ones
|
||||||
|
for (const type of types) {
|
||||||
|
document.getElementById("scrollable-" + type).scrollLeft = document.getElementById(accessories[type][curr_id]).offsetLeft;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expand the first collapsible so the user can get the gist of it.
|
||||||
|
Collapsibles.expandFirst();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{% endblock content %}
|
@ -7,15 +7,10 @@
|
|||||||
{% include 'titles/chuni/templates/chuni_header.jinja' %}
|
{% include 'titles/chuni/templates/chuni_header.jinja' %}
|
||||||
{% if favorites_by_genre is defined and favorites_by_genre is not none %}
|
{% if favorites_by_genre is defined and favorites_by_genre is not none %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<h1 style="text-align: center;">{{ cur_version_name }}</h1>
|
|
||||||
<h4 style="text-align: center;">Favorite Count: {{ favorites_count }}</h4>
|
<h4 style="text-align: center;">Favorite Count: {{ favorites_count }}</h4>
|
||||||
{% for key, genre in favorites_by_genre.items() %}
|
{% for key, genre in favorites_by_genre.items() %}
|
||||||
<h2 style="text-align: center; padding-top: 32px">{{ key }}</h2>
|
<h2 style="text-align: center; padding-top: 32px">{{ key }}</h2>
|
||||||
{% for favorite in genre %}
|
{% for favorite in genre %}
|
||||||
<form id="fav_{{ favorite.idx }}" action="/game/chuni/update.favorite_music_favorites" method="post" style="display: none;">
|
|
||||||
<input class="form-control" form="fav_{{ favorite.idx }}" id="musicId" name="musicId" type="hidden" value="{{ favorite.favId }}">
|
|
||||||
<input class="form-control" form="fav_{{ favorite.idx }}" id="isAdd" name="isAdd" type="hidden" value="0">
|
|
||||||
</form>
|
|
||||||
<div class="col-lg-6 mt-3">
|
<div class="col-lg-6 mt-3">
|
||||||
<div class="card bg-card rounded card-hover">
|
<div class="card bg-card rounded card-hover">
|
||||||
<div class="card-body row">
|
<div class="card-body row">
|
||||||
@ -28,7 +23,7 @@
|
|||||||
<h6 class="card-text"> {{ favorite.artist }} </h6>
|
<h6 class="card-text"> {{ favorite.artist }} </h6>
|
||||||
<br><br>
|
<br><br>
|
||||||
<div style="text-align: right;">
|
<div style="text-align: right;">
|
||||||
<input type=submit class="btn btn-secondary btn-fav-remove" type="button" form="fav_{{ favorite.idx }}" value="Remove">
|
<button onclick="removeFavorite({{ favorite.favId }})" class="btn btn-secondary btn-fav-remove">Remove</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -51,5 +46,16 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Remove Favorite
|
||||||
|
function removeFavorite(musicId) {
|
||||||
|
$.post("/game/chuni/update.favorite_music_favorites", { musicId: musicId, isAdd: 0 })
|
||||||
|
.done(function (data) {
|
||||||
|
location.reload();
|
||||||
|
})
|
||||||
|
.fail(function () {
|
||||||
|
alert("Failed to remove favorite.");
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
@ -1,5 +1,5 @@
|
|||||||
<div class="chuni-header">
|
<div class="chuni-header">
|
||||||
<h1>Chunithm</h1>
|
<h1>{{ cur_version_name }}</h1>
|
||||||
<ul class="chuni-navi">
|
<ul class="chuni-navi">
|
||||||
<li><a class="nav-link" href="/game/chuni">PROFILE</a></li>
|
<li><a class="nav-link" href="/game/chuni">PROFILE</a></li>
|
||||||
<li><a class="nav-link" href="/game/chuni/rating">RATING</a></li>
|
<li><a class="nav-link" href="/game/chuni/rating">RATING</a></li>
|
||||||
@ -7,6 +7,9 @@
|
|||||||
<li><a class="nav-link" href="/game/chuni/favorites">FAVORITES</a></li>
|
<li><a class="nav-link" href="/game/chuni/favorites">FAVORITES</a></li>
|
||||||
<li><a class="nav-link" href="/game/chuni/musics">MUSICS</a></li>
|
<li><a class="nav-link" href="/game/chuni/musics">MUSICS</a></li>
|
||||||
<li><a class="nav-link" href="/game/chuni/userbox">USER BOX</a></li>
|
<li><a class="nav-link" href="/game/chuni/userbox">USER BOX</a></li>
|
||||||
|
{% if cur_version >= 11 %} <!-- avatar config introduced in NEW!! -->
|
||||||
|
<li><a class="nav-link" href="/game/chuni/avatar">AVATAR</a></li>
|
||||||
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
@ -22,6 +25,13 @@
|
|||||||
$('.nav-link[href="/game/chuni/favorites"]').addClass('active');
|
$('.nav-link[href="/game/chuni/favorites"]').addClass('active');
|
||||||
} else if (currentPath.startsWith('/game/chuni/musics')) {
|
} else if (currentPath.startsWith('/game/chuni/musics')) {
|
||||||
$('.nav-link[href="/game/chuni/musics"]').addClass('active');
|
$('.nav-link[href="/game/chuni/musics"]').addClass('active');
|
||||||
|
} else if (currentPath.startsWith('/game/chuni/userbox')) {
|
||||||
|
$('.nav-link[href="/game/chuni/userbox"]').addClass('active');
|
||||||
|
}
|
||||||
|
{% if cur_version >= 11 %} <!-- avatar config introduced in NEW!! -->
|
||||||
|
else if (currentPath.startsWith('/game/chuni/avatar')) {
|
||||||
|
$('.nav-link[href="/game/chuni/avatar"]').addClass('active');
|
||||||
}
|
}
|
||||||
|
{% endif %}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
@ -69,9 +69,48 @@
|
|||||||
<td>Last Play Date:</td>
|
<td>Last Play Date:</td>
|
||||||
<td>{{ profile.lastPlayDate }}</td>
|
<td>{{ profile.lastPlayDate }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
{% if cur_version >= 6 %} <!-- MAP ICON and SYSTEM VOICE introduced in AMAZON -->
|
||||||
|
<tr>
|
||||||
|
<td>Map Icon:</td>
|
||||||
|
<td><div id="map-icon-name">{{ map_icons[profile.mapIconId]["name"] }}</div></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>System Voice:</td>
|
||||||
|
<td><div id="system-voice-name">{{ system_voices[profile.voiceId]["name"] }}</div></td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if cur_version >= 6 %} <!-- MAP ICON and SYSTEM VOICE introduced in AMAZON -->
|
||||||
|
<!-- MAP ICON SELECTION -->
|
||||||
|
<div class="col-lg-8 m-auto mt-3 scrolling-lists">
|
||||||
|
<div class="card bg-card rounded">
|
||||||
|
<button class="collapsible">Map Icon: {{ map_icons|length }}/{{ total_map_icons }}</button>
|
||||||
|
<div id="scrollable-map-icon" class="collapsible-content">
|
||||||
|
{% for item in map_icons.values() %}
|
||||||
|
<img id="map-icon-{{ item["id"] }}" style="padding: 8px 8px;" onclick="saveItem('map-icon', '{{ item["id"] }}', '{{ item["name"] }}')" src="img/mapIcon/{{ item["iconPath"] }}" alt="{{ item["name"] }}">
|
||||||
|
<span id="map-icon-br-{{ loop.index }}"></span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- SYSTEM VOICE SELECTION -->
|
||||||
|
<div class="col-lg-8 m-auto mt-3 scrolling-lists">
|
||||||
|
<div class="card bg-card rounded">
|
||||||
|
<button class="collapsible">System Voice: {{ system_voices|length }}/{{ total_system_voices }}</button>
|
||||||
|
<div id="scrollable-system-voice" class="collapsible-content">
|
||||||
|
{% for item in system_voices.values() %}
|
||||||
|
<img id="system-voice-{{ item["id"] }}" style="padding: 8px 8px;" onclick="saveItem('system-voice', '{{ item["id"] }}', '{{ item["name"] }}')" src="img/systemVoice/{{ item["imagePath"] }}" alt="{{ item["name"] }}">
|
||||||
|
<span id="system-voice-br-{{ loop.index }}"></span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div class="col-lg-8 m-auto mt-3">
|
<div class="col-lg-8 m-auto mt-3">
|
||||||
<div class="card bg-card rounded">
|
<div class="card bg-card rounded">
|
||||||
<table class="table-large table-rowdistinct">
|
<table class="table-large table-rowdistinct">
|
||||||
@ -147,4 +186,93 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
{% if cur_version >= 6 %} <!-- MAP ICON and SYSTEM VOICE introduced in AMAZON -->
|
||||||
|
<script>
|
||||||
|
{% include 'titles/chuni/templates/scripts/collapsibles.js' %}
|
||||||
|
|
||||||
|
///
|
||||||
|
/// This script handles all updates to the map icon and system voice
|
||||||
|
///
|
||||||
|
total_items = 0;
|
||||||
|
curr_id = 1;
|
||||||
|
items = {
|
||||||
|
// [total_items, curr_id]
|
||||||
|
"map-icon": ["{{ map_icons|length }}", "{{ profile.mapIconId }}"],
|
||||||
|
"system-voice":["{{ system_voices|length }}", "{{ profile.voiceId }}"]
|
||||||
|
};
|
||||||
|
types = Object.keys(items);
|
||||||
|
|
||||||
|
function changeItem(type, id, name) {
|
||||||
|
// clear select style for old selection
|
||||||
|
var element = document.getElementById(type + "-" + items[type][curr_id]);
|
||||||
|
if (element) {
|
||||||
|
element.style.backgroundColor="inherit";
|
||||||
|
}
|
||||||
|
|
||||||
|
// set new item
|
||||||
|
items[type][curr_id] = id;
|
||||||
|
document.getElementById(type + "-name").innerHTML = name;
|
||||||
|
|
||||||
|
// update select style for new accessory
|
||||||
|
element = document.getElementById(type + "-" + id);
|
||||||
|
if (element) {
|
||||||
|
element.style.backgroundColor="#5F5";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveItem(type, id, name) {
|
||||||
|
$.post("/game/chuni/update." + type, { id: id })
|
||||||
|
.done(function (data) {
|
||||||
|
changeItem(type, id, name);
|
||||||
|
})
|
||||||
|
.fail(function () {
|
||||||
|
alert("Failed to set " + type + " to " + name);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function resizePage() {
|
||||||
|
//
|
||||||
|
// Handles item organization in the collapsible scrollables to try to keep the items-per-row presentable
|
||||||
|
//
|
||||||
|
// @note Yes, we could simply let the div overflow like usual. This could however get really nasty looking
|
||||||
|
// when dealing with something like userbox characters where there are 1000s of possible items being
|
||||||
|
// display. This approach gives us full control over where items in the div wrap, allowing us to try
|
||||||
|
// to keep things presentable.
|
||||||
|
//
|
||||||
|
for (const type of types) {
|
||||||
|
var numPerRow = Math.floor(document.getElementById("scrollable-" + type).offsetWidth / 132);
|
||||||
|
|
||||||
|
// Dont put fewer than 4 per row
|
||||||
|
numPerRow = Math.max(numPerRow, 4);
|
||||||
|
|
||||||
|
// Dont populate more than 6 rows
|
||||||
|
numPerRow = Math.max(numPerRow, Math.ceil(items[type][total_items] / 6));
|
||||||
|
|
||||||
|
// update the locations of the <br>
|
||||||
|
for (var i = 1; document.getElementById(type + "-br-" + i) != null; i++) {
|
||||||
|
var spanBr = document.getElementById(type + "-br-" + i);
|
||||||
|
if ( i % numPerRow == 0 ) {
|
||||||
|
spanBr.innerHTML = "<br>";
|
||||||
|
} else {
|
||||||
|
spanBr.innerHTML = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// update the max height for any currently visible containers
|
||||||
|
Collapsibles.updateAllHeights();
|
||||||
|
}
|
||||||
|
resizePage();
|
||||||
|
window.addEventListener('resize', resizePage);
|
||||||
|
|
||||||
|
// Set initial style for current and scroll to selected
|
||||||
|
for (const type of types) {
|
||||||
|
changeItem(type, items[type][curr_id], document.getElementById(type + "-name").innerHTML);
|
||||||
|
document.getElementById("scrollable-" + type).scrollLeft = document.getElementById(type + "-" + items[type][curr_id]).offsetLeft;
|
||||||
|
}
|
||||||
|
|
||||||
|
Collapsibles.expandAll();
|
||||||
|
</script>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% endblock content %}
|
{% endblock content %}
|
@ -7,20 +7,15 @@
|
|||||||
{% include 'titles/chuni/templates/chuni_header.jinja' %}
|
{% include 'titles/chuni/templates/chuni_header.jinja' %}
|
||||||
{% if playlog is defined and playlog is not none %}
|
{% if playlog is defined and playlog is not none %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<h1 style="text-align: center;">{{ cur_version_name }}</h1>
|
|
||||||
<h4 style="text-align: center;">Playlog Count: {{ playlog_count }}</h4>
|
<h4 style="text-align: center;">Playlog Count: {{ playlog_count }}</h4>
|
||||||
{% set rankName = ['D', 'C', 'B', 'BB', 'BBB', 'A', 'AA', 'AAA', 'S', 'S+', 'SS', 'SS+', 'SSS', 'SSS+'] %}
|
{% set rankName = ['D', 'C', 'B', 'BB', 'BBB', 'A', 'AA', 'AAA', 'S', 'S+', 'SS', 'SS+', 'SSS', 'SSS+'] %}
|
||||||
{% set difficultyName = ['normal', 'hard', 'expert', 'master', 'ultimate'] %}
|
{% set difficultyName = ['normal', 'hard', 'expert', 'master', 'ultimate'] %}
|
||||||
{% for record in playlog %}
|
{% for record in playlog %}
|
||||||
<form id="fav_{{ record.idx }}" action="/game/chuni/update.favorite_music_playlog" method="post" style="display: none;">
|
|
||||||
<input class="form-control" form="fav_{{ record.idx }}" id="musicId" name="musicId" type="hidden" value="{{ record.musicId }}">
|
|
||||||
<input class="form-control" form="fav_{{ record.idx }}" id="isAdd" name="isAdd" type="hidden" value="{{ 0 if record.isFav else 1 }}">
|
|
||||||
</form>
|
|
||||||
<div class="col-lg-6 mt-3">
|
<div class="col-lg-6 mt-3">
|
||||||
<div class="card bg-card rounded card-hover">
|
<div class="card bg-card rounded card-hover">
|
||||||
<div class="card-header row">
|
<div class="card-header row">
|
||||||
<div class="col-auto fav" title="{{ ('Remove' if record.isFav else 'Add') + ' Favorite'}}">
|
<div class="col-auto fav" title="{{ ('Remove' if record.isFav else 'Add') + ' Favorite'}}">
|
||||||
<h1><input type=submit class="fav {{ 'fav-set' if record.isFav else '' }}" type="button" form="fav_{{ record.idx }}" value="{{ '★' if record.isFav else '☆' }} "></h1>
|
<h1><span id="{{ record.idx }}" class="fav {{ 'fav-set' if record.isFav else '' }}" onclick="updateFavorite({{ record.idx }}, {{ record.musicId }})">{{ '★' if record.isFav else '☆' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="col scrolling-text">
|
<div class="col scrolling-text">
|
||||||
<h5 class="card-text"> {{ record.title }} </h5>
|
<h5 class="card-text"> {{ record.title }} </h5>
|
||||||
@ -191,5 +186,23 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add/Remove Favorite
|
||||||
|
function updateFavorite(elementId, musicId) {
|
||||||
|
element = document.getElementById(elementId);
|
||||||
|
isAdd = 1;
|
||||||
|
if (element.classList.contains("fav-set"))
|
||||||
|
{
|
||||||
|
isAdd = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$.post("/game/chuni/update.favorite_music_favorites", { musicId: musicId, isAdd: isAdd })
|
||||||
|
.done(function (data) {
|
||||||
|
location.reload();
|
||||||
|
})
|
||||||
|
.fail(function () {
|
||||||
|
alert("Failed to update favorite.");
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
256
titles/chuni/templates/chuni_userbox.jinja
Normal file
256
titles/chuni/templates/chuni_userbox.jinja
Normal file
@ -0,0 +1,256 @@
|
|||||||
|
{% extends "core/templates/index.jinja" %}
|
||||||
|
{% block content %}
|
||||||
|
<style>
|
||||||
|
{% include 'titles/chuni/templates/css/chuni_style.css' %}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
{% include 'titles/chuni/templates/chuni_header.jinja' %}
|
||||||
|
|
||||||
|
<!-- USER BOX PREVIEW -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-8 m-auto mt-3">
|
||||||
|
<div class="card bg-card rounded">
|
||||||
|
<table class="table-large table-rowdistinct">
|
||||||
|
<caption align="top">USER BOX</caption>
|
||||||
|
<tr><td colspan=2 style="height:240px;">
|
||||||
|
<!-- NAMEPLATE -->
|
||||||
|
<img id="preview_nameplate" class="userbox userbox-nameplate" src="">
|
||||||
|
|
||||||
|
<!-- TEAM -->
|
||||||
|
<img class="userbox userbox-teamframe" src="img/rank/team3.png">
|
||||||
|
<div class="userbox userbox-teamname">{{team_name}}</div>
|
||||||
|
|
||||||
|
<!-- TROPHY/TITLE -->
|
||||||
|
<img id="preview_trophy_rank" class="userbox userbox-trophy" src="">
|
||||||
|
<div id="preview_trophy_name" class="userbox userbox-trophy userbox-trophy-name"></div>
|
||||||
|
|
||||||
|
<!-- NAME/RATING -->
|
||||||
|
<img class="userbox userbox-ratingframe" src="img/rank/rating0.png">
|
||||||
|
<div class="userbox userbox-name">
|
||||||
|
<span class="userbox-name-level-label">Lv.</span>
|
||||||
|
{{ profile.level }} {{ profile.userName }}
|
||||||
|
</div>
|
||||||
|
<div class="userbox userbox-rating rating rating-rank{{ rating_rank }}">
|
||||||
|
<span class="userbox-rating-label">RATING</span>
|
||||||
|
{{ profile.playerRating/100 }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- CHARACTER -->
|
||||||
|
<img class="userbox userbox-charaframe" src="img/character-bg.png">
|
||||||
|
<img id="preview_character" class="userbox userbox-chara" src="">
|
||||||
|
</td></tr>
|
||||||
|
|
||||||
|
<tr><td>Nameplate:</td><td style="width: 80%;"><div id="name_nameplate"></div></td></tr>
|
||||||
|
|
||||||
|
<tr><td>Trophy:</td><td><div id="name_trophy">
|
||||||
|
<select name="trophy" id="trophy" onchange="changeTrophy()">
|
||||||
|
{% for item in trophies.values() %}
|
||||||
|
<option value="{{ item["id"] }}" class="trophy-rank{{ item["rarity"] }}">{{ item["name"] }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div></td></tr>
|
||||||
|
|
||||||
|
<tr><td>Character:</td><td><div id="name_character"></div></td></tr>
|
||||||
|
|
||||||
|
<tr><td colspan=2 style="padding:8px 0px; text-align: center;">
|
||||||
|
<button id="save-btn" class="btn btn-primary" style="width:140px;" onClick="saveUserbox()">SAVE</button>
|
||||||
|
<button id="reset-btn" class="btn btn-danger" style="width:140px;" onClick="resetUserbox()">RESET</button>
|
||||||
|
</td></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- USERBOX SELECTION -->
|
||||||
|
<div class="row col-lg-8 m-auto mt-3 scrolling-lists-lg card bg-card rounded">
|
||||||
|
|
||||||
|
<!-- NAMEPLATE -->
|
||||||
|
<button class="collapsible">Nameplate: {{ nameplates|length }}/{{ total_nameplates }}</button>
|
||||||
|
<div id="scrollable-nameplate" class="collapsible-content">
|
||||||
|
{% for item in nameplates.values() %}
|
||||||
|
<img id="nameplate-{{ item["id"] }}" style="padding: 8px 8px;" onclick="changeItem('nameplate', '{{ item["id"] }}', '{{ item["name"] }}', '{{ item["texturePath"] }}')" src="img/nameplate/{{ item["texturePath"] }}" alt="{{ item["name"] }}">
|
||||||
|
<span id="nameplate-br-{{ loop.index }}"></span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<!-- CHARACTER -->
|
||||||
|
<button class="collapsible">Character: {{ characters|length }}/{{ total_characters }}</button>
|
||||||
|
<div id="scrollable-character" class="collapsible-content">
|
||||||
|
{% for item in characters.values() %}
|
||||||
|
<img id="character-{{ item["id"] }}" onclick="changeItem('character', '{{ item["id"] }}', '{{ item["name"] }}', '{{ item["iconPath"] }}')" src="img/character/{{ item["iconPath"] }}" alt="{{ item["name"] }}">
|
||||||
|
<span id="character-br-{{ loop.index }}"></span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if error is defined %}
|
||||||
|
{% include "core/templates/widgets/err_banner.jinja" %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
{% include 'titles/chuni/templates/scripts/collapsibles.js' %}
|
||||||
|
|
||||||
|
///
|
||||||
|
/// This script handles all updates to the user box
|
||||||
|
///
|
||||||
|
total_items = 0;
|
||||||
|
orig_id = 1;
|
||||||
|
orig_name = 2;
|
||||||
|
orig_img = 3;
|
||||||
|
curr_id = 4;
|
||||||
|
curr_name = 5;
|
||||||
|
curr_img = 6;
|
||||||
|
userbox_components = {
|
||||||
|
// [total_items, orig_id, orig_name, orig_img, curr_id, curr_name, curr_img]
|
||||||
|
"nameplate":["{{ nameplates|length }}",
|
||||||
|
"{{ profile.nameplateId }}",
|
||||||
|
"{{ nameplates[profile.nameplateId]["name"] }}",
|
||||||
|
"{{ nameplates[profile.nameplateId]["texturePath"] }}", "", "", ""],
|
||||||
|
|
||||||
|
"character":["{{ characters|length }}",
|
||||||
|
"{{ profile.charaIllustId }}",
|
||||||
|
"{{ characters[profile.charaIllustId]["name"] }}",
|
||||||
|
"{{ characters[profile.charaIllustId]["iconPath"] }}", "", "", ""]
|
||||||
|
};
|
||||||
|
types = Object.keys(userbox_components);
|
||||||
|
orig_trophy = curr_trophy = "{{ profile.trophyId }}";
|
||||||
|
curr_trophy_img = "";
|
||||||
|
|
||||||
|
function enableButtons(enabled) {
|
||||||
|
document.getElementById("reset-btn").disabled = !enabled;
|
||||||
|
document.getElementById("save-btn").disabled = !enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeItem(type, id, name, img) {
|
||||||
|
// clear select style for old component
|
||||||
|
var element = document.getElementById(type + "-" + userbox_components[type][curr_id]);
|
||||||
|
if (element) {
|
||||||
|
element.style.backgroundColor="inherit";
|
||||||
|
}
|
||||||
|
|
||||||
|
// set new component
|
||||||
|
userbox_components[type][curr_id] = id;
|
||||||
|
userbox_components[type][curr_name] = name;
|
||||||
|
userbox_components[type][curr_img] = img;
|
||||||
|
|
||||||
|
// update select style for new accessory
|
||||||
|
element = document.getElementById(type + "-" + id);
|
||||||
|
if (element) {
|
||||||
|
element.style.backgroundColor="#5F5";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the userbox preview and enable buttons
|
||||||
|
updatePreview();
|
||||||
|
if (id != userbox_components[type][orig_id]) {
|
||||||
|
enableButtons(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRankImage(selected_rank) {
|
||||||
|
for (const x of Array(12).keys()) {
|
||||||
|
if (selected_rank.classList.contains("trophy-rank" + x.toString())) {
|
||||||
|
return "rank" + x.toString() + ".png";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "rank0.png"; // shouldnt ever happen
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeTrophy() {
|
||||||
|
var trophy_element = document.getElementById("trophy");
|
||||||
|
|
||||||
|
curr_trophy = trophy_element.value;
|
||||||
|
curr_trophy_img = getRankImage(trophy_element[trophy_element.selectedIndex]);
|
||||||
|
updatePreview();
|
||||||
|
if (curr_trophy != orig_trophy) {
|
||||||
|
enableButtons(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetUserbox() {
|
||||||
|
for (const type of types) {
|
||||||
|
changeItem(type, userbox_components[type][orig_id], userbox_components[type][orig_name], userbox_components[type][orig_img]);
|
||||||
|
}
|
||||||
|
// reset trophy
|
||||||
|
document.getElementById("trophy").value = orig_trophy;
|
||||||
|
changeTrophy();
|
||||||
|
// disable the save/reset buttons until something changes
|
||||||
|
enableButtons(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePreview() {
|
||||||
|
for (const type of types) {
|
||||||
|
document.getElementById("preview_" + type).src = "img/" + type + "/" + userbox_components[type][curr_img];
|
||||||
|
document.getElementById("name_" + type).innerHTML = userbox_components[type][curr_name];
|
||||||
|
}
|
||||||
|
document.getElementById("preview_trophy_rank").src = "img/rank/" + curr_trophy_img;
|
||||||
|
document.getElementById("preview_trophy_name").innerHTML = document.getElementById("trophy")[document.getElementById("trophy").selectedIndex].innerText;
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveUserbox() {
|
||||||
|
$.post("/game/chuni/update.userbox", { nameplate: userbox_components["nameplate"][curr_id],
|
||||||
|
trophy: curr_trophy,
|
||||||
|
character: userbox_components["character"][curr_id] })
|
||||||
|
.done(function (data) {
|
||||||
|
// set the current as the original and disable buttons
|
||||||
|
for (const type of types) {
|
||||||
|
userbox_components[type][orig_id] = userbox_components[type][curr_id];
|
||||||
|
userbox_components[type][orig_name] = userbox_components[type][orig_name];
|
||||||
|
userbox_components[type][orig_img] = userbox_components[type][curr_img];
|
||||||
|
}
|
||||||
|
orig_trophy = curr_trophy
|
||||||
|
enableButtons(false);
|
||||||
|
})
|
||||||
|
.fail(function () {
|
||||||
|
alert("Failed to save userbox.");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function resizePage() {
|
||||||
|
//
|
||||||
|
// Handles item organization in the collapsible scrollables to try to keep the items-per-row presentable
|
||||||
|
//
|
||||||
|
// @note Yes, we could simply let the div overflow like usual. This could however get really nasty looking
|
||||||
|
// when dealing with something like userbox characters where there are 1000s of possible items being
|
||||||
|
// display. This approach gives us full control over where items in the div wrap, allowing us to try
|
||||||
|
// to keep things presentable.
|
||||||
|
//
|
||||||
|
for (const type of types) {
|
||||||
|
var numPerRow = Math.floor(document.getElementById("scrollable-" + type).offsetWidth / 132);
|
||||||
|
|
||||||
|
// Dont put fewer than 4 per row
|
||||||
|
numPerRow = Math.max(numPerRow, 4);
|
||||||
|
|
||||||
|
// Dont populate more than 6 rows
|
||||||
|
numPerRow = Math.max(numPerRow, Math.ceil(userbox_components[type][total_items] / 8));
|
||||||
|
|
||||||
|
// update the locations of the <br>
|
||||||
|
for (var i = 1; document.getElementById(type + "-br-" + i) != null; i++) {
|
||||||
|
var spanBr = document.getElementById(type + "-br-" + i);
|
||||||
|
if ( i % numPerRow == 0 ) {
|
||||||
|
spanBr.innerHTML = "<br>";
|
||||||
|
} else {
|
||||||
|
spanBr.innerHTML = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// update the max height for any currently visible containers
|
||||||
|
Collapsibles.updateAllHeights();
|
||||||
|
}
|
||||||
|
resizePage();
|
||||||
|
window.addEventListener('resize', resizePage);
|
||||||
|
|
||||||
|
// Set initial preview for current userbox
|
||||||
|
resetUserbox();
|
||||||
|
// Initialize scroll on all current items so we can see the selected ones
|
||||||
|
for (const type of types) {
|
||||||
|
document.getElementById("scrollable-" + type).scrollLeft = document.getElementById(type + "-" + userbox_components[type][curr_id]).offsetLeft;
|
||||||
|
}
|
||||||
|
|
||||||
|
Collapsibles.expandAll();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{% endblock content %}
|
@ -159,6 +159,45 @@ caption {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.rating {
|
||||||
|
font-weight: bold;
|
||||||
|
-webkit-text-stroke-width: 1px;
|
||||||
|
-webkit-text-stroke-color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rating-rank0 {
|
||||||
|
color: #008000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rating-rank1 {
|
||||||
|
color: #ffa500;
|
||||||
|
}
|
||||||
|
.rating-rank2 {
|
||||||
|
color: #ff0000;
|
||||||
|
}
|
||||||
|
.rating-rank3 {
|
||||||
|
color: #800080;
|
||||||
|
}
|
||||||
|
.rating-rank4 {
|
||||||
|
color: #cd853f;
|
||||||
|
}
|
||||||
|
.rating-rank5 {
|
||||||
|
color: #c0c0c0;
|
||||||
|
}
|
||||||
|
.rating-rank6 {
|
||||||
|
color: #ffd700;
|
||||||
|
}
|
||||||
|
.rating-rank7 {
|
||||||
|
color: #a9a9a9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rating-rank8 {
|
||||||
|
background: linear-gradient(to right, red, yellow, lime, aqua, blue, fuchsia) 0 / 5em;
|
||||||
|
background-clip: text;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
.scrolling-text {
|
.scrolling-text {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
@ -194,6 +233,41 @@ caption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Styles to support collapsible boxes (used for browsing images)
|
||||||
|
*/
|
||||||
|
.collapsible {
|
||||||
|
background-color: #555;
|
||||||
|
cursor: pointer;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
width: 100%;
|
||||||
|
border: none;
|
||||||
|
text-align: left;
|
||||||
|
outline: none;
|
||||||
|
font-family: monospace;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible:after {
|
||||||
|
content: '[+]';
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible-active:after {
|
||||||
|
content: "[-]";
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible-content {
|
||||||
|
max-height: 0px;
|
||||||
|
overflow: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
transition: max-height 0.2s ease-out;
|
||||||
|
background-color: #DDD;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Styles for favorites star in /playlog
|
||||||
|
*/
|
||||||
.fav {
|
.fav {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
padding-left: 4px;
|
padding-left: 4px;
|
||||||
@ -206,7 +280,257 @@ caption {
|
|||||||
color: gold;
|
color: gold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Styles for favorites in /favorites
|
||||||
|
*/
|
||||||
.btn-fav-remove {
|
.btn-fav-remove {
|
||||||
padding:10px;
|
padding:10px;
|
||||||
width:100%;
|
width:100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Styles for userbox configuration
|
||||||
|
*/
|
||||||
|
.userbox {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userbox-nameplate {
|
||||||
|
top: 72px;
|
||||||
|
left: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userbox-teamframe {
|
||||||
|
top: 74px;
|
||||||
|
left: 156px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userbox-teamname {
|
||||||
|
top: 72px;
|
||||||
|
left: 254px;
|
||||||
|
padding: 8px 20px;
|
||||||
|
font-size: 22px;
|
||||||
|
text-shadow: rgba(0,0,0,0.8) 2px 2px;
|
||||||
|
color: #DDD;
|
||||||
|
width: 588px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userbox-trophy {
|
||||||
|
top: 170px;
|
||||||
|
left: 250px;
|
||||||
|
zoom: 0.70;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userbox-trophy-name {
|
||||||
|
top: 170px;
|
||||||
|
left: 250px;
|
||||||
|
padding: 8px 20px;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
width: 588px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userbox-ratingframe {
|
||||||
|
top: 160px;
|
||||||
|
left: 175px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userbox-charaframe {
|
||||||
|
top: 267px;
|
||||||
|
left: 824px;
|
||||||
|
zoom: 0.61;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userbox-chara {
|
||||||
|
top: 266px;
|
||||||
|
left: 814px;
|
||||||
|
zoom: 0.62;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userbox-name {
|
||||||
|
top: 160px;
|
||||||
|
left: 162px;
|
||||||
|
padding: 8px 20px;
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userbox-name-level-label {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userbox-rating {
|
||||||
|
top: 204px;
|
||||||
|
left: 166px;
|
||||||
|
padding: 8px 20px;
|
||||||
|
font-size: 24px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userbox-rating-label {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trophy-rank0 {
|
||||||
|
color: #111;
|
||||||
|
background-color: #DDD;
|
||||||
|
}
|
||||||
|
.trophy-rank1 {
|
||||||
|
color: #111;
|
||||||
|
background-color: #D85;
|
||||||
|
}
|
||||||
|
.trophy-rank2 {
|
||||||
|
color: #111;
|
||||||
|
background-color: #ADF;
|
||||||
|
}
|
||||||
|
.trophy-rank3 {
|
||||||
|
color: #111;
|
||||||
|
background-color: #EB3;
|
||||||
|
}
|
||||||
|
.trophy-rank4 {
|
||||||
|
color: #111;
|
||||||
|
background-color: #EB3;
|
||||||
|
}
|
||||||
|
.trophy-rank5 {
|
||||||
|
color: #111;
|
||||||
|
background-color: #FFA;
|
||||||
|
}
|
||||||
|
.trophy-rank6 {
|
||||||
|
color: #111;
|
||||||
|
background-color: #FFA;
|
||||||
|
}
|
||||||
|
.trophy-rank7 {
|
||||||
|
color: #111;
|
||||||
|
background-color: #FCF;
|
||||||
|
}
|
||||||
|
.trophy-rank8 {
|
||||||
|
color: #111;
|
||||||
|
background-color: #FCF;
|
||||||
|
}
|
||||||
|
.trophy-rank9 {
|
||||||
|
color: #111;
|
||||||
|
background-color: #07C;
|
||||||
|
}
|
||||||
|
.trophy-rank10 {
|
||||||
|
color: #111;
|
||||||
|
background-color: #7FE;
|
||||||
|
}
|
||||||
|
.trophy-rank11 {
|
||||||
|
color: #111;
|
||||||
|
background-color: #8D7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Styles for scrollable divs (used for browsing images)
|
||||||
|
*/
|
||||||
|
.scrolling-lists {
|
||||||
|
table-layout: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrolling-lists div {
|
||||||
|
overflow: auto;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrolling-lists img {
|
||||||
|
width: 128px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrolling-lists-lg {
|
||||||
|
table-layout: fixed;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrolling-lists-lg div {
|
||||||
|
overflow: auto;
|
||||||
|
white-space: nowrap;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrolling-lists-lg img {
|
||||||
|
padding: 4px;
|
||||||
|
width: 128px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Styles for avatar configuration
|
||||||
|
*/
|
||||||
|
.avatar-preview {
|
||||||
|
position:absolute;
|
||||||
|
zoom:0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-preview-wear {
|
||||||
|
top: 280px;
|
||||||
|
left: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-preview-face {
|
||||||
|
top: 262px;
|
||||||
|
left: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-preview-head {
|
||||||
|
top: 130px;
|
||||||
|
left: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-preview-skin-body {
|
||||||
|
top: 250px;
|
||||||
|
left: 190px;
|
||||||
|
height: 406px;
|
||||||
|
width: 256px;
|
||||||
|
object-fit: cover;
|
||||||
|
object-position: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-preview-skin-leftfoot {
|
||||||
|
top: 625px;
|
||||||
|
left: 340px;
|
||||||
|
object-position: -84px -406px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-preview-skin-rightfoot {
|
||||||
|
top: 625px;
|
||||||
|
left: 40px;
|
||||||
|
object-position: 172px -406px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-preview-common {
|
||||||
|
top: 250px;
|
||||||
|
left: 135px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-preview-item-lefthand {
|
||||||
|
top: 180px;
|
||||||
|
left: 370px;
|
||||||
|
height: 544px;
|
||||||
|
width: 200px;
|
||||||
|
object-fit: cover;
|
||||||
|
object-position: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-preview-item-righthand {
|
||||||
|
top: 180px;
|
||||||
|
left: 65px;
|
||||||
|
height: 544px;
|
||||||
|
width: 200px;
|
||||||
|
object-fit: cover;
|
||||||
|
object-position: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-preview-back {
|
||||||
|
top: 140px;
|
||||||
|
left: 46px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-preview-platform {
|
||||||
|
top: 310px;
|
||||||
|
left: 55px;
|
||||||
|
zoom: 1;
|
||||||
}
|
}
|
66
titles/chuni/templates/scripts/collapsibles.js
Normal file
66
titles/chuni/templates/scripts/collapsibles.js
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
///
|
||||||
|
/// Handles the collapsible behavior of each of the scrollable containers
|
||||||
|
///
|
||||||
|
/// @note Intent is to include this file via jinja in the same <script> tag as
|
||||||
|
/// any page-specific logic that interacts with the collapisbles.
|
||||||
|
///
|
||||||
|
class Collapsibles {
|
||||||
|
static setHeight(content) {
|
||||||
|
// @note Add an extra 20% height buffer - the div will autosize but we
|
||||||
|
// want to make sure we dont clip due to the horizontal scroll.
|
||||||
|
content.style.maxHeight = (content.scrollHeight * 1.2) + "px";
|
||||||
|
}
|
||||||
|
|
||||||
|
static updateAllHeights() {
|
||||||
|
// Updates the height of all expanded collapsibles.
|
||||||
|
// Intended for use when resolution changes cause the contents within the collapsible to move around.
|
||||||
|
var coll = document.getElementsByClassName("collapsible");
|
||||||
|
for (var i = 0; i < coll.length; i++) {
|
||||||
|
var content = coll[i].nextElementSibling;
|
||||||
|
if (content.style.maxHeight) {
|
||||||
|
// currently visible. update height
|
||||||
|
Collapsibles.setHeight(content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static expandFirst() {
|
||||||
|
// Activate the first collapsible once loaded so the user can get an idea of how things work
|
||||||
|
window.addEventListener('load', function () {
|
||||||
|
var coll = document.getElementsByClassName("collapsible");
|
||||||
|
if (coll && coll.length > 0) {
|
||||||
|
coll[0].click();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
static expandAll() {
|
||||||
|
// Activate all collapsibles so everything is visible immediately
|
||||||
|
window.addEventListener('load', function () {
|
||||||
|
var coll = document.getElementsByClassName("collapsible");
|
||||||
|
for (var i = 0; i < coll.length; i++) {
|
||||||
|
coll[i].click();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Initial Collapsible Setup
|
||||||
|
//
|
||||||
|
|
||||||
|
// Add an event listener for a click on any collapsible object. This will change the style to collapse/expand the content.
|
||||||
|
var coll = document.getElementsByClassName("collapsible");
|
||||||
|
for (var i = 0; i < coll.length; i++) {
|
||||||
|
coll[i].addEventListener("click", function () {
|
||||||
|
this.classList.toggle("collapsible-active");
|
||||||
|
var content = this.nextElementSibling;
|
||||||
|
if (content.style.maxHeight) {
|
||||||
|
content.style.maxHeight = null;
|
||||||
|
content.style.opacity = 0;
|
||||||
|
} else {
|
||||||
|
Collapsibles.setHeight(content);
|
||||||
|
content.style.opacity = 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user