mirror of
https://github.com/zkldi/Tachi.git
synced 2026-10-10 16:28:17 +03:00
feat: things (#1098)
* feat: clean up this disgusting filter directive thing * fix: THIS FIX WAS THIS EASY FOR THIS LONG??? * fix: version is obsolete * feat: much prettier... * feat: big import warnings * feat: sour times
This commit is contained in:
+1
-12
@@ -2,23 +2,12 @@ const MAJOR = 2;
|
||||
const MINOR = 2;
|
||||
const PATCH = 0;
|
||||
|
||||
const TOKYO_SHOEGAZER_MOONDIVER = [
|
||||
"Into the Deep Sky",
|
||||
"Your Relief",
|
||||
"In Full Bloom",
|
||||
"Night Dance",
|
||||
"Bedtime Story",
|
||||
"Moondive",
|
||||
"Stay White",
|
||||
];
|
||||
|
||||
export const VERSION_INFO = {
|
||||
major: MAJOR,
|
||||
minor: MINOR,
|
||||
patch: PATCH,
|
||||
name: TOKYO_SHOEGAZER_MOONDIVER[MINOR],
|
||||
};
|
||||
|
||||
export const VERSION_STR = `${MAJOR}.${MINOR}.${PATCH}`;
|
||||
|
||||
export const VERSION_PRETTY = `v${VERSION_STR} (${VERSION_INFO.name})`;
|
||||
export const VERSION_PRETTY = `v${VERSION_STR}`;
|
||||
|
||||
@@ -23,6 +23,8 @@ import {
|
||||
UserDocument,
|
||||
} from "tachi-common";
|
||||
import Col from "react-bootstrap/Col";
|
||||
import { Alert } from "react-bootstrap";
|
||||
import Icon from "components/util/Icon";
|
||||
|
||||
export default function ImportPage({ user }: { user: UserDocument }) {
|
||||
useSetSubheader(["Import Scores"]);
|
||||
@@ -369,6 +371,11 @@ function ImportInfoDisplayer({ game }: { game: Game }) {
|
||||
<div className="text-center mb-4">
|
||||
<h1>{gameConfig.name}</h1>
|
||||
</div>
|
||||
<Row xs={{ cols: 1 }}>
|
||||
<Col xs={12}>
|
||||
<InputAlert game={game} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row xs={{ cols: 1 }} lg={{ cols: 2 }}>
|
||||
{Content}
|
||||
</Row>
|
||||
@@ -376,6 +383,84 @@ function ImportInfoDisplayer({ game }: { game: Game }) {
|
||||
);
|
||||
}
|
||||
|
||||
function InputAlert({ game }: { game: Game }) {
|
||||
function doit(game: Game): JSX.Element | null {
|
||||
switch (game) {
|
||||
case "jubeat":
|
||||
case "maimai":
|
||||
case "iidx":
|
||||
case "museca":
|
||||
case "chunithm":
|
||||
case "gitadora":
|
||||
case "maimaidx":
|
||||
case "popn":
|
||||
case "wacca":
|
||||
case "ongeki":
|
||||
return (
|
||||
<>
|
||||
<strong>Scores must be achieved on an arcade-size controller!</strong>
|
||||
<br />
|
||||
Playing on other input devices (like a keyboard) will get you in trouble.
|
||||
</>
|
||||
);
|
||||
case "sdvx":
|
||||
return (
|
||||
<>
|
||||
<strong>Scores must be achieved on an arcade-size controller!</strong>
|
||||
<br />
|
||||
Playing on other input devices (like a keyboard) will get you in trouble.
|
||||
<br />
|
||||
<strong>
|
||||
A Pocket-Voltex DOES NOT count as an <i>ARCADE SIZE</i> controller!
|
||||
</strong>
|
||||
</>
|
||||
);
|
||||
|
||||
case "usc":
|
||||
return (
|
||||
<>
|
||||
<strong>Please use the right leaderboard for your controller!</strong>
|
||||
<br />
|
||||
<br />
|
||||
<strong>Arcade-sized controllers</strong> should go on the controller
|
||||
leaderboards.
|
||||
<br />
|
||||
<strong>Keyboards and anything else</strong> go on the Keyboard/Other
|
||||
leaderboards.
|
||||
<br />
|
||||
<br />
|
||||
<strong>
|
||||
A Pocket-Voltex DOES NOT count as an <i>ARCADE SIZE</i> controller!
|
||||
</strong>
|
||||
</>
|
||||
);
|
||||
|
||||
case "itg":
|
||||
case "arcaea":
|
||||
case "bms":
|
||||
case "pms":
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const ct = doit(game);
|
||||
|
||||
if (!ct) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Alert variant="warning">
|
||||
<div className="d-flex" style={{ alignItems: "center", gap: "1rem", fontSize: "2rem" }}>
|
||||
<div style={{ fontSize: "4rem" }}>
|
||||
<Icon type="exclamation-triangle" />
|
||||
</div>
|
||||
<div className="text-body">{ct}</div>
|
||||
</div>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
function ImportTypeInfoCard({
|
||||
importType,
|
||||
}: {
|
||||
|
||||
@@ -33,10 +33,17 @@ export default function USCIRPage() {
|
||||
Fill out IR Base URL with <code>{ToServerURL("/ir/usc/Keyboard")}</code>.
|
||||
</li>
|
||||
<Alert variant="danger" className="mt-4">
|
||||
You <b>MUST</b> select the right IR! These leaderboards are separated because it
|
||||
is unfair to compare the two input devices.
|
||||
You <b>MUST</b> select the right IR! These leaderboards are separated because
|
||||
it's unfair to compare controller and keyboard scores.
|
||||
<br />
|
||||
<b>DELIBERATELY SELECTING THE WRONG IR WILL RESULT IN A PERMANENT BAN!</b>
|
||||
Ignoring this will get you in trouble, please don't!
|
||||
<br />
|
||||
<br />
|
||||
<strong>Arcade-sized controllers</strong> should go on the controller
|
||||
leaderboards.
|
||||
<br />
|
||||
<strong>Keyboards and anything else</strong> go on the Keyboard/Other
|
||||
leaderboards.
|
||||
</Alert>
|
||||
<Divider />
|
||||
<li>
|
||||
@@ -46,12 +53,6 @@ export default function USCIRPage() {
|
||||
</li>
|
||||
<li>That's it! Your scores will now automatically upload to the server.</li>
|
||||
</ol>
|
||||
<Divider />
|
||||
<Muted>
|
||||
Note: If you submit a score on a chart that {TachiConfig.NAME} doesn't recognise,
|
||||
you'll need to wait until atleast 2 other players submit scores for that chart
|
||||
before it'll show up. This is to combat accidental IR spam.
|
||||
</Muted>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { CreateChartMap, CreateScoreIDMap, CreateSongMap } from "util/data";
|
||||
import useSetSubheader from "components/layout/header/useSetSubheader";
|
||||
import Card from "components/layout/page/Card";
|
||||
import SessionOverview from "components/sessions/SessionOverview";
|
||||
import ScoreTable from "components/tables/scores/ScoreTable";
|
||||
import ApiError from "components/util/ApiError";
|
||||
import DebugContent from "components/util/DebugContent";
|
||||
import Divider from "components/util/Divider";
|
||||
@@ -250,27 +249,8 @@ function SessionPage({ data, game, playtype }: UGPT & { data: SessionReturns })
|
||||
scoreDataset={scoreDataset}
|
||||
sessionData={sessionData}
|
||||
setSessionData={setSessionData}
|
||||
reqUser={data.user}
|
||||
/>
|
||||
<Col xs={12}>
|
||||
<Divider />
|
||||
|
||||
<ScoreTable
|
||||
dataset={scoreDataset}
|
||||
game={session.game}
|
||||
playtype={session.playtype}
|
||||
onScoreUpdate={(sc) => {
|
||||
const newScores = [
|
||||
...sessionData.scores.filter((e) => e.scoreID !== sc.scoreID),
|
||||
sc,
|
||||
];
|
||||
|
||||
setSessionData({
|
||||
...sessionData,
|
||||
scores: newScores,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
{settings?.preferences.developerMode && (
|
||||
<Col xs={12}>
|
||||
<Divider />
|
||||
|
||||
@@ -7,7 +7,6 @@ import DropdownIndicatorCell from "components/tables/cells/DropdownIndicatorCell
|
||||
import DropdownRow from "components/tables/components/DropdownRow";
|
||||
import MiniTable from "components/tables/components/MiniTable";
|
||||
import ApiError from "components/util/ApiError";
|
||||
import Divider from "components/util/Divider";
|
||||
import Loading from "components/util/Loading";
|
||||
import Muted from "components/util/Muted";
|
||||
import useApiQuery from "components/util/query/useApiQuery";
|
||||
@@ -48,7 +47,6 @@ export default function SessionFolderRaiseBreakdown({
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
|
||||
const [selectedTable, setSelectedTable] = useState<"LOADING" | null | TableDocument>("LOADING");
|
||||
const [shouldLimit, setShouldLimit] = useState(true);
|
||||
|
||||
const { data, error } = useApiQuery<Array<SessionFolderRaises>>(
|
||||
`/sessions/${sessionData.session.sessionID}/folder-raises`
|
||||
@@ -139,16 +137,14 @@ export default function SessionFolderRaiseBreakdown({
|
||||
}
|
||||
|
||||
return (
|
||||
<Col xs={12}>
|
||||
<Divider />
|
||||
|
||||
<>
|
||||
<h1 className="w-100 text-center">Folder Raises</h1>
|
||||
|
||||
{filteredTables.length > 1 && (
|
||||
{filteredTables.length > 1 && selectedTable !== null && (
|
||||
<div className="w-100">
|
||||
<Select
|
||||
name="Table"
|
||||
value={selectedTable?.tableID ?? null}
|
||||
value={selectedTable.tableID}
|
||||
setValue={(t) => setSelectedTable(tableData.find((e) => e.tableID === t)!)}
|
||||
>
|
||||
{filteredTables.map((e) => (
|
||||
@@ -161,7 +157,7 @@ export default function SessionFolderRaiseBreakdown({
|
||||
)}
|
||||
|
||||
<Row>
|
||||
{folders.slice(0, shouldLimit ? 6 : Infinity).map((folder, i) => (
|
||||
{folders.map((folder, i) => (
|
||||
<Col key={i} xs={12} lg={6} xl={4}>
|
||||
<Card
|
||||
className="my-4"
|
||||
@@ -195,18 +191,7 @@ export default function SessionFolderRaiseBreakdown({
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
{shouldLimit && folders.length > 6 && (
|
||||
<Row>
|
||||
<div
|
||||
className="w-100 text-center text-hover-primary"
|
||||
onClick={() => setShouldLimit(false)}
|
||||
>
|
||||
{folders.length - 6} {folders.length > 6 + 1 ? "folders" : "folder"} hidden.
|
||||
View more?
|
||||
</div>
|
||||
</Row>
|
||||
)}
|
||||
</Col>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,25 +3,44 @@ import { FormatDuration } from "util/time";
|
||||
import Card from "components/layout/page/Card";
|
||||
import ScoreTable from "components/tables/scores/ScoreTable";
|
||||
import Divider from "components/util/Divider";
|
||||
import React from "react";
|
||||
import { Col, Row } from "react-bootstrap";
|
||||
import { AnySessionRatingAlg, GetGamePTConfig, ScoreDocument, SessionDocument } from "tachi-common";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Badge, Col, Row } from "react-bootstrap";
|
||||
import {
|
||||
AnySessionRatingAlg,
|
||||
GetGamePTConfig,
|
||||
GetGPTString,
|
||||
PBScoreDocument,
|
||||
ScoreDocument,
|
||||
SessionDocument,
|
||||
UserDocument,
|
||||
} from "tachi-common";
|
||||
import { SessionReturns } from "types/api-returns";
|
||||
import { ScoreDataset } from "types/tables";
|
||||
import { SetState } from "types/react";
|
||||
import SessionRaiseBreakdown from "./SessionRaiseBreakdown";
|
||||
import SelectLinkButton from "components/util/SelectLinkButton";
|
||||
import Icon from "components/util/Icon";
|
||||
import useUGPTBase from "components/util/useUGPTBase";
|
||||
import { Route, Switch } from "react-router-dom";
|
||||
import useApiQuery from "components/util/query/useApiQuery";
|
||||
import { GPT_CLIENT_IMPLEMENTATIONS } from "lib/game-implementations";
|
||||
import SessionFolderRaiseBreakdown from "./SessionFolderRaiseBreakdown";
|
||||
import SessionRaiseBreakdown from "./SessionRaiseBreakdown";
|
||||
|
||||
export default function SessionOverview({
|
||||
sessionData,
|
||||
setSessionData,
|
||||
scoreDataset,
|
||||
reqUser,
|
||||
}: {
|
||||
sessionData: SessionReturns;
|
||||
setSessionData: SetState<SessionReturns>;
|
||||
scoreDataset: ScoreDataset;
|
||||
reqUser: UserDocument;
|
||||
}) {
|
||||
const { scores, session } = sessionData;
|
||||
const gptConfig = GetGamePTConfig(session.game, session.playtype);
|
||||
const gptImpl = GPT_CLIENT_IMPLEMENTATIONS[GetGPTString(session.game, session.playtype)];
|
||||
const MAX_SCORES = gptImpl.sessionImportantScoreCount;
|
||||
|
||||
const setScores = (scores: ScoreDocument[]) => {
|
||||
setSessionData({
|
||||
@@ -30,6 +49,41 @@ export default function SessionOverview({
|
||||
});
|
||||
};
|
||||
|
||||
const [importantScores, setImportantScores] = useState<null | ScoreDataset>(null);
|
||||
|
||||
const { data } = useApiQuery<Array<{ pbs: Array<PBScoreDocument> }>>(
|
||||
Object.keys(gptConfig.profileRatingAlgs).map(
|
||||
(e) =>
|
||||
`/users/${reqUser.id}/games/${session.game}/${session.playtype}/pbs/best?alg=${e}`
|
||||
)
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!data) {
|
||||
setImportantScores(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const important: Array<string> = [];
|
||||
|
||||
for (const d of data) {
|
||||
for (const pb of d.pbs.slice(0, MAX_SCORES)) {
|
||||
const didFind = pb.composedFrom.find((e) => session.scoreIDs.includes(e.scoreID));
|
||||
|
||||
if (didFind) {
|
||||
important.push(didFind.scoreID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setImportantScores(scoreDataset.filter((e) => important.includes(e.scoreID)));
|
||||
}, [data]);
|
||||
|
||||
const base = useUGPTBase({ game: session.game, playtype: session.playtype, reqUser });
|
||||
const baseUrl = `${base}/sessions/${session.sessionID}`;
|
||||
|
||||
const highlightedScores = scoreDataset.filter((e) => e.highlight);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Row xs={{ cols: 1 }} lg={{ cols: 3 }} className="p-0 row-gap-4">
|
||||
@@ -40,28 +94,107 @@ export default function SessionOverview({
|
||||
/>
|
||||
<StatThing name="Highlights" value={scores.filter((e) => e.highlight).length} />
|
||||
</Row>
|
||||
<SessionFolderRaiseBreakdown sessionData={sessionData} />
|
||||
<Col xs={12}>
|
||||
|
||||
<div className="p-4">
|
||||
<RatingsOverview session={session} />
|
||||
</div>
|
||||
|
||||
<Row xs={12}>
|
||||
<Divider />
|
||||
|
||||
<Card header="Raise Breakdown">
|
||||
<Row>
|
||||
<SessionRaiseBreakdown sessionData={sessionData} setScores={setScores} />
|
||||
</Row>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col className="text-center">
|
||||
<div className="btn-group d-flex justify-content-center mb-4">
|
||||
<SelectLinkButton to={`${baseUrl}`}>
|
||||
<Icon type="chart-line" /> Raises
|
||||
</SelectLinkButton>
|
||||
<SelectLinkButton to={`${baseUrl}/folders`}>
|
||||
<Icon type="sort-numeric-up-alt" /> Folder Stats
|
||||
</SelectLinkButton>
|
||||
<SelectLinkButton to={`${baseUrl}/important`}>
|
||||
<Icon type="star" /> Important Scores{" "}
|
||||
{importantScores && importantScores.length > 0 && (
|
||||
<Badge style={{ marginLeft: "5px" }} bg="primary">
|
||||
{importantScores.length} new top {MAX_SCORES}
|
||||
{importantScores.length === 1 ? "" : "s"}!
|
||||
</Badge>
|
||||
)}
|
||||
</SelectLinkButton>
|
||||
<SelectLinkButton to={`${baseUrl}/scores`}>
|
||||
<Icon type="database" /> All Scores
|
||||
</SelectLinkButton>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Col xs={12}>
|
||||
<Divider />
|
||||
<Card header="Highlights">
|
||||
<ScoreTable
|
||||
game={session.game}
|
||||
playtype={session.playtype}
|
||||
dataset={scoreDataset.filter((e) => e.highlight)}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<RatingsOverview session={session} />
|
||||
<Col xs={12}>
|
||||
<Switch>
|
||||
<Route exact path="/u/:userID/games/:game/:playtype/sessions/:sessionID">
|
||||
<Card header="Raise Breakdown">
|
||||
<Row>
|
||||
<SessionRaiseBreakdown
|
||||
sessionData={sessionData}
|
||||
setScores={setScores}
|
||||
/>
|
||||
</Row>
|
||||
</Card>
|
||||
</Route>
|
||||
|
||||
<Route
|
||||
exact
|
||||
path="/u/:userID/games/:game/:playtype/sessions/:sessionID/folders"
|
||||
>
|
||||
<SessionFolderRaiseBreakdown sessionData={sessionData} />
|
||||
</Route>
|
||||
|
||||
<Route
|
||||
exact
|
||||
path="/u/:userID/games/:game/:playtype/sessions/:sessionID/important"
|
||||
>
|
||||
{importantScores && importantScores.length > 0 && (
|
||||
<Card header={`Scores in your top ${MAX_SCORES}!`}>
|
||||
<ScoreTable
|
||||
game={session.game}
|
||||
playtype={session.playtype}
|
||||
dataset={importantScores}
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
{importantScores &&
|
||||
importantScores?.length > 0 &&
|
||||
highlightedScores.length > 0 && <Divider />}
|
||||
<Card header="Scores you've highlighted">
|
||||
<ScoreTable
|
||||
game={session.game}
|
||||
playtype={session.playtype}
|
||||
dataset={highlightedScores}
|
||||
/>
|
||||
</Card>
|
||||
</Route>
|
||||
|
||||
<Route exact path="/u/:userID/games/:game/:playtype/sessions/:sessionID/scores">
|
||||
<ScoreTable
|
||||
dataset={scoreDataset}
|
||||
game={session.game}
|
||||
playtype={session.playtype}
|
||||
onScoreUpdate={(sc) => {
|
||||
const newScores = [
|
||||
...sessionData.scores.filter((e) => e.scoreID !== sc.scoreID),
|
||||
sc,
|
||||
];
|
||||
|
||||
setSessionData({
|
||||
...sessionData,
|
||||
scores: newScores,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Route>
|
||||
</Switch>
|
||||
</Col>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -71,27 +204,34 @@ export default function SessionOverview({
|
||||
function RatingsOverview({ session }: { session: SessionDocument }) {
|
||||
const gptConfig = GetGamePTConfig(session.game, session.playtype);
|
||||
|
||||
return (
|
||||
<Col xs={12}>
|
||||
<Divider />
|
||||
<Card header="Ratings">
|
||||
<div className="d-flex text-center" style={{ justifyContent: "space-evenly" }}>
|
||||
{Object.keys(gptConfig.sessionRatingAlgs).map((e) => (
|
||||
<div key={e}>
|
||||
<div className="display-4">{UppercaseFirst(e)}</div>
|
||||
<div style={{ fontSize: "1.2rem" }}>
|
||||
{FormatSessionRating(
|
||||
session.game,
|
||||
session.playtype,
|
||||
e as AnySessionRatingAlg,
|
||||
session.calculatedData[e as AnySessionRatingAlg]
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
function Thing({ value, name }: { value: string | number; name: string }) {
|
||||
return (
|
||||
<div className="d-flex" style={{ flexGrow: 1 }}>
|
||||
<div className="card" style={{ flexGrow: 1 }}>
|
||||
<div className="card-body">
|
||||
<div className="display-4">{value}</div>
|
||||
<div style={{ fontSize: "1.2rem" }}>{name}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="d-flex text-center" style={{ justifyContent: "space-evenly", gap: "1rem" }}>
|
||||
{Object.keys(gptConfig.sessionRatingAlgs).map((e) => (
|
||||
<Thing
|
||||
key={e}
|
||||
name={`Average ${UppercaseFirst(e)}`}
|
||||
value={FormatSessionRating(
|
||||
session.game,
|
||||
session.playtype,
|
||||
e as AnySessionRatingAlg,
|
||||
session.calculatedData[e as AnySessionRatingAlg]
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ export default function DropdownRow({
|
||||
{children}
|
||||
</tr>
|
||||
<tr className={nested ? "nested-expandable-pseudo-row" : "expandable-pseudo-row"}>
|
||||
<td colSpan={100} style={{ padding: 0, borderTop: "none" }}>
|
||||
<td colSpan={100} style={{ padding: 0, borderTop: "none", width: "100%" }}>
|
||||
<Collapse in={showDropdown}>
|
||||
{renderDropdown ? (
|
||||
<div className="tr-dropdown-container">{dropdown}</div>
|
||||
|
||||
@@ -1,43 +1,56 @@
|
||||
import { GetValueGetter, SearchFunctions } from "util/ztable/search";
|
||||
import { RFA } from "util/misc";
|
||||
import Divider from "components/util/Divider";
|
||||
import ExternalLink from "components/util/ExternalLink";
|
||||
import HoverText from "components/util/HoverText";
|
||||
import SmallText from "components/util/SmallText";
|
||||
import StickyPopover from "components/util/StickyPopover";
|
||||
import { nanoid } from "nanoid";
|
||||
import React, { useState } from "react";
|
||||
import { Modal, OverlayTrigger, Tooltip } from "react-bootstrap";
|
||||
import { Modal } from "react-bootstrap";
|
||||
|
||||
function AdditionalTableProps({
|
||||
function ExampleQueries({
|
||||
keyName: key,
|
||||
value,
|
||||
}: {
|
||||
keyName: string;
|
||||
value: string | number | [string, number] | boolean | null;
|
||||
}) {
|
||||
let typeName;
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
typeName = "Hybrid";
|
||||
} else if (typeof value === "string") {
|
||||
typeName = "Text";
|
||||
if (typeof value === "string") {
|
||||
return (
|
||||
<>
|
||||
<code>
|
||||
{key}={value}
|
||||
</code>
|
||||
.{" "}
|
||||
<small className="text-body-secondary" style={{ fontSize: "1rem" }}>
|
||||
(You can use <code>==</code> for exact matching)
|
||||
</small>
|
||||
</>
|
||||
);
|
||||
} else if (value === null) {
|
||||
typeName = "Cannot Infer :(";
|
||||
return <code>{key}=asdf</code>;
|
||||
} else if (typeof value === "boolean") {
|
||||
typeName = "Presence";
|
||||
} else {
|
||||
typeName = "Number";
|
||||
return (
|
||||
<>
|
||||
<code>{key}=yes</code>, <code>{key}=no</code>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
let v;
|
||||
if (Array.isArray(value)) {
|
||||
v = value[0];
|
||||
} else {
|
||||
v = Number.isInteger(value) ? value : value.toFixed(2);
|
||||
}
|
||||
|
||||
const randomMathOp = RFA([">", "<", "<=", ">="]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<td>{typeName}</td>
|
||||
<td>
|
||||
{typeof value === "boolean"
|
||||
? "Yes/No/True/False"
|
||||
: Array.isArray(value)
|
||||
? `${value[0]} (${value[1]})`
|
||||
: value}
|
||||
</td>
|
||||
</>
|
||||
<code>
|
||||
{key}
|
||||
{randomMathOp}
|
||||
{v}
|
||||
</code>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -46,7 +59,7 @@ export default function FilterDirectivesIndicator<D>({
|
||||
doc,
|
||||
}: {
|
||||
searchFunctions: SearchFunctions<D>;
|
||||
doc?: D;
|
||||
doc: D;
|
||||
}) {
|
||||
const [modalShow, setModalShow] = useState(false);
|
||||
|
||||
@@ -72,99 +85,41 @@ export default function FilterDirectivesIndicator<D>({
|
||||
</StickyPopover>
|
||||
<Modal show={modalShow} onHide={() => setModalShow(false)} size="xl">
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>Available Directives</Modal.Title>
|
||||
<Modal.Title>Table Filters</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<p>
|
||||
This is a slightly advanced feature for{" "}
|
||||
<HoverText hover="nerds">power users</HoverText>. You can read about it{" "}
|
||||
<a href="https://docs.tachi.ac/wiki/filter-directives/">here</a>
|
||||
.<br />
|
||||
The quick explanation is that you can use directives like{" "}
|
||||
<code>Title=conflict Score>=100</code> to perform advanced filters!
|
||||
You can use filters on this table to show just what you care about. The list
|
||||
of filters you can use here is shown below!
|
||||
</p>
|
||||
<Divider className="mb-4" />
|
||||
|
||||
{doc ? (
|
||||
<table className="table table-striped table-hover text-center">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Key</th>
|
||||
<OverlayTrigger
|
||||
overlay={
|
||||
<Tooltip className="tooltip tooltip-wide" id={nanoid()}>
|
||||
<table className="table table-sm table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<strong>Text</strong>
|
||||
</td>
|
||||
<td>
|
||||
Works like text. <code>></code>{" "}
|
||||
and similar operators compare
|
||||
alphabetically.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<strong>Number</strong>
|
||||
</td>
|
||||
<td>Works like a number.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<strong>Hybrid</strong>
|
||||
</td>
|
||||
<td>
|
||||
Although displayed as a string, and
|
||||
works like a string,{" "}
|
||||
<code>></code> and similar
|
||||
operators compare numerically on a
|
||||
hidden number. This is so things
|
||||
like <code>grade:>AAA</code>
|
||||
get all the grades better than AAA,
|
||||
instead of alphabeticalising.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</Tooltip>
|
||||
}
|
||||
placement="right"
|
||||
>
|
||||
<th>Data Type</th>
|
||||
</OverlayTrigger>
|
||||
<th>Example Data</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{Object.keys(searchFunctions).map((key) => (
|
||||
<tr key={key}>
|
||||
<td>
|
||||
<strong>
|
||||
{key[0].toUpperCase()}
|
||||
{key.substring(1)}
|
||||
</strong>
|
||||
</td>
|
||||
<AdditionalTableProps
|
||||
value={GetValueGetter(searchFunctions[key])(doc)}
|
||||
/>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
) : (
|
||||
<span className="text-center">
|
||||
Looks like this table has no data. We can't show anything about the
|
||||
directives.
|
||||
</span>
|
||||
)}
|
||||
<ul style={{ fontSize: "1.5rem" }}>
|
||||
{Object.keys(searchFunctions).map((key) => (
|
||||
<li>
|
||||
<ExampleQueries
|
||||
keyName={key}
|
||||
value={GetValueGetter(searchFunctions[key])(doc)}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<Divider className="mb-4" />
|
||||
Option List
|
||||
<ul>
|
||||
<li>
|
||||
<code>=</code> (Equal to)
|
||||
</li>
|
||||
<li>
|
||||
<code>==</code> (Text is EXACTLY equal to)
|
||||
</li>
|
||||
<li>
|
||||
<code>></code>, <code><</code> (Greater than, Less than)
|
||||
</li>
|
||||
<li>
|
||||
<code>>=</code>, <code><=</code> (Greater than or equal to, Less
|
||||
than or equal to)
|
||||
</li>
|
||||
</ul>
|
||||
<span>
|
||||
<small className="text-body-secondary">
|
||||
You can <SmallText small="tap" large="click" /> anywhere off this dialog
|
||||
|
||||
@@ -151,10 +151,12 @@ export default function TachiTable<D>({
|
||||
placeholder={`Filter ${entryName}`}
|
||||
value={search}
|
||||
/>
|
||||
<FilterDirectivesIndicator
|
||||
searchFunctions={searchFunctions}
|
||||
doc={dataset[0]}
|
||||
/>
|
||||
{dataset[0] && (
|
||||
<FilterDirectivesIndicator
|
||||
searchFunctions={searchFunctions}
|
||||
doc={dataset[0]}
|
||||
/>
|
||||
)}
|
||||
</InputGroup>
|
||||
)}
|
||||
</div>
|
||||
@@ -168,16 +170,18 @@ export default function TachiTable<D>({
|
||||
</div>
|
||||
<div className="row row-gap-4">
|
||||
<div className="col-lg-4 d-flex justify-content-center justify-content-lg-start">
|
||||
<Select
|
||||
name={`Show this many ${entryName}:`}
|
||||
value={ztable.pageLen.toString()}
|
||||
setValue={(e) => ztable.setPageLen(Number(e))}
|
||||
>
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</Select>
|
||||
{dataset.length > 10 && (
|
||||
<Select
|
||||
name={`Show this many ${entryName}:`}
|
||||
value={ztable.pageLen.toString()}
|
||||
setValue={(e) => ztable.setPageLen(Number(e))}
|
||||
>
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</Select>
|
||||
)}
|
||||
</div>
|
||||
<div className="d-none d-lg-flex col-lg-4 justify-content-center align-items-center">
|
||||
{settings?.preferences.developerMode && (
|
||||
@@ -199,23 +203,25 @@ export default function TachiTable<D>({
|
||||
)}
|
||||
</div>
|
||||
<div className="col-lg-4 ms-auto d-flex justify-content-center justify-content-lg-end">
|
||||
<div className="btn-group">
|
||||
<Button
|
||||
variant="secondary"
|
||||
disabled={pageState === "start" || pageState === "start-end"}
|
||||
onClick={decrementPage}
|
||||
>
|
||||
<SmallText small="<" large="Previous" />
|
||||
</Button>
|
||||
<PageSelector currentPage={page} maxPage={maxPage} setPage={setPage} />
|
||||
<Button
|
||||
variant="secondary"
|
||||
disabled={pageState === "end" || pageState === "start-end"}
|
||||
onClick={incrementPage}
|
||||
>
|
||||
<SmallText small=">" large="Next" />
|
||||
</Button>
|
||||
</div>
|
||||
{dataset.length > 10 && (
|
||||
<div className="btn-group">
|
||||
<Button
|
||||
variant="secondary"
|
||||
disabled={pageState === "start" || pageState === "start-end"}
|
||||
onClick={decrementPage}
|
||||
>
|
||||
<SmallText small="<" large="Previous" />
|
||||
</Button>
|
||||
<PageSelector currentPage={page} maxPage={maxPage} setPage={setPage} />
|
||||
<Button
|
||||
variant="secondary"
|
||||
disabled={pageState === "end" || pageState === "start-end"}
|
||||
onClick={incrementPage}
|
||||
>
|
||||
<SmallText small=">" large="Next" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -19,17 +19,26 @@ export default function SelectLinkButton({
|
||||
} & LinkButtonProps) {
|
||||
const match = DoesMatchRoute(window.location.href, to, !matchIfStartsWith);
|
||||
const variant = match ? onVariant : offVariant;
|
||||
const classNames = `${match ? "" : "text-body text-light-hover text-light-focus"} ${className}`;
|
||||
const classNames = `${
|
||||
match ? "" : "text-body text-light-hover text-light-focus"
|
||||
} ${className} text-wrap`;
|
||||
|
||||
return (
|
||||
<LinkButton
|
||||
to={to}
|
||||
size="lg"
|
||||
isActive={() => match}
|
||||
variant={variant}
|
||||
{...props}
|
||||
className={classNames}
|
||||
>
|
||||
{children}
|
||||
<div className="d-none d-lg-block">{children}</div>
|
||||
<div
|
||||
className="d-lg-none"
|
||||
style={{ display: "flex", flexDirection: "column", gap: "1rem" }}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</LinkButton>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { NumericSOV } from "util/sorts";
|
||||
import { ChangeOpacity } from "util/color-opacity";
|
||||
import { FormatMillions } from "util/misc";
|
||||
import { COLOUR_SET, GPTString, GetGPTString, PBScoreDocument, ScoreDocument } from "tachi-common";
|
||||
import CHUNITHMJudgementCell from "components/tables/cells/CHUNITHMJudgementCell";
|
||||
import ITGJudgementCell from "components/tables/cells/ITGJudgementCell";
|
||||
@@ -42,6 +41,7 @@ export const GPT_CLIENT_IMPLEMENTATIONS: GPTClientImplementations = {
|
||||
"iidx:SP": IIDX_SP_IMPL,
|
||||
"iidx:DP": IIDX_DP_IMPL,
|
||||
"chunithm:Single": {
|
||||
sessionImportantScoreCount: 30,
|
||||
enumIcons: defaultEnumIcons,
|
||||
classColours: {
|
||||
colour: {
|
||||
@@ -129,6 +129,7 @@ export const GPT_CLIENT_IMPLEMENTATIONS: GPTClientImplementations = {
|
||||
ratingCell: ({ sc, rating }) => <RatingCell score={sc} rating={rating} />,
|
||||
},
|
||||
"jubeat:Single": {
|
||||
sessionImportantScoreCount: 30,
|
||||
enumIcons: defaultEnumIcons,
|
||||
enumColours: {
|
||||
lamp: {
|
||||
@@ -187,6 +188,7 @@ export const GPT_CLIENT_IMPLEMENTATIONS: GPTClientImplementations = {
|
||||
ratingCell: ({ sc }) => <JubilityCell score={sc} />,
|
||||
},
|
||||
"maimai:Single": {
|
||||
sessionImportantScoreCount: 30,
|
||||
enumIcons: defaultEnumIcons,
|
||||
enumColours: {
|
||||
lamp: {
|
||||
@@ -287,6 +289,7 @@ export const GPT_CLIENT_IMPLEMENTATIONS: GPTClientImplementations = {
|
||||
ratingCell: ({ sc, rating }) => <RatingCell score={sc} rating={rating} />,
|
||||
},
|
||||
"maimaidx:Single": {
|
||||
sessionImportantScoreCount: 30,
|
||||
enumIcons: defaultEnumIcons,
|
||||
enumColours: {
|
||||
lamp: {
|
||||
@@ -429,6 +432,7 @@ export const GPT_CLIENT_IMPLEMENTATIONS: GPTClientImplementations = {
|
||||
ratingCell: ({ sc, rating }) => <RatingCell score={sc} rating={rating} />,
|
||||
},
|
||||
"museca:Single": {
|
||||
sessionImportantScoreCount: 20,
|
||||
enumIcons: defaultEnumIcons,
|
||||
enumColours: {
|
||||
grade: {
|
||||
@@ -476,6 +480,7 @@ export const GPT_CLIENT_IMPLEMENTATIONS: GPTClientImplementations = {
|
||||
ratingCell: ({ sc, rating }) => <RatingCell score={sc} rating={rating} />,
|
||||
},
|
||||
"popn:9B": {
|
||||
sessionImportantScoreCount: 20,
|
||||
enumIcons: {
|
||||
grade: "sort-alpha-up",
|
||||
lamp: "lightbulb",
|
||||
@@ -551,6 +556,7 @@ export const GPT_CLIENT_IMPLEMENTATIONS: GPTClientImplementations = {
|
||||
ratingCell: ({ sc, rating }) => <RatingCell score={sc} rating={rating} />,
|
||||
},
|
||||
"wacca:Single": {
|
||||
sessionImportantScoreCount: 50,
|
||||
enumIcons: defaultEnumIcons,
|
||||
enumColours: {
|
||||
grade: {
|
||||
@@ -635,6 +641,7 @@ export const GPT_CLIENT_IMPLEMENTATIONS: GPTClientImplementations = {
|
||||
ratingCell: ({ sc, rating }) => <RatingCell score={sc} rating={rating} />,
|
||||
},
|
||||
"itg:Stamina": {
|
||||
sessionImportantScoreCount: 10,
|
||||
enumIcons: defaultEnumIcons,
|
||||
enumColours: {
|
||||
grade: {
|
||||
@@ -722,6 +729,7 @@ export const GPT_CLIENT_IMPLEMENTATIONS: GPTClientImplementations = {
|
||||
),
|
||||
},
|
||||
"ongeki:Single": {
|
||||
sessionImportantScoreCount: 45,
|
||||
enumIcons: {
|
||||
grade: "sort-alpha-up",
|
||||
noteLamp: "lightbulb",
|
||||
|
||||
@@ -89,6 +89,7 @@ const ArcaeaRatingCell: GPTClientImplementation<GPTStrings["arcaea"]>["ratingCel
|
||||
}) => <RatingCell score={sc} rating={rating} />;
|
||||
|
||||
export const ARCAEA_TOUCH_IMPL: GPTClientImplementation<"arcaea:Touch"> = {
|
||||
sessionImportantScoreCount: 30,
|
||||
ratingSystems: [],
|
||||
enumIcons: {
|
||||
grade: "sort-alpha-up",
|
||||
|
||||
@@ -12,6 +12,7 @@ import { GetEnumColour } from "lib/game-implementations";
|
||||
import { CreateRatingSys, bgc } from "./_util";
|
||||
|
||||
const BASE_IMPL: GPTClientImplementation<GPTStrings["bms" | "pms"]> = {
|
||||
sessionImportantScoreCount: 20,
|
||||
difficultyColours: {
|
||||
CHART: COLOUR_SET.gray, // lol
|
||||
},
|
||||
|
||||
@@ -7,7 +7,7 @@ import ScoreCell from "components/tables/cells/ScoreCell";
|
||||
import { GetEnumColour } from "lib/game-implementations";
|
||||
import React from "react";
|
||||
import RatingCell from "components/tables/cells/RatingCell";
|
||||
import { bg, bgc } from "./_util";
|
||||
import { bgc } from "./_util";
|
||||
|
||||
const GITADORA_ENUM_COLOURS: GPTClientImplementation<
|
||||
"gitadora:Dora" | "gitadora:Gita"
|
||||
@@ -82,6 +82,7 @@ const GITADORARatingCell: GPTClientImplementation<GPTStrings["gitadora"]>["ratin
|
||||
}) => <RatingCell score={sc} rating={rating} />;
|
||||
|
||||
export const GITADORA_GITA_IMPL: GPTClientImplementation<"gitadora:Gita"> = {
|
||||
sessionImportantScoreCount: 50,
|
||||
enumIcons: {
|
||||
grade: "sort-alpha-up",
|
||||
lamp: "lightbulb",
|
||||
@@ -105,6 +106,7 @@ export const GITADORA_GITA_IMPL: GPTClientImplementation<"gitadora:Gita"> = {
|
||||
};
|
||||
|
||||
export const GITADORA_DORA_IMPL: GPTClientImplementation<"gitadora:Dora"> = {
|
||||
sessionImportantScoreCount: 50,
|
||||
enumIcons: {
|
||||
grade: "sort-alpha-up",
|
||||
lamp: "lightbulb",
|
||||
|
||||
@@ -128,6 +128,7 @@ const IIDXRatingCell: GPTClientImplementation<GPTStrings["iidx"]>["ratingCell"]
|
||||
);
|
||||
|
||||
export const IIDX_SP_IMPL: GPTClientImplementation<"iidx:SP"> = {
|
||||
sessionImportantScoreCount: 20,
|
||||
difficultyColours: IIDX_DIFF_COLOURS,
|
||||
enumColours: IIDX_ENUM_COLOURS,
|
||||
enumIcons: {
|
||||
@@ -167,6 +168,7 @@ export const IIDX_SP_IMPL: GPTClientImplementation<"iidx:SP"> = {
|
||||
};
|
||||
|
||||
export const IIDX_DP_IMPL: GPTClientImplementation<"iidx:DP"> = {
|
||||
sessionImportantScoreCount: 20,
|
||||
difficultyColours: IIDX_DIFF_COLOURS,
|
||||
enumColours: IIDX_ENUM_COLOURS,
|
||||
enumIcons: {
|
||||
|
||||
@@ -9,7 +9,7 @@ import SDVXLampCell from "components/tables/cells/SDVXLampCell";
|
||||
import { GetEnumColour } from "lib/game-implementations";
|
||||
import React from "react";
|
||||
import VF6Cell from "components/tables/cells/VF6Cell";
|
||||
import { CreateRatingSys, bg, bgc } from "./_util";
|
||||
import { CreateRatingSys, bgc } from "./_util";
|
||||
|
||||
type SDVXLikes = GPTStrings["usc" | "sdvx"];
|
||||
|
||||
@@ -75,6 +75,7 @@ const SDVXRatingCell: GPTClientImplementation<SDVXLikes>["ratingCell"] = ({ sc,
|
||||
);
|
||||
|
||||
export const SDVX_IMPL: GPTClientImplementation<"sdvx:Single"> = {
|
||||
sessionImportantScoreCount: 50,
|
||||
enumColours: SDVXLIKE_ENUM_COLOURS,
|
||||
enumIcons: {
|
||||
grade: "sort-alpha-up",
|
||||
@@ -168,6 +169,7 @@ export const SDVX_IMPL: GPTClientImplementation<"sdvx:Single"> = {
|
||||
ratingCell: SDVXRatingCell,
|
||||
};
|
||||
export const USC_IMPL: GPTClientImplementation<GPTStrings["usc"]> = {
|
||||
sessionImportantScoreCount: 50,
|
||||
enumColours: SDVXLIKE_ENUM_COLOURS,
|
||||
enumIcons: {
|
||||
grade: "sort-alpha-up",
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
Difficulties,
|
||||
ExtractedClasses,
|
||||
GPTString,
|
||||
integer,
|
||||
PBScoreDocument,
|
||||
ScoreDocument,
|
||||
ScoreRatingAlgorithms,
|
||||
@@ -124,4 +125,10 @@ export interface GPTClientImplementation<GPT extends GPTString = GPTString> {
|
||||
chart: ChartDocument<GPT>;
|
||||
rating: ScoreRatingAlgorithms[GPT];
|
||||
}) => JSX.Element;
|
||||
|
||||
/**
|
||||
* How many scores should we consider for the topX in the session important scores
|
||||
* display?
|
||||
*/
|
||||
sessionImportantScoreCount: integer;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
version: "3.9"
|
||||
services:
|
||||
mongo:
|
||||
container_name: mongo
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tachi",
|
||||
"version": "2.5.1",
|
||||
"version": "2.5.2",
|
||||
"private": true,
|
||||
"description": "The root of the Tachi monorepo. Contains common utils like linters.",
|
||||
"scripts": {
|
||||
|
||||
@@ -8,47 +8,6 @@ import { execSync } from "child_process";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
// As is with all front-facing zkldi projects, the version names for tachi-server
|
||||
// are from an album I like. In this case, the al(bum is Portishead - Dummy.
|
||||
// The fact that this album is very similar to Mezzanine
|
||||
// - the album used for Kamaitachi v1 - is deliberate. :) - zkldi
|
||||
const PORTISHEAD_DUMMY = [
|
||||
// v2.0.0
|
||||
"Mysterons",
|
||||
|
||||
// v2.1.0
|
||||
"Sour Times",
|
||||
|
||||
// v2.2.0
|
||||
"Strangers",
|
||||
|
||||
// v2.3.0
|
||||
"It Could Be Sweet",
|
||||
|
||||
// v2.4.0
|
||||
"Wandering Star",
|
||||
|
||||
// v2.5.0
|
||||
"It's a Fire",
|
||||
|
||||
// v2.6.0
|
||||
"Numb",
|
||||
|
||||
// v2.7.0
|
||||
"Roads",
|
||||
|
||||
// v2.8.0
|
||||
"Pedestal",
|
||||
|
||||
// v2.9.0
|
||||
"Biscuit",
|
||||
|
||||
// v2.10.0
|
||||
"Glory Box",
|
||||
|
||||
// v2.11+ onwards are not expected to occur, but if they do, we'll figure something out.
|
||||
];
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
// we want to read the monorepo root package.json.
|
||||
@@ -85,9 +44,8 @@ export const VERSION_INFO = {
|
||||
minor: semverInfo?.minor ?? 0,
|
||||
patch: semverInfo?.patch ?? 0,
|
||||
commit,
|
||||
name: PORTISHEAD_DUMMY[semverInfo?.minor ?? 0] ?? "No Version Name",
|
||||
};
|
||||
|
||||
export const VERSION_STR = semverInfo?.raw ?? "0.0.0-unknown";
|
||||
|
||||
export const VERSION_PRETTY = `v${VERSION_STR} (${VERSION_INFO.name}) [${commit}]`;
|
||||
export const VERSION_PRETTY = `v${VERSION_STR} [${commit}]`;
|
||||
|
||||
Reference in New Issue
Block a user