diff --git a/changelog.md b/changelog.md
index 07f17c8..8148a9d 100644
--- a/changelog.md
+++ b/changelog.md
@@ -5,6 +5,10 @@ Documenting updates to ARTEMiS, to be updated every time the master branch is pu
### CHUNITHM
+ CHUNITHM LUMINOUS support
+## 20240616
+### DIVA
++ Working frontend with name and level strings edit and playlog
+
## 20240530
### DIVA
+ Fix reader for when dificulty is not a int
diff --git a/titles/diva/__init__.py b/titles/diva/__init__.py
index d298ba2..7bfa2cc 100644
--- a/titles/diva/__init__.py
+++ b/titles/diva/__init__.py
@@ -2,8 +2,10 @@ from titles.diva.index import DivaServlet
from titles.diva.const import DivaConstants
from titles.diva.database import DivaData
from titles.diva.read import DivaReader
+from .frontend import DivaFrontend
index = DivaServlet
database = DivaData
reader = DivaReader
+frontend = DivaFrontend
game_codes = [DivaConstants.GAME_CODE]
diff --git a/titles/diva/frontend.py b/titles/diva/frontend.py
new file mode 100644
index 0000000..cc5c332
--- /dev/null
+++ b/titles/diva/frontend.py
@@ -0,0 +1,182 @@
+from typing import List
+from starlette.routing import Route, Mount
+from starlette.requests import Request
+from starlette.responses import Response, RedirectResponse
+from os import path
+import yaml
+import jinja2
+
+from core.frontend import FE_Base, UserSession
+from core.config import CoreConfig
+from .database import DivaData
+from .config import DivaConfig
+from .const import DivaConstants
+
+class DivaFrontend(FE_Base):
+ def __init__(
+ self, cfg: CoreConfig, environment: jinja2.Environment, cfg_dir: str
+ ) -> None:
+ super().__init__(cfg, environment)
+ self.data = DivaData(cfg)
+ self.game_cfg = DivaConfig()
+ if path.exists(f"{cfg_dir}/{DivaConstants.CONFIG_NAME}"):
+ self.game_cfg.update(
+ yaml.safe_load(open(f"{cfg_dir}/{DivaConstants.CONFIG_NAME}"))
+ )
+ self.nav_name = "diva"
+
+ def get_routes(self) -> List[Route]:
+ return [
+ Route("/", self.render_GET, methods=['GET']),
+ Mount("/playlog", routes=[
+ Route("/", self.render_GET_playlog, methods=['GET']),
+ Route("/{index}", self.render_GET_playlog, methods=['GET']),
+ ]),
+ Route("/update.name", self.update_name, methods=['POST']),
+ Route("/update.lv", self.update_lv, methods=['POST']),
+ ]
+
+ async def render_GET(self, request: Request) -> bytes:
+ template = self.environment.get_template(
+ "titles/diva/templates/diva_index.jinja"
+ )
+ usr_sesh = self.validate_session(request)
+ if not usr_sesh:
+ usr_sesh = UserSession()
+
+ if usr_sesh.user_id > 0:
+ profile = await self.data.profile.get_profile(usr_sesh.user_id, 1)
+
+ resp = 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=usr_sesh.user_id,
+ profile=profile
+ ), media_type="text/html; charset=utf-8")
+ return resp
+ else:
+ return RedirectResponse("/gate")
+
+ async def render_GET_playlog(self, request: Request) -> bytes:
+ template = self.environment.get_template(
+ "titles/diva/templates/diva_playlog.jinja"
+ )
+ usr_sesh = self.validate_session(request)
+ if not usr_sesh:
+ usr_sesh = UserSession()
+
+ if usr_sesh.user_id > 0:
+ path_index = request.path_params.get("index")
+ if not path_index or int(path_index) < 1:
+ index = 0
+ else:
+ index = int(path_index) - 1 # 0 and 1 are 1st page
+ user_id = usr_sesh.user_id
+ playlog_count = await self.data.score.get_user_playlogs_count(user_id)
+ if playlog_count < index * 20 :
+ 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),
+ score_count=0
+ ), media_type="text/html; charset=utf-8")
+ playlog = await self.data.score.get_playlogs(user_id, index, 20) #Maybe change to the playlog instead of direct scores
+ playlog_with_title = []
+ for record in playlog:
+ song = await self.data.static.get_music_chart(record[2], record[3], record[4])
+ if song:
+ title = song.title
+ vocaloid_arranger = song.vocaloid_arranger
+ else:
+ title = "Unknown"
+ vocaloid_arranger = "Unknown"
+ playlog_with_title.append({
+ "raw": record,
+ "title": title,
+ "vocaloid_arranger": vocaloid_arranger
+ })
+ 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=usr_sesh.user_id,
+ playlog=playlog_with_title,
+ playlog_count=playlog_count
+ ), media_type="text/html; charset=utf-8")
+ else:
+ return RedirectResponse("/gate/", 300)
+
+ async def update_name(self, request: Request) -> Response:
+ usr_sesh = self.validate_session(request)
+ if not usr_sesh:
+ return RedirectResponse("/gate")
+
+ form_data = await request.form()
+ new_name: str = form_data.get("new_name")
+ new_name_full = ""
+
+ if not new_name:
+ return RedirectResponse("/gate/?e=4", 303)
+
+ if len(new_name) > 8:
+ return RedirectResponse("/gate/?e=8", 303)
+
+ for x in new_name: # FIXME: This will let some invalid characters through atm
+ o = ord(x)
+ try:
+ if o == 0x20:
+ new_name_full += chr(0x3000)
+ elif o < 0x7F and o > 0x20:
+ new_name_full += chr(o + 0xFEE0)
+ elif o <= 0x7F:
+ self.logger.warn(f"Invalid ascii character {o:02X}")
+ return RedirectResponse("/gate/?e=4", 303)
+ else:
+ new_name_full += x
+
+ except Exception as e:
+ self.logger.error(f"Something went wrong parsing character {o:04X} - {e}")
+ return RedirectResponse("/gate/?e=4", 303)
+
+ if not await self.data.profile.update_profile(usr_sesh.user_id, player_name=new_name_full):
+ return RedirectResponse("/gate/?e=999", 303)
+
+ return RedirectResponse("/game/diva", 303)
+
+ async def update_lv(self, request: Request) -> Response:
+ usr_sesh = self.validate_session(request)
+ if not usr_sesh:
+ return RedirectResponse("/gate")
+
+ form_data = await request.form()
+ new_lv: str = form_data.get("new_lv")
+ new_lv_full = ""
+
+ if not new_lv:
+ return RedirectResponse("/gate/?e=4", 303)
+
+ if len(new_lv) > 8:
+ return RedirectResponse("/gate/?e=8", 303)
+
+ for x in new_lv: # FIXME: This will let some invalid characters through atm
+ o = ord(x)
+ try:
+ if o == 0x20:
+ new_lv_full += chr(0x3000)
+ elif o < 0x7F and o > 0x20:
+ new_lv_full += chr(o + 0xFEE0)
+ elif o <= 0x7F:
+ self.logger.warn(f"Invalid ascii character {o:02X}")
+ return RedirectResponse("/gate/?e=4", 303)
+ else:
+ new_lv_full += x
+
+ except Exception as e:
+ self.logger.error(f"Something went wrong parsing character {o:04X} - {e}")
+ return RedirectResponse("/gate/?e=4", 303)
+
+ if not await self.data.profile.update_profile(usr_sesh.user_id, lv_str=new_lv_full):
+ return RedirectResponse("/gate/?e=999", 303)
+
+ return RedirectResponse("/game/diva", 303)
diff --git a/titles/diva/schema/profile.py b/titles/diva/schema/profile.py
index f3d00ae..10d3b51 100644
--- a/titles/diva/schema/profile.py
+++ b/titles/diva/schema/profile.py
@@ -90,7 +90,7 @@ class DivaProfileData(BaseData):
return None
return result.lastrowid
- async def update_profile(self, aime_id: int, **profile_args) -> None:
+ async def update_profile(self, aime_id: int, **profile_args) -> bool:
"""
Given an aime_id update the profile corresponding to the arguments
which are the diva_profile Columns
@@ -102,7 +102,9 @@ class DivaProfileData(BaseData):
self.logger.error(
f"update_profile: failed to update profile! profile: {aime_id}"
)
- return None
+ return False
+
+ return True
async def get_profile(self, aime_id: int, version: int) -> Optional[List[Dict]]:
"""
diff --git a/titles/diva/schema/score.py b/titles/diva/schema/score.py
index e802a41..ce89f74 100644
--- a/titles/diva/schema/score.py
+++ b/titles/diva/schema/score.py
@@ -239,3 +239,23 @@ class DivaScoreData(BaseData):
if result is None:
return None
return result.fetchall()
+
+ async def get_playlogs(self, aime_id: int, idx: int = 0, limit: int = 0) -> Optional[Row]:
+ sql = select(playlog).where(playlog.c.user == aime_id).order_by(playlog.c.date_scored.desc())
+
+ if limit:
+ sql = sql.limit(limit)
+ if idx:
+ sql = sql.offset(idx)
+
+ result = await self.execute(sql)
+ if result:
+ return result.fetchall()
+
+ async def get_user_playlogs_count(self, aime_id: int) -> Optional[int]:
+ sql = select(func.count()).where(playlog.c.user == aime_id)
+ result = await self.execute(sql)
+ if result is None:
+ self.logger.warning(f"aimu_id {aime_id} has no scores ")
+ return None
+ return result.scalar()
diff --git a/titles/diva/templates/css/diva_style.css b/titles/diva/templates/css/diva_style.css
new file mode 100644
index 0000000..672db0f
--- /dev/null
+++ b/titles/diva/templates/css/diva_style.css
@@ -0,0 +1,195 @@
+.diva-header {
+ text-align: center;
+}
+
+ul.diva-navi {
+ list-style-type: none;
+ padding: 0;
+ overflow: hidden;
+ background-color: #333;
+ text-align: center;
+ display: inline-block;
+}
+
+ul.diva-navi li {
+ display: inline-block;
+}
+
+ul.diva-navi li a {
+ display: block;
+ color: white;
+ text-align: center;
+ padding: 14px 16px;
+ text-decoration: none;
+}
+
+ul.diva-navi li a:hover:not(.active) {
+ background-color: #111;
+}
+
+ul.diva-navi li a.active {
+ background-color: #4CAF50;
+}
+
+ul.diva-navi li.right {
+ float: right;
+}
+
+@media screen and (max-width: 600px) {
+
+ ul.diva-navi li.right,
+ ul.diva-navi li {
+ float: none;
+ display: block;
+ text-align: center;
+ }
+}
+
+table {
+ border-collapse: collapse;
+ border-spacing: 0;
+ border-collapse: separate;
+ overflow: hidden;
+ background-color: #555555;
+
+}
+
+th, td {
+ text-align: left;
+ border: none;
+
+}
+
+th {
+ color: white;
+}
+
+.table-rowdistinct tr:nth-child(even) {
+ background-color: #303030;
+}
+
+.table-rowdistinct tr:nth-child(odd) {
+ background-color: #555555;
+}
+
+caption {
+ text-align: center;
+ color: white;
+ font-size: 18px;
+ font-weight: bold;
+}
+
+.table-large {
+ margin: 16px;
+}
+
+.table-large th,
+.table-large td {
+ padding: 8px;
+}
+
+.table-small {
+ width: 100%;
+ margin: 4px;
+}
+
+.table-small th,
+.table-small td {
+ padding: 2px;
+}
+
+.bg-card {
+ background-color: #555555;
+}
+
+.card-hover {
+ transition: all 0.2s ease-in-out;
+}
+
+.card-hover:hover {
+ transform: scale(1.02);
+}
+
+.basic {
+ color: #28a745;
+ font-weight: bold;
+}
+
+.hard {
+ color: #ffc107;
+
+ font-weight: bold;
+}
+
+.expert {
+ color: #dc3545;
+ font-weight: bold;
+}
+
+.master {
+ color: #dd09e8;
+ font-weight: bold;
+}
+
+.ultimate {
+ color: #000000;
+ font-weight: bold;
+}
+
+.score {
+ color: #ffffff;
+ font-weight: bold;
+}
+
+.rainbow {
+ 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;
+ font-weight: bold;
+}
+
+.platinum {
+ color: #FFFF00;
+ font-weight: bold;
+}
+
+.gold {
+ color: #FFFF00;
+ font-weight: bold;
+}
+
+.scrolling-text {
+ overflow: hidden;
+}
+
+.scrolling-text p {
+ white-space: nowrap;
+ display: inline-block;
+
+}
+
+.scrolling-text h6 {
+ white-space: nowrap;
+ display: inline-block;
+
+}
+
+.scrolling-text h5 {
+ white-space: nowrap;
+ display: inline-block;
+
+}
+
+.scrolling {
+ animation: scroll 10s linear infinite;
+}
+
+@keyframes scroll {
+ 0% {
+ transform: translateX(100%);
+ }
+ 100% {
+ transform: translateX(-100%);
+ }
+}
\ No newline at end of file
diff --git a/titles/diva/templates/diva_header.jinja b/titles/diva/templates/diva_header.jinja
new file mode 100644
index 0000000..b92379a
--- /dev/null
+++ b/titles/diva/templates/diva_header.jinja
@@ -0,0 +1,17 @@
+
+
\ No newline at end of file
diff --git a/titles/diva/templates/diva_index.jinja b/titles/diva/templates/diva_index.jinja
new file mode 100644
index 0000000..c2f0888
--- /dev/null
+++ b/titles/diva/templates/diva_index.jinja
@@ -0,0 +1,111 @@
+{% extends "core/templates/index.jinja" %}
+{% block content %}
+
+
+ {% include 'titles/diva/templates/diva_header.jinja' %}
+ {% if profile is defined and profile is not none and profile|length > 0 %}
+
+
+
+
+ OVERVIEW
+
+ Player name:
+ {{ profile[3] }}
+
+ Edit
+
+ Level string:
+ {{ profile[4] }}
+
+ Edit
+
+
+
+ Lvl:
+ {{ profile[5] }}
+
+
+
+
+
+
+ Lvl points:
+ {{ profile[6] }}
+
+
+
+
+
+
+ Vocaloid points:
+ {{ profile[7] }}
+
+
+
+
+
+
+
+
+
+ {% if error is defined %}
+ {% include "core/templates/widgets/err_banner.jinja" %}
+ {% endif %}
+ {% elif sesh is defined and sesh is not none and sesh.user_id > 0 %}
+ No profile information found for this account.
+ {% else %}
+ Login to view profile information.
+ {% endif %}
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/titles/diva/templates/diva_playlog.jinja b/titles/diva/templates/diva_playlog.jinja
new file mode 100644
index 0000000..c5a5618
--- /dev/null
+++ b/titles/diva/templates/diva_playlog.jinja
@@ -0,0 +1,169 @@
+{% extends "core/templates/index.jinja" %}
+{% block content %}
+
+
+ {% include 'titles/diva/templates/diva_header.jinja' %}
+ {% if playlog is defined and playlog is not none %}
+
+
Score counts: {{ playlog_count }}
+ {% set difficultyName = ['easy', 'normal', 'hard', 'extreme', 'extra extreme'] %}
+ {% set clearState = ['MISSxTAKE', 'STANDARD', 'GREAT', 'EXELLENT', 'PERFECT'] %}
+ {% for record in playlog %}
+
+
+
+
+
+
+
{{ record.raw.score }}
+ {{ record.raw.atn_pnt / 100 }}%
+ {{ difficultyName[record.raw.difficulty] }}
+
+
+
+
+ COOL
+ {{ record.raw.cool }}
+
+
+ FINE
+ {{ record.raw.fine }}
+
+
+ SAFE
+ {{ record.raw.safe }}
+
+
+ SAD
+ {{ record.raw.sad }}
+
+
+ WORST
+ {{ record.raw.worst }}
+
+
+
+
+
{{ record.raw.max_combo }}
+ {% if record.raw.clr_kind == -1 %}
+ {{ clearState[0] }}
+ {% elif record.raw.clr_kind == 2 %}
+ {{ clearState[1] }}
+ {% elif record.raw.clr_kind == 3 %}
+ {{ clearState[2] }}
+ {% elif record.raw.clr_kind == 4 %}
+ {{ clearState[3] }}
+ {% elif record.raw.clr_kind == 5 %}
+ {{ clearState[4] }}
+ {% endif %}
+ {% if record.raw.clr_kind == -1 %}
+ NOT CLEAR
+ {% else %}
+ CLEAR
+ {% endif %}
+
+
+
+
+
+ {% endfor %}
+
+ {% set playlog_pages = playlog_count // 20 + 1 %}
+ {% elif sesh is defined and sesh is not none and sesh.user_id > 0 %}
+ No Score information found for this account.
+ {% else %}
+ Login to view profile information.
+ {% endif %}
+
+
+
+
+{% endblock content %}
\ No newline at end of file