mirror of
https://github.com/Melledy/Grasscutter.git
synced 2024-11-27 07:34:27 +00:00
Implement avatars/characters page
Handle edge-cases for avatar image rendering
This commit is contained in:
parent
1f27f83616
commit
757d682cd6
@ -40,6 +40,13 @@ export function getAvatars(): AvatarDump {
|
|||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches and lists all the avatars in the file.
|
||||||
|
*/
|
||||||
|
export function listAvatars(): Avatar[] {
|
||||||
|
return Object.values(getAvatars());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches and casts all items in the file.
|
* Fetches and casts all items in the file.
|
||||||
*/
|
*/
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
export type Page = "Home" | "Commands";
|
export type Page = "Home" | "Commands" | "Avatars";
|
||||||
|
|
||||||
export type Command = {
|
export type Command = {
|
||||||
name: string[];
|
name: string[];
|
||||||
|
@ -38,10 +38,6 @@ body {
|
|||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
div {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
|
31
src/handbook/src/css/pages/AvatarsPage.scss
Normal file
31
src/handbook/src/css/pages/AvatarsPage.scss
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
.AvatarsPage {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
background-color: var(--background-color);
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.AvatarsPage_Title {
|
||||||
|
max-width: 275px;
|
||||||
|
max-height: 60px;
|
||||||
|
|
||||||
|
font-size: 48px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.AvatarsPage_List {
|
||||||
|
display: grid;
|
||||||
|
gap: 15px 15px;
|
||||||
|
|
||||||
|
grid-template-columns: repeat(15, 100px);
|
||||||
|
|
||||||
|
margin-bottom: 28px;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
@ -6,6 +6,10 @@
|
|||||||
background-color: var(--background-color);
|
background-color: var(--background-color);
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
||||||
|
div {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.HomePage_Top {
|
.HomePage_Top {
|
||||||
|
@ -2,12 +2,12 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
background-color: var(--legendary-color);
|
|
||||||
|
|
||||||
max-width: 100px;
|
max-width: 100px;
|
||||||
max-height: 125px;
|
max-height: 150px;
|
||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
|
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -20,15 +20,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.Character_Label {
|
.Character_Label {
|
||||||
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background-color: var(--secondary-color);
|
background-color: var(--secondary-color);
|
||||||
|
|
||||||
max-width: 100px;
|
max-width: 100px;
|
||||||
height: 25px;
|
height: 50px;
|
||||||
|
|
||||||
p {
|
p {
|
||||||
color: var(--text-primary-color);
|
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
|
text-align: center;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
28
src/handbook/src/ui/pages/AvatarsPage.tsx
Normal file
28
src/handbook/src/ui/pages/AvatarsPage.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import Character from "@app/ui/widgets/Character";
|
||||||
|
|
||||||
|
import { listAvatars } from "@backend/data";
|
||||||
|
|
||||||
|
import "@css/pages/AvatarsPage.scss";
|
||||||
|
|
||||||
|
class AvatarsPage extends React.PureComponent {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className={"AvatarsPage"}>
|
||||||
|
<h1 className={"AvatarsPage_Title"}>Characters</h1>
|
||||||
|
|
||||||
|
<div className={"AvatarsPage_List"}>
|
||||||
|
{
|
||||||
|
listAvatars().map(avatar => (
|
||||||
|
avatar.id > 11000000 ? undefined :
|
||||||
|
<Character key={avatar.id} data={avatar} />
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AvatarsPage;
|
@ -16,6 +16,7 @@ class CommandsPage extends React.PureComponent {
|
|||||||
{
|
{
|
||||||
listCommands().map(command => (
|
listCommands().map(command => (
|
||||||
<Card
|
<Card
|
||||||
|
key={command.name[0]}
|
||||||
title={command.name[0]}
|
title={command.name[0]}
|
||||||
alternate={command.name.length == 1 ? undefined :
|
alternate={command.name.length == 1 ? undefined :
|
||||||
`(aka /${command.name.slice(1).join(", /")})`}
|
`(aka /${command.name.slice(1).join(", /")})`}
|
||||||
|
@ -19,7 +19,7 @@ class HomePage extends React.Component<any, any> {
|
|||||||
|
|
||||||
<div className={"HomePage_Buttons"}>
|
<div className={"HomePage_Buttons"}>
|
||||||
<HomeButton name={"Commands"} anchor={"Commands"} />
|
<HomeButton name={"Commands"} anchor={"Commands"} />
|
||||||
<HomeButton name={"Characters"} anchor={"Home"} />
|
<HomeButton name={"Characters"} anchor={"Avatars"} />
|
||||||
<HomeButton name={"Items"} anchor={"Home"} />
|
<HomeButton name={"Items"} anchor={"Home"} />
|
||||||
<HomeButton name={"Entities"} anchor={"Home"} />
|
<HomeButton name={"Entities"} anchor={"Home"} />
|
||||||
<HomeButton name={"Scenes"} anchor={"Home"} />
|
<HomeButton name={"Scenes"} anchor={"Home"} />
|
||||||
|
@ -2,11 +2,12 @@ import React from "react";
|
|||||||
|
|
||||||
import HomePage from "@pages/HomePage";
|
import HomePage from "@pages/HomePage";
|
||||||
import CommandsPage from "@pages/CommandsPage";
|
import CommandsPage from "@pages/CommandsPage";
|
||||||
|
import AvatarsPage from "@pages/AvatarsPage";
|
||||||
|
|
||||||
import type { Page } from "@backend/types";
|
import type { Page } from "@backend/types";
|
||||||
|
import { addNavListener, removeNavListener } from "@backend/events";
|
||||||
|
|
||||||
import "@css/views/Content.scss";
|
import "@css/views/Content.scss";
|
||||||
import { addNavListener, removeNavListener } from "@backend/events";
|
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
initial?: Page | null;
|
initial?: Page | null;
|
||||||
@ -48,6 +49,7 @@ class Content extends React.Component<IProps, IState> {
|
|||||||
default: return undefined;
|
default: return undefined;
|
||||||
case "Home": return <HomePage />;
|
case "Home": return <HomePage />;
|
||||||
case "Commands": return <CommandsPage />;
|
case "Commands": return <CommandsPage />;
|
||||||
|
case "Avatars": return <AvatarsPage />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,7 @@ class SideBar extends React.Component<any, any> {
|
|||||||
|
|
||||||
<div className={"SideBar_Buttons"}>
|
<div className={"SideBar_Buttons"}>
|
||||||
<SideBarButton name={"Commands"} anchor={"Commands"} />
|
<SideBarButton name={"Commands"} anchor={"Commands"} />
|
||||||
<SideBarButton name={"Characters"} anchor={"Home"} />
|
<SideBarButton name={"Characters"} anchor={"Avatars"} />
|
||||||
<SideBarButton name={"Items"} anchor={"Home"} />
|
<SideBarButton name={"Items"} anchor={"Home"} />
|
||||||
<SideBarButton name={"Entities"} anchor={"Home"} />
|
<SideBarButton name={"Entities"} anchor={"Home"} />
|
||||||
<SideBarButton name={"Scenes"} anchor={"Home"} />
|
<SideBarButton name={"Scenes"} anchor={"Home"} />
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
|
import type { Avatar } from "@backend/types";
|
||||||
|
import { colorFor } from "@app/utils";
|
||||||
|
|
||||||
import "@css/widgets/Character.scss";
|
import "@css/widgets/Character.scss";
|
||||||
|
|
||||||
// Image base URL: https://paimon.moe/images/characters/(name).png
|
// Image base URL: https://paimon.moe/images/characters/(name).png
|
||||||
@ -9,13 +12,30 @@ import "@css/widgets/Character.scss";
|
|||||||
* Example: Hu Tao -> hu_tao
|
* Example: Hu Tao -> hu_tao
|
||||||
*
|
*
|
||||||
* @param name The character's name.
|
* @param name The character's name.
|
||||||
|
* @param id The character's ID.
|
||||||
*/
|
*/
|
||||||
function formatName(name: string): string {
|
function formatName(name: string, id: number): string {
|
||||||
|
// Check if a different name is used for the character.
|
||||||
|
if (refSwitch[id]) name = refSwitch[id];
|
||||||
return name.toLowerCase().replace(" ", "_");
|
return name.toLowerCase().replace(" ", "_");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ignored = [
|
||||||
|
10000001 // Kate
|
||||||
|
];
|
||||||
|
|
||||||
|
const refSwitch: { [key: number]: string } = {
|
||||||
|
10000005: "traveler_anemo",
|
||||||
|
10000007: "traveler_geo",
|
||||||
|
};
|
||||||
|
|
||||||
|
const nameSwitch: { [key: number]: string } = {
|
||||||
|
10000005: "Lumine",
|
||||||
|
10000007: "Aether",
|
||||||
|
};
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
name: string; // paimon.moe reference name.
|
data: Avatar;
|
||||||
}
|
}
|
||||||
|
|
||||||
class Character extends React.PureComponent<IProps> {
|
class Character extends React.PureComponent<IProps> {
|
||||||
@ -24,16 +44,26 @@ class Character extends React.PureComponent<IProps> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const { name, quality, id } = this.props.data;
|
||||||
|
const qualityColor = colorFor(quality);
|
||||||
|
|
||||||
|
// Check if the avatar is blacklisted.
|
||||||
|
if (ignored.includes(id))
|
||||||
|
return undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={"Character"}>
|
<div
|
||||||
|
className={"Character"}
|
||||||
|
style={{ backgroundColor: `var(${qualityColor})` }}
|
||||||
|
>
|
||||||
<img
|
<img
|
||||||
className={"Character_Icon"}
|
className={"Character_Icon"}
|
||||||
alt={this.props.name}
|
alt={name}
|
||||||
src={`https://paimon.moe/images/characters/${formatName(this.props.name)}.png`}
|
src={`https://paimon.moe/images/characters/${formatName(name, id)}.png`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className={"Character_Label"}>
|
<div className={"Character_Label"}>
|
||||||
<p>{this.props.name}</p>
|
<p>{nameSwitch[id] ?? name}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
17
src/handbook/src/utils.ts
Normal file
17
src/handbook/src/utils.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { Quality } from "@backend/types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the name of the CSS variable for the quality.
|
||||||
|
*
|
||||||
|
* @param quality The quality of the item.
|
||||||
|
*/
|
||||||
|
export function colorFor(quality: Quality): string {
|
||||||
|
switch (quality) {
|
||||||
|
default: return "--legendary-color";
|
||||||
|
case "EPIC": return "--epic-color";
|
||||||
|
case "RARE": return "--rare-color";
|
||||||
|
case "UNCOMMON": return "--uncommon-color";
|
||||||
|
case "COMMON": return "--common-color";
|
||||||
|
case "UNKNOWN": return "--unknown-color";
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user