Custom kits widget
Some checks failed
E2E Tests / e2e (push) Has been cancelled
Tests and checks on push / run-checks-and-tests (push) Has been cancelled
Updates translation progress / update-translation-progress-issue (push) Has been cancelled

This commit is contained in:
Kalle
2026-09-21 17:27:50 +03:00
parent 807ab2eb96
commit 6048512a08
46 changed files with 383 additions and 9 deletions

View File

@@ -153,6 +153,54 @@
background-color: var(--color-bg-high);
}
.customKits {
display: flex;
flex-direction: column;
padding-inline: var(--s-2);
border: 1px solid var(--color-border);
border-radius: var(--radius-box);
background-color: var(--color-bg-high);
}
.customKit {
display: flex;
align-items: center;
gap: var(--s-2);
padding-block: var(--s-2);
& + & {
border-top: 1px solid var(--color-border);
}
}
.customKitWeapon {
display: flex;
flex-shrink: 0;
padding: var(--s-1-5);
border-radius: 100%;
background-color: var(--color-bg-higher);
}
.customKitName {
flex: 1;
min-width: 0;
font-size: var(--font-xs);
font-weight: var(--weight-semi);
}
.customKitParts {
display: flex;
flex-shrink: 0;
gap: var(--s-1);
}
.customKitPart {
display: flex;
padding: var(--s-1);
border-radius: 100%;
background-color: var(--color-bg-higher);
}
.weaponGrid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(40px, 1fr));

View File

@@ -6,7 +6,14 @@ import { BuildCard } from "~/components/BuildCard";
import { Divider } from "~/components/Divider";
import { SendouButton } from "~/components/elements/Button";
import { SendouPopover } from "~/components/elements/Popover";
import { Image, ModeImage, StageImage, WeaponImage } from "~/components/Image";
import {
Image,
ModeImage,
SpecialWeaponImage,
StageImage,
SubWeaponImage,
WeaponImage,
} from "~/components/Image";
import { BskyIcon } from "~/components/icons/Bsky";
import { DiscordIcon } from "~/components/icons/Discord";
import { TwitchIcon } from "~/components/icons/Twitch";
@@ -255,6 +262,10 @@ export function Widget({
return widget.data.length === 0 ? null : (
<WeaponPool weapons={widget.data} />
);
case "custom-kits":
return widget.data.length === 0 ? null : (
<CustomKits kits={widget.data} />
);
case "sens":
return typeof widget.data.motionSens !== "number" &&
typeof widget.data.stickSens !== "number" ? null : (
@@ -700,6 +711,44 @@ function WeaponPool({
);
}
function CustomKits({
kits,
}: {
kits: Extract<LoadedWidget, { id: "custom-kits" }>["data"];
}) {
const { t } = useTranslation(["weapons"]);
return (
<div className={styles.customKits}>
{kits.map((kit, i) => (
<div key={i} className={styles.customKit}>
<div className={styles.customKitWeapon}>
<WeaponImage
weaponSplId={kit.weaponSplId}
variant="build"
size={28}
/>
</div>
<div className={styles.customKitName}>
{t(`weapons:MAIN_${kit.weaponSplId}`)}
</div>
<div className={styles.customKitParts}>
<div className={styles.customKitPart}>
<SubWeaponImage subWeaponId={kit.subWeaponId} size={20} />
</div>
<div className={styles.customKitPart}>
<SpecialWeaponImage
specialWeaponId={kit.specialWeaponId}
size={20}
/>
</div>
</div>
</div>
))}
</div>
);
}
function PeakXpWeapon({
weaponSplId,
peakXp,

View File

@@ -87,6 +87,8 @@ function WidgetFormFields({ widgetId }: { widgetId: string }) {
return <FormField name="weaponSplId" />;
case "weapon-pool":
return <FormField name="weaponPool" />;
case "custom-kits":
return <FormField name="kits" />;
case "sens":
return <SensFields />;
case "art":

View File

@@ -316,6 +316,12 @@ export const WIDGET_LOADERS = {
isTenStar: tenStarWeaponSplIds.includes(weapon.id) ? 1 : 0,
}));
},
"custom-kits": async (
_userId: number,
settings: ExtractWidgetSettings<"custom-kits">,
) => {
return settings.kits;
},
"social-links": async (userId: number) => {
return UserRepository.findSocialLinksByUserId(userId);
},

View File

@@ -9,6 +9,7 @@ import {
bioMdSchema,
bioSchema,
countdownSchema,
customKitsSchema,
favoriteStageSchema,
gameBadgesSchema,
gameBadgesSmallSchema,
@@ -61,6 +62,12 @@ export const ALL_WIDGETS = {
schema: weaponPoolWidgetSchema,
defaultSettings: { weaponPool: [] },
}),
defineWidget({
id: "custom-kits",
slot: "side",
schema: customKitsSchema,
defaultSettings: { kits: [] },
}),
defineWidget({ id: "lfg-posts", slot: "main", navItem: "lfg" }),
defineWidget({
id: "sens",

View File

@@ -9,10 +9,13 @@ import {
badges,
customField,
datetime,
fieldset,
numberField,
select,
selectDynamic,
specialWeaponSelect,
stageSelect,
subWeaponSelect,
textArea,
textAreaOptional,
textField,
@@ -104,6 +107,22 @@ export const weaponPoolWidgetSchema = v.object({
}),
});
export const customKitsSchema = v.object({
kits: array({
label: "labels.customKits",
max: USER.CUSTOM_KITS_MAX,
field: fieldset({
fields: v.object({
weaponSplId: weaponSelect({ label: "labels.weapon" }),
subWeaponId: subWeaponSelect({ label: "labels.subWeapon" }),
specialWeaponId: specialWeaponSelect({
label: "labels.specialWeapon",
}),
}),
}),
}),
});
const CONTROLLERS = [
"s1-pro-con",
"s2-pro-con",
@@ -220,6 +239,7 @@ const WIDGET_FORM_SCHEMAS: Record<string, FormObjectSchema> = {
"peak-xp-unverified": peakXpUnverifiedSchema,
"peak-xp-weapon": peakXpWeaponSchema,
"weapon-pool": weaponPoolWidgetSchema,
"custom-kits": customKitsSchema,
sens: sensSchema,
art: artSchema,
links: linksSchema,

View File

@@ -215,7 +215,7 @@
.widgetSettings {
padding: var(--s-3);
margin-top: var(--s-1);
background-color: var(--color-bg-higher);
background-color: var(--color-bg);
border-radius: var(--radius-box);
}

View File

@@ -15,6 +15,7 @@ export const USER = {
GAME_BADGES_MAX: 8,
GAME_BADGES_SMALL_MAX: 4,
WEAPON_POOL_WIDGET_MAX: 7,
CUSTOM_KITS_MAX: 3,
COUNTDOWN_TITLE_MAX_LENGTH: 50,
MARKDOWN_WIDGET_MAX_LENGTH: 2000,
TIER_LIST_WIDGET_MAX_LENGTH: 4000,

View File

@@ -1,5 +1,10 @@
import * as React from "react";
import type { MainWeaponId, StageId } from "~/modules/in-game-lists/types";
import type {
MainWeaponId,
SpecialWeaponId,
StageId,
SubWeaponId,
} from "~/modules/in-game-lists/types";
import type { AnySyncSchema } from "~/utils/schema";
import { formRegistry } from "./fields";
import { ArrayFormField } from "./fields/ArrayFormField";
@@ -16,6 +21,7 @@ import {
} from "./fields/InputGroupFormField";
import { SelectFormField } from "./fields/SelectFormField";
import { StageSelectFormField } from "./fields/StageSelectFormField";
import { SubSpecialSelectFormField } from "./fields/SubSpecialSelectFormField";
import { SwitchFormField } from "./fields/SwitchFormField";
import { TeamSearchFormField } from "./fields/TeamSearchFormField";
import { TextareaFormField } from "./fields/TextareaFormField";
@@ -596,6 +602,24 @@ export function FormField({
);
}
if (
formField.type === "sub-weapon-select" ||
formField.type === "special-weapon-select"
) {
return (
<SubSpecialSelectFormField
{...commonProps}
{...formField}
weaponType={formField.type === "sub-weapon-select" ? "SUB" : "SPECIAL"}
disabled={isDisabled}
value={value as SubWeaponId | SpecialWeaponId | null}
onChange={
handleChange as (v: SubWeaponId | SpecialWeaponId | null) => void
}
/>
);
}
return (
<div>Unsupported form field type: {(formField as FormFieldType).type}</div>
);

View File

@@ -5,7 +5,12 @@ import {
inGameNameIsValid,
normalizeInGameName,
} from "~/features/user-page/in-game-name";
import type { MainWeaponId, StageId } from "~/modules/in-game-lists/types";
import type {
MainWeaponId,
SpecialWeaponId,
StageId,
SubWeaponId,
} from "~/modules/in-game-lists/types";
import { canonicalWeaponSplId } from "~/modules/in-game-lists/weapon-ids";
import type { AnySyncSchema, DayMonthYear } from "~/utils/schema";
import {
@@ -16,7 +21,9 @@ import {
preprocess,
safeNullableStringSchema,
safeStringSchema,
specialWeaponId,
stageId,
subWeaponId,
timeString,
weaponSplId,
} from "~/utils/schema";
@@ -993,7 +1000,7 @@ type WeaponSelectArgs = WithTypedTranslationKeys<
export function weaponSelect(
args: WeaponSelectArgs,
): v.GenericSchema<MainWeaponId> {
return register(weaponSplId, weaponSelectMetadata(args, true)) as never;
return register(weaponSplId, weaponSelectMetadata(args)) as never;
}
export function weaponSelectOptional(
@@ -1001,16 +1008,41 @@ export function weaponSelectOptional(
): v.OptionalSchema<v.GenericSchema<MainWeaponId>, undefined> {
return register(
v.optional(weaponSplId),
weaponSelectMetadata(args, false),
weaponSelectMetadata(args, "weapon-select", false),
) as never;
}
function weaponSelectMetadata(args: WeaponSelectArgs, required: boolean) {
export function subWeaponSelect(
args: WeaponSelectArgs,
): v.GenericSchema<SubWeaponId> {
return register(
subWeaponId,
weaponSelectMetadata(args, "sub-weapon-select"),
) as never;
}
export function specialWeaponSelect(
args: WeaponSelectArgs,
): v.GenericSchema<SpecialWeaponId> {
return register(
specialWeaponId,
weaponSelectMetadata(args, "special-weapon-select"),
) as never;
}
function weaponSelectMetadata(
args: WeaponSelectArgs,
type:
| "weapon-select"
| "sub-weapon-select"
| "special-weapon-select" = "weapon-select",
required = true,
) {
return {
...args,
label: prefixKey(args.label),
bottomText: prefixKey(args.bottomText),
type: "weapon-select" as const,
type,
initialValue: null,
required,
};

View File

@@ -0,0 +1,9 @@
.root {
width: 100%;
}
.option {
display: flex;
align-items: center;
gap: var(--s-2);
}

View File

@@ -0,0 +1,85 @@
import { useTranslation } from "react-i18next";
import { SendouSelect, SendouSelectItem } from "~/components/elements/Select";
import { Image } from "~/components/Image";
import type {
SpecialWeaponId,
SubWeaponId,
} from "~/modules/in-game-lists/types";
import {
specialWeaponIds,
subWeaponIds,
} from "~/modules/in-game-lists/weapon-ids";
import { specialWeaponImageUrl, subWeaponImageUrl } from "~/utils/urls";
import type { FormFieldProps } from "../types";
import { FormFieldMessages, useTranslatedTexts } from "./FormFieldWrapper";
import styles from "./SubSpecialSelectFormField.module.css";
type SubSpecialSelectFormFieldProps = FormFieldProps<
"sub-weapon-select" | "special-weapon-select"
> & {
weaponType: "SUB" | "SPECIAL";
value: SubWeaponId | SpecialWeaponId | null;
onChange: (value: SubWeaponId | SpecialWeaponId | null) => void;
disabled?: boolean;
};
export function SubSpecialSelectFormField({
name,
label,
bottomText,
error,
required,
weaponType,
value,
onChange,
onBlur,
disabled,
}: SubSpecialSelectFormFieldProps) {
const { t } = useTranslation(["weapons"]);
const { translatedLabel } = useTranslatedTexts({ label });
const options: Array<{ id: number; name: string; imgPath: string }> =
weaponType === "SUB"
? subWeaponIds.map((id) => ({
id,
name: t(`weapons:SUB_${id}`),
imgPath: subWeaponImageUrl(id),
}))
: specialWeaponIds.map((id) => ({
id,
name: t(`weapons:SPECIAL_${id}`),
imgPath: specialWeaponImageUrl(id),
}));
return (
<div className={styles.root}>
<SendouSelect
label={translatedLabel}
items={options}
selectedKey={value}
onSelectionChange={(key) => {
const newValue = key === null ? null : (Number(key) as SubWeaponId);
onChange(newValue);
onBlur?.(newValue);
}}
isRequired={required}
clearable={!required}
isDisabled={disabled}
>
{(option) => (
<SendouSelectItem
key={option.id}
id={option.id}
textValue={option.name}
>
<span className={styles.option}>
<Image path={option.imgPath} size={24} alt="" />
{option.name}
</span>
</SendouSelectItem>
)}
</SendouSelect>
<FormFieldMessages name={name} error={error} bottomText={bottomText} />
</div>
);
}

View File

@@ -182,6 +182,7 @@ interface FormFieldStageSelect<T extends string> extends FormFieldBase<T> {
required: boolean;
}
/** Shared by the main, sub and special weapon selects, which differ only in what they list. */
interface FormFieldWeaponSelect<T extends string> extends FormFieldBase<T> {
required: boolean;
}
@@ -213,7 +214,9 @@ export type FormField<V extends string = string> =
| FormFieldBadges<"badges">
| FormFieldTrophies<"trophies">
| FormFieldStageSelect<"stage-select">
| FormFieldWeaponSelect<"weapon-select">;
| FormFieldWeaponSelect<"weapon-select">
| FormFieldWeaponSelect<"sub-weapon-select">
| FormFieldWeaponSelect<"special-weapon-select">;
export type FormFieldProps<T extends FormField["type"]> = Omit<
Extract<FormField, { type: T }>,

View File

@@ -0,0 +1,8 @@
---
navItem: u
type: feature
---
New profile widget: Custom Kits
- Build weapon kits of your own design by mixing any main, sub and special weapon
- Show up to 3 of them on your profile

View File

@@ -252,6 +252,9 @@
"labels.favoriteStage": "",
"labels.peakXp": "",
"labels.weapon": "",
"labels.customKits": "",
"labels.subWeapon": "",
"labels.specialWeapon": "",
"labels.artSource": "",
"options.artSource.ALL": "",
"options.artSource.MADE-BY": "",

View File

@@ -38,6 +38,7 @@
"widget.x-rank-peaks": "",
"widget.builds": "",
"widget.weapon-pool": "",
"widget.custom-kits": "",
"widget.sens": "",
"widget.art": "",
"widget.commissions": "",
@@ -128,6 +129,7 @@
"widgets.description.x-rank-peaks": "",
"widgets.description.builds": "",
"widgets.description.weapon-pool": "",
"widgets.description.custom-kits": "",
"widgets.description.sens": "",
"widgets.description.art": "",
"widgets.description.commissions": "",

View File

@@ -252,6 +252,9 @@
"labels.favoriteStage": "",
"labels.peakXp": "",
"labels.weapon": "",
"labels.customKits": "",
"labels.subWeapon": "",
"labels.specialWeapon": "",
"labels.artSource": "",
"options.artSource.ALL": "",
"options.artSource.MADE-BY": "",

View File

@@ -38,6 +38,7 @@
"widget.x-rank-peaks": "",
"widget.builds": "",
"widget.weapon-pool": "",
"widget.custom-kits": "",
"widget.sens": "",
"widget.art": "",
"widget.commissions": "",
@@ -128,6 +129,7 @@
"widgets.description.x-rank-peaks": "",
"widgets.description.builds": "",
"widgets.description.weapon-pool": "",
"widgets.description.custom-kits": "",
"widgets.description.sens": "",
"widgets.description.art": "",
"widgets.description.commissions": "",

View File

@@ -252,6 +252,9 @@
"labels.favoriteStage": "Favorite Stage",
"labels.peakXp": "Peak XP",
"labels.weapon": "Weapon",
"labels.customKits": "Kits",
"labels.subWeapon": "Sub weapon",
"labels.specialWeapon": "Special weapon",
"labels.artSource": "Art source",
"options.artSource.ALL": "All",
"options.artSource.MADE-BY": "Made by me",

View File

@@ -38,6 +38,7 @@
"widget.x-rank-peaks": "X Rank Peaks Per Mode",
"widget.builds": "Builds",
"widget.weapon-pool": "Weapon Pool",
"widget.custom-kits": "Custom Kits",
"widget.sens": "Sensitivity",
"widget.art": "Art",
"widget.commissions": "Commissions",
@@ -128,6 +129,7 @@
"widgets.description.x-rank-peaks": "Show your peak X Rank placement for each mode, optionally select division",
"widgets.description.builds": "Display your 3 most recent builds",
"widgets.description.weapon-pool": "Display your match profile weapon pool or a custom list of weapons",
"widgets.description.custom-kits": "Show off weapon kits of your own design",
"widgets.description.sens": "Show your sensitivity settings and controller of choice",
"widgets.description.art": "Display your 3 most recent art pieces",
"widgets.description.commissions": "Show your commission status and details",

View File

@@ -252,6 +252,9 @@
"labels.favoriteStage": "Escenario favorito",
"labels.peakXp": "XP máximo",
"labels.weapon": "Arma",
"labels.customKits": "",
"labels.subWeapon": "",
"labels.specialWeapon": "",
"labels.artSource": "Fuente del arte",
"options.artSource.ALL": "Todos",
"options.artSource.MADE-BY": "Creado por mí",

View File

@@ -38,6 +38,7 @@
"widget.x-rank-peaks": "Picos de Rango X por modo",
"widget.builds": "Builds",
"widget.weapon-pool": "Grupo de armas",
"widget.custom-kits": "",
"widget.sens": "Sensibilidad",
"widget.art": "Arte",
"widget.commissions": "Comisiones",
@@ -129,6 +130,7 @@
"widgets.description.x-rank-peaks": "Muestra tu récord en Rango X de cada modo, con opción de elegir la división",
"widgets.description.builds": "Muestra tus 3 builds más recientes",
"widgets.description.weapon-pool": "",
"widgets.description.custom-kits": "",
"widgets.description.sens": "Muestra tus ajustes de sensibilidad y el mando que usas",
"widgets.description.art": "Muestra tus 3 obras de arte más recientes",
"widgets.description.commissions": "Muestra el estado y los detalles de tus comisiones",

View File

@@ -252,6 +252,9 @@
"labels.favoriteStage": "Escenario favorito",
"labels.peakXp": "XP máximo",
"labels.weapon": "Arma",
"labels.customKits": "",
"labels.subWeapon": "",
"labels.specialWeapon": "",
"labels.artSource": "Fuente del arte",
"options.artSource.ALL": "Todos",
"options.artSource.MADE-BY": "Creado por mí",

View File

@@ -38,6 +38,7 @@
"widget.x-rank-peaks": "Picos de Rango X por modo",
"widget.builds": "Builds",
"widget.weapon-pool": "Grupo de armas",
"widget.custom-kits": "",
"widget.sens": "Sensibilidad",
"widget.art": "Arte",
"widget.commissions": "Comisiones",
@@ -129,6 +130,7 @@
"widgets.description.x-rank-peaks": "Muestra tu récord en Rango X de cada modo, con opción de elegir la división",
"widgets.description.builds": "Muestra tus 3 builds más recientes",
"widgets.description.weapon-pool": "",
"widgets.description.custom-kits": "",
"widgets.description.sens": "Muestra tus ajustes de sensibilidad y el control que usas",
"widgets.description.art": "Muestra tus 3 obras de arte más recientes",
"widgets.description.commissions": "Muestra el estado y los detalles de tus comisiones",

View File

@@ -252,6 +252,9 @@
"labels.favoriteStage": "",
"labels.peakXp": "",
"labels.weapon": "",
"labels.customKits": "",
"labels.subWeapon": "",
"labels.specialWeapon": "",
"labels.artSource": "",
"options.artSource.ALL": "",
"options.artSource.MADE-BY": "",

View File

@@ -38,6 +38,7 @@
"widget.x-rank-peaks": "",
"widget.builds": "",
"widget.weapon-pool": "",
"widget.custom-kits": "",
"widget.sens": "",
"widget.art": "",
"widget.commissions": "",
@@ -129,6 +130,7 @@
"widgets.description.x-rank-peaks": "",
"widgets.description.builds": "",
"widgets.description.weapon-pool": "",
"widgets.description.custom-kits": "",
"widgets.description.sens": "",
"widgets.description.art": "",
"widgets.description.commissions": "",

View File

@@ -252,6 +252,9 @@
"labels.favoriteStage": "",
"labels.peakXp": "",
"labels.weapon": "",
"labels.customKits": "",
"labels.subWeapon": "",
"labels.specialWeapon": "",
"labels.artSource": "",
"options.artSource.ALL": "",
"options.artSource.MADE-BY": "",

View File

@@ -38,6 +38,7 @@
"widget.x-rank-peaks": "",
"widget.builds": "",
"widget.weapon-pool": "",
"widget.custom-kits": "",
"widget.sens": "",
"widget.art": "",
"widget.commissions": "",
@@ -129,6 +130,7 @@
"widgets.description.x-rank-peaks": "",
"widgets.description.builds": "",
"widgets.description.weapon-pool": "",
"widgets.description.custom-kits": "",
"widgets.description.sens": "",
"widgets.description.art": "",
"widgets.description.commissions": "",

View File

@@ -252,6 +252,9 @@
"labels.favoriteStage": "",
"labels.peakXp": "",
"labels.weapon": "",
"labels.customKits": "",
"labels.subWeapon": "",
"labels.specialWeapon": "",
"labels.artSource": "",
"options.artSource.ALL": "",
"options.artSource.MADE-BY": "",

View File

@@ -38,6 +38,7 @@
"widget.x-rank-peaks": "",
"widget.builds": "",
"widget.weapon-pool": "",
"widget.custom-kits": "",
"widget.sens": "",
"widget.art": "",
"widget.commissions": "",
@@ -129,6 +130,7 @@
"widgets.description.x-rank-peaks": "",
"widgets.description.builds": "",
"widgets.description.weapon-pool": "",
"widgets.description.custom-kits": "",
"widgets.description.sens": "",
"widgets.description.art": "",
"widgets.description.commissions": "",

View File

@@ -252,6 +252,9 @@
"labels.favoriteStage": "",
"labels.peakXp": "",
"labels.weapon": "",
"labels.customKits": "",
"labels.subWeapon": "",
"labels.specialWeapon": "",
"labels.artSource": "",
"options.artSource.ALL": "",
"options.artSource.MADE-BY": "",

View File

@@ -38,6 +38,7 @@
"widget.x-rank-peaks": "",
"widget.builds": "",
"widget.weapon-pool": "",
"widget.custom-kits": "",
"widget.sens": "",
"widget.art": "",
"widget.commissions": "",
@@ -129,6 +130,7 @@
"widgets.description.x-rank-peaks": "",
"widgets.description.builds": "",
"widgets.description.weapon-pool": "",
"widgets.description.custom-kits": "",
"widgets.description.sens": "",
"widgets.description.art": "",
"widgets.description.commissions": "",

View File

@@ -252,6 +252,9 @@
"labels.favoriteStage": "",
"labels.peakXp": "",
"labels.weapon": "",
"labels.customKits": "",
"labels.subWeapon": "",
"labels.specialWeapon": "",
"labels.artSource": "",
"options.artSource.ALL": "",
"options.artSource.MADE-BY": "",

View File

@@ -38,6 +38,7 @@
"widget.x-rank-peaks": "",
"widget.builds": "",
"widget.weapon-pool": "",
"widget.custom-kits": "",
"widget.sens": "",
"widget.art": "",
"widget.commissions": "",
@@ -126,6 +127,7 @@
"widgets.description.x-rank-peaks": "",
"widgets.description.builds": "",
"widgets.description.weapon-pool": "",
"widgets.description.custom-kits": "",
"widgets.description.sens": "",
"widgets.description.art": "",
"widgets.description.commissions": "",

View File

@@ -252,6 +252,9 @@
"labels.favoriteStage": "",
"labels.peakXp": "",
"labels.weapon": "",
"labels.customKits": "",
"labels.subWeapon": "",
"labels.specialWeapon": "",
"labels.artSource": "",
"options.artSource.ALL": "",
"options.artSource.MADE-BY": "",

View File

@@ -38,6 +38,7 @@
"widget.x-rank-peaks": "",
"widget.builds": "",
"widget.weapon-pool": "",
"widget.custom-kits": "",
"widget.sens": "",
"widget.art": "",
"widget.commissions": "",
@@ -126,6 +127,7 @@
"widgets.description.x-rank-peaks": "",
"widgets.description.builds": "",
"widgets.description.weapon-pool": "",
"widgets.description.custom-kits": "",
"widgets.description.sens": "",
"widgets.description.art": "",
"widgets.description.commissions": "",

View File

@@ -252,6 +252,9 @@
"labels.favoriteStage": "",
"labels.peakXp": "",
"labels.weapon": "",
"labels.customKits": "",
"labels.subWeapon": "",
"labels.specialWeapon": "",
"labels.artSource": "",
"options.artSource.ALL": "",
"options.artSource.MADE-BY": "",

View File

@@ -38,6 +38,7 @@
"widget.x-rank-peaks": "",
"widget.builds": "",
"widget.weapon-pool": "",
"widget.custom-kits": "",
"widget.sens": "",
"widget.art": "",
"widget.commissions": "",
@@ -128,6 +129,7 @@
"widgets.description.x-rank-peaks": "",
"widgets.description.builds": "",
"widgets.description.weapon-pool": "",
"widgets.description.custom-kits": "",
"widgets.description.sens": "",
"widgets.description.art": "",
"widgets.description.commissions": "",

View File

@@ -252,6 +252,9 @@
"labels.favoriteStage": "",
"labels.peakXp": "",
"labels.weapon": "",
"labels.customKits": "",
"labels.subWeapon": "",
"labels.specialWeapon": "",
"labels.artSource": "",
"options.artSource.ALL": "",
"options.artSource.MADE-BY": "",

View File

@@ -38,6 +38,7 @@
"widget.x-rank-peaks": "",
"widget.builds": "",
"widget.weapon-pool": "",
"widget.custom-kits": "",
"widget.sens": "",
"widget.art": "",
"widget.commissions": "",
@@ -130,6 +131,7 @@
"widgets.description.x-rank-peaks": "",
"widgets.description.builds": "",
"widgets.description.weapon-pool": "",
"widgets.description.custom-kits": "",
"widgets.description.sens": "",
"widgets.description.art": "",
"widgets.description.commissions": "",

View File

@@ -252,6 +252,9 @@
"labels.favoriteStage": "",
"labels.peakXp": "",
"labels.weapon": "",
"labels.customKits": "",
"labels.subWeapon": "",
"labels.specialWeapon": "",
"labels.artSource": "",
"options.artSource.ALL": "",
"options.artSource.MADE-BY": "",

View File

@@ -38,6 +38,7 @@
"widget.x-rank-peaks": "",
"widget.builds": "",
"widget.weapon-pool": "",
"widget.custom-kits": "",
"widget.sens": "",
"widget.art": "",
"widget.commissions": "",
@@ -129,6 +130,7 @@
"widgets.description.x-rank-peaks": "",
"widgets.description.builds": "",
"widgets.description.weapon-pool": "",
"widgets.description.custom-kits": "",
"widgets.description.sens": "",
"widgets.description.art": "",
"widgets.description.commissions": "",

View File

@@ -252,6 +252,9 @@
"labels.favoriteStage": "",
"labels.peakXp": "",
"labels.weapon": "",
"labels.customKits": "",
"labels.subWeapon": "",
"labels.specialWeapon": "",
"labels.artSource": "",
"options.artSource.ALL": "",
"options.artSource.MADE-BY": "",

View File

@@ -38,6 +38,7 @@
"widget.x-rank-peaks": "",
"widget.builds": "",
"widget.weapon-pool": "",
"widget.custom-kits": "",
"widget.sens": "",
"widget.art": "",
"widget.commissions": "",
@@ -130,6 +131,7 @@
"widgets.description.x-rank-peaks": "",
"widgets.description.builds": "",
"widgets.description.weapon-pool": "",
"widgets.description.custom-kits": "",
"widgets.description.sens": "",
"widgets.description.art": "",
"widgets.description.commissions": "",

View File

@@ -252,6 +252,9 @@
"labels.favoriteStage": "喜爱的场地",
"labels.peakXp": "最高 XP",
"labels.weapon": "武器",
"labels.customKits": "",
"labels.subWeapon": "",
"labels.specialWeapon": "",
"labels.artSource": "作品来源",
"options.artSource.ALL": "全部",
"options.artSource.MADE-BY": "由我创作",

View File

@@ -38,6 +38,7 @@
"widget.x-rank-peaks": "各模式最高X战力",
"widget.builds": "配装",
"widget.weapon-pool": "武器池",
"widget.custom-kits": "",
"widget.sens": "灵敏度设置",
"widget.art": "插画",
"widget.commissions": "约稿",
@@ -126,6 +127,7 @@
"widgets.description.x-rank-peaks": "展示您各个模式的最高X战力,可自由选择组别",
"widgets.description.builds": "展示您最近的 3 套配装",
"widgets.description.weapon-pool": "",
"widgets.description.custom-kits": "",
"widgets.description.sens": "展示您的灵敏度设置和首选控制器",
"widgets.description.art": "展示您最近发布的 3 件插画作品",
"widgets.description.commissions": "展示您的约稿开放状态与详情",