Skip to content

Commit

Permalink
refactor ExamScroll, move "playArrayBuffer" to core
Browse files Browse the repository at this point in the history
  • Loading branch information
nknapp committed Apr 7, 2024
1 parent fd750c4 commit 334ee7e
Show file tree
Hide file tree
Showing 30 changed files with 284 additions and 70 deletions.
1 change: 1 addition & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ module.exports = {
},
{
files: ["src/core/**/*"],
excludedFiles: [ "*.manual-test.tsx" ],
rules: {
"no-restricted-imports": [
"error",
Expand Down
1 change: 0 additions & 1 deletion src/adapters/playArrayBuffer/index.ts

This file was deleted.

46 changes: 20 additions & 26 deletions src/components/solid/organisms/Reader/ExamScroll.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { type Component, createMemo, For } from "solid-js";
import { type Technique } from "$core/model";
import { withoutIrrelevantParts } from "$core/relevantTechniqueParts";
import { buildTechniqueId, type Technique } from "$core/model";
import { cls } from "$core/utils/cls.ts";
import type { WithoutIrrelevantPartsReturn } from "$core/relevantTechniqueParts/withoutIrrelevantParts.ts";
import { t } from "@/i18n";
import { buildExamScroll, type ExamScrollEntry } from "$core/buildExamScroll";
import type { ExamScrollField } from "$core/buildExamScroll/buildExamScroll.ts";

interface ExamScrollProps {
techniques: Technique[];
Expand All @@ -12,47 +12,41 @@ interface ExamScrollProps {
}

export const ExamScroll: Component<ExamScrollProps> = (props) => {
const model = createMemo(() => Array.from(buildExamScroll(props.techniques)));
return (
<div class={cls("overflow-scroll grid gap-2 snap-y shadow-lg", props.class)}>
<div class={"bg-info-light snap-start border-info border p-2 rounded"}>{t("player.does.not.work")}</div>
<For each={props.techniques}>
{(technique, index) => {
return (
<Row
technique={technique}
previous={props.techniques[index() - 1]}
active={techniqueId(technique) == techniqueId(props.currentTechnique)}
/>
);
<For each={model()}>
{(entry) => {
return <Row entry={entry} active={entry.id == buildTechniqueId(props.currentTechnique)} />;
}}
</For>
</div>
);
};

const Row: Component<{ technique: Technique; previous?: Technique; active: boolean }> = (props) => {
const relevant = createMemo(() => withoutIrrelevantParts(props.technique, props.previous));
const color = (prop: keyof WithoutIrrelevantPartsReturn): string => {
return relevant()[prop] ? "text-black font-bold" : "text-secondary";
};

const Row: Component<{ entry: ExamScrollEntry; active: boolean }> = (props) => {
return (
<div
id={techniqueId(props.technique)}
id={props.entry.id}
class={cls(
props.active && "bg-primary-light",
"border-primary-light border p-2 rounded snap-mandatory",
"snap-start",
)}
>
<div class={cls(color("execution"), "text-sm h-6")}>{props.technique.execution}</div>
<div class={cls(color("attack"), "text-sm h-6")}>{props.technique.attack}</div>
<div class={cls(color("defence"), "text-xl h-8")}>{props.technique.defence}</div>
<div class={cls(color("direction"), "text-sm h-6")}>{relevant().direction}</div>
<ShowField field={props.entry.execution} class={"text-sm h-6"} />
<ShowField field={props.entry.attack} class={"text-sm h-6"} />
<ShowField field={props.entry.defence} class={"text-xl h-8"} />
<ShowField field={props.entry.direction} class={"text-sm h-6"} />
</div>
);
};

function techniqueId(technique: Technique) {
return `${technique.execution} ${technique.attack} ${technique.defence} ${technique.direction}`.replace(/\W/g, "_");
}
const ShowField: Component<{ field: ExamScrollField<string>; class?: string }> = (props) => {
return (
<div class={cls(props.field.relevant ? "text-black font-bold" : "text-secondary", props.class)}>
{props.field.value}
</div>
);
};
3 changes: 1 addition & 2 deletions src/components/solid/organisms/Reader/Reader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import type { DojoInfo } from "$core/model/Dojo.ts";
import { createResource } from "solid-js";
import { createTechniqueStore } from "$core/store";
import { loadSpeechPackPlayer } from "@/core";
import { playArrayBuffer } from "@/adapters/playArrayBuffer";
import speechPack from "@/data/speechpacks/default";
import { SINGLE_DIRECTION, type Technique } from "$core/model";
import { SimpleButton } from "@/components/solid/atoms/SimpleButton.tsx";
Expand All @@ -18,7 +17,7 @@ export const Reader: Component<{ dojoInfo: DojoInfo }> = (props) => {
const [playing, setPlaying] = createSignal(false);
const [player] = createResource(
async () => {
const speechPackPlayer = await loadSpeechPackPlayer(speechPack, playArrayBuffer);
const speechPackPlayer = await loadSpeechPackPlayer(speechPack);
return {
play: async (technique: Technique) => {
setPlaying(true);
Expand Down
1 change: 1 addition & 0 deletions src/core/SpeechPackPlayer/Player.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
describe("Player", () => {});
13 changes: 6 additions & 7 deletions src/core/SpeechPackPlayer/Player.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
import { type ResolvedSpeechPack, resolveSpeechPack } from "$core/SpeechPackPlayer/resolveSpeechPack";
import type { PlayArrayBuffer, SpeechFile, SpeechPack } from "$core/slots";
import type { SpeechFile, SpeechPack } from "$core/model";
import { playArrayBuffer } from "$core/playArrayBuffer";

export async function loadSpeechPackPlayer(speechPack: SpeechPack, playArrayBuffer: PlayArrayBuffer) {
return new SpeechPackPlayer(resolveSpeechPack(speechPack), playArrayBuffer);
export async function loadSpeechPackPlayer(speechPack: SpeechPack) {
return new SpeechPackPlayer(resolveSpeechPack(speechPack));
}

class SpeechPackPlayer {
private readonly speechPack: Promise<ResolvedSpeechPack>;
private readonly playArrayBuffer: PlayArrayBuffer;
private abortController: AbortController;
constructor(speechPack: Promise<ResolvedSpeechPack>, playArrayBuffer: PlayArrayBuffer) {
constructor(speechPack: Promise<ResolvedSpeechPack>) {
this.abortController = new AbortController();
this.speechPack = speechPack;
this.playArrayBuffer = playArrayBuffer;
}

async play(audioFiles: SpeechFile[]): Promise<void> {
Expand All @@ -31,6 +30,6 @@ class SpeechPackPlayer {

private async playSingle(audioFile: SpeechFile, abortController: AbortController) {
const loadedSpeechPack = await this.speechPack;
await this.playArrayBuffer(loadedSpeechPack[audioFile], { abortSignal: abortController.signal });
await playArrayBuffer(loadedSpeechPack[audioFile], { abortSignal: abortController.signal });
}
}
4 changes: 2 additions & 2 deletions src/core/SpeechPackPlayer/resolveSpeechPack.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { SpeechFile } from "$core/slots";
import type { SpeechFile } from "$core/model";
import { useMockEndpoints } from "$core/test-utils/mock-api";
import { http } from "msw";
import { createMockSpeechPack } from "$core/slots/SpeechPack.test-helper";
import { createMockSpeechPack } from "$core/model/SpeechPack.test-helper.ts";
import { type ResolvedSpeechPack, resolveSpeechPack } from "$core/SpeechPackPlayer/resolveSpeechPack";
import { logWarn } from "$core/utils/logger";

Expand Down
2 changes: 1 addition & 1 deletion src/core/SpeechPackPlayer/resolveSpeechPack.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { SpeechPack } from "$core/slots";
import type { SpeechPack } from "$core/model";
import { logWarn } from "$core/utils/logger.ts";

export type ResolvedSpeechPack = Record<keyof SpeechPack, ArrayBuffer>;
Expand Down
166 changes: 166 additions & 0 deletions src/core/buildExamScroll/buildExamScroll.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import { buildExamScroll, type ExamScrollEntry } from "$core/buildExamScroll/buildExamScroll.ts";
import { createTechnique } from "$core/model/Technique.test-helper.ts";
import { relevantTechniqueProperties } from "$core/relevantTechniqueParts/relevantTechniqueProperties.ts";
import { buildTechniqueId } from "$core/model";

describe("buildExamScroll", () => {
it("returns empty scroll for empty array", () => {
expect([...buildExamScroll([])]).toHaveLength(0);
});

it("returns field values of each technique", () => {
expect([
...buildExamScroll([
createTechnique("tachi waza", "chudan tsuki", "ikkyo", "omote"),
createTechnique("hanmi handachi waza", "ai hanmi katate dori", "nikyo", "ura"),
]),
]).toEqual([
{
id: expect.any(String),
execution: { relevant: true, value: "tachi waza" },
attack: { relevant: true, value: "chudan tsuki" },
defence: { relevant: true, value: "ikkyo" },
direction: { relevant: true, value: "omote" },
},
{
id: expect.any(String),
execution: { relevant: true, value: "hanmi handachi waza" },
attack: { relevant: true, value: "ai hanmi katate dori" },
defence: { relevant: true, value: "nikyo" },
direction: { relevant: true, value: "ura" },
},
] satisfies ExamScrollEntry[]);
});

it("uses all props for first technique", () => {
expect([...buildExamScroll([createTechnique("tachi waza", "chudan tsuki", "ikkyo", "omote")])]).toEqual([
{
id: expect.any(String),
execution: { relevant: true, value: "tachi waza" },
attack: { relevant: true, value: "chudan tsuki" },
defence: { relevant: true, value: "ikkyo" },
direction: { relevant: true, value: "omote" },
},
] satisfies ExamScrollEntry[]);

expect(relevantTechniqueProperties(createTechnique("suwari waza", "kata dori", "ikkyo", "ura"), undefined)).toEqual(
["execution", "attack", "defence", "direction"],
);
});

it("uses only direction if everything else is equal", () => {
expect([
...buildExamScroll([
createTechnique("tachi waza", "chudan tsuki", "ikkyo", "omote"),
createTechnique("tachi waza", "chudan tsuki", "ikkyo", "ura"),
]),
]).toEqual([
{
id: expect.any(String),
execution: { relevant: true, value: "tachi waza" },
attack: { relevant: true, value: "chudan tsuki" },
defence: { relevant: true, value: "ikkyo" },
direction: { relevant: true, value: "omote" },
},
{
id: expect.any(String),
execution: { relevant: false, value: "tachi waza" },
attack: { relevant: false, value: "chudan tsuki" },
defence: { relevant: false, value: "ikkyo" },
direction: { relevant: true, value: "ura" },
},
] satisfies ExamScrollEntry[]);
});

it("uses direction and defence, if execution and attack is equal", () => {
expect([
...buildExamScroll([
createTechnique("tachi waza", "chudan tsuki", "ikkyo", "omote"),
createTechnique("tachi waza", "chudan tsuki", "nikyo", "omote"),
]),
]).toEqual([
{
id: expect.any(String),
execution: { relevant: true, value: "tachi waza" },
attack: { relevant: true, value: "chudan tsuki" },
defence: { relevant: true, value: "ikkyo" },
direction: { relevant: true, value: "omote" },
},
{
id: expect.any(String),
execution: { relevant: false, value: "tachi waza" },
attack: { relevant: false, value: "chudan tsuki" },
defence: { relevant: true, value: "nikyo" },
direction: { relevant: true, value: "omote" },
},
] satisfies ExamScrollEntry[]);
});

it("uses attack, direction and defence, if execution is equal", () => {
expect([
...buildExamScroll([
createTechnique("tachi waza", "chudan tsuki", "ikkyo", "omote"),
createTechnique("tachi waza", "jodan tsuki", "ikkyo", "omote"),
]),
]).toEqual([
{
id: expect.any(String),
execution: { relevant: true, value: "tachi waza" },
attack: { relevant: true, value: "chudan tsuki" },
defence: { relevant: true, value: "ikkyo" },
direction: { relevant: true, value: "omote" },
},
{
id: expect.any(String),
execution: { relevant: false, value: "tachi waza" },
attack: { relevant: true, value: "jodan tsuki" },
defence: { relevant: true, value: "ikkyo" },
direction: { relevant: true, value: "omote" },
},
] satisfies ExamScrollEntry[]);
});

it("uses execution, attack, direction and defence, if execution is different", () => {
expect([
...buildExamScroll([
createTechnique("suwari waza", "chudan tsuki", "ikkyo", "omote"),
createTechnique("tachi waza", "chudan tsuki", "ikkyo", "omote"),
]),
]).toEqual([
{
id: expect.any(String),
execution: { relevant: true, value: "suwari waza" },
attack: { relevant: true, value: "chudan tsuki" },
defence: { relevant: true, value: "ikkyo" },
direction: { relevant: true, value: "omote" },
},
{
id: expect.any(String),
execution: { relevant: true, value: "tachi waza" },
attack: { relevant: true, value: "chudan tsuki" },
defence: { relevant: true, value: "ikkyo" },
direction: { relevant: true, value: "omote" },
},
] satisfies ExamScrollEntry[]);
});

it("omits the value for 'single-direction'", () => {
expect([...buildExamScroll([createTechnique("suwari waza", "chudan tsuki", "ikkyo", "single-direction")])]).toEqual(
[
{
id: expect.any(String),
execution: { relevant: true, value: "suwari waza" },
attack: { relevant: true, value: "chudan tsuki" },
defence: { relevant: true, value: "ikkyo" },
direction: { relevant: false, value: "single-direction" },
},
] satisfies ExamScrollEntry[],
);
});

it("adds the id to each technique", () => {
const technique = createTechnique("suwari waza", "chudan tsuki", "ikkyo", "single-direction");
const examScrollEntry = Array.from(buildExamScroll([technique]))[0];
expect(examScrollEntry.id).toEqual(buildTechniqueId(technique));
});
});
49 changes: 49 additions & 0 deletions src/core/buildExamScroll/buildExamScroll.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import {
type Attack,
type BaseTechnique,
buildTechniqueId,
type Defence,
type Direction,
type Execution,
} from "$core/model";
import { relevantTechniqueProperties } from "$core/relevantTechniqueParts/relevantTechniqueProperties.ts";

export interface ExamScrollField<T> {
value: T;
relevant: boolean;
}

export type ExamScrollEntry = {
id: string;
execution: ExamScrollField<Execution>;
attack: ExamScrollField<Attack>;
defence: ExamScrollField<Defence>;
direction: ExamScrollField<Direction>;
};

export function* buildExamScroll(techniques: BaseTechnique[]): Generator<ExamScrollEntry> {
let lastTechnique: BaseTechnique | undefined = undefined;
for (const technique of techniques) {
const relevantProps = relevantTechniqueProperties(technique, lastTechnique);
yield {
id: buildTechniqueId(technique),
execution: {
value: technique.execution,
relevant: relevantProps.includes("execution"),
},
attack: {
value: technique.attack,
relevant: relevantProps.includes("attack"),
},
defence: {
value: technique.defence,
relevant: relevantProps.includes("defence"),
},
direction: {
value: technique.direction,
relevant: relevantProps.includes("direction"),
},
};
lastTechnique = technique;
}
}
1 change: 1 addition & 0 deletions src/core/buildExamScroll/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { type ExamScrollEntry, buildExamScroll } from "./buildExamScroll.ts";
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe } from "vitest";
import { createMockSpeechPack } from "./SpeechPack.test-helper";
import { createMockSpeechPack } from "./SpeechPack.test-helper.ts";

describe("createMockSpeechPack", () => {
it("contains loadable urls", async () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { SpeechPack } from "$core/slots";
import { attacks, defences, executions, directions } from "$core/model";
import type { SpeechPack } from "$core/model";
import { attacks, defences, executions, directions } from "$core/model/index.ts";

export function createMockSpeechPack(partial: Partial<SpeechPack> = {}): SpeechPack {
const keys: (keyof SpeechPack)[] = [...attacks, ...defences, ...executions, ...directions];
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Attack, Execution, Defence, Direction, SINGLE_DIRECTION } from "$core/model";
import type { Attack, Execution, Defence, Direction, SINGLE_DIRECTION } from "$core/model/index.ts";

export type SpeechFile = Exclude<Execution | Attack | Defence | Direction, typeof SINGLE_DIRECTION>;
export type SpeechPack = Record<SpeechFile, string | URL>;
Loading

0 comments on commit 334ee7e

Please sign in to comment.