[chuni] misc frontend improvements/fixes (i.e. webp instead of png; css error; hide subtrophies on old version) (#234)
TL;DR avatar and userbox frontend pages can get hella slow when loading the first time when a ton of stuff is unlocked. Its driven primarily by all the images the server has to push to the client. To reduce the burden, these changes switch from using png to webp for all scaped images during import, reducing image sizes to roughly 20% of their png-equivalent. The filelist is long so here's a summary list of changes: - Replaced png assets with webp versions - Updated read.py to save assets as webp instead of png - Updated frontend.py and jinja to use webp instead of png - Added a conversion function ran by both the importer and the frontend on launch that looks for previously imported png files and converts them to webp. Only included for the sake of anyone who already did imports since the frontend improvements were introduced. - [bugfix] Fixed a css bug in the avatar jinja that affected Save/Reset button use on super narrow screens Reviewed-on: Hay1tsme/artemis#234 Co-authored-by: daydensteve <daydensteve@gmail.com> Co-committed-by: daydensteve <daydensteve@gmail.com>
@ -13,6 +13,7 @@ from core.config import CoreConfig
|
||||
from .database import ChuniData
|
||||
from .config import ChuniConfig
|
||||
from .const import ChuniConstants, AvatarCategory, ItemKind
|
||||
from .read import ChuniReader
|
||||
|
||||
|
||||
def pairwise(iterable):
|
||||
@ -91,6 +92,9 @@ class ChuniFrontend(FE_Base):
|
||||
self.data = ChuniData(cfg, self.game_cfg)
|
||||
self.nav_name = "Chunithm"
|
||||
|
||||
# Convert any old assets created with a previous version of the importer
|
||||
ChuniReader.ConvertOldAssets(self.logger)
|
||||
|
||||
def get_routes(self) -> List[Route]:
|
||||
return [
|
||||
Route("/", self.render_GET, methods=['GET']),
|
||||
@ -252,12 +256,12 @@ class ChuniFrontend(FE_Base):
|
||||
artist=music_chart.artist
|
||||
title=music_chart.title
|
||||
(jacket, ext) = path.splitext(music_chart.jacketPath)
|
||||
jacket += ".png"
|
||||
jacket += ".webp"
|
||||
else:
|
||||
difficultyNum=0
|
||||
artist="unknown"
|
||||
title="musicid: " + str(record.musicId)
|
||||
jacket = "unknown.png"
|
||||
jacket = "unknown.webp"
|
||||
|
||||
# Check if this song is a favorite so we can populate the add/remove button
|
||||
is_favorite = await self.data.item.is_favorite(user_id, version, record.musicId)
|
||||
@ -313,12 +317,12 @@ class ChuniFrontend(FE_Base):
|
||||
title=song.title
|
||||
genre=song.genre
|
||||
(jacket, ext) = path.splitext(song.jacketPath)
|
||||
jacket += ".png"
|
||||
jacket += ".webp"
|
||||
else:
|
||||
artist="unknown"
|
||||
title="musicid: " + str(favorite.favId)
|
||||
genre="unknown"
|
||||
jacket = "unknown.png"
|
||||
jacket = "unknown.webp"
|
||||
|
||||
# add a new collection for the genre if this is our first time seeing it
|
||||
if genre not in favorites_by_genre:
|
||||
@ -370,7 +374,7 @@ class ChuniFrontend(FE_Base):
|
||||
item = dict()
|
||||
item["id"] = row["mapIconId"]
|
||||
item["name"] = row["name"]
|
||||
item["iconPath"] = path.splitext(row["iconPath"])[0] + ".png"
|
||||
item["iconPath"] = path.splitext(row["iconPath"])[0] + ".webp"
|
||||
items[row["mapIconId"]] = item
|
||||
|
||||
return (items, len(rows))
|
||||
@ -395,7 +399,7 @@ class ChuniFrontend(FE_Base):
|
||||
item = dict()
|
||||
item["id"] = row["voiceId"]
|
||||
item["name"] = row["name"]
|
||||
item["imagePath"] = path.splitext(row["imagePath"])[0] + ".png"
|
||||
item["imagePath"] = path.splitext(row["imagePath"])[0] + ".webp"
|
||||
items[row["voiceId"]] = item
|
||||
|
||||
return (items, len(rows))
|
||||
@ -418,7 +422,7 @@ class ChuniFrontend(FE_Base):
|
||||
item = dict()
|
||||
item["id"] = row["nameplateId"]
|
||||
item["name"] = row["name"]
|
||||
item["texturePath"] = path.splitext(row["texturePath"])[0] + ".png"
|
||||
item["texturePath"] = path.splitext(row["texturePath"])[0] + ".webp"
|
||||
items[row["nameplateId"]] = item
|
||||
|
||||
return (items, len(rows))
|
||||
@ -464,7 +468,7 @@ class ChuniFrontend(FE_Base):
|
||||
item = dict()
|
||||
item["id"] = row["characterId"]
|
||||
item["name"] = row["name"]
|
||||
item["iconPath"] = path.splitext(row["imagePath3"])[0] + ".png"
|
||||
item["iconPath"] = path.splitext(row["imagePath3"])[0] + ".webp"
|
||||
items[row["characterId"]] = item
|
||||
|
||||
return (items, len(rows))
|
||||
@ -482,8 +486,8 @@ class ChuniFrontend(FE_Base):
|
||||
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"
|
||||
item["iconPath"] = path.splitext(row["iconPath"])[0] + ".webp"
|
||||
item["texturePath"] = path.splitext(row["texturePath"])[0] + ".webp"
|
||||
items[row["avatarAccessoryId"]] = item
|
||||
|
||||
return (items, len(rows))
|
||||
|
Before Width: | Height: | Size: 34 KiB |
BIN
titles/chuni/img/avatar-common.webp
Normal file
After Width: | Height: | Size: 6.9 KiB |
Before Width: | Height: | Size: 25 KiB |
BIN
titles/chuni/img/avatar-platform.webp
Normal file
After Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 30 KiB |
BIN
titles/chuni/img/character-bg.webp
Normal file
After Width: | Height: | Size: 1.6 KiB |
2
titles/chuni/img/jacket/.gitignore
vendored
@ -2,4 +2,4 @@
|
||||
*
|
||||
# Except this file and default unknown
|
||||
!.gitignore
|
||||
!unknown.png
|
||||
!unknown.webp
|
Before Width: | Height: | Size: 27 KiB |
BIN
titles/chuni/img/jacket/unknown.webp
Normal file
After Width: | Height: | Size: 6.4 KiB |
Before Width: | Height: | Size: 21 KiB |
BIN
titles/chuni/img/rank/rank0.webp
Normal file
After Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 33 KiB |
BIN
titles/chuni/img/rank/rank1.webp
Normal file
After Width: | Height: | Size: 4.9 KiB |
Before Width: | Height: | Size: 30 KiB |
BIN
titles/chuni/img/rank/rank10.webp
Normal file
After Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 25 KiB |
BIN
titles/chuni/img/rank/rank11.webp
Normal file
After Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 26 KiB |
BIN
titles/chuni/img/rank/rank2.webp
Normal file
After Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 30 KiB |
BIN
titles/chuni/img/rank/rank3.webp
Normal file
After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 30 KiB |
BIN
titles/chuni/img/rank/rank4.webp
Normal file
After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 28 KiB |
BIN
titles/chuni/img/rank/rank5.webp
Normal file
After Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 28 KiB |
BIN
titles/chuni/img/rank/rank6.webp
Normal file
After Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 42 KiB |
BIN
titles/chuni/img/rank/rank7.webp
Normal file
After Width: | Height: | Size: 5.0 KiB |
Before Width: | Height: | Size: 42 KiB |
BIN
titles/chuni/img/rank/rank8.webp
Normal file
After Width: | Height: | Size: 5.0 KiB |
Before Width: | Height: | Size: 21 KiB |
BIN
titles/chuni/img/rank/rank9.webp
Normal file
After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 20 KiB |
BIN
titles/chuni/img/rank/rating0.webp
Normal file
After Width: | Height: | Size: 570 B |
Before Width: | Height: | Size: 44 KiB |
BIN
titles/chuni/img/rank/team3.webp
Normal file
After Width: | Height: | Size: 4.1 KiB |
@ -1,9 +1,11 @@
|
||||
from logging import Logger
|
||||
from typing import Optional
|
||||
from os import walk, path
|
||||
from os import walk, path, remove
|
||||
import xml.etree.ElementTree as ET
|
||||
from read import BaseReader
|
||||
from PIL import Image
|
||||
import configparser
|
||||
import glob
|
||||
|
||||
from core.config import CoreConfig
|
||||
from titles.chuni.database import ChuniData
|
||||
@ -43,6 +45,9 @@ class ChuniReader(BaseReader):
|
||||
if self.version >= ChuniConstants.VER_CHUNITHM_NEW:
|
||||
we_diff = "5"
|
||||
|
||||
# Convert any old assets created with a previous version of the importer
|
||||
ChuniReader.ConvertOldAssets(self.logger)
|
||||
|
||||
# character images could be stored anywhere across all the data dirs. Map them first
|
||||
self.logger.info(f"Mapping DDS image files...")
|
||||
dds_images = dict()
|
||||
@ -533,17 +538,40 @@ class ChuniReader(BaseReader):
|
||||
self.logger.warning(f"Failed to unlock challenge {id}")
|
||||
|
||||
def copy_image(self, filename: str, src_dir: str, dst_dir: str) -> None:
|
||||
# Convert the image to png so we can easily display it in the frontend
|
||||
# Convert the image to webp so we can easily display it in the frontend
|
||||
file_src = path.join(src_dir, filename)
|
||||
(basename, ext) = path.splitext(filename)
|
||||
file_dst = path.join(dst_dir, basename) + ".png"
|
||||
file_dst = path.join(dst_dir, basename) + ".webp"
|
||||
|
||||
if path.exists(file_src) and not path.exists(file_dst):
|
||||
try:
|
||||
im = Image.open(file_src)
|
||||
im.save(file_dst)
|
||||
except Exception:
|
||||
self.logger.warning(f"Failed to convert {filename} to png")
|
||||
self.logger.warning(f"Failed to convert {filename} to webp")
|
||||
|
||||
def ConvertOldAssets(logger: Logger):
|
||||
"""
|
||||
Converts any previously-imported png files to webp.
|
||||
In the initial version of the userbox/avatar frontend support, png images were used, scraped via read.py.
|
||||
The amount of data pushed once a lot of stuff was unlocked was noticeable so the frontend now uses webp format
|
||||
for these assets. If any png files are present, convert them to webp now.
|
||||
"""
|
||||
# Find all pngs under the /img directory
|
||||
png_files = glob.glob(f'titles/chuni/img/**/*.png', recursive=True)
|
||||
if len(png_files) > 0:
|
||||
logger.info(f'Found {len(png_files)} old assets. Converting to webp... (may take a few minutes)')
|
||||
for img_png in png_files:
|
||||
img_webp = path.splitext(img_png)[0] + '.webp'
|
||||
try:
|
||||
# convert to webp
|
||||
im = Image.open(img_png)
|
||||
im.save(img_webp)
|
||||
# delete the original file
|
||||
remove(img_png)
|
||||
except Exception as e:
|
||||
logger.warning(f'Failed to convert {img_png} to webp')
|
||||
logger.info(f'Conversion complete')
|
||||
|
||||
def map_dds_images(self, image_dict: dict, dds_dir: str) -> None:
|
||||
for root, dirs, files in walk(dds_dir):
|
||||
|
@ -14,13 +14,13 @@
|
||||
<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 class="avatar-preview avatar-preview-platform" src="img/avatar-platform.webp">
|
||||
<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 class="avatar-preview avatar-preview-common" src="img/avatar-common.webp">
|
||||
<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="">
|
||||
@ -36,7 +36,7 @@
|
||||
<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;">
|
||||
<tr><td colspan=3 style="padding:8px 0px; text-align: center; position: relative;">
|
||||
<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>
|
||||
|
@ -18,7 +18,7 @@
|
||||
<img id="preview_nameplate" class="userbox userbox-nameplate" src="">
|
||||
|
||||
<!-- TEAM -->
|
||||
<img class="userbox userbox-teamframe" src="img/rank/team3.png">
|
||||
<img class="userbox userbox-teamframe" src="img/rank/team3.webp">
|
||||
<div class="userbox userbox-teamname">{{team_name}}</div>
|
||||
|
||||
<!-- TROPHY/TITLE -->
|
||||
@ -26,7 +26,7 @@
|
||||
<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">
|
||||
<img class="userbox userbox-ratingframe" src="img/rank/rating0.webp">
|
||||
<div class="userbox userbox-name">
|
||||
<span class="userbox-name-level-label">Lv.</span>
|
||||
{{ profile.level }} {{ profile.userName }}
|
||||
@ -37,7 +37,7 @@
|
||||
</div>
|
||||
|
||||
<!-- CHARACTER -->
|
||||
<img class="userbox userbox-charaframe" src="img/character-bg.png">
|
||||
<img class="userbox userbox-charaframe" src="img/character-bg.webp">
|
||||
<img id="preview_character" class="userbox userbox-chara" src="">
|
||||
</td></tr>
|
||||
|
||||
@ -50,7 +50,7 @@
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div></td></tr>
|
||||
|
||||
{% if cur_version >= 17 %} <!-- SubTrophies introduced in VERSE -->
|
||||
<tr><td>Trophy Sub 1:</td><td><div id="name_trophy">
|
||||
<select name="trophy-sub-1" id="trophy-sub-1" onclick="changeTrophySub1()" style="width:100%;">
|
||||
<option value="-1"></option>
|
||||
@ -68,7 +68,8 @@
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div></td></tr>
|
||||
|
||||
{% endif %}
|
||||
|
||||
<tr><td>Character:</td><td><div id="name_character"></div></td></tr>
|
||||
|
||||
<tr><td colspan=2 style="padding:8px 0px; text-align: center;">
|
||||
@ -180,10 +181,10 @@ function changeItem(type, id, name, img) {
|
||||
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 "rank" + x.toString() + ".webp";
|
||||
}
|
||||
}
|
||||
return "rank0.png"; // shouldnt ever happen
|
||||
return "rank0.webp"; // shouldnt ever happen
|
||||
}
|
||||
|
||||
function changeTrophy() {
|
||||
|