|
| 1 | +diff --git a/README.md b/README.md |
| 2 | +index a0df01d..8a2ea7a 100644 |
| 3 | +--- a/README.md |
| 4 | ++++ b/README.md |
| 5 | +@@ -74,3 +74,10 @@ The project also served as a way for me to experiment with different approaches, |
| 6 | + ## Credits |
| 7 | + |
| 8 | + Thanks to [@breadbored](https://github.com/breadbored/) and its original concept [Mario Kart World Toolkit](https://bread.codes/posts/mario-kart-world-toolkit/). |
| 9 | ++ |
| 10 | ++// TODO: Add warning about obs and the window pixel to refresh |
| 11 | ++// TODO: Add screen models in the issue model (TV Samsung, TV whatever) |
| 12 | ++// TODO: Add how you can make the window hidden in OBS |
| 13 | ++// TODO: Modal changelog first time access |
| 14 | ++// TODO: Improve some OBS integration and explanation (Make sure to select Source and your switch source) |
| 15 | ++// Move total time of tracks from History to Result. Tries stays on History, and move WR in second tab |
| 16 | +diff --git a/package.json b/package.json |
| 17 | +index fe88eb2..4b35876 100644 |
| 18 | +--- a/package.json |
| 19 | ++++ b/package.json |
| 20 | +@@ -23,6 +23,7 @@ |
| 21 | + "dependencies": { |
| 22 | + "@emotion/css": "11.13.5", |
| 23 | + "@henrygd/queue": "1.2.0", |
| 24 | ++ "@solid-primitives/keyed": "1.5.3", |
| 25 | + "@solidjs/router": "0.15.4", |
| 26 | + "date-and-time": "4.2.0", |
| 27 | + "idb-keyval": "6.2.2", |
| 28 | +diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml |
| 29 | +index 278f069..2325a22 100644 |
| 30 | +--- a/pnpm-lock.yaml |
| 31 | ++++ b/pnpm-lock.yaml |
| 32 | +@@ -14,6 +14,9 @@ importers: |
| 33 | + '@henrygd/queue': |
| 34 | + specifier: 1.2.0 |
| 35 | + version: 1.2.0 |
| 36 | ++ '@solid-primitives/keyed': |
| 37 | ++ specifier: 1.5.3 |
| 38 | ++ version: 1.5.3(solid-js@1.9.11) |
| 39 | + '@solidjs/router': |
| 40 | + specifier: 0.15.4 |
| 41 | + version: 0.15.4(solid-js@1.9.11) |
| 42 | +@@ -1022,6 +1025,11 @@ packages: |
| 43 | + peerDependencies: |
| 44 | + solid-js: ^1.6.12 |
| 45 | + |
| 46 | ++ '@solid-primitives/keyed@1.5.3': |
| 47 | ++ resolution: {integrity: sha512-zNadtyYBhJSOjXtogkGHmRxjGdz9KHc8sGGVAGlUABkE8BED2tbIZoxkwSqzOwde8OcUEH0bb5DLZUWIMvyBSA==} |
| 48 | ++ peerDependencies: |
| 49 | ++ solid-js: ^1.6.12 |
| 50 | ++ |
| 51 | + '@solid-primitives/media@2.3.3': |
| 52 | + resolution: {integrity: sha512-hQ4hLOGvfbugQi5Eu1BFWAIJGIAzztq9x0h02xgBGl2l0Jaa3h7tg6bz5tV1NSuNYVGio4rPoa7zVQQLkkx9dA==} |
| 53 | + peerDependencies: |
| 54 | +@@ -2736,6 +2744,10 @@ snapshots: |
| 55 | + '@solid-primitives/utils': 6.3.2(solid-js@1.9.11) |
| 56 | + solid-js: 1.9.11 |
| 57 | + |
| 58 | ++ '@solid-primitives/keyed@1.5.3(solid-js@1.9.11)': |
| 59 | ++ dependencies: |
| 60 | ++ solid-js: 1.9.11 |
| 61 | ++ |
| 62 | + '@solid-primitives/media@2.3.3(solid-js@1.9.11)': |
| 63 | + dependencies: |
| 64 | + '@solid-primitives/event-listener': 2.4.3(solid-js@1.9.11) |
| 65 | +diff --git a/src/domains/attempt/components/attempts-table.tsx b/src/domains/attempt/components/attempts-table.tsx |
| 66 | +index 78ee99d..072a9ef 100644 |
| 67 | +--- a/src/domains/attempt/components/attempts-table.tsx |
| 68 | ++++ b/src/domains/attempt/components/attempts-table.tsx |
| 69 | +@@ -6,6 +6,8 @@ import { HorizontalDivider } from "../../ui/components/grid/horizontal-divider"; |
| 70 | + import { VerticalDivider } from "../../ui/components/grid/vertical-divider"; |
| 71 | + import { cellCss } from "../../ui/css/cell-css"; |
| 72 | + import { isDefined } from "remeda"; |
| 73 | ++import { SymbolButton } from "../../ui/components/symbol-button"; |
| 74 | ++import { Key } from "@solid-primitives/keyed"; |
| 75 | + |
| 76 | + const sFirstColumn = cellCss({ |
| 77 | + extraPadding: "left", |
| 78 | +@@ -32,25 +34,28 @@ interface AttemptsTableProps { |
| 79 | + showTime?: boolean; |
| 80 | + showTrack?: boolean; |
| 81 | + limit?: number; |
| 82 | ++ onDelete?: (attempt: Attempt) => void; |
| 83 | + } |
| 84 | + |
| 85 | + export const AttemptsTable: Component<AttemptsTableProps> = (props) => { |
| 86 | +- const GRID_COLUMNS = 16; |
| 87 | ++ const GRID_COLUMNS = 18; |
| 88 | + const GRID_SPLITS_COLUMNS = 7; |
| 89 | + const GRID_RESULT_COLUMNS = 3; |
| 90 | + const GRID_SEPARATION_THICKNESS = 2; |
| 91 | + |
| 92 | + const showTime = createMemo(() => props.showTime ?? true); |
| 93 | + const showTrack = createMemo(() => props.showTrack ?? true); |
| 94 | +- const gridColumns = createMemo(() => GRID_COLUMNS - (Number(!showTime()) + Number(!showTrack()))); |
| 95 | +- |
| 96 | ++ const showDelete = createMemo(() => !!props.onDelete); |
| 97 | ++ const showActions = createMemo(() => showDelete()); |
| 98 | ++ const gridColumns = createMemo(() => GRID_COLUMNS - (Number(!showTime()) + Number(!showTrack()) + 2*Number(!showActions()))); |
| 99 | ++ console.log(showActions()) |
| 100 | + const attempts = createMemo(() => props.attempts.slice(0, props.limit)); |
| 101 | + |
| 102 | + return ( |
| 103 | + <Switch> |
| 104 | + <Match when={attempts().length}> |
| 105 | + <GridColumn template={`repeat(${gridColumns()}, max-content)`} xAlign="center"> |
| 106 | +- <For each={attempts()}> |
| 107 | ++ <Key each={attempts()} by={(attempt) => attempt.id}> |
| 108 | + {(attempt, aIndex) => ( |
| 109 | + <> |
| 110 | + <Show when={aIndex() % 7 === 0}> |
| 111 | +@@ -73,26 +78,30 @@ export const AttemptsTable: Component<AttemptsTableProps> = (props) => { |
| 112 | + <VerticalDivider /> |
| 113 | + <Cell text="⏱️" css={[sTitle]} /> |
| 114 | + <VerticalDivider /> |
| 115 | +- <Cell text="️🟡" css={[sTitle, sLastColumn]} /> |
| 116 | ++ <Cell text="️🟡" css={[sTitle, showActions() ? undefined : sLastColumn]} /> |
| 117 | ++ <Show when={showActions()}> |
| 118 | ++ <VerticalDivider /> |
| 119 | ++ <Cell text="🎬" css={[sTitle, sLastColumn]} /> |
| 120 | ++ </Show> |
| 121 | + <HorizontalDivider column={gridColumns()} thicknessFactor={GRID_SEPARATION_THICKNESS} /> |
| 122 | + </Show> |
| 123 | + |
| 124 | +- <Cell row={attempt.rowSplits} text={attempt.date} css={[sFirstColumn, sInfo]} /> |
| 125 | ++ <Cell row={attempt().rowSplits} text={attempt().date} css={[sFirstColumn, sInfo]} /> |
| 126 | + <Show when={showTime()}> |
| 127 | +- <Cell row={attempt.rowSplits} text={attempt.datetime} css={[sInfo]} /> |
| 128 | ++ <Cell row={attempt().rowSplits} text={attempt().datetime} css={[sInfo]} /> |
| 129 | + </Show> |
| 130 | +- <Cell row={attempt.rowSplits} text={attempt.raw.player} css={[sInfo]} /> |
| 131 | ++ <Cell row={attempt().rowSplits} text={attempt().raw.player} css={[sInfo]} /> |
| 132 | + <Show when={showTrack()}> |
| 133 | +- <Cell row={attempt.rowSplits} text={attempt.raw.track} css={[sInfo]} /> |
| 134 | ++ <Cell row={attempt().rowSplits} text={attempt().raw.track} css={[sInfo]} /> |
| 135 | + </Show> |
| 136 | +- <VerticalDivider row={attempt.rowSplits} /> |
| 137 | ++ <VerticalDivider row={attempt().rowSplits} /> |
| 138 | + <Switch> |
| 139 | +- <Match when={attempt.splits.length > 0}> |
| 140 | +- <For each={attempt.laps}> |
| 141 | ++ <Match when={attempt().splits.length > 0}> |
| 142 | ++ <For each={attempt().laps}> |
| 143 | + {(_, sIndex) => ( |
| 144 | + <> |
| 145 | + <Switch> |
| 146 | +- <Match when={attempt.splits.at(sIndex())}> |
| 147 | ++ <Match when={attempt().splits.at(sIndex())}> |
| 148 | + {(split) => ( |
| 149 | + <> |
| 150 | + <Cell text={`S${_}`} /> |
| 151 | +@@ -102,7 +111,7 @@ export const AttemptsTable: Component<AttemptsTableProps> = (props) => { |
| 152 | + <Cell text={split().raw.coins} css={[sValue]} /> |
| 153 | + <VerticalDivider /> |
| 154 | + <Cell text={split().raw.shrooms} css={[sValue]} /> |
| 155 | +- <Show when={sIndex() > 0 && sIndex() < attempt.laps.length - 1}> |
| 156 | ++ <Show when={sIndex() > 0 && sIndex() < attempt().laps.length - 1}> |
| 157 | + <HorizontalDivider column={GRID_SPLITS_COLUMNS} /> |
| 158 | + </Show> |
| 159 | + </> |
| 160 | +@@ -110,23 +119,29 @@ export const AttemptsTable: Component<AttemptsTableProps> = (props) => { |
| 161 | + </Match> |
| 162 | + <Match when={true}> |
| 163 | + <Cell column={GRID_SPLITS_COLUMNS} /> |
| 164 | +- <Show when={sIndex() > 0 && sIndex() < attempt.laps.length - 1}> |
| 165 | ++ <Show when={sIndex() > 0 && sIndex() < attempt().laps.length - 1}> |
| 166 | + <HorizontalDivider column={GRID_SPLITS_COLUMNS} /> |
| 167 | + </Show> |
| 168 | + </Match> |
| 169 | + </Switch> |
| 170 | + <Show when={sIndex() === 0}> |
| 171 | +- <VerticalDivider row={attempt.rowSplits} /> |
| 172 | ++ <VerticalDivider row={attempt().rowSplits} /> |
| 173 | + <Switch> |
| 174 | +- <Match when={!isDefined(attempt.time) && !isDefined(attempt.coins)}> |
| 175 | +- <Cell column={GRID_RESULT_COLUMNS} row={attempt.rowSplits} css={[sLastColumn]} /> |
| 176 | ++ <Match when={!isDefined(attempt().time) && !isDefined(attempt().coins)}> |
| 177 | ++ <Cell column={GRID_RESULT_COLUMNS} row={attempt().rowSplits} css={[sLastColumn]} /> |
| 178 | + </Match> |
| 179 | + <Match when> |
| 180 | +- <Cell row={attempt.rowSplits} text={attempt.time} css={[sValue]} /> |
| 181 | +- <VerticalDivider row={attempt.rowSplits} /> |
| 182 | +- <Cell row={attempt.rowSplits} text={attempt.coins} css={[sValue, sLastColumn]} /> |
| 183 | ++ <Cell row={attempt().rowSplits} text={attempt().time} css={[sValue]} /> |
| 184 | ++ <VerticalDivider row={attempt().rowSplits} /> |
| 185 | ++ <Cell row={attempt().rowSplits} text={attempt().coins} css={[sValue, sLastColumn]} /> |
| 186 | + </Match> |
| 187 | + </Switch> |
| 188 | ++ <Show when={showActions()}> |
| 189 | ++ <VerticalDivider row={attempt().rowSplits} /> |
| 190 | ++ <div style={`grid-row: span ${attempt().rowSplits}`}> |
| 191 | ++ <SymbolButton symbol="🗑" onClick={() => props.onDelete?.(attempt())}></SymbolButton> |
| 192 | ++ </div> |
| 193 | ++ </Show> |
| 194 | + <HorizontalDivider column={GRID_SPLITS_COLUMNS} /> |
| 195 | + </Show> |
| 196 | + </> |
| 197 | +@@ -134,15 +149,23 @@ export const AttemptsTable: Component<AttemptsTableProps> = (props) => { |
| 198 | + </For> |
| 199 | + </Match> |
| 200 | + <Match when={true}> |
| 201 | +- <Cell column={GRID_SPLITS_COLUMNS} row={attempt.rowSplits} text="Not even one split 😭" /> |
| 202 | +- <VerticalDivider row={attempt.rowSplits} /> |
| 203 | +- <Cell row={attempt.rowSplits} column={GRID_RESULT_COLUMNS} css={[sLastColumn]} /> |
| 204 | ++ <Cell column={GRID_SPLITS_COLUMNS} row={attempt().rowSplits} text="Not even one split 😭" /> |
| 205 | ++ <VerticalDivider row={attempt().rowSplits} /> |
| 206 | ++ <Cell row={attempt().rowSplits} column={GRID_RESULT_COLUMNS} css={[sLastColumn]}/> |
| 207 | ++ <Show when={showActions()}> |
| 208 | ++ <VerticalDivider row={attempt().rowSplits} /> |
| 209 | ++ <div style={`grid-row: span ${attempt().rowSplits}`}> |
| 210 | ++ <button> |
| 211 | ++ 🗑 |
| 212 | ++ </button> |
| 213 | ++ </div> |
| 214 | ++ </Show> |
| 215 | + </Match> |
| 216 | + </Switch> |
| 217 | + <HorizontalDivider column={gridColumns()} thicknessFactor={GRID_SEPARATION_THICKNESS} /> |
| 218 | + </> |
| 219 | + )} |
| 220 | +- </For> |
| 221 | ++ </Key> |
| 222 | + </GridColumn> |
| 223 | + </Match> |
| 224 | + <Match when={true}> |
| 225 | +diff --git a/src/domains/attempt/schemas/attempt.ts b/src/domains/attempt/schemas/attempt.ts |
| 226 | +index dcd5df8..3aa9926 100644 |
| 227 | +--- a/src/domains/attempt/schemas/attempt.ts |
| 228 | ++++ b/src/domains/attempt/schemas/attempt.ts |
| 229 | +@@ -30,7 +30,22 @@ export const AttemptSchema = v.pipe( |
| 230 | + }; |
| 231 | + }); |
| 232 | + |
| 233 | ++ /* |
| 234 | ++ * We have to add an id because <For of SolidJS is based on reference of the objects |
| 235 | ++ * However, as our attempts list is a memo of the store.attempts which is augmented by this AttemptSchema |
| 236 | ++ * parsing, everytime the list store.attempts is updated, all the table is rerender which by side effect resets the scroll at the top of the page. |
| 237 | ++ * The workaround is to use <Key of solidjs/primitive that allows to define a ID as keys. |
| 238 | ++ */ |
| 239 | ++ const id = [ |
| 240 | ++ attemptEntity.timestamp, |
| 241 | ++ attemptEntity.player, |
| 242 | ++ attemptEntity.track, |
| 243 | ++ ...attemptEntity.splits.map((split) => split.time), |
| 244 | ++ ].join("|"); |
| 245 | ++ |
| 246 | + return { |
| 247 | ++ // The id is needed in conjunction with <Key each={} by={}> to avoid rerender of all the lists |
| 248 | ++ id: id, |
| 249 | + raw: attemptEntity, |
| 250 | + date: date, |
| 251 | + datetime: datetime, |
| 252 | +diff --git a/src/domains/database/compositions/use-personal-repository.ts b/src/domains/database/compositions/use-personal-repository.ts |
| 253 | +index 7b0a1c4..623f49a 100644 |
| 254 | +--- a/src/domains/database/compositions/use-personal-repository.ts |
| 255 | ++++ b/src/domains/database/compositions/use-personal-repository.ts |
| 256 | +@@ -1,5 +1,5 @@ |
| 257 | + import * as v from "valibot"; |
| 258 | +-import { entries, sum, values } from "remeda"; |
| 259 | ++import { entries, isDeepEqual, isStrictEqual, sum, values } from "remeda"; |
| 260 | + import type { AttemptEntity } from "../schemas/attempt-entity"; |
| 261 | + import type { AttemptsEntity } from "../schemas/attempts-entity"; |
| 262 | + import type { Brand } from "../../_core/utils/brand"; |
| 263 | +@@ -7,7 +7,7 @@ import { JSONUtils } from "../../_core/utils/json-utils"; |
| 264 | + import { PersonalEntitySchema } from "../schemas/personal-entity"; |
| 265 | + import { createMemo } from "solid-js"; |
| 266 | + import { createSingletonRoot } from "../../_core/utils/solid-js"; |
| 267 | +-import { produce } from "solid-js/store"; |
| 268 | ++import { produce, reconcile } from "solid-js/store"; |
| 269 | + import { useAttempts } from "../../attempt/compositions/use-attempts"; |
| 270 | + import { useDatabases } from "./use-databases"; |
| 271 | + |
| 272 | +@@ -46,6 +46,14 @@ function usePersonalRepositorySingleton() { |
| 273 | + ); |
| 274 | + }; |
| 275 | + |
| 276 | ++ const deleteAttempt = (delAttempt: AttemptEntity): void => { |
| 277 | ++ setStore( |
| 278 | ++ produce((store) => { |
| 279 | ++ store.attempts = store.attempts.filter((el) => !isDeepEqual(el, delAttempt)); |
| 280 | ++ }), |
| 281 | ++ ); |
| 282 | ++ }; |
| 283 | ++ |
| 284 | + const getAttemptsCountByTrack = (track: string): number => { |
| 285 | + return store.attemptsCountByTrack[track] ?? 0; |
| 286 | + }; |
| 287 | +@@ -82,6 +90,7 @@ function usePersonalRepositorySingleton() { |
| 288 | + |
| 289 | + return { |
| 290 | + upsertAttempt, |
| 291 | ++ deleteAttempt, |
| 292 | + attempts, |
| 293 | + lastAttempt, |
| 294 | + getAttemptsCount, |
| 295 | +diff --git a/src/views/history.tsx b/src/views/history.tsx |
| 296 | +index 4186043..7a20671 100644 |
| 297 | +--- a/src/views/history.tsx |
| 298 | ++++ b/src/views/history.tsx |
| 299 | +@@ -10,6 +10,7 @@ import { css } from "@emotion/css"; |
| 300 | + import { useAttemptsFilter } from "../domains/attempt/components/attempts-filter/use-attempts-filter"; |
| 301 | + import { usePageTitle } from "./compositions/use-page-title"; |
| 302 | + import { usePersonalRepository } from "../domains/database/compositions/use-personal-repository"; |
| 303 | ++import type { Attempt } from "../domains/attempt/schemas/attempt"; |
| 304 | + |
| 305 | + const sActions = css({ |
| 306 | + display: "flex", |
| 307 | +@@ -75,6 +76,10 @@ export const History: Component = () => { |
| 308 | + ); |
| 309 | + }; |
| 310 | + |
| 311 | ++ const onDelete = (a: Attempt) => { |
| 312 | ++ personal.deleteAttempt(a.raw); |
| 313 | ++ }; |
| 314 | ++ |
| 315 | + return ( |
| 316 | + <> |
| 317 | + <div class={sActions}> |
| 318 | +@@ -90,7 +95,7 @@ export const History: Component = () => { |
| 319 | + <AttemptsFilter /> |
| 320 | + <OverallData /> |
| 321 | + </div> |
| 322 | +- <AttemptsTable attempts={filtered().attempts}></AttemptsTable> |
| 323 | ++ <AttemptsTable attempts={filtered().attempts} onDelete={onDelete} /> |
| 324 | + </> |
| 325 | + ); |
| 326 | + }; |
| 327 | +diff --git a/src/views/readme/faq-obs-studio.tsx b/src/views/readme/faq-obs-studio.tsx |
| 328 | +index 787d722..4b7d4d8 100644 |
| 329 | +--- a/src/views/readme/faq-obs-studio.tsx |
| 330 | ++++ b/src/views/readme/faq-obs-studio.tsx |
| 331 | +@@ -30,7 +30,8 @@ export const FaqObsStudio: Component = () => { |
| 332 | + <A target="_blank" href="https://obsproject.com/kb/virtual-camera-guide"> |
| 333 | + OBS Virtual Camera |
| 334 | + </A>{" "} |
| 335 | +- that mirrors your capture card. Then you use the OBS Virtual Camera as a source in your browser. |
| 336 | ++ that mirrors your capture card. Select Type of source Source, and then your switch source. Then you use the |
| 337 | ++ OBS Virtual Camera as a source in your browser. |
| 338 | + </p> |
| 339 | + </details> |
| 340 | + |
| 341 | +@@ -45,12 +46,13 @@ export const FaqObsStudio: Component = () => { |
| 342 | + <p> |
| 343 | + On OBS Studio, add a new source <code>Window Capture</code>, and you select the popup. When you configure it |
| 344 | + for the first time, I suggest that you go on the track DK Spaceport as it is the worst-case scenario in terms |
| 345 | +- of number of splits. Do not forget to crop the header of the window 😊. |
| 346 | ++ of number of splits (height). Do not forget to crop the header of the window 😊. |
| 347 | + </p> |
| 348 | + <p> |
| 349 | + After the first configuration, every time you open again the popup, OBS Studio should capture it |
| 350 | + automatically. |
| 351 | + </p> |
| 352 | ++ <p>Extra: You can make the background of the winrdow transparent. Just click left filter add ColorKey 212121</p> |
| 353 | + </details> |
| 354 | + </> |
| 355 | + ); |
| 356 | +diff --git a/src/views/result.tsx b/src/views/result.tsx |
| 357 | +index 706b371..5097797 100644 |
| 358 | +--- a/src/views/result.tsx |
| 359 | ++++ b/src/views/result.tsx |
| 360 | +@@ -22,9 +22,9 @@ export const Result: Component = () => { |
| 361 | + <> |
| 362 | + <AttemptsFilter /> |
| 363 | + <h2>Personal best</h2> |
| 364 | +- <AttemptsTable attempts={filtered().timeRecords}></AttemptsTable> |
| 365 | ++ <AttemptsTable attempts={filtered().timeRecords} /> |
| 366 | + <h2>Best personal splits</h2> |
| 367 | +- <AttemptsTable attempts={filtered().splitRecords}></AttemptsTable> |
| 368 | ++ <AttemptsTable attempts={filtered().splitRecords} /> |
| 369 | + </> |
| 370 | + ); |
| 371 | + }; |
0 commit comments