mirror of
https://github.com/zkldi/Tachi.git
synced 2026-09-26 08:57:58 +03:00
feat: just a bunch of cleanup
- no deduping, ui improvements, general fixups
This commit is contained in:
@@ -98,9 +98,7 @@ function FervidexForm({
|
||||
<br />
|
||||
<span className="text-warning">
|
||||
Warning: You should always import from your network first. Statically
|
||||
imported scores have the bare minimum data (Lamp + EX Score + BP), but due
|
||||
to score de-duplication rules, future network imports cannot append more
|
||||
data (Timestamps, Graphs, etc.) to these scores.
|
||||
imported scores have the bare minimum data (No timestamps!).
|
||||
</span>
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
|
||||
@@ -88,9 +88,7 @@ function KsHookSV6CForm({
|
||||
<br />
|
||||
<span className="text-warning">
|
||||
Warning: You should always import from your network first. Statically
|
||||
imported scores have the bare minimum data (Lamp + EX Score + BP), but due
|
||||
to score de-duplication rules, future network imports cannot append more
|
||||
data (Timestamps, Graphs, etc.) to these scores.
|
||||
imported scores have the bare minimum data (No timestamps!).
|
||||
</span>
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
|
||||
@@ -259,8 +259,8 @@ function ImageForm({ reqUser }: { reqUser: UserDocument }) {
|
||||
return (
|
||||
<div>
|
||||
<Alert variant="danger">
|
||||
Remember Rule 6! Do not set inappropriate stuff as your avatar/banner. If you have
|
||||
to ask, the answer is probably no.
|
||||
Do not set inappropriate stuff as your avatar/banner. If you have to ask, the answer
|
||||
is probably no.
|
||||
</Alert>
|
||||
<Form.Group>
|
||||
<Form.Label>Profile Picture</Form.Label>
|
||||
|
||||
@@ -211,38 +211,37 @@ function PreferencesForm({ reqUser, game, playtype }: UGPT) {
|
||||
</Form.Group>
|
||||
)}
|
||||
<Form.Group>
|
||||
<Form.Label>Preferred Score Info</Form.Label>
|
||||
<Form.Label>Preferred Folder Info</Form.Label>
|
||||
<Form.Control
|
||||
as="select"
|
||||
id="scoreBucket"
|
||||
value={formik.values.scoreBucket}
|
||||
onChange={formik.handleChange}
|
||||
>
|
||||
<option value="grade">Score</option>
|
||||
<option value="lamp">Lamp</option>
|
||||
<option value="grade">Grades</option>
|
||||
<option value="lamp">Lamps</option>
|
||||
</Form.Control>
|
||||
<Form.Text className="text-muted">
|
||||
What should {TachiConfig.name} prefer to show you about scores?
|
||||
<br />
|
||||
Note: This will only affect defaults, such as what graph is shown in the folder
|
||||
breakdown. You can still view all the same stats!
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
<Form.Group>
|
||||
<Form.Label>Preferred Ranking</Form.Label>
|
||||
<Form.Control
|
||||
as="select"
|
||||
id="preferredRanking"
|
||||
value={formik.values.preferredRanking}
|
||||
onChange={formik.handleChange}
|
||||
>
|
||||
<option value="global">Global Rankings</option>
|
||||
<option value="rival">Rival Rankings</option>
|
||||
</Form.Control>
|
||||
<Form.Text className="text-muted">
|
||||
What should {TachiConfig.name} default to when showing your score rankings?
|
||||
What should {TachiConfig.name} default to showing you about folders?
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
{settings.rivals.length !== 0 && (
|
||||
<Form.Group>
|
||||
<Form.Label>Preferred Ranking</Form.Label>
|
||||
<Form.Control
|
||||
as="select"
|
||||
id="preferredRanking"
|
||||
value={formik.values.preferredRanking}
|
||||
onChange={formik.handleChange}
|
||||
>
|
||||
<option value="global">Global Rankings</option>
|
||||
<option value="rival">Rival Rankings</option>
|
||||
</Form.Control>
|
||||
<Form.Text className="text-muted">
|
||||
What should {TachiConfig.name} default to when showing your score rankings?
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
)}
|
||||
<Form.Group>
|
||||
<Form.Label>Preferred Table</Form.Label>
|
||||
<Form.Control
|
||||
@@ -262,7 +261,7 @@ function PreferencesForm({ reqUser, game, playtype }: UGPT) {
|
||||
))}
|
||||
</Form.Control>
|
||||
<Form.Text className="text-muted">
|
||||
What folders would you like to see when you go to the folders page by default?
|
||||
What folders would you like to see by default?
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
{game === "iidx" && (
|
||||
@@ -572,13 +571,13 @@ function ManageAccount({ reqUser, game, playtype }: UGPT) {
|
||||
<Col xs={12}>
|
||||
<h4>Delete Score</h4>
|
||||
If you have an invalid score, you can delete it by going to that score and clicking
|
||||
"Manage Score".
|
||||
"Delete Score".
|
||||
</Col>
|
||||
<Col xs={12} className="mt-8">
|
||||
<h4>Undo Import</h4>
|
||||
If you messed up an import, you can undo it by going to{" "}
|
||||
<Link to={`/u/${reqUser.username}/imports`}>your imports page</Link> and click
|
||||
"Manage Import".
|
||||
"Revert Import".
|
||||
</Col>
|
||||
<Col xs={12} className="mt-8">
|
||||
<h3>Completely Wipe Profile</h3>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { Fragment } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { GetGameConfig } from "tachi-common";
|
||||
|
||||
export function Breadcrumbs({ items }: { items: string[] }) {
|
||||
const url = location.pathname;
|
||||
@@ -12,17 +13,28 @@ export function Breadcrumbs({ items }: { items: string[] }) {
|
||||
<Link className="opacity-75 hover-opacity-100" to="/">
|
||||
<i className="flaticon2-shelter text-white icon-1x" />
|
||||
</Link>
|
||||
{items.map((name, index) => (
|
||||
<Fragment key={index}>
|
||||
<span className="label label-dot label-sm bg-white opacity-75 mx-3" />
|
||||
<Link
|
||||
className="text-white text-hover-white opacity-75 hover-opacity-100"
|
||||
to={`/${parts.slice(0, index + 1).join("/")}`}
|
||||
>
|
||||
{name}
|
||||
</Link>
|
||||
</Fragment>
|
||||
))}
|
||||
{items.map((name, index) => {
|
||||
// Skip playtype in the breadcrumbs if the game only has one playtype.
|
||||
// @hack
|
||||
// this only works if the game has one playtype called "Single".
|
||||
// Some games (like pop'n) have one playtype, but it's called
|
||||
// 9B. They'll just have to cope.
|
||||
if (items[index - 2] === "Games" && name === "Single") {
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment key={index}>
|
||||
<span className="label label-dot label-sm bg-white opacity-75 mx-3" />
|
||||
<Link
|
||||
className="text-white text-hover-white opacity-75 hover-opacity-100"
|
||||
to={`/${parts.slice(0, index + 1).join("/")}`}
|
||||
>
|
||||
{name}
|
||||
</Link>
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -129,6 +129,10 @@ function SessionScoreStatBreakdown({
|
||||
Record<Grades[IDStrings], { score: ScoreDocument; scoreInfo: SessionScoreInfo }[]>
|
||||
> = {};
|
||||
|
||||
// dedupe grades/lamps for repeat plays
|
||||
const isBestGradeThisSession = new Map();
|
||||
const isBestLampThisSession = new Map();
|
||||
|
||||
for (const scoreInfo of sessionData.session.scoreInfo) {
|
||||
const score = scoreMap.get(scoreInfo.scoreID);
|
||||
|
||||
@@ -139,15 +143,51 @@ function SessionScoreStatBreakdown({
|
||||
continue;
|
||||
}
|
||||
|
||||
const g = isBestGradeThisSession.get(score.chartID);
|
||||
const l = isBestLampThisSession.get(score.chartID);
|
||||
|
||||
// if we haven't saw this chart before
|
||||
// or the score we have saw for this chart before had a worse grade
|
||||
if (!g || (g.scoreData.gradeIndex && g < score.scoreData.gradeIndex)) {
|
||||
isBestGradeThisSession.set(score.chartID, score);
|
||||
}
|
||||
|
||||
// same but for lamp
|
||||
if (!l || (l.scoreData.lampIndex && l < score.scoreData.lampIndex)) {
|
||||
isBestLampThisSession.set(score.chartID, score);
|
||||
}
|
||||
}
|
||||
|
||||
for (const scoreInfo of sessionData.session.scoreInfo) {
|
||||
const score = scoreMap.get(scoreInfo.scoreID);
|
||||
|
||||
if (!score) {
|
||||
console.error(
|
||||
`Session score info contains scoreID ${scoreInfo.scoreID}, but no score exists?`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const wasBestLamp = isBestLampThisSession.get(score.chartID)?.scoreID === score.scoreID;
|
||||
const wasBestGrade =
|
||||
isBestGradeThisSession.get(score.chartID)?.scoreID === score.scoreID;
|
||||
|
||||
if (scoreInfo.isNewScore) {
|
||||
PartialArrayRecordAssign(newLamps, score.scoreData.lamp, { score, scoreInfo });
|
||||
PartialArrayRecordAssign(newGrades, score.scoreData.grade, { score, scoreInfo });
|
||||
if (wasBestLamp) {
|
||||
PartialArrayRecordAssign(newLamps, score.scoreData.lamp, { score, scoreInfo });
|
||||
}
|
||||
if (wasBestGrade) {
|
||||
PartialArrayRecordAssign(newGrades, score.scoreData.grade, {
|
||||
score,
|
||||
scoreInfo,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (scoreInfo.lampDelta > 0) {
|
||||
if (scoreInfo.lampDelta > 0 && wasBestLamp) {
|
||||
PartialArrayRecordAssign(newLamps, score.scoreData.lamp, { score, scoreInfo });
|
||||
}
|
||||
|
||||
if (scoreInfo.gradeDelta > 0) {
|
||||
if (scoreInfo.gradeDelta > 0 && wasBestLamp) {
|
||||
PartialArrayRecordAssign(newGrades, score.scoreData.grade, {
|
||||
score,
|
||||
scoreInfo,
|
||||
|
||||
@@ -17,7 +17,12 @@ export default function SelectableRanking({
|
||||
} & ZTableTHProps) {
|
||||
const { settings } = useLUGPTSettings();
|
||||
|
||||
if (rankingViewMode === "both-if-self" || rankingViewMode === "global-no-switch" || !settings) {
|
||||
if (
|
||||
rankingViewMode === "both-if-self" ||
|
||||
rankingViewMode === "global-no-switch" ||
|
||||
!settings ||
|
||||
settings.rivals.length === 0
|
||||
) {
|
||||
return (
|
||||
<SortableTH
|
||||
changeSort={changeSort}
|
||||
|
||||
@@ -53,7 +53,7 @@ export default function ImportDropdown({ data }: { data: ImportDataset[0] }) {
|
||||
{currentUser?.id === data.userID && (
|
||||
<SelectButton setValue={setView} value={view} id="manage">
|
||||
<Icon type="trash" />
|
||||
Manage Import
|
||||
Revert Import
|
||||
</SelectButton>
|
||||
)}
|
||||
<HasDevModeOn>
|
||||
|
||||
@@ -166,18 +166,18 @@ export default function ScoreDropdown<I extends IDStrings = IDStrings>({
|
||||
Goals & Quests{targetData && ` (${targetData.goals.length})`}
|
||||
</SelectButton>
|
||||
)}
|
||||
{currentUser?.id === user.id && (
|
||||
<SelectButton setValue={setView} value={view} id="manage">
|
||||
<Icon type="trash" />
|
||||
Manage Score
|
||||
</SelectButton>
|
||||
)}
|
||||
{currentUser && (
|
||||
<SelectButton setValue={setView} value={view} id="rivals">
|
||||
<Icon type="users" />
|
||||
Rivals
|
||||
</SelectButton>
|
||||
)}
|
||||
{currentUser?.id === user.id && (
|
||||
<SelectButton setValue={setView} value={view} id="manage">
|
||||
<Icon type="trash" />
|
||||
Delete Score
|
||||
</SelectButton>
|
||||
)}
|
||||
<HasDevModeOn>
|
||||
<SelectButton setValue={setView} value={view} id="debug">
|
||||
<Icon type="bug" />
|
||||
|
||||
@@ -41,10 +41,7 @@ function Row({ data }: { data: ImportDataset[0] }) {
|
||||
return (
|
||||
<DropdownRow dropdown={<ImportDropdown data={data} />}>
|
||||
<td>
|
||||
<Link
|
||||
className="gentle-link"
|
||||
to={`/u/${data.__related.user.username}`}
|
||||
>
|
||||
<Link className="gentle-link" to={`/u/${data.__related.user.username}`}>
|
||||
{data.__related.user.username}
|
||||
</Link>
|
||||
</td>
|
||||
|
||||
@@ -19,7 +19,7 @@ export default function ImportViewerOptions({
|
||||
<>
|
||||
<Card header="Options">
|
||||
<Select
|
||||
name="Made with intent (i.e. not by an automated hook or IR)"
|
||||
name="Made with user intent (i.e. not an automatic upload by an IR or hook)"
|
||||
allowNull
|
||||
unselectedName="Either"
|
||||
setValue={setUserIntent}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Game, GetGamePTConfig } from "tachi-common";
|
||||
import { Playtype } from "tachi-common";
|
||||
import { Game, GetGamePTConfig, Playtype } from "tachi-common";
|
||||
import useLUGPTSettings from "./useLUGPTSettings";
|
||||
|
||||
export function useBucket(game: Game, playtype: Playtype) {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tachi",
|
||||
"version": "2.2.0",
|
||||
"version": "2.3.0-dev",
|
||||
"private": true,
|
||||
"description": "The root of the Tachi monorepo. Contains common utils like linters.",
|
||||
"scripts": {
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
REDIRECT_URI: "https://example.com",
|
||||
},
|
||||
ARC_AUTH_TOKEN: "unused",
|
||||
OUR_URL: "https://127.0.0.1:8080/",
|
||||
OUR_URL: "https://127.0.0.1:8080",
|
||||
CLIENT_DEV_SERVER: "http://127.0.0.1:3000",
|
||||
ENABLE_SERVER_HTTPS: true,
|
||||
OPTIONS_ALWAYS_SUCCEEDS: true,
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tachi-server",
|
||||
"version": "2.2.0",
|
||||
"version": "2.3.0-dev",
|
||||
"description": "A score tracking server.",
|
||||
"main": "./js/main.js",
|
||||
"files": [
|
||||
|
||||
@@ -190,7 +190,20 @@ const err = p(config, {
|
||||
MAX_QUEST_SUBSCRIPTIONS: p.optional(p.isPositiveInteger),
|
||||
MAX_FOLLOWING_AMOUNT: p.optional(p.isPositiveInteger),
|
||||
MAX_RIVALS: p.optional(p.isPositiveInteger),
|
||||
OUR_URL: "string",
|
||||
OUR_URL: (self) => {
|
||||
if (typeof self !== "string") {
|
||||
return "Expected a string.";
|
||||
}
|
||||
|
||||
if (self.endsWith("/")) {
|
||||
return `OUR_URL should not end with a trailing slash. Use ${self.substring(
|
||||
0,
|
||||
self.length - 1
|
||||
)} instead.`;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
INVITE_CODE_CONFIG: p.optional({
|
||||
BATCH_SIZE: p.isPositiveInteger,
|
||||
INVITE_CAP: p.isPositiveInteger,
|
||||
|
||||
Reference in New Issue
Block a user