mirror of
https://github.com/zkldi/Tachi.git
synced 2026-10-14 01:59:05 +03:00
feat: basic seeds table rendering
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import { FormatTime } from "util/time";
|
||||
import { APIFetchV1 } from "util/api";
|
||||
import { LoadSeeds, MakeDataset } from "util/seeds";
|
||||
import { StrSOV } from "util/sorts";
|
||||
import useSetSubheader from "components/layout/header/useSetSubheader";
|
||||
import Loading from "components/util/Loading";
|
||||
import useApiQuery from "components/util/query/useApiQuery";
|
||||
@@ -13,6 +15,9 @@ import { TachiConfig } from "lib/config";
|
||||
import ExternalLink from "components/util/ExternalLink";
|
||||
import Card from "components/layout/page/Card";
|
||||
import { GitCommit, Revision } from "types/git";
|
||||
import { AllDatabaseSeeds } from "tachi-common";
|
||||
import SelectButton from "components/util/SelectButton";
|
||||
import BMSCourseLookupTable from "components/tables/seeds/BMSCourseLookupTable";
|
||||
|
||||
export default function SeedsViewer() {
|
||||
useSetSubheader(["Developer Utils", "Database Seeds Management"]);
|
||||
@@ -90,7 +95,9 @@ function InnerSeedsViewer({ hasLocalAPI }: { hasLocalAPI: boolean }) {
|
||||
</Row>
|
||||
{baseRev && (
|
||||
<Row>
|
||||
<Divider />
|
||||
<Col xs={12}>
|
||||
<Divider />
|
||||
</Col>
|
||||
<SeedsLoaderViewer baseRev={baseRev} headRev={headRev} />
|
||||
</Row>
|
||||
)}
|
||||
@@ -99,8 +106,92 @@ function InnerSeedsViewer({ hasLocalAPI }: { hasLocalAPI: boolean }) {
|
||||
}
|
||||
|
||||
function SeedsLoaderViewer({ baseRev, headRev }: { baseRev: Revision; headRev: Revision | null }) {
|
||||
if (headRev !== null) {
|
||||
return <>temp</>;
|
||||
const [baseData, setBaseData] = useState<Partial<AllDatabaseSeeds> | null>(null);
|
||||
const [headData, setHeadData] = useState<Partial<AllDatabaseSeeds> | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const data = await LoadSeeds(baseRev.repo, baseRev.c.sha);
|
||||
|
||||
setBaseData(data);
|
||||
})();
|
||||
}, [baseRev]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (headRev === null) {
|
||||
setHeadData(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await LoadSeeds(headRev.repo, headRev.c.sha);
|
||||
|
||||
setHeadData(data);
|
||||
})();
|
||||
}, [headRev]);
|
||||
|
||||
if (baseData === null) {
|
||||
return (
|
||||
<Col xs={12}>
|
||||
<div className="d-flex justify-content-center">
|
||||
<div>
|
||||
<Loading />
|
||||
<br />
|
||||
Reading data...
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
|
||||
if (headData !== null) {
|
||||
return <>{Object.keys(headData)}</>;
|
||||
}
|
||||
|
||||
return <SeedsAbsoluteState seedsData={baseData} />;
|
||||
}
|
||||
|
||||
function SeedsAbsoluteState({ seedsData }: { seedsData: Partial<AllDatabaseSeeds> }) {
|
||||
const files = Object.keys(seedsData).sort(StrSOV((k) => k)) as Array<keyof AllDatabaseSeeds>;
|
||||
|
||||
const [file, setFile] = useState(files[0]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Col xs={12}>
|
||||
<Card header="Collections">
|
||||
<div className="d-flex flex-wrap">
|
||||
{files.map((e) => (
|
||||
<div key={e}>
|
||||
<SelectButton id={e} setValue={setFile} value={file}>
|
||||
{e} ({seedsData[e]?.length ?? 0})
|
||||
</SelectButton>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={12}>
|
||||
<Divider />
|
||||
<h1 className="text-center">{file}</h1>
|
||||
<Divider />
|
||||
<SeedsTable data={seedsData} file={file} />
|
||||
</Col>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function SeedsTable({
|
||||
data,
|
||||
file,
|
||||
}: {
|
||||
data: Partial<AllDatabaseSeeds>;
|
||||
file: keyof AllDatabaseSeeds;
|
||||
}) {
|
||||
const dataset = useMemo(() => MakeDataset(file, data), [file, data]);
|
||||
|
||||
if (file === "bms-course-lookup.json") {
|
||||
return <BMSCourseLookupTable dataset={dataset as any} />;
|
||||
}
|
||||
|
||||
return <></>;
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import { NumericSOV, StrSOV } from "util/sorts";
|
||||
import Muted from "components/util/Muted";
|
||||
import React from "react";
|
||||
import { BMSCourseDataset } from "types/tables";
|
||||
import { BMSCourseWithRelated } from "types/seeds";
|
||||
import { FormatChart } from "tachi-common";
|
||||
import TachiTable, { Header } from "../components/TachiTable";
|
||||
|
||||
export default function BMSCourseLookupTable({ dataset }: { dataset: BMSCourseDataset }) {
|
||||
const headers: Header<BMSCourseDataset[0]>[] = [
|
||||
export default function BMSCourseLookupTable({ dataset }: { dataset: BMSCourseWithRelated[] }) {
|
||||
const headers: Header<BMSCourseWithRelated>[] = [
|
||||
["Title", "Title", StrSOV((x) => x.title)],
|
||||
["Set", "Set", StrSOV((x) => x.set)],
|
||||
["Playtype", "Playtype", StrSOV((x) => x.playtype)],
|
||||
["Value", "Value", NumericSOV((x) => x.value)],
|
||||
["Set (Index)", "Set (Idx)", StrSOV((x) => x.set)],
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -22,57 +21,45 @@ export default function BMSCourseLookupTable({ dataset }: { dataset: BMSCourseDa
|
||||
set: (x) => x.set,
|
||||
md5: (x) => x.md5sums,
|
||||
value: (x) => x.value,
|
||||
// todo: figure out how to put song titles in here, maybe?
|
||||
// don't ask
|
||||
// it gets all the songtitles and joins them
|
||||
song: (x) =>
|
||||
x.__related.entries
|
||||
.map((e) => {
|
||||
if (typeof e === "string") {
|
||||
return `UNKNOWN CHART (${e})`;
|
||||
}
|
||||
|
||||
return FormatChart("bms", e.song, e.chart);
|
||||
})
|
||||
.join("\n"),
|
||||
}}
|
||||
rowFunction={(x) => <Row data={x} />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function Row({ data }: { data: BMSCourseDataset[0] }) {
|
||||
// md5 hashes are 16 chars long; a bms course may be comprised of any amount of
|
||||
// md5s. This splits the string every 16 characters into an array.
|
||||
const md5s = data.md5sums.match(/.{16}/gu);
|
||||
|
||||
if (!md5s) {
|
||||
return <div>TEMP ERRMSG</div>;
|
||||
}
|
||||
|
||||
// sort songs by their md5 appearance.
|
||||
const songData = [];
|
||||
|
||||
for (const md5 of md5s) {
|
||||
const chart = data.__related.charts.find((e) => e.data.hashMD5 === md5);
|
||||
|
||||
if (!chart) {
|
||||
throw new Error(`MD5 of ${md5} points to chart that doesn't exist.`);
|
||||
}
|
||||
|
||||
const song = data.__related.songs.find((e) => e.id === chart.songID);
|
||||
|
||||
if (!song) {
|
||||
throw new Error(`Song ${chart.songID} does not exist, but has a child chart?`);
|
||||
}
|
||||
|
||||
songData.push({ title: song.title, md5 });
|
||||
}
|
||||
|
||||
function Row({ data }: { data: BMSCourseWithRelated }) {
|
||||
return (
|
||||
<tr>
|
||||
<td>
|
||||
<span>{data.title}</span>
|
||||
{songData.map((e) => (
|
||||
<td className="text-left">
|
||||
<div className="mb-4">
|
||||
<strong>{data.title}</strong>
|
||||
</div>
|
||||
{data.__related.entries.map((e) => (
|
||||
<>
|
||||
<br />
|
||||
<span>{e.title}</span> <Muted>({e.md5})</Muted>
|
||||
{typeof e === "string" ? (
|
||||
<span className="text-danger">UNKNOWN CHART ({e})</span>
|
||||
) : (
|
||||
<span>{FormatChart("bms", e.song, e.chart)}</span>
|
||||
)}
|
||||
</>
|
||||
))}
|
||||
</td>
|
||||
<td>
|
||||
<span>{data.set}</span>
|
||||
{data.playtype} {data.set} ({data.value})
|
||||
</td>
|
||||
<td>{data.playtype}</td>
|
||||
<td>{data.value}</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from "react";
|
||||
import React, { CSSProperties } from "react";
|
||||
import { Button } from "react-bootstrap";
|
||||
import { ButtonVariant } from "react-bootstrap/esm/types";
|
||||
import { JustChildren, SetState } from "types/react";
|
||||
@@ -11,6 +11,7 @@ export default function SelectButton<T>({
|
||||
onVariant = "primary",
|
||||
offVariant = "outline-secondary",
|
||||
disabled = false,
|
||||
style,
|
||||
}: {
|
||||
id: T;
|
||||
value: T;
|
||||
@@ -18,12 +19,14 @@ export default function SelectButton<T>({
|
||||
onVariant?: ButtonVariant;
|
||||
offVariant?: ButtonVariant;
|
||||
disabled?: boolean;
|
||||
style?: CSSProperties;
|
||||
} & JustChildren) {
|
||||
return (
|
||||
<Button
|
||||
disabled={disabled}
|
||||
variant={id === value ? onVariant : offVariant}
|
||||
onClick={() => setValue(id)}
|
||||
style={style}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import {
|
||||
BMSCourseDocument,
|
||||
ChartDocument,
|
||||
FolderDocument,
|
||||
Game,
|
||||
GameToIDStrings,
|
||||
GoalDocument,
|
||||
GoalDocumentAny,
|
||||
GoalDocumentFolder,
|
||||
GoalDocumentMulti,
|
||||
GoalDocumentSingle,
|
||||
IDStrings,
|
||||
IDStringToGame,
|
||||
MilestoneDocument,
|
||||
MilestoneSetDocument,
|
||||
SongDocument,
|
||||
TableDocument,
|
||||
} from "tachi-common";
|
||||
|
||||
// To render seeds with their tables properly, we need to conjoin our data with
|
||||
// any other relevant info. These are used for rendering tables.
|
||||
export type BMSCourseWithRelated = BMSCourseDocument & {
|
||||
__related: {
|
||||
/**
|
||||
* Entry data is just a string in the case where the chart doesn't exist in
|
||||
* the seeds. This string is just the MD5 of the chart that was expected.
|
||||
*/
|
||||
entries: Array<
|
||||
| {
|
||||
chart: ChartDocument<"bms:7K" | "bms:14K">;
|
||||
song: SongDocument<"bms">;
|
||||
}
|
||||
| string
|
||||
>;
|
||||
};
|
||||
};
|
||||
|
||||
export type TableWithRelated = TableDocument & {
|
||||
__related: {
|
||||
folders: Array<FolderDocument>;
|
||||
};
|
||||
};
|
||||
|
||||
export type GoalWithRelated =
|
||||
| (GoalDocumentSingle & {
|
||||
__related: {
|
||||
chart: ChartDocument;
|
||||
song: SongDocument;
|
||||
};
|
||||
})
|
||||
| (GoalDocumentMulti & {
|
||||
__related: {
|
||||
charts: Array<ChartDocument>;
|
||||
songs: Array<SongDocument>;
|
||||
};
|
||||
})
|
||||
| (GoalDocumentFolder & {
|
||||
__related: {
|
||||
folder: FolderDocument;
|
||||
};
|
||||
})
|
||||
| GoalDocumentAny;
|
||||
|
||||
export type MilestoneSetWithRelated = MilestoneSetDocument & {
|
||||
__related: {
|
||||
milestones: Array<MilestoneDocument>;
|
||||
};
|
||||
};
|
||||
|
||||
export type MilestoneWithRelated = MilestoneDocument & {
|
||||
__related: {
|
||||
goals: Array<GoalDocument>;
|
||||
};
|
||||
};
|
||||
|
||||
export type ChartWithRelated<T extends IDStrings> = ChartDocument<T> & {
|
||||
__related: {
|
||||
song: SongDocument<IDStringToGame[T]>;
|
||||
};
|
||||
};
|
||||
|
||||
type SongSeedsWithRelated = {
|
||||
[G in Game as `songs-${G}.json`]: Array<SongDocument<G>>;
|
||||
};
|
||||
|
||||
type ChartSeedsWithRelated = {
|
||||
[G in Game as `charts-${G}.json`]: Array<ChartWithRelated<GameToIDStrings[G]>>;
|
||||
};
|
||||
|
||||
/**
|
||||
* A lookup type for turning database seeds into their corresponding datasets.
|
||||
* This involves things like adding __related song and chart info, which is useful
|
||||
* for rendering.
|
||||
*/
|
||||
export type DatabaseSeedsWithRelated = {
|
||||
"bms-course-lookup.json": Array<BMSCourseWithRelated>;
|
||||
|
||||
// intentional: folders don't need to be joined with anything.
|
||||
"folders.json": Array<FolderDocument>;
|
||||
|
||||
"tables.json": Array<TableWithRelated>;
|
||||
"goals.json": Array<GoalWithRelated>;
|
||||
"milestone-sets.json": Array<MilestoneSetWithRelated>;
|
||||
"milestones.json": Array<MilestoneWithRelated>;
|
||||
} & SongSeedsWithRelated &
|
||||
ChartSeedsWithRelated;
|
||||
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
BMSCourseDocument,
|
||||
ChartDocument,
|
||||
IDStrings,
|
||||
IDStringToGame,
|
||||
@@ -50,10 +49,3 @@ export type UGSDataset<I extends IDStrings = IDStrings> = (UserGameStats<I> & {
|
||||
index: integer;
|
||||
};
|
||||
})[];
|
||||
|
||||
export type BMSCourseDataset = (BMSCourseDocument & {
|
||||
__related: {
|
||||
charts: ChartDocument<"bms:7K"|"bms:14K">[];
|
||||
songs: SongDocument<"bms">[];
|
||||
};
|
||||
})[];
|
||||
|
||||
@@ -1,15 +1,25 @@
|
||||
import { AllDatabaseSeeds, DatabaseSeedNames } from "tachi-common";
|
||||
import {
|
||||
AllDatabaseSeeds,
|
||||
ChartDocument,
|
||||
CreateSongMap,
|
||||
DatabaseSeedNames,
|
||||
Game,
|
||||
} from "tachi-common";
|
||||
import { BMSCourseWithRelated, DatabaseSeedsWithRelated } from "types/seeds";
|
||||
import { APIFetchV1 } from "./api";
|
||||
|
||||
/**
|
||||
* Given a repo and a reference return the status of database-seeds/collections
|
||||
* as of that commit.
|
||||
*
|
||||
* If "WORKING_DIRECTORY" is passed as a ref, and the repository is local, this will
|
||||
* be inferred as reading from the current state of collections on-disk.
|
||||
*/
|
||||
export async function LoadSeeds(repo: string, ref: string): Promise<Partial<AllDatabaseSeeds>> {
|
||||
if (repo === "local") {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (ref) {
|
||||
if (ref !== "WORKING_DIRECTORY") {
|
||||
params.set("revision", ref);
|
||||
}
|
||||
|
||||
@@ -54,3 +64,86 @@ export async function LoadSeeds(repo: string, ref: string): Promise<Partial<AllD
|
||||
|
||||
throw new Error(`Unknown repository type '${repo}'.`);
|
||||
}
|
||||
|
||||
type NotSongsChartsSeeds = Exclude<
|
||||
keyof AllDatabaseSeeds,
|
||||
`songs-${Game}.json` | `charts-${Game}.json`
|
||||
>;
|
||||
|
||||
export function MakeDataset<K extends keyof AllDatabaseSeeds>(
|
||||
file: K,
|
||||
data: Partial<AllDatabaseSeeds>
|
||||
): DatabaseSeedsWithRelated[K] {
|
||||
if (file.startsWith("songs-")) {
|
||||
} else if (file.startsWith("charts-")) {
|
||||
}
|
||||
|
||||
const f = file as NotSongsChartsSeeds;
|
||||
|
||||
switch (f) {
|
||||
case "bms-course-lookup.json":
|
||||
// temporarily hacking around this stuff with -as-any as i can't be bothered
|
||||
// to figure out how to get the typesystem to accept this.
|
||||
return RelateBMSCourses(data) as any;
|
||||
|
||||
case "folders.json":
|
||||
case "goals.json":
|
||||
case "milestone-sets.json":
|
||||
case "milestones.json":
|
||||
case "tables.json":
|
||||
}
|
||||
|
||||
throw new Error("i've coooome undonne");
|
||||
}
|
||||
|
||||
function RelateBMSCourses(data: Partial<AllDatabaseSeeds>): BMSCourseWithRelated[] {
|
||||
const base = data["bms-course-lookup.json"];
|
||||
|
||||
if (!base) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const bmsSongs = data["songs-bms.json"];
|
||||
const bmsCharts = data["charts-bms.json"];
|
||||
|
||||
if (!bmsSongs || !bmsCharts) {
|
||||
throw new Error(
|
||||
`Couldn't find songs-bms/charts-bms, but tried to render bms-course-lookup. Not possible?`
|
||||
);
|
||||
}
|
||||
|
||||
const songMap = CreateSongMap(bmsSongs);
|
||||
const chartMap = new Map<string, ChartDocument<"bms:7K" | "bms:14K">>();
|
||||
|
||||
for (const chart of bmsCharts) {
|
||||
chartMap.set(chart.data.hashMD5, chart);
|
||||
}
|
||||
|
||||
return base.map((course) => {
|
||||
// md5 hashes are 32 chars long; a bms course may be comprised of any amount of
|
||||
// md5s. This splits the string every 32 characters into an array.
|
||||
const md5s = course.md5sums.match(/.{32}/gu);
|
||||
|
||||
if (!md5s) {
|
||||
throw new Error(`Invalid MD5s in BMS Course? Got ${md5s}.`);
|
||||
}
|
||||
|
||||
const dwr: BMSCourseWithRelated = {
|
||||
...course,
|
||||
__related: {
|
||||
entries: md5s.map((e) => {
|
||||
if (!chartMap.has(e)) {
|
||||
return e;
|
||||
}
|
||||
|
||||
return {
|
||||
chart: chartMap.get(e)!,
|
||||
song: songMap.get(chartMap.get(e)!.songID)!,
|
||||
};
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
return dwr;
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user