1
0
mirror of synced 2025-02-13 08:52:32 +01:00

userbox, avatar, mapicon, and voice ui configuration

This commit is contained in:
daydensteve 2024-11-03 18:37:09 -05:00
parent e49c70b738
commit 626ce6bd96
9 changed files with 1458 additions and 17 deletions

View File

@ -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:

View 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>&nbsp;&nbsp;&nbsp;&nbsp;
<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:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{{ 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:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{{ 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:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{{ 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:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{{ 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:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{{ 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:&nbsp;&nbsp;&nbsp;&nbsp;{{ 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:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{{ 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 %}

View File

@ -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 %}

View File

@ -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>

View File

@ -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:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{{ 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:&nbsp;&nbsp;&nbsp;{{ 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 %}

View File

@ -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="{{ '&#9733' if record.isFav else '&#9734' }} "></h1> <h1><span id="{{ record.idx }}" class="fav {{ 'fav-set' if record.isFav else '' }}" onclick="updateFavorite({{ record.idx }}, {{ record.musicId }})">{{ '&#9733' if record.isFav else '&#9734' }}</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 %}

View 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 }}&nbsp;&nbsp;&nbsp;{{ profile.userName }}
</div>
<div class="userbox userbox-rating rating rating-rank{{ rating_rank }}">
<span class="userbox-rating-label">RATING</span>
&nbsp;&nbsp;{{ 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>&nbsp;&nbsp;&nbsp;&nbsp;
<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:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{{ 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:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{{ 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 %}

View File

@ -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;
} }

View 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;
}
});
}