diff --git a/package.json b/package.json index 92d96a1..4d9411c 100644 --- a/package.json +++ b/package.json @@ -52,5 +52,12 @@ "webpack-cli": "^6.0.1", "webpack-dev-server": "^5.2.0", "workbox-webpack-plugin": "^7.0.0" + }, + "prettier": { + "quoteProps": "consistent", + "trailingComma": "all", + "tabWidth": 4, + "semi": true, + "singleQuote": false } } diff --git a/src/api/GameList.ts b/src/api/GameList.ts index ca90fa9..8813930 100644 --- a/src/api/GameList.ts +++ b/src/api/GameList.ts @@ -1,7 +1,10 @@ import { - getGameHeaders, getCategories as apiGetCategories, Category, + getGameHeaders, + getCategories as apiGetCategories, + Category, getRuns as apiGetRuns, - getPlatforms as apiGetPlatforms, getRegions as apiGetRegions, + getPlatforms as apiGetPlatforms, + getRegions as apiGetRegions, getGame as apiGetGame, Game, Run, @@ -19,8 +22,14 @@ const gameIdToCategoriesMap: Map = new Map(); const gameIdToCategoriesPromises: Map> = new Map(); const gameIdToGameInfoMap: Map = new Map(); const gameIdToGameInfoPromises: Map> = new Map(); -const gameAndCategoryToLeaderboardPromises: Map> = new Map(); -const gameAndCategoryToLeaderboardMap: Map>> = new Map(); +const gameAndCategoryToLeaderboardPromises: Map< + string, + Promise +> = new Map(); +const gameAndCategoryToLeaderboardMap: Map< + string, + Array> +> = new Map(); let gameListPromise: Option> = null; let platformListPromise: Option> = null; let regionListPromise: Option> = null; @@ -61,17 +70,24 @@ export function getRegions(): Map { return regionList; } -export function getLeaderboard(gameName: string, categoryName: string): Array> | undefined { +export function getLeaderboard( + gameName: string, + categoryName: string, +): Array> | undefined { const key = JSON.stringify({ gameName, categoryName }); return gameAndCategoryToLeaderboardMap.get(key); } -export function downloadCategoriesByGameId(gameId: string): Promise { +export function downloadCategoriesByGameId( + gameId: string, +): Promise { let categoryPromise = gameIdToCategoriesPromises.get(gameId); if (categoryPromise === undefined) { categoryPromise = (async () => { const categories = await apiGetCategories(gameId); - const categoryNames = categories.filter((c) => c.type === "per-game"); + const categoryNames = categories.filter( + (c) => c.type === "per-game", + ); gameIdToCategoriesMap.set(gameId, categoryNames); return categories; })(); @@ -80,7 +96,9 @@ export function downloadCategoriesByGameId(gameId: string): Promise return categoryPromise; } -export async function downloadCategories(gameName: string): Promise> { +export async function downloadCategories( + gameName: string, +): Promise> { await downloadGameList(); const gameId = getGameId(gameName); if (gameId !== undefined) { @@ -163,7 +181,10 @@ export function downloadRegionList(): Promise { return regionListPromise; } -export async function downloadLeaderboard(gameName: string, categoryName: string): Promise { +export async function downloadLeaderboard( + gameName: string, + categoryName: string, +): Promise { const key = JSON.stringify({ gameName, categoryName }); let promise = gameAndCategoryToLeaderboardPromises.get(key); if (promise === undefined) { @@ -181,9 +202,16 @@ export async function downloadLeaderboard(gameName: string, categoryName: string return; } const category = categories[index]; - const runPages = await apiGetRuns(true, category.id, 500, "verified"); + const runPages = await apiGetRuns( + true, + category.id, + 500, + "verified", + ); const runsUnsorted = await runPages.evaluateAll(); - const runsSorted = runsUnsorted.sort((a, b) => a.times.primary_t - b.times.primary_t); + const runsSorted = runsUnsorted.sort( + (a, b) => a.times.primary_t - b.times.primary_t, + ); gameAndCategoryToLeaderboardMap.set(key, runsSorted); })(); gameAndCategoryToLeaderboardPromises.set(key, promise); diff --git a/src/api/LiveSplitServer.ts b/src/api/LiveSplitServer.ts index fa629c9..f5b66b4 100644 --- a/src/api/LiveSplitServer.ts +++ b/src/api/LiveSplitServer.ts @@ -33,7 +33,9 @@ export class LiveSplitServer { this.connection.onerror = () => { if (wasConnected) { // The onerror event does not contain any useful information. - toast.error("An error while communicating with the server occurred."); + toast.error( + "An error while communicating with the server occurred.", + ); } }; @@ -45,7 +47,10 @@ export class LiveSplitServer { // the response only after all previous responses have been // sent. - const promise = ServerProtocol.handleCommand(e.data, commandSink.getCommandSink().ptr); + const promise = ServerProtocol.handleCommand( + e.data, + commandSink.getCommandSink().ptr, + ); sendQueue = sendQueue.then(async () => { const message = await promise; if (this.connection.readyState === WebSocket.OPEN) { diff --git a/src/api/SpeedrunCom.ts b/src/api/SpeedrunCom.ts index 23ba241..57c1d02 100644 --- a/src/api/SpeedrunCom.ts +++ b/src/api/SpeedrunCom.ts @@ -3,214 +3,214 @@ import { Option, map } from "../util/OptionUtil"; const BASE_URI = "https://www.speedrun.com/api/v1/"; export interface Game { - id: string, - names: Names, - abbreviation: string, - weblink: string, - released: number, - "release-date": string, - assets: Assets, - ruleset: Rules, - platforms: string[], - regions: string[], - variables?: Variables, + "id": string; + "names": Names; + "abbreviation": string; + "weblink": string; + "released": number; + "release-date": string; + "assets": Assets; + "ruleset": Rules; + "platforms": string[]; + "regions": string[]; + "variables"?: Variables; } export interface Rules { - "show-milliseconds": boolean, - "require-verification": boolean, - "require-video": boolean, - "run-times": TimingMethod[], - "default-time": TimingMethod, - "emulators-allowed": boolean, + "show-milliseconds": boolean; + "require-verification": boolean; + "require-video": boolean; + "run-times": TimingMethod[]; + "default-time": TimingMethod; + "emulators-allowed": boolean; } export type TimingMethod = "realtime" | "realtime_noloads" | "ingame"; export interface Variables { - data: Variable[], + data: Variable[]; } export interface Variable { - id: string, - name: string, - category: Option, - scope: VariableScope, - values: VariableValues, - mandatory: boolean, - "is-subcategory": boolean, + "id": string; + "name": string; + "category": Option; + "scope": VariableScope; + "values": VariableValues; + "mandatory": boolean; + "is-subcategory": boolean; } export interface VariableScope { - type: "global" | "full-game" | "all-levels" | "single-level", + type: "global" | "full-game" | "all-levels" | "single-level"; } export interface VariableValues { - values: { [id: string]: VariableValue }, - default: Option, + values: { [id: string]: VariableValue }; + default: Option; } export interface VariableValue { - label: string, - rules?: Option, + label: string; + rules?: Option; } export interface GameHeader { - id: string, - names: Names, - abbreviation: string, - weblink: string, + id: string; + names: Names; + abbreviation: string; + weblink: string; } export interface Names { - international: string, - japanese: Option, - twitch?: Option, + international: string; + japanese: Option; + twitch?: Option; } export interface Assets { - logo: Asset, - "cover-tiny": Asset, - "cover-small": Asset, - "cover-medium": Asset, - "cover-large": Asset, - icon: Asset, - "trophy-1st": Asset, - "trophy-2nd": Asset, - "trophy-3rd": Asset, - "trophy-4th": Option, - background: Asset, - foreground: Option, + "logo": Asset; + "cover-tiny": Asset; + "cover-small": Asset; + "cover-medium": Asset; + "cover-large": Asset; + "icon": Asset; + "trophy-1st": Asset; + "trophy-2nd": Asset; + "trophy-3rd": Asset; + "trophy-4th": Option; + "background": Asset; + "foreground": Option; } export interface Asset { - uri: string, - width: number, - height: number, + uri: string; + width: number; + height: number; } export interface Category { - id: string, - weblink: string, - name: string, - type: "per-game" | "per-level", - rules: Option, + id: string; + weblink: string; + name: string; + type: "per-game" | "per-level"; + rules: Option; } export interface Leaderboard { - weblink: string, - runs: Record[], - players?: PlayerData, + weblink: string; + runs: Record[]; + players?: PlayerData; } export interface PlayerData { - data: User[], + data: User[]; } export interface User { - id: string, - names: Names, - weblink: string, - "name-style": NameStyleSolid | NameStyleGradient, - location: Option, + "id": string; + "names": Names; + "weblink": string; + "name-style": NameStyleSolid | NameStyleGradient; + "location": Option; } export interface PlayerUser extends User { - rel: "user", + rel: "user"; } export interface UserLocation { - country: UserCountry, + country: UserCountry; } export interface UserCountry { - code: string, + code: string; } export interface NameStyleSolid { - style: "solid", - color: Color, + style: "solid"; + color: Color; } export interface NameStyleGradient { - style: "gradient", - "color-from": Color, - "color-to": Color, + "style": "gradient"; + "color-from": Color; + "color-to": Color; } export interface Color { - light: string, - dark: string, + light: string; + dark: string; } export interface Record { - place: number, - run: Run, + place: number; + run: Run; } export type PlayersNotEmbedded = Array; export interface PlayersEmbedded { - data: Array, + data: Array; } export interface Run { - id: string, - weblink: string, - game: string, - category: string, - videos: Option, - comment: Option, - players: PlayerEmbedding, - date: Option, - submitted: Option, - times: Times, - system: RunSystem, - splits: Option, - values: { [key: string]: string | undefined }, + id: string; + weblink: string; + game: string; + category: string; + videos: Option; + comment: Option; + players: PlayerEmbedding; + date: Option; + submitted: Option; + times: Times; + system: RunSystem; + splits: Option; + values: { [key: string]: string | undefined }; } export interface RunSystem { - emulated: boolean, - platform: string, - region: Option, + emulated: boolean; + platform: string; + region: Option; } export interface Videos { - links: Option, + links: Option; } export interface Video { - uri: string, + uri: string; } export interface PlayerUserRef { - rel: "user", - id: string, + rel: "user"; + id: string; } export interface PlayerGuest { - rel: "guest", - name: string, + rel: "guest"; + name: string; } export interface Times { - primary: string, - primary_t: number, + primary: string; + primary_t: number; } export interface Splits { - uri: string, + uri: string; } export interface Platform { - id: string, - name: string, + id: string; + name: string; } export interface Region { - id: string, - name: string, + id: string; + name: string; } export type RunStatus = "new" | "verified" | "rejected"; @@ -259,7 +259,7 @@ export class Page { public constructor( public elements: T[], public next: Option<() => Promise>>, - ) { } + ) {} public async evaluateAll(): Promise { const elements = this.elements; @@ -276,7 +276,9 @@ export class Page { return elements; } - public async iterElementsWith(closure: (element: T) => boolean | undefined | void) { + public async iterElementsWith( + closure: (element: T) => boolean | undefined | void, + ) { let elements = this.elements; let next = this.next; while (true) { @@ -318,7 +320,10 @@ async function executePaginatedRequest(uri: string): Promise> { return new Page(data as T[], next); } -export async function getGame(gameId: string, embeds?: Array<"variables">): Promise { +export async function getGame( + gameId: string, + embeds?: Array<"variables">, +): Promise { const parameters = []; if (embeds !== undefined) { parameters.push(`embed=${embeds.join(",")}`); @@ -337,7 +342,9 @@ export async function getGames(name?: string): Promise> { return executePaginatedRequest(uri); } -export async function getGameHeaders(elementsPerPage: number = 1000): Promise> { +export async function getGameHeaders( + elementsPerPage: number = 1000, +): Promise> { const parameters = ["_bulk=yes", `max=${elementsPerPage}`]; // TODO Remaining parameters const uri = getGamesUri(evaluateParameters(parameters)); @@ -359,11 +366,15 @@ export async function getLeaderboard( if (embeds !== undefined) { parameters.push(`embed=${embeds.join(",")}`); } - const uri = getLeaderboardsUri(`/${gameId}/category/${categoryId}${evaluateParameters(parameters)}`); + const uri = getLeaderboardsUri( + `/${gameId}/category/${categoryId}${evaluateParameters(parameters)}`, + ); return executeRequest(uri); } -export async function getPlatforms(elementsPerPage?: number): Promise> { +export async function getPlatforms( + elementsPerPage?: number, +): Promise> { const parameters = []; if (elementsPerPage !== undefined) { parameters.push(`max=${elementsPerPage}`); @@ -372,7 +383,9 @@ export async function getPlatforms(elementsPerPage?: number): Promise(uri); } -export async function getRegions(elementsPerPage?: number): Promise> { +export async function getRegions( + elementsPerPage?: number, +): Promise> { const parameters = []; if (elementsPerPage !== undefined) { parameters.push(`max=${elementsPerPage}`); @@ -416,7 +429,9 @@ export async function getRuns( } parameters.push("orderby=submitted", "direction=desc"); const uri = getRunsUri(evaluateParameters(parameters)); - return executePaginatedRequest>(uri); + return executePaginatedRequest>( + uri, + ); } export async function getRun(runId: string, embeds?: never[]): Promise { diff --git a/src/css/About.scss b/src/css/About.scss index 2e04ebf..16caaf8 100644 --- a/src/css/About.scss +++ b/src/css/About.scss @@ -1,5 +1,5 @@ -@use 'mobile'; -@use 'variables.icss'; +@use "mobile"; +@use "variables.icss"; $icon-size: 40px; $title-font-size: 40px; @@ -7,82 +7,83 @@ $build-version-font-size: 12px; $link-color: #56b0ff; .about { - .about-inner-container { - background-color: variables.$light-row-color; - padding: variables.$ui-large-margin; - border: 1px solid variables.$border-color; - width: fit-content; + .about-inner-container { + background-color: variables.$light-row-color; + padding: variables.$ui-large-margin; + border: 1px solid variables.$border-color; + width: fit-content; - .livesplit-title { - display: flex; - align-items: center; + .livesplit-title { + display: flex; + align-items: center; - .livesplit-icon { - height: $icon-size; - margin-right: variables.$ui-margin; + .livesplit-icon { + height: $icon-size; + margin-right: variables.$ui-margin; - img { - height: 100%; + img { + height: 100%; + } + } + + .title-text { + font-weight: bold; + font-size: $title-font-size; + } } - } - .title-text { - font-weight: bold; - font-size: $title-font-size; - } - } - - .build-version { - font-size: $build-version-font-size; - } - - h2 { - margin-bottom: variables.$ui-large-margin; - } - - a { - color: $link-color; - } - - .changelog { - margin-left: variables.$ui-large-margin; - - >div { - margin: variables.$ui-large-margin 0 variables.$ui-large-margin variables.$ui-large-margin; - } - } - - .contributors { - margin: 0 auto; - display: flex; - flex-wrap: wrap; - gap: 14px; - justify-content: space-evenly; - align-items: end; - font-size: 14px; - - a { - display: flex; - align-items: center; - flex-direction: column; - gap: 4px; - - img { - width: variables.$contributor-avatar-size; - height: variables.$contributor-avatar-size; - border-radius: 50%; + .build-version { + font-size: $build-version-font-size; + } + + h2 { + margin-bottom: variables.$ui-large-margin; + } + + a { + color: $link-color; + } + + .changelog { + margin-left: variables.$ui-large-margin; + + > div { + margin: variables.$ui-large-margin 0 variables.$ui-large-margin + variables.$ui-large-margin; + } + } + + .contributors { + margin: 0 auto; + display: flex; + flex-wrap: wrap; + gap: 14px; + justify-content: space-evenly; + align-items: end; + font-size: 14px; + + a { + display: flex; + align-items: center; + flex-direction: column; + gap: 4px; + + img { + width: variables.$contributor-avatar-size; + height: variables.$contributor-avatar-size; + border-radius: 50%; + } + } + } + + @include mobile.mobile { + box-sizing: border-box; } - } } + max-width: 700px; + @include mobile.mobile { - box-sizing: border-box; + max-width: 100%; } - } - - max-width: 700px; - - @include mobile.mobile { - max-width: 100%; - } } diff --git a/src/css/ColorPicker.module.scss b/src/css/ColorPicker.module.scss index ca90d7a..63020a9 100644 --- a/src/css/ColorPicker.module.scss +++ b/src/css/ColorPicker.module.scss @@ -1,225 +1,237 @@ -@use 'variables.icss'; +@use "variables.icss"; .colorPickerButton { - border: 2px solid white; - border-radius: 2px; - box-shadow: 0 0 0 1px rgba(0,0,0,.1); - cursor: pointer; - box-sizing: border-box; - height: variables.$settings-row-height; + border: 2px solid white; + border-radius: 2px; + box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1); + cursor: pointer; + box-sizing: border-box; + height: variables.$settings-row-height; } .colorPickerDialogPositioning { - margin: 0 auto; - width: 0; + margin: 0 auto; + width: 0; } .overlay { - inset: 0; - position: fixed; - z-index: 1; + inset: 0; + position: fixed; + z-index: 1; } .glassPanel { - background-color: rgba(28, 28, 28, 0.8); - backdrop-filter: blur(5px); - z-index: 1; - position: absolute; - margin-top: 5px; - margin-left: -113.5px; - border: 1px solid rgba(255, 255, 255, 0.25); - box-shadow: 0 5px 10px 0px rgba(28, 28, 28, 0.8); + background-color: rgba(28, 28, 28, 0.8); + backdrop-filter: blur(5px); + z-index: 1; + position: absolute; + margin-top: 5px; + margin-left: -113.5px; + border: 1px solid rgba(255, 255, 255, 0.25); + box-shadow: 0 5px 10px 0px rgba(28, 28, 28, 0.8); } .hr { - margin: 0; - height: 1px; - border-width: 0px; - background: rgba(255, 255, 255, 0.25); + margin: 0; + height: 1px; + border-width: 0px; + background: rgba(255, 255, 255, 0.25); } .gradientSelector { - overflow: hidden; - position: relative; + overflow: hidden; + position: relative; - .whiteGradient { - background: linear-gradient(to right, white, transparent); + .whiteGradient { + background: linear-gradient(to right, white, transparent); - .cursor { - pointer-events: none; - position: absolute; - width: 12px; - height: 12px; - border-radius: 6px; - box-shadow: black 0 0 0 2px inset; + .cursor { + pointer-events: none; + position: absolute; + width: 12px; + height: 12px; + border-radius: 6px; + box-shadow: black 0 0 0 2px inset; - >div { - width: 100%; - height: 100%; - border-radius: 6px; - box-shadow: white 0 0 0 1px inset; - } + > div { + width: 100%; + height: 100%; + border-radius: 6px; + box-shadow: white 0 0 0 1px inset; + } + } + + .blackGradient { + width: 225px; + height: 125px; + background: linear-gradient(to top, black, transparent); + } } - - .blackGradient { - width: 225px; - height: 125px; - background: linear-gradient(to top, black, transparent); - } - } } .controlPanel { - margin: 10px; - display: flex; - flex-direction: column; - gap: 10px; - - .controlPanelTop { + margin: 10px; display: flex; + flex-direction: column; gap: 10px; - align-items: center; - .checker { - background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAADFJREFUOE9jZGBgEGHAD97gk2YcNYBhmIQBgWSAP52AwoAQwJvQRg1gACckQoC2gQgAIF8IscwEtKYAAAAASUVORK5CYII=") left center; + .controlPanelTop { + display: flex; + gap: 10px; + align-items: center; + + .checker { + background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAADFJREFUOE9jZGBgEGHAD97gk2YcNYBhmIQBgWSAP52AwoAQwJvQRg1gACckQoC2gQgAIF8IscwEtKYAAAAASUVORK5CYII=") + left center; + } + + .sliders { + flex-grow: 1; + display: flex; + flex-direction: column; + gap: 10px; + + .hueSlider { + background: linear-gradient( + to right, + #f00 0%, + #ff0 17%, + #0f0 33%, + #0ff 50%, + #00f 67%, + #f0f 83%, + #f00 100% + ); + height: 18px; + position: relative; + border-radius: 9px; + + input { + margin: 0; + background: none; + height: 16px; + position: absolute; + top: 1px; + left: 1px; + } + } + + .alphaSlider { + background: white; + border-radius: 9px; + overflow: hidden; + height: 18px; + position: relative; + + input { + margin: 0; + background: none; + height: 16px; + position: absolute; + top: 1px; + left: 1px; + } + } + } } - .sliders { - flex-grow: 1; - display: flex; - flex-direction: column; - gap: 10px; - - .hueSlider { - background: linear-gradient(to right, #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%); - height: 18px; - position: relative; - border-radius: 9px; - - input { - margin: 0; - background: none; - height: 16px; - position: absolute; - top: 1px; - left: 1px; - } - } - - .alphaSlider { - background: white; - border-radius: 9px; - overflow: hidden; - height: 18px; - position: relative; - - input { - margin: 0; - background: none; - height: 16px; - position: absolute; - top: 1px; - left: 1px; - } - } + .controlPanelBottom { + display: flex; + gap: 10px; + cursor: ew-resize; } - } - .controlPanelBottom { - display: flex; - gap: 10px; - cursor: ew-resize; - } - - input[type="range"] { - /* Hack for LSO to prevent global style */ - all: revert; - appearance: none; - accent-color: white; - width: calc(100% - 2px); - } + input[type="range"] { + /* Hack for LSO to prevent global style */ + all: revert; + appearance: none; + accent-color: white; + width: calc(100% - 2px); + } } .colorPreview { - border: 1px solid rgba(255, 255, 255, 0.25); - border-radius: 50%; - background: white; - position: relative; - overflow: hidden; + border: 1px solid rgba(255, 255, 255, 0.25); + border-radius: 50%; + background: white; + position: relative; + overflow: hidden; - .colorPreviewInner { - width: 35px; - height: 35px; - display: flex; - justify-content: center; - align-items: center; - } + .colorPreviewInner { + width: 35px; + height: 35px; + display: flex; + justify-content: center; + align-items: center; + } } .colorInput { - display: flex; - flex-direction: column; - gap: 5px; - font-size: 12px; - color: rgba(255, 255, 255, 0.7); - align-items: center; - flex: 1; - width: 0; - user-select: none; + display: flex; + flex-direction: column; + gap: 5px; + font-size: 12px; + color: rgba(255, 255, 255, 0.7); + align-items: center; + flex: 1; + width: 0; + user-select: none; - >input { - width: 100%; - /* Hack for LSO to prevent global input height style */ - height: 25px !important; - font-family: inherit; - font-size: 14px; - text-align: center; - color: white; - font-variant-numeric: tabular-nums; - background: transparent; - border: 1px solid rgba(255, 255, 255, 0.25); - border-radius: 5px; - padding: 0; - } + > input { + width: 100%; + /* Hack for LSO to prevent global input height style */ + height: 25px !important; + font-family: inherit; + font-size: 14px; + text-align: center; + color: white; + font-variant-numeric: tabular-nums; + background: transparent; + border: 1px solid rgba(255, 255, 255, 0.25); + border-radius: 5px; + padding: 0; + } } .predefinedColors { - margin: 10px; - display: flex; - flex-direction: column; - gap: 10px; - - .predefinedColorsRow { + margin: 10px; display: flex; - justify-content: space-between; - } + flex-direction: column; + gap: 10px; + + .predefinedColorsRow { + display: flex; + justify-content: space-between; + } } .predefinedColor { - /* Hack for LSO to prevent global button style */ - all: revert; - border: 3px solid transparent; - border-radius: 50%; - width: 20px; - height: 20px; - cursor: pointer; - transition: all .05s, border-color .25s; - /* Fix for iOS minimum button width */ - font-size: 0; - - &:hover { /* Hack for LSO to prevent global button style */ - background: revert; - border: 3px solid white; - } + all: revert; + border: 3px solid transparent; + border-radius: 50%; + width: 20px; + height: 20px; + cursor: pointer; + transition: + all 0.05s, + border-color 0.25s; + /* Fix for iOS minimum button width */ + font-size: 0; - &:active { - margin-top: 3px; - height: 17px; - } + &:hover { + /* Hack for LSO to prevent global button style */ + background: revert; + border: 3px solid white; + } - &:active:hover { - /* Hack for LSO to prevent global button style */ - background: revert; - } + &:active { + margin-top: 3px; + height: 17px; + } + + &:active:hover { + /* Hack for LSO to prevent global button style */ + background: revert; + } } diff --git a/src/css/ContextMenu.module.css b/src/css/ContextMenu.module.css index e7e9981..f7eea52 100644 --- a/src/css/ContextMenu.module.css +++ b/src/css/ContextMenu.module.css @@ -5,13 +5,13 @@ .panel { z-index: 3; - background-color: rgba(28, 28, 28, .8); + background-color: rgba(28, 28, 28, 0.8); backdrop-filter: blur(5px); min-width: 100px; padding: 4px 0; - border: 1px solid rgba(255, 255, 255, .25); + border: 1px solid rgba(255, 255, 255, 0.25); border-radius: 9px; - box-shadow: 0 5px 10px 0px rgba(28, 28, 28, .8); + box-shadow: 0 5px 10px 0px rgba(28, 28, 28, 0.8); display: flex; flex-direction: column; user-select: none; @@ -28,18 +28,18 @@ padding: 4px; cursor: pointer; font-size: 16px; - transition: background-color .25s; + transition: background-color 0.25s; border-radius: 5px; margin: 0 4px; &:hover { - background-color: rgba(255, 255, 255, .25); + background-color: rgba(255, 255, 255, 0.25); } } .hr { height: 1px; border-width: 0px; - background: rgba(255, 255, 255, .25); + background: rgba(255, 255, 255, 0.25); margin: 4px 0; } diff --git a/src/css/Dialog.scss b/src/css/Dialog.scss index 32c93dc..a59053e 100644 --- a/src/css/Dialog.scss +++ b/src/css/Dialog.scss @@ -1,6 +1,6 @@ -@use 'variables.icss'; +@use "variables.icss"; -.is-mobile+dialog>.dialog { +.is-mobile + dialog > .dialog { min-width: auto; } @@ -15,9 +15,9 @@ dialog { .dialog { color: #eee; - background-color: rgba(28, 28, 28, .8); + background-color: rgba(28, 28, 28, 0.8); backdrop-filter: blur(5px); - border: 2px solid rgba(255, 255, 255, .25); + border: 2px solid rgba(255, 255, 255, 0.25); border-radius: 10px; min-width: 225px; @@ -51,7 +51,7 @@ dialog { input { width: 100%; border: none; - border-bottom: 1px solid rgba(255, 255, 255, .25); + border-bottom: 1px solid rgba(255, 255, 255, 0.25); background: transparent; color: white; text-overflow: ellipsis; diff --git a/src/css/DragUpload.scss b/src/css/DragUpload.scss index bf60f3f..560c01a 100644 --- a/src/css/DragUpload.scss +++ b/src/css/DragUpload.scss @@ -1,35 +1,35 @@ -@use 'mobile'; -@use 'variables.icss'; +@use "mobile"; +@use "variables.icss"; #upload-drop-zone { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - - #upload-drop-zone-overlay { position: absolute; - width: calc(100% + 2 * #{variables.$main-content-margin}); - height: calc(100% + 2 * #{variables.$main-content-margin}); - margin: -(variables.$main-content-margin); - visibility: hidden; - pointer-events: none; - background-color: rgba(50, 50, 50, 0.7); - z-index: 3; - display: flex; - justify-content: center; - align-items: center; + top: 0; + left: 0; + right: 0; + bottom: 0; - @include mobile.mobile { - width: 100%; - height: 100%; - margin: 0; - } + #upload-drop-zone-overlay { + position: absolute; + width: calc(100% + 2 * #{variables.$main-content-margin}); + height: calc(100% + 2 * #{variables.$main-content-margin}); + margin: -(variables.$main-content-margin); + visibility: hidden; + pointer-events: none; + background-color: rgba(50, 50, 50, 0.7); + z-index: 3; + display: flex; + justify-content: center; + align-items: center; - .overlay-text { - font-size: 50px; - text-align: center; + @include mobile.mobile { + width: 100%; + height: 100%; + margin: 0; + } + + .overlay-text { + font-size: 50px; + text-align: center; + } } - } } diff --git a/src/css/HotkeyButton.scss b/src/css/HotkeyButton.scss index e31247d..ad56ee8 100644 --- a/src/css/HotkeyButton.scss +++ b/src/css/HotkeyButton.scss @@ -1,23 +1,23 @@ -@use 'variables.icss'; +@use "variables.icss"; .hotkey-box { - button { - margin: 0; - font-size: 16px; - min-height: 22px; - padding-top: 0; - padding-bottom: 0; - } + button { + margin: 0; + font-size: 16px; + min-height: 22px; + padding-top: 0; + padding-bottom: 0; + } - .hotkey-button.focused { - color: red; - } + .hotkey-button.focused { + color: red; + } - .trash { - cursor: pointer; - } + .trash { + cursor: pointer; + } - display: grid; - grid-template-columns: 1fr 20px; - column-gap: variables.$ui-margin; + display: grid; + grid-template-columns: 1fr 20px; + column-gap: variables.$ui-margin; } diff --git a/src/css/Layout.scss b/src/css/Layout.scss index 00ea0b1..f4c4674 100644 --- a/src/css/Layout.scss +++ b/src/css/Layout.scss @@ -1,42 +1,42 @@ -@use 'Font'; +@use "Font"; .layout { - .resizable-layout { - position: absolute; - top: 0; - width: inherit; - height: inherit; + .resizable-layout { + position: absolute; + top: 0; + width: inherit; + height: inherit; - .react-resizable { - width: 0 !important; - height: 0 !important; - } + .react-resizable { + width: 0 !important; + height: 0 !important; + } - .resizable-handle-east { - cursor: e-resize; - right: -10px; - bottom: 10px; - top: 0; - position: absolute; - width: 20px; - } + .resizable-handle-east { + cursor: e-resize; + right: -10px; + bottom: 10px; + top: 0; + position: absolute; + width: 20px; + } - .resizable-handle-south { - cursor: s-resize; - bottom: -10px; - left: 0; - right: 10px; - position: absolute; - height: 20px; - } + .resizable-handle-south { + cursor: s-resize; + bottom: -10px; + left: 0; + right: 10px; + position: absolute; + height: 20px; + } - .resizable-handle-south-east { - cursor: se-resize; - bottom: -10px; - right: -10px; - position: absolute; - width: 20px; - height: 20px; + .resizable-handle-south-east { + cursor: se-resize; + bottom: -10px; + right: -10px; + position: absolute; + width: 20px; + height: 20px; + } } - } } diff --git a/src/css/LayoutEditor.scss b/src/css/LayoutEditor.scss index b5ed231..12b5a65 100644 --- a/src/css/LayoutEditor.scss +++ b/src/css/LayoutEditor.scss @@ -1,77 +1,77 @@ -@use 'mobile'; -@use 'Table'; -@use 'Toggle'; +@use "mobile"; +@use "Table"; +@use "Toggle"; @use "variables.icss"; .layout-editor-outer { - display: inline-flex; - - @include Table.table; - @include Toggle.toggle; - - .layout-editor-inner { - width: variables.$settings-table-width; display: inline-flex; - margin-bottom: variables.$ui-large-margin; - table { - width: 100%; + @include Table.table; + @include Toggle.toggle; + + .layout-editor-inner { + width: variables.$settings-table-width; + display: inline-flex; + margin-bottom: variables.$ui-large-margin; + + table { + width: 100%; + } + + .btn-group { + display: flex; + flex-direction: column; + gap: variables.$ui-margin; + + button { + width: 40px; + font-size: 15px; + } + + @include mobile.mobile { + flex-direction: row; + margin: variables.$ui-large-margin; + } + } + + @include mobile.mobile { + flex-wrap: wrap; + width: 100%; + margin-bottom: 0; + } } - .btn-group { - display: flex; - flex-direction: column; - gap: variables.$ui-margin; + .layout-editor-component-list { + margin-left: variables.$ui-large-margin; - button { - width: 40px; - font-size: 15px; - } + @include mobile.mobile { + margin-left: 0; + margin-bottom: variables.$ui-large-margin; + } + } - @include mobile.mobile { - flex-direction: row; - margin: variables.$ui-large-margin; - } + .layout-editor-component { + cursor: pointer; + } + + .layout-container { + margin-left: variables.$ui-large-margin; + + @include mobile.mobile { + margin-left: 0; + margin-top: variables.$ui-large-margin; + } } @include mobile.mobile { - flex-wrap: wrap; - width: 100%; - margin-bottom: 0; + flex-wrap: wrap; + + .layout-editor-inner-container { + width: 100%; + } + + .layout-editor-tabs button { + margin-top: 0; + } } - } - - .layout-editor-component-list { - margin-left: variables.$ui-large-margin; - - @include mobile.mobile { - margin-left: 0; - margin-bottom: variables.$ui-large-margin; - } - } - - .layout-editor-component { - cursor: pointer; - } - - .layout-container { - margin-left: variables.$ui-large-margin; - - @include mobile.mobile { - margin-left: 0; - margin-top: variables.$ui-large-margin; - } - } - - @include mobile.mobile { - flex-wrap: wrap; - - .layout-editor-inner-container { - width: 100%; - } - - .layout-editor-tabs button { - margin-top: 0; - } - } } diff --git a/src/css/LiveSplit.scss b/src/css/LiveSplit.scss index 617d84b..49df7f9 100644 --- a/src/css/LiveSplit.scss +++ b/src/css/LiveSplit.scss @@ -1,34 +1,36 @@ -@use 'mobile'; -@use 'Sidebar'; -@use 'variables.icss'; +@use "mobile"; +@use "Sidebar"; +@use "variables.icss"; $sidebar-button-size: 40px; .livesplit-container { - .open-sidebar-button { - position: fixed; - bottom: variables.$ui-margin; - left: variables.$ui-margin; - z-index: 10; - font-size: 24px; - padding: 0; - width: $sidebar-button-size; - height: $sidebar-button-size; - } - - .view-container { - position: relative; - display: inline-block; - vertical-align: top; - min-width: calc(100% - 2 * #{variables.$main-content-margin}); - min-height: calc(100% - 2 * #{variables.$main-content-margin}); - margin: variables.$main-content-margin; - - @include mobile.mobile { - display: block; - margin: 0 0 ($sidebar-button-size + 2 * variables.$ui-margin) 0; - min-width: 100%; - min-height: calc(100% - #{$sidebar-button-size + 2 * variables.$ui-margin}); + .open-sidebar-button { + position: fixed; + bottom: variables.$ui-margin; + left: variables.$ui-margin; + z-index: 10; + font-size: 24px; + padding: 0; + width: $sidebar-button-size; + height: $sidebar-button-size; + } + + .view-container { + position: relative; + display: inline-block; + vertical-align: top; + min-width: calc(100% - 2 * #{variables.$main-content-margin}); + min-height: calc(100% - 2 * #{variables.$main-content-margin}); + margin: variables.$main-content-margin; + + @include mobile.mobile { + display: block; + margin: 0 0 ($sidebar-button-size + 2 * variables.$ui-margin) 0; + min-width: 100%; + min-height: calc( + 100% - #{$sidebar-button-size + 2 * variables.$ui-margin} + ); + } } - } } diff --git a/src/css/LiveSplitServerButton.scss b/src/css/LiveSplitServerButton.scss index 4daa6ba..ccf948c 100644 --- a/src/css/LiveSplitServerButton.scss +++ b/src/css/LiveSplitServerButton.scss @@ -1,4 +1,4 @@ -@use 'variables.icss'; +@use "variables.icss"; .livesplit-server-button { margin: 0; diff --git a/src/css/Markdown.scss b/src/css/Markdown.scss index 1c9d42a..a16e51c 100644 --- a/src/css/Markdown.scss +++ b/src/css/Markdown.scss @@ -1,42 +1,42 @@ -@use 'variables.icss'; +@use "variables.icss"; @mixin markdown { - .flag { - padding-right: 4px; - height: 14px; - width: 18px; - object-fit: contain; - vertical-align: middle; - } - - .markdown { - span { - line-height: 125%; + .flag { + padding-right: 4px; + height: 14px; + width: 18px; + object-fit: contain; + vertical-align: middle; } - img { - vertical-align: middle; + .markdown { + span { + line-height: 125%; + } + + img { + vertical-align: middle; + } } - } - code { - padding: 2px 4px; - font-size: 90%; - color: hsla(50, 100%, 75%, 1); - } + code { + padding: 2px 4px; + font-size: 90%; + color: hsla(50, 100%, 75%, 1); + } - pre { - border-radius: 4px; - border-color: variables.$border-color; - border-style: solid; - border-width: 2px; - padding: 10px; - } + pre { + border-radius: 4px; + border-color: variables.$border-color; + border-style: solid; + border-width: 2px; + padding: 10px; + } - code, - kbd, - pre, - samp { - font-family: Menlo, Monaco, Consolas, "Courier New", monospace; - } + code, + kbd, + pre, + samp { + font-family: Menlo, Monaco, Consolas, "Courier New", monospace; + } } diff --git a/src/css/RunEditor.scss b/src/css/RunEditor.scss index b4780e2..bed42e6 100644 --- a/src/css/RunEditor.scss +++ b/src/css/RunEditor.scss @@ -1,4 +1,4 @@ -@use 'sass:math'; +@use "sass:math"; @use "Markdown"; @use "mobile"; @@ -17,445 +17,449 @@ $small-font-size: 13px; $small-button-padding: 1px 3px 1px 3px; .run-editor { - @include Markdown.markdown; - @include Table.table; - @include Toggle.toggle; + @include Markdown.markdown; + @include Table.table; + @include Toggle.toggle; - thead.table-header { - border-bottom: solid 1px variables.$border-color; - } - - .table-header { - >tr>th { - font-weight: bold; - text-align: center; + thead.table-header { + border-bottom: solid 1px variables.$border-color; } - >tr>th:nth-child(1):after { - margin: 0; - margin-left: -4px; - margin-right: 7px; - } - } - - .table>.table-header>tr { - display: table-row; - background: variables.$header-row-color; - - >th { - padding: math.div(variables.$ui-margin, 2) math.div(variables.$ui-margin, 2); - display: table-cell; - - &:first-child { - padding-left: variables.$ui-margin; - } - - &:last-child { - padding-right: variables.$ui-margin; - } - } - } - - .run-editor-additional-info { - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); - background-color: variables.$light-row-color; - width: $tab-width; - - .run-editor-rules { - padding-left: variables.$ui-margin; - padding-right: variables.$ui-margin; - border: 1px solid variables.$border-color; - border-collapse: collapse; - } - } - - .run-editor-tab { - margin-left: math.div(variables.$ui-large-margin, 2); - min-width: $tab-width; - box-sizing: border-box; - - .settings-table { - width: 100%; - } - - @include mobile.mobile { - width: 100%; - min-width: 0; - margin-left: 0; - } - } - - .video-outer-container { - width: 100%; - padding-top: calc(100% * 9 / 16); - margin-top: math.div(variables.$ui-margin, 2); - position: relative; - - .video-inner-container { - width: 100%; - height: 100%; - position: absolute; - top: 0; - - iframe { - width: 100%; - height: 100% - } - } - } - - .run-editor-info { - display: inline-flex; - align-items: flex-start; - margin-bottom: variables.$ui-large-margin - math.div(variables.$ui-margin, 2); - - .game-icon-container { - background-color: variables.$light-row-color; - border: 1px solid variables.$border-color; - cursor: pointer; - box-sizing: border-box; - width: $button-width; - height: $button-width; - padding: variables.$ui-margin; - - .game-icon-image { - object-fit: contain; - width: $button-width - 2 * variables.$ui-margin; - height: $button-width - 2 * variables.$ui-margin; - - @include mobile.mobile { - width: $mobile-game-icon-size - 2 * variables.$ui-margin; - height: $mobile-game-icon-size - 2 * variables.$ui-margin; - } - } - - @include mobile.mobile { - width: $mobile-game-icon-size; - height: $mobile-game-icon-size; - flex-shrink: 0; - } - } - - .run-editor-info-table { - margin-left: variables.$ui-large-margin; - width: $tab-width; - display: flex; - flex-wrap: wrap; - - .info-table-row { - flex-grow: 1; - width: 100%; - display: flex; - - .info-table-cell { - padding: 0 variables.$ui-margin variables.$ui-margin 0; - flex-grow: 1; + .table-header { + > tr > th { + font-weight: bold; + text-align: center; } - .info-table-cell:last-child { - padding-right: 0; - - @include mobile.mobile { - padding-right: variables.$ui-margin; - } + > tr > th:nth-child(1):after { + margin: 0; + margin-left: -4px; + margin-right: 7px; } + } - @include mobile.mobile { - flex-wrap: wrap; + .table > .table-header > tr { + display: table-row; + background: variables.$header-row-color; + + > th { + padding: math.div(variables.$ui-margin, 2) + math.div(variables.$ui-margin, 2); + display: table-cell; + + &:first-child { + padding-left: variables.$ui-margin; + } + + &:last-child { + padding-right: variables.$ui-margin; + } } - } - - .info-table-row:last-child .info-table-cell { - padding-bottom: 0; - - @include mobile.mobile { - padding-bottom: variables.$ui-margin; - } - } - - @include mobile.mobile { - width: 100%; - margin-left: variables.$ui-margin; - margin-bottom: variables.$ui-large-margin - variables.$ui-margin; - } } - @include mobile.mobile { - margin: variables.$ui-margin 0 0 variables.$ui-margin; - } - } - - table.run-editor-table { - .number { - font-family: inherit; - } - - td { - overflow: hidden; - - >input { - width: 100%; - text-overflow: ellipsis; - - @include mobile.mobile { - font-size: $small-font-size; - } - } - - &.segment-icon-container { - height: $segment-icon-size; - cursor: pointer; - display: flex; - justify-content: center; - } - } - - @include mobile.mobile { - font-size: $small-font-size; - } - } - - .bottom-section { - display: flex; - flex-wrap: wrap; - - .editor-group { - .tab-bar { - margin-left: math.div(variables.$ui-large-margin, 2); - height: $tab-bar-height; - - @include mobile.mobile { - height: inherit; - margin-left: 0; - - button { - font-size: $small-font-size; - padding: $small-button-padding; - margin-top: 0; - } - } - } - - @include mobile.mobile { - min-width: 100%; - margin-top: variables.$ui-large-margin - variables.$ui-margin; - } - } - - .side-buttons { - margin-bottom: variables.$ui-margin; - @include mobile.mobile { - width: 100%; - } - } - } - - .btn-group { - display: flex; - flex-direction: column; - gap: variables.$ui-margin; - margin-top: $tab-bar-height; - margin-right: math.div(variables.$ui-large-margin, 2); - width: $button-width; - - button { - font-size: 15px; - width: $button-width; - min-height: 30px; - - @include mobile.mobile { - width: 100%; - } - } - - @include mobile.mobile { - width: calc(100% - 2 * #{variables.$ui-margin}); - display: grid; - grid-template-columns: 1fr 1fr; - margin-top: 0; - margin-left: variables.$ui-margin; - } - } - - .best-segment-time { - color: hsla(50, 100%, 50%, 1); - } - - .leaderboard-table { - .leaderboard-row:hover { - background: variables.$hover-row-color !important; - } - - .leaderboard-rank-column, - .splits-download-column { - width: 36px; - } - - .leaderboard-time-column, - .variable-column { - width: 100px; - } - - .variable-column, - .splits-download-column { - text-align: center; - } - - .leaderboard-expanded-row { - &>td { - max-width: 0; - } - - .run-meta-table { - border-spacing: variables.$ui-margin 2px; - margin-left: -(variables.$ui-margin); - } - } - - .unregistered-user { - font-style: italic; - color: silver; - } - - @include mobile.mobile { - font-size: $small-font-size; - } - } - - .group { - position: relative; - border-bottom: 1px solid variables.$border-color; - - >input { - font-size: 18px; - padding: $label-size+variables.$ui-margin 0 math.div(variables.$ui-margin, 2) math.div(variables.$ui-margin, 2); - display: block; - width: 100%; - border: none; - background: transparent; - color: white; - font-family: "fira", sans-serif; - } - - >input:focus { - outline: none; - } - - >label { - color: hsla(50, 0%, 75%, 1); - font-size: $label-size; - font-weight: normal; - position: absolute; - pointer-events: none; - left: math.div(variables.$ui-margin, 2); - top: 0; - transition: 0.2s ease all; - -moz-transition: 0.2s ease all; - -webkit-transition: 0.2s ease all; - } - - >input:focus~label { - color: hsla(50, 100%, 50%, 1); - } - - &.invalid>input:focus~label { - color: hsla(0, 100%, 50%, 1); - } - - >.bar { - position: relative; - display: block; - width: 100%; - } - - >.bar:before, - >.bar:after { - content: ""; - height: 2px; - width: 0; - bottom: 0; - position: absolute; - background: hsla(50, 100%, 50%, 1); - transition: 0.2s ease all; - -moz-transition: 0.2s ease all; - -webkit-transition: 0.2s ease all; - } - - >.bar:before { - left: 50%; - } - - >.bar:after { - right: 50%; - } - - &.invalid>.bar:before, - &.invalid>.bar:after { - background: hsla(0, 100%, 50%, 1); - } - - >input:focus~.bar:before, - >input:focus~.bar:after { - width: 50%; - } - } - - .filter-table { - width: 100%; - - &.table { - td { - padding: math.div(variables.$ui-margin, 2) variables.$ui-margin; - } - - tr:first-child>td { - padding-top: variables.$ui-margin; - } - - tr:last-child>td { - padding-bottom: variables.$ui-margin; - } - - >tbody.table-body>tr { + .run-editor-additional-info { + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); background-color: variables.$light-row-color; - } + width: $tab-width; - >thead.table-header>tr { - background-color: variables.$header-row-color; - } - - &.subcategory-table { - >tbody>tr { - border: 1px solid variables.$border-color; - text-align: center; - cursor: pointer; + .run-editor-rules { + padding-left: variables.$ui-margin; + padding-right: variables.$ui-margin; + border: 1px solid variables.$border-color; + border-collapse: collapse; } - - tr:first-child>td { - padding-top: math.div(variables.$ui-margin, 2); - } - - tr:last-child>td { - padding-bottom: math.div(variables.$ui-margin, 2); - } - - >tbody.table-body>tr { - >td:hover { - background: variables.$hover-row-color; - } - - >td.selected:hover { - background: variables.$selected-row-hover-color !important; - } - } - } } - @include mobile.mobile { - margin: 0 0 variables.$ui-margin ( - -(variables.$ui-margin) - ); - width: calc(100% + #{variables.$ui-margin}); - } -} + .run-editor-tab { + margin-left: math.div(variables.$ui-large-margin, 2); + min-width: $tab-width; + box-sizing: border-box; + + .settings-table { + width: 100%; + } + + @include mobile.mobile { + width: 100%; + min-width: 0; + margin-left: 0; + } + } + + .video-outer-container { + width: 100%; + padding-top: calc(100% * 9 / 16); + margin-top: math.div(variables.$ui-margin, 2); + position: relative; + + .video-inner-container { + width: 100%; + height: 100%; + position: absolute; + top: 0; + + iframe { + width: 100%; + height: 100%; + } + } + } + + .run-editor-info { + display: inline-flex; + align-items: flex-start; + margin-bottom: variables.$ui-large-margin - math.div( + variables.$ui-margin, + 2 + ); + + .game-icon-container { + background-color: variables.$light-row-color; + border: 1px solid variables.$border-color; + cursor: pointer; + box-sizing: border-box; + width: $button-width; + height: $button-width; + padding: variables.$ui-margin; + + .game-icon-image { + object-fit: contain; + width: $button-width - 2 * variables.$ui-margin; + height: $button-width - 2 * variables.$ui-margin; + + @include mobile.mobile { + width: $mobile-game-icon-size - 2 * variables.$ui-margin; + height: $mobile-game-icon-size - 2 * variables.$ui-margin; + } + } + + @include mobile.mobile { + width: $mobile-game-icon-size; + height: $mobile-game-icon-size; + flex-shrink: 0; + } + } + + .run-editor-info-table { + margin-left: variables.$ui-large-margin; + width: $tab-width; + display: flex; + flex-wrap: wrap; + + .info-table-row { + flex-grow: 1; + width: 100%; + display: flex; + + .info-table-cell { + padding: 0 variables.$ui-margin variables.$ui-margin 0; + flex-grow: 1; + } + + .info-table-cell:last-child { + padding-right: 0; + + @include mobile.mobile { + padding-right: variables.$ui-margin; + } + } + + @include mobile.mobile { + flex-wrap: wrap; + } + } + + .info-table-row:last-child .info-table-cell { + padding-bottom: 0; + + @include mobile.mobile { + padding-bottom: variables.$ui-margin; + } + } + + @include mobile.mobile { + width: 100%; + margin-left: variables.$ui-margin; + margin-bottom: variables.$ui-large-margin - variables.$ui-margin; + } + } + + @include mobile.mobile { + margin: variables.$ui-margin 0 0 variables.$ui-margin; + } + } + + table.run-editor-table { + .number { + font-family: inherit; + } + + td { + overflow: hidden; + + > input { + width: 100%; + text-overflow: ellipsis; + + @include mobile.mobile { + font-size: $small-font-size; + } + } + + &.segment-icon-container { + height: $segment-icon-size; + cursor: pointer; + display: flex; + justify-content: center; + } + } + + @include mobile.mobile { + font-size: $small-font-size; + } + } + + .bottom-section { + display: flex; + flex-wrap: wrap; + + .editor-group { + .tab-bar { + margin-left: math.div(variables.$ui-large-margin, 2); + height: $tab-bar-height; + + @include mobile.mobile { + height: inherit; + margin-left: 0; + + button { + font-size: $small-font-size; + padding: $small-button-padding; + margin-top: 0; + } + } + } + + @include mobile.mobile { + min-width: 100%; + margin-top: variables.$ui-large-margin - variables.$ui-margin; + } + } + + .side-buttons { + margin-bottom: variables.$ui-margin; + @include mobile.mobile { + width: 100%; + } + } + } + + .btn-group { + display: flex; + flex-direction: column; + gap: variables.$ui-margin; + margin-top: $tab-bar-height; + margin-right: math.div(variables.$ui-large-margin, 2); + width: $button-width; + + button { + font-size: 15px; + width: $button-width; + min-height: 30px; + + @include mobile.mobile { + width: 100%; + } + } + + @include mobile.mobile { + width: calc(100% - 2 * #{variables.$ui-margin}); + display: grid; + grid-template-columns: 1fr 1fr; + margin-top: 0; + margin-left: variables.$ui-margin; + } + } + + .best-segment-time { + color: hsla(50, 100%, 50%, 1); + } + + .leaderboard-table { + .leaderboard-row:hover { + background: variables.$hover-row-color !important; + } + + .leaderboard-rank-column, + .splits-download-column { + width: 36px; + } + + .leaderboard-time-column, + .variable-column { + width: 100px; + } + + .variable-column, + .splits-download-column { + text-align: center; + } + + .leaderboard-expanded-row { + & > td { + max-width: 0; + } + + .run-meta-table { + border-spacing: variables.$ui-margin 2px; + margin-left: -(variables.$ui-margin); + } + } + + .unregistered-user { + font-style: italic; + color: silver; + } + + @include mobile.mobile { + font-size: $small-font-size; + } + } + + .group { + position: relative; + border-bottom: 1px solid variables.$border-color; + + > input { + font-size: 18px; + padding: $label-size + variables.$ui-margin 0 + math.div(variables.$ui-margin, 2) + math.div(variables.$ui-margin, 2); + display: block; + width: 100%; + border: none; + background: transparent; + color: white; + font-family: "fira", sans-serif; + } + + > input:focus { + outline: none; + } + + > label { + color: hsla(50, 0%, 75%, 1); + font-size: $label-size; + font-weight: normal; + position: absolute; + pointer-events: none; + left: math.div(variables.$ui-margin, 2); + top: 0; + transition: 0.2s ease all; + -moz-transition: 0.2s ease all; + -webkit-transition: 0.2s ease all; + } + + > input:focus ~ label { + color: hsla(50, 100%, 50%, 1); + } + + &.invalid > input:focus ~ label { + color: hsla(0, 100%, 50%, 1); + } + + > .bar { + position: relative; + display: block; + width: 100%; + } + + > .bar:before, + > .bar:after { + content: ""; + height: 2px; + width: 0; + bottom: 0; + position: absolute; + background: hsla(50, 100%, 50%, 1); + transition: 0.2s ease all; + -moz-transition: 0.2s ease all; + -webkit-transition: 0.2s ease all; + } + + > .bar:before { + left: 50%; + } + + > .bar:after { + right: 50%; + } + + &.invalid > .bar:before, + &.invalid > .bar:after { + background: hsla(0, 100%, 50%, 1); + } + + > input:focus ~ .bar:before, + > input:focus ~ .bar:after { + width: 50%; + } + } + + .filter-table { + width: 100%; + + &.table { + td { + padding: math.div(variables.$ui-margin, 2) variables.$ui-margin; + } + + tr:first-child > td { + padding-top: variables.$ui-margin; + } + + tr:last-child > td { + padding-bottom: variables.$ui-margin; + } + + > tbody.table-body > tr { + background-color: variables.$light-row-color; + } + + > thead.table-header > tr { + background-color: variables.$header-row-color; + } + + &.subcategory-table { + > tbody > tr { + border: 1px solid variables.$border-color; + text-align: center; + cursor: pointer; + } + + tr:first-child > td { + padding-top: math.div(variables.$ui-margin, 2); + } + + tr:last-child > td { + padding-bottom: math.div(variables.$ui-margin, 2); + } + + > tbody.table-body > tr { + > td:hover { + background: variables.$hover-row-color; + } + + > td.selected:hover { + background: variables.$selected-row-hover-color !important; + } + } + } + } + + @include mobile.mobile { + margin: 0 0 variables.$ui-margin (-(variables.$ui-margin)); + width: calc(100% + #{variables.$ui-margin}); + } + } } diff --git a/src/css/SettingsEditor.scss b/src/css/SettingsEditor.scss index 5a49615..f8f63bd 100644 --- a/src/css/SettingsEditor.scss +++ b/src/css/SettingsEditor.scss @@ -1,16 +1,16 @@ -@use 'mobile'; -@use 'Table'; +@use "mobile"; +@use "Table"; .settings-editor { - @include Table.table; + @include Table.table; - @include mobile.mobile { - .settings-table { - width: 100%; - - & > tbody > tr > td:nth-child(1) { - width: 50%; - } + @include mobile.mobile { + .settings-table { + width: 100%; + + & > tbody > tr > td:nth-child(1) { + width: 50%; + } + } } - } } diff --git a/src/css/Sidebar.scss b/src/css/Sidebar.scss index 690b680..317836b 100644 --- a/src/css/Sidebar.scss +++ b/src/css/Sidebar.scss @@ -1,105 +1,105 @@ -@use 'sass:math'; +@use "sass:math"; -@use 'Toggle'; -@use 'variables.icss'; +@use "Toggle"; +@use "variables.icss"; $h1-font-size: 24px; $h2-font-size: 22px; .sidebar-overlay { - z-index: 3 !important; + z-index: 3 !important; } .sidebar { - display: flex; - flex-direction: column; - gap: variables.$ui-margin; - background: variables.$sidebar-background-color; - padding: variables.$ui-margin; - width: 250px; - z-index: 4 !important; - - @include Toggle.toggle; - - >div>div.small { display: flex; + flex-direction: column; + gap: variables.$ui-margin; + background: variables.$sidebar-background-color; + padding: variables.$ui-margin; + width: 250px; + z-index: 4 !important; - >button { - width: 50%; - font-size: 18px; - } - } + @include Toggle.toggle; - .sidebar-buttons { - display: contents; - hr { - border-color: variables.$border-color; - margin: variables.$ui-margin 0; + > div > div.small { + display: flex; - &.livesplit-title-separator { - margin-top: 0; - } - } - - h1, - h2 { - text-align: center; - margin: 0; - } - - h1 { - font-size: $h1-font-size; - margin-top: variables.$ui-large-margin; - } - - h2 { - font-size: $h2-font-size; - } - - button { - width: 100%; - } - - .livesplit-title { - display: flex; - justify-content: center; - align-items: center; - gap: variables.$ui-margin; - margin-top: variables.$ui-margin; - - h1 { - margin: 0; - } - - .livesplit-icon { - height: 40px; - - img { - height: 100%; + > button { + width: 50%; + font-size: 18px; } - } } - .modified-icon { - position: absolute; - padding-bottom: 10px; - padding-left: 5px; - } - } + .sidebar-buttons { + display: contents; + hr { + border-color: variables.$border-color; + margin: variables.$ui-margin 0; - .choose-comparison { - padding: 0 6px 0 6px; - width: 100%; - height: 40px; - appearance: none; - background: variables.$button-middle-color; - border: 1px solid variables.$border-color; - border-radius: 5px; - color: white; - cursor: pointer; - font-family: "fira", sans-serif; - font-size: 20px; - text-align: center; - text-overflow: ellipsis; - } + &.livesplit-title-separator { + margin-top: 0; + } + } + + h1, + h2 { + text-align: center; + margin: 0; + } + + h1 { + font-size: $h1-font-size; + margin-top: variables.$ui-large-margin; + } + + h2 { + font-size: $h2-font-size; + } + + button { + width: 100%; + } + + .livesplit-title { + display: flex; + justify-content: center; + align-items: center; + gap: variables.$ui-margin; + margin-top: variables.$ui-margin; + + h1 { + margin: 0; + } + + .livesplit-icon { + height: 40px; + + img { + height: 100%; + } + } + } + + .modified-icon { + position: absolute; + padding-bottom: 10px; + padding-left: 5px; + } + } + + .choose-comparison { + padding: 0 6px 0 6px; + width: 100%; + height: 40px; + appearance: none; + background: variables.$button-middle-color; + border: 1px solid variables.$border-color; + border-radius: 5px; + color: white; + cursor: pointer; + font-family: "fira", sans-serif; + font-size: 20px; + text-align: center; + text-overflow: ellipsis; + } } diff --git a/src/css/SplitsSelection.scss b/src/css/SplitsSelection.scss index 764ad23..d9a4904 100644 --- a/src/css/SplitsSelection.scss +++ b/src/css/SplitsSelection.scss @@ -1,118 +1,118 @@ -@use 'sass:math'; +@use "sass:math"; -@use 'mobile'; -@use 'variables.icss'; -@use 'Table'; +@use "mobile"; +@use "variables.icss"; +@use "Table"; $loading-text-font-size: 40px; $splits-row-width: 500px; $splits-row-height: 40px; .splits-selection { - .loading { - display: flex; - width: fit-content; - font-size: $loading-text-font-size; - - .loading-text { - margin-left: variables.$ui-margin; - } - } - - .splits-selection-container { - display: flex; - flex-wrap: nowrap; - flex-direction: column; - - .main-actions { - display: flex; - justify-content: flex-start; - - button { - margin-top: 0; - margin-bottom: math.div(variables.$ui-large-margin, 2); - margin-right: variables.$ui-margin; - } - - @include mobile.mobile { - margin-top: variables.$ui-margin; - margin-left: variables.$ui-margin; - } - } - - .splits-table { - background-color: variables.$dark-row-color; - border: 1px solid variables.$border-color; - margin: math.div(variables.$ui-large-margin, 2) 0; - width: fit-content; - - .splits-row { + .loading { display: flex; - gap: variables.$ui-large-margin; - flex-wrap: nowrap; - align-items: center; - padding: variables.$ui-margin; - height: $splits-row-height; - width: $splits-row-width; + width: fit-content; + font-size: $loading-text-font-size; - &:nth-of-type(odd) { - background-color: variables.$light-row-color; + .loading-text { + margin-left: variables.$ui-margin; + } + } + + .splits-selection-container { + display: flex; + flex-wrap: nowrap; + flex-direction: column; + + .main-actions { + display: flex; + justify-content: flex-start; + + button { + margin-top: 0; + margin-bottom: math.div(variables.$ui-large-margin, 2); + margin-right: variables.$ui-margin; + } + + @include mobile.mobile { + margin-top: variables.$ui-margin; + margin-left: variables.$ui-margin; + } + } + + .splits-table { + background-color: variables.$dark-row-color; + border: 1px solid variables.$border-color; + margin: math.div(variables.$ui-large-margin, 2) 0; + width: fit-content; + + .splits-row { + display: flex; + gap: variables.$ui-large-margin; + flex-wrap: nowrap; + align-items: center; + padding: variables.$ui-margin; + height: $splits-row-height; + width: $splits-row-width; + + &:nth-of-type(odd) { + background-color: variables.$light-row-color; + } + + &.selected { + background: variables.$selected-row-color; + } + + .splits-title-text { + flex-grow: 1; + overflow: hidden; + + .splits-text { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + + &.splits-game { + margin-bottom: variables.$ui-margin; + } + } + } + + &.selected .splits-row-buttons button { + opacity: 70%; + } + + .splits-row-buttons { + display: contents; + flex-shrink: 0; + margin-left: variables.$ui-large-margin; + + button { + background: transparent; + border: 0; + opacity: 50%; + margin: 0; + transition: 0.3s; + color: white; + padding: 0; + + &:hover { + opacity: 100%; + } + } + } + + @include mobile.mobile { + width: 100%; + box-sizing: border-box; + height: $splits-row-height + 2 * variables.$ui-margin; + } + } + + @include mobile.mobile { + width: 100%; + box-sizing: border-box; + } } - - &.selected { - background: variables.$selected-row-color; - } - - .splits-title-text { - flex-grow: 1; - overflow: hidden; - - .splits-text { - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - - &.splits-game { - margin-bottom: variables.$ui-margin; - } - } - } - - &.selected .splits-row-buttons button { - opacity: 70%; - } - - .splits-row-buttons { - display: contents; - flex-shrink: 0; - margin-left: variables.$ui-large-margin; - - button { - background: transparent; - border: 0; - opacity: 50%; - margin: 0; - transition: 0.3s; - color: white; - padding: 0; - - &:hover { - opacity: 100%; - } - } - } - - @include mobile.mobile { - width: 100%; - box-sizing: border-box; - height: $splits-row-height + 2 * variables.$ui-margin; - } - } - - @include mobile.mobile { - width: 100%; - box-sizing: border-box; - } } - } } diff --git a/src/css/Switch.module.scss b/src/css/Switch.module.scss index e96d00d..e9879e6 100644 --- a/src/css/Switch.module.scss +++ b/src/css/Switch.module.scss @@ -19,7 +19,7 @@ left: 0; right: 0; bottom: 0; - transition: background-color .25s; + transition: background-color 0.25s; } span::before { @@ -27,26 +27,26 @@ border-radius: 50%; content: ""; position: absolute; - transition: all .25s; + transition: all 0.25s; left: 3px; bottom: 3px; height: 14px; width: 14px; } - &:hover input+span { + &:hover input + span { background-color: #ffffff40; } - &:hover input:checked+span { + &:hover input:checked + span { background-color: #ffffff80; } - input:checked+span { + input:checked + span { background-color: #ffffff60; } - input:checked+span::before { + input:checked + span::before { transform: translateX(15px); } diff --git a/src/css/Table.scss b/src/css/Table.scss index 882e706..4654148 100644 --- a/src/css/Table.scss +++ b/src/css/Table.scss @@ -1,172 +1,176 @@ -@use 'sass:math'; +@use "sass:math"; -@use 'mobile'; -@use 'variables.icss'; +@use "mobile"; +@use "variables.icss"; @mixin table { - .table { - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); - display: table; - border: 1px solid variables.$border-color; - border-collapse: collapse; + .table { + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); + display: table; + border: 1px solid variables.$border-color; + border-collapse: collapse; - .table-row-even { - display: table-row; - background: variables.$dark-row-color !important; - } - - .table-row-odd { - display: table-row; - background: variables.$light-row-color !important; - } - } - - .table-body { - display: table-row-group; - - >tr { - display: table-row; - background: variables.$dark-row-color; - - >td { - padding: math.div(variables.$ui-margin, 2) math.div(variables.$ui-margin, 2); - display: table-cell; - - &:first-child { - padding-left: variables.$ui-margin; + .table-row-even { + display: table-row; + background: variables.$dark-row-color !important; } - &:last-child { - padding-right: variables.$ui-margin; + .table-row-odd { + display: table-row; + background: variables.$light-row-color !important; } - } - - &:nth-of-type(odd) { - background: variables.$light-row-color; - } } - } - .number { - text-align: right; - font-weight: bold; - font-variant-numeric: tabular-nums; - } + .table-body { + display: table-row-group; - .selected { - background: variables.$selected-row-color !important; - } + > tr { + display: table-row; + background: variables.$dark-row-color; - .tab-bar { - display: flex; + > td { + padding: math.div(variables.$ui-margin, 2) + math.div(variables.$ui-margin, 2); + display: table-cell; - >button { - font-size: 15px; - min-height: 30px; - border-bottom-right-radius: 0; - border-bottom-left-radius: 0; - border-bottom: 0; + &:first-child { + padding-left: variables.$ui-margin; + } + + &:last-child { + padding-right: variables.$ui-margin; + } + } + + &:nth-of-type(odd) { + background: variables.$light-row-color; + } + } } - } - tr>td>input { - margin-right: -40px; - - &:focus { - outline: 0; + .number { + text-align: right; + font-weight: bold; + font-variant-numeric: tabular-nums; } - } - input.text-box { - font-family: inherit; - background: transparent; - color: white; - text-overflow: ellipsis; - font-size: 15px; - border: none; - border-bottom: 1px solid hsla(0, 0%, 100%, 0.25); - } + .selected { + background: variables.$selected-row-color !important; + } - input[Type="text"] { - font-family: inherit; - } + .tab-bar { + display: flex; - select { - background: variables.$dark-row-color url("data:image/svg+xml;charset=UTF-8,%3Csvg width='12' height='12' xmlns='http://www.w3.org/2000/svg'%3E%3Cpolygon points='0,4 2,4 5,7 8,4 10,4 5,9' fill='%23fff'/%3E%3C/svg%3E") no-repeat right 4px center; - font-size: 15px; - border: 1px solid variables.$border-color; - color: white; - text-overflow: ellipsis; - font-family: inherit; - padding-left: 4px; - padding-right: 20px; - border-radius: 0; - appearance: none; - } + > button { + font-size: 15px; + min-height: 30px; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; + border-bottom: 0; + } + } - .settings-table { - width: variables.$settings-table-width; + tr > td > input { + margin-right: -40px; - tr { - height: 30px; - - td:first-child { - width: 100%; - } - - .settings-value-box { - width: math.div(variables.$settings-table-width, 2) - 2*variables.$ui-margin; - display: grid; - column-gap: variables.$ui-margin; - row-gap: variables.$ui-margin; - grid-template-columns: 100%; - - input { - height: variables.$settings-row-height; - box-sizing: border-box; - - &:focus { + &:focus { outline: 0; - } } - - select { - height: variables.$settings-row-height; - min-width: 80px; - } - - &.optional-value { - grid-template-columns: max-content 1fr; - } - - &.two-colors { - grid-template-columns: 1fr 45px 45px; - } - - &.one-color { - grid-template-columns: 1fr 45px; - } - - &.removable-string { - grid-template-columns: 1fr 20px; - - .trash { - cursor: pointer; - } - - button { - margin: 0; - font-size: 12px; - min-height: variables.$settings-row-height; - padding-top: 0; - padding-bottom: 0; - } - } - } } - @include mobile.mobile { - width: 100%; + input.text-box { + font-family: inherit; + background: transparent; + color: white; + text-overflow: ellipsis; + font-size: 15px; + border: none; + border-bottom: 1px solid hsla(0, 0%, 100%, 0.25); + } + + input[Type="text"] { + font-family: inherit; + } + + select { + background: variables.$dark-row-color + url("data:image/svg+xml;charset=UTF-8,%3Csvg width='12' height='12' xmlns='http://www.w3.org/2000/svg'%3E%3Cpolygon points='0,4 2,4 5,7 8,4 10,4 5,9' fill='%23fff'/%3E%3C/svg%3E") + no-repeat right 4px center; + font-size: 15px; + border: 1px solid variables.$border-color; + color: white; + text-overflow: ellipsis; + font-family: inherit; + padding-left: 4px; + padding-right: 20px; + border-radius: 0; + appearance: none; + } + + .settings-table { + width: variables.$settings-table-width; + + tr { + height: 30px; + + td:first-child { + width: 100%; + } + + .settings-value-box { + width: math.div(variables.$settings-table-width, 2) - 2 * + variables.$ui-margin; + display: grid; + column-gap: variables.$ui-margin; + row-gap: variables.$ui-margin; + grid-template-columns: 100%; + + input { + height: variables.$settings-row-height; + box-sizing: border-box; + + &:focus { + outline: 0; + } + } + + select { + height: variables.$settings-row-height; + min-width: 80px; + } + + &.optional-value { + grid-template-columns: max-content 1fr; + } + + &.two-colors { + grid-template-columns: 1fr 45px 45px; + } + + &.one-color { + grid-template-columns: 1fr 45px; + } + + &.removable-string { + grid-template-columns: 1fr 20px; + + .trash { + cursor: pointer; + } + + button { + margin: 0; + font-size: 12px; + min-height: variables.$settings-row-height; + padding-top: 0; + padding-bottom: 0; + } + } + } + } + + @include mobile.mobile { + width: 100%; + } } - } } diff --git a/src/css/TimerView.scss b/src/css/TimerView.scss index 90a75ec..35502e2 100644 --- a/src/css/TimerView.scss +++ b/src/css/TimerView.scss @@ -1,45 +1,46 @@ -@use 'sass:math'; +@use "sass:math"; -@use 'mobile'; -@use 'variables.icss'; +@use "mobile"; +@use "variables.icss"; .buttons { - margin-top: variables.$ui-large-margin - math.div(variables.$ui-margin, 2); - max-width: 2 * variables.$button-max-width + math.div(variables.$ui-margin, 2); + margin-top: variables.$ui-large-margin - math.div(variables.$ui-margin, 2); + max-width: 2 * variables.$button-max-width + + math.div(variables.$ui-margin, 2); - .control-buttons { - display: grid; - grid-template-columns: 1fr 1fr; - gap: variables.$ui-margin; + .control-buttons { + display: grid; + grid-template-columns: 1fr 1fr; + gap: variables.$ui-margin; - button { - width: 100%; + button { + width: 100%; + } } - } - .manual-game-time { - width: calc(100% - math.div(variables.$ui-margin, 2)); - color: variables.$button-text-color; - background-color: transparent; - border: none; - border-bottom: 2px solid #aaa; - text-align: right; - font-family: "fira", sans-serif; - font-weight: bold; - font-size: 25px; - height: variables.$manual-game-time-height; - } + .manual-game-time { + width: calc(100% - math.div(variables.$ui-margin, 2)); + color: variables.$button-text-color; + background-color: transparent; + border: none; + border-bottom: 2px solid #aaa; + text-align: right; + font-family: "fira", sans-serif; + font-weight: bold; + font-size: 25px; + height: variables.$manual-game-time-height; + } - .manual-game-time:focus { - outline: none; - } + .manual-game-time:focus { + outline: none; + } - .manual-game-time::placeholder { - font-size: 15px; - } + .manual-game-time::placeholder { + font-size: 15px; + } - @include mobile.mobile { - padding: 0 variables.$ui-margin; - width: calc(100% - #{2 * variables.$ui-margin}) !important; - } + @include mobile.mobile { + padding: 0 variables.$ui-margin; + width: calc(100% - #{2 * variables.$ui-margin}) !important; + } } diff --git a/src/css/Toggle.scss b/src/css/Toggle.scss index d139046..3ef8616 100644 --- a/src/css/Toggle.scss +++ b/src/css/Toggle.scss @@ -1,28 +1,28 @@ -@use 'variables.icss'; +@use "variables.icss"; @mixin toggle { - .toggle-left { - border-top-right-radius: 0; - border-bottom-right-radius: 0; - } + .toggle-left { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } - .toggle-right { - border-top-left-radius: 0; - border-bottom-left-radius: 0; - } + .toggle-right { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } - .toggle-middle { - border-radius: 0; - } + .toggle-middle { + border-radius: 0; + } - .button-pressed, - .button-pressed:hover { - background: variables.$button-active-color; - } + .button-pressed, + .button-pressed:hover { + background: variables.$button-active-color; + } - .button-pressed.disabled { - background: variables.$button-disabled-color; - color: variables.$button-disabled-text-color; - cursor: default; - } + .button-pressed.disabled { + background: variables.$button-disabled-color; + color: variables.$button-disabled-text-color; + cursor: default; + } } diff --git a/src/css/Tooltip.scss b/src/css/Tooltip.scss index b1ea6e4..a4395a4 100644 --- a/src/css/Tooltip.scss +++ b/src/css/Tooltip.scss @@ -1,5 +1,5 @@ -@use 'mobile'; -@use 'variables.icss'; +@use "mobile"; +@use "variables.icss"; .tooltip { position: relative; @@ -8,9 +8,9 @@ .tooltip .tooltip-text { visibility: hidden; width: 300px; - background-color: rgba(28, 28, 28, .8); + background-color: rgba(28, 28, 28, 0.8); backdrop-filter: blur(5px); - border: 1px solid rgba(255, 255, 255, .25); + border: 1px solid rgba(255, 255, 255, 0.25); color: #fff; text-align: center; border-radius: 6px; diff --git a/src/css/browser_source.scss b/src/css/browser_source.scss index 11212aa..527f903 100644 --- a/src/css/browser_source.scss +++ b/src/css/browser_source.scss @@ -1,17 +1,17 @@ body.browser-source { - &, - .livesplit-container, - .buttons { - background-color: rgba(0, 0, 0, 0); - overflow: hidden !important; - } + &, + .livesplit-container, + .buttons { + background-color: rgba(0, 0, 0, 0); + overflow: hidden !important; + } - .sidebar { - box-shadow: none !important; - } + .sidebar { + box-shadow: none !important; + } - .buttons, - .sidebar-button { - visibility: hidden; - } + .buttons, + .sidebar-button { + visibility: hidden; + } } diff --git a/src/css/main.scss b/src/css/main.scss index 4243a3b..3acde7f 100644 --- a/src/css/main.scss +++ b/src/css/main.scss @@ -1,159 +1,159 @@ -@use 'sass:math'; +@use "sass:math"; -@use 'browser_source'; -@use 'variables.icss'; +@use "browser_source"; +@use "variables.icss"; @font-face { - font-family: timer; - font-display: swap; - src: url(timer.woff); + font-family: timer; + font-display: swap; + src: url(timer.woff); } @font-face { - font-family: fira; - font-display: swap; - src: url(FiraSans-Regular.woff); + font-family: fira; + font-display: swap; + src: url(FiraSans-Regular.woff); } body { - font-family: "fira", sans-serif; - color: #eee; - text-shadow: 2px 2px 2px rgba(0, 0, 0, 0.5); - background: variables.$main-background-color; + font-family: "fira", sans-serif; + color: #eee; + text-shadow: 2px 2px 2px rgba(0, 0, 0, 0.5); + background: variables.$main-background-color; } .Toastify { - .toast-class { - font-family: "fira", sans-serif; - background: rgba(28, 28, 28, .8) !important; - backdrop-filter: blur(5px); - border: 2px solid rgba(255, 255, 255, .25) !important; - border-radius: 10px !important; - min-height: 48px !important; + .toast-class { + font-family: "fira", sans-serif; + background: rgba(28, 28, 28, 0.8) !important; + backdrop-filter: blur(5px); + border: 2px solid rgba(255, 255, 255, 0.25) !important; + border-radius: 10px !important; + min-height: 48px !important; - &.toast-bug { - border-color: red !important; + &.toast-bug { + border-color: red !important; - a { - color: red; - } + a { + color: red; + } + } } - } } .toast-body { - margin: 5px 5px 5px 5px !important; + margin: 5px 5px 5px 5px !important; } -.toast-class>button { - margin-bottom: initial; - margin-top: initial; +.toast-class > button { + margin-bottom: initial; + margin-top: initial; } #wrapper { - transition: all 0.5s ease; + transition: all 0.5s ease; } td#dif { - border-right: 10px solid transparent; - text-align: right; + border-right: 10px solid transparent; + text-align: right; } table { - border-spacing: 0 2px; + border-spacing: 0 2px; } button:active, button:active:hover { - background: variables.$button-active-color; + background: variables.$button-active-color; } button:focus, div:focus { - outline: 0; + outline: 0; } button:hover { - background: variables.$button-hover-color; + background: variables.$button-hover-color; } button { - display: flex; - align-items: center; - justify-content: center; - margin: 0; - gap: 0.25em; - background: variables.$button-color; - color: variables.$button-text-color; - cursor: pointer; - font-size: 20px; - line-height: 1.1; - border-width: 1px; - border-style: solid; - border-color: variables.$border-color; - border-image: initial; - border-radius: 5px; - font-weight: bold; - font-family: "fira", sans-serif; - min-height: variables.$button-height; - padding: 5px variables.$ui-margin; + display: flex; + align-items: center; + justify-content: center; + margin: 0; + gap: 0.25em; + background: variables.$button-color; + color: variables.$button-text-color; + cursor: pointer; + font-size: 20px; + line-height: 1.1; + border-width: 1px; + border-style: solid; + border-color: variables.$border-color; + border-image: initial; + border-radius: 5px; + font-weight: bold; + font-family: "fira", sans-serif; + min-height: variables.$button-height; + padding: 5px variables.$ui-margin; } a { - color: hsla(50, 100%, 50%, 1); - text-decoration: none; + color: hsla(50, 100%, 50%, 1); + text-decoration: none; } a:hover { - text-decoration: underline; + text-decoration: underline; } button:disabled, button:disabled:active { - background: variables.$button-disabled-color; - color: variables.$button-disabled-text-color; - cursor: default; + background: variables.$button-disabled-color; + color: variables.$button-disabled-text-color; + cursor: default; } .chrome-picker { - font-family: inherit !important; - margin-top: 5px; - margin-left: -50%; + font-family: inherit !important; + margin-top: 5px; + margin-left: -50%; } /* WebKit Scrollbars */ ::-webkit-scrollbar { - width: 10px; - height: 10px; + width: 10px; + height: 10px; } ::-webkit-scrollbar-thumb { - background-clip: padding-box; - background-color: #303030; - border: 0 solid #0000; - border-radius: 10px; + background-clip: padding-box; + background-color: #303030; + border: 0 solid #0000; + border-radius: 10px; } ::-webkit-scrollbar-track-piece { - background-color: #0000; + background-color: #0000; } ::-webkit-scrollbar-corner { - background-color: hsla(0, 0%, 9%, 1); + background-color: hsla(0, 0%, 9%, 1); } .initial-load { - position: absolute; - left: 0; - top: 0; - width: 100%; - height: 100%; - display: flex; - justify-content: center; - align-items: center; - font-size: 50px; + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + font-size: 50px; - .initial-load-text { - margin-left: 10px; - } + .initial-load-text { + margin-left: 10px; + } } diff --git a/src/css/mobile.scss b/src/css/mobile.scss index 53bb42e..dd92f81 100644 --- a/src/css/mobile.scss +++ b/src/css/mobile.scss @@ -1,5 +1,5 @@ @mixin mobile { - .is-mobile & { - @content; - } + .is-mobile & { + @content; + } } diff --git a/src/css/variables.icss.scss b/src/css/variables.icss.scss index 7fb7350..faf492e 100644 --- a/src/css/variables.icss.scss +++ b/src/css/variables.icss.scss @@ -1,4 +1,4 @@ -@use 'sass:math'; +@use "sass:math"; $ui-margin: 8px; $ui-large-margin: 16px; @@ -24,19 +24,25 @@ $button-height: 40px; $manual-game-time-height: 25px; $main-background-color: #171717; -$sidebar-background-color: #1A1A1A; +$sidebar-background-color: #1a1a1a; -$dark-row-color: #0B0B0B; +$dark-row-color: #0b0b0b; $header-row-color: #090909; $hover-row-color: #404040; $light-row-color: #121212; -$selected-row-color: linear-gradient(rgb(51, 115, 244) 0%, rgb(21, 53, 116) 100%); -$selected-row-hover-color: linear-gradient(hsl(220, 90%, 70%) 0%, hsl(220, 69%, 40%) 100%); +$selected-row-color: linear-gradient( + rgb(51, 115, 244) 0%, + rgb(21, 53, 116) 100% +); +$selected-row-hover-color: linear-gradient( + hsl(220, 90%, 70%) 0%, + hsl(220, 69%, 40%) 100% +); :export { - buttonHeight: $button-height; - largeMargin: $ui-large-margin; - manualGameTimeHeight: $manual-game-time-height; - contributorAvatarSize: $contributor-avatar-size; + buttonHeight: $button-height; + largeMargin: $ui-large-margin; + manualGameTimeHeight: $manual-game-time-height; + contributorAvatarSize: $contributor-avatar-size; } diff --git a/src/index.html b/src/index.html index 1d1e2d5..e8c46f4 100644 --- a/src/index.html +++ b/src/index.html @@ -1,19 +1,23 @@ - + + + + + + LiveSplit One + - - - - - LiveSplit One - - - -
-
-
Loading...
-
-
- - + +
+
+
Loading...
+
+
+ diff --git a/src/index.tsx b/src/index.tsx index d38dba0..6e7c152 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -7,7 +7,7 @@ if (typeof Symbol.dispose !== "symbol") { configurable: false, enumerable: false, writable: false, - value: Symbol.for("dispose") + value: Symbol.for("dispose"), }); } @@ -16,11 +16,15 @@ if (typeof Symbol.asyncDispose !== "symbol") { configurable: false, enumerable: false, writable: false, - value: Symbol.for("asyncDispose") + value: Symbol.for("asyncDispose"), }); } -if (process.env.NODE_ENV === "production" && window.__TAURI__ == null && "serviceWorker" in navigator) { +if ( + process.env.NODE_ENV === "production" && + window.__TAURI__ == null && + "serviceWorker" in navigator +) { navigator.serviceWorker.register("/service-worker.js"); } @@ -54,8 +58,8 @@ try { requestWakeLock(); - document.addEventListener('visibilitychange', () => { - if (document.visibilityState === 'visible') { + document.addEventListener("visibilitychange", () => { + if (document.visibilityState === "visible") { requestWakeLock(); } }); @@ -65,8 +69,8 @@ try { try { const promises = []; // TypeScript doesn't seem to know that the fonts are iterable. - for (const fontFace of (document.fonts as any as Iterable)) { - if (fontFace.family === 'timer' || fontFace.family === 'fira') { + for (const fontFace of document.fonts as any as Iterable) { + if (fontFace.family === "timer" || fontFace.family === "fira") { promises.push(fontFace.load()); } } diff --git a/src/layout/Layout.tsx b/src/layout/Layout.tsx index a95308b..2ff3233 100644 --- a/src/layout/Layout.tsx +++ b/src/layout/Layout.tsx @@ -8,20 +8,23 @@ import "../css/Layout.scss"; import { GeneralSettings } from "../ui/MainSettings"; export interface Props { - getState: () => LayoutStateRef, - layoutUrlCache: UrlCache, - allowResize: boolean, - width: number, - height: number, - generalSettings: GeneralSettings, - renderer: WebRenderer, - onResize(width: number, height: number): void, + getState: () => LayoutStateRef; + layoutUrlCache: UrlCache; + allowResize: boolean; + width: number; + height: number; + generalSettings: GeneralSettings; + renderer: WebRenderer; + onResize(width: number, height: number): void; } export default class Layout extends React.Component { public refreshLayout() { const layoutState = this.props.getState(); - const newDims = this.props.renderer.render(layoutState.ptr, this.props.layoutUrlCache.imageCache.ptr); + const newDims = this.props.renderer.render( + layoutState.ptr, + this.props.layoutUrlCache.imageCache.ptr, + ); if (newDims !== undefined) { this.props.onResize(newDims[0], newDims[1]); } @@ -33,39 +36,77 @@ export default class Layout extends React.Component { frameRate={this.props.generalSettings.frameRate} update={() => this.refreshLayout()} > -
+
{ element?.appendChild(this.props.renderer.element()); }} + ref={(element) => { + element?.appendChild(this.props.renderer.element()); + }} /> - { - this.props.allowResize &&
+ {this.props.allowResize && ( +
e.stopPropagation()} className="resizable-handle-east" />} - onResize={(_event, data) => this.props.onResize(data.size.width, data.size.height)} + handle={ +
e.stopPropagation()} + className="resizable-handle-east" + /> + } + onResize={(_event, data) => + this.props.onResize( + data.size.width, + data.size.height, + ) + } /> e.stopPropagation()} className="resizable-handle-south" />} - onResize={(_event, data) => this.props.onResize(data.size.width, data.size.height)} + handle={ +
e.stopPropagation()} + className="resizable-handle-south" + /> + } + onResize={(_event, data) => + this.props.onResize( + data.size.width, + data.size.height, + ) + } /> e.stopPropagation()} className="resizable-handle-south-east" />} - onResize={(_event, data) => this.props.onResize(data.size.width, data.size.height)} + handle={ +
e.stopPropagation()} + className="resizable-handle-south-east" + /> + } + onResize={(_event, data) => + this.props.onResize( + data.size.width, + data.size.height, + ) + } />
- } + )}
); diff --git a/src/platform/CORS.ts b/src/platform/CORS.ts index 42225ef..a7ec766 100644 --- a/src/platform/CORS.ts +++ b/src/platform/CORS.ts @@ -1,4 +1,7 @@ -export async function corsBustingFetch(url: string, signal?: AbortSignal): Promise { +export async function corsBustingFetch( + url: string, + signal?: AbortSignal, +): Promise { let response: Response | undefined; if (window.__TAURI__ != null) { response = await window.__TAURI__.http.fetch(url, { signal }); diff --git a/src/platform/Hotkeys.ts b/src/platform/Hotkeys.ts index 5800f2b..cfd3947 100644 --- a/src/platform/Hotkeys.ts +++ b/src/platform/Hotkeys.ts @@ -2,24 +2,29 @@ import { CommandSinkRef, HotkeyConfig, HotkeySystem } from "../livesplit-core"; import { expect } from "../util/OptionUtil"; export interface HotkeyImplementation { - config(): Promise | HotkeyConfig, - setConfig(config: HotkeyConfig): void, - activate(): void, - deactivate(): void, - resolve(keyCode: string): Promise | string, + config(): Promise | HotkeyConfig; + setConfig(config: HotkeyConfig): void; + activate(): void; + deactivate(): void; + resolve(keyCode: string): Promise | string; } class GlobalHotkeys implements HotkeyImplementation { - constructor(private hotkeySystem?: HotkeySystem) { } + constructor(private hotkeySystem?: HotkeySystem) {} public async config(): Promise { - return expect(HotkeyConfig.parseJson( - await window.__TAURI__!.core.invoke("get_hotkey_config"), - ), "Couldn't parse the hotkey config."); + return expect( + HotkeyConfig.parseJson( + await window.__TAURI__!.core.invoke("get_hotkey_config"), + ), + "Couldn't parse the hotkey config.", + ); } public setConfig(config: HotkeyConfig): void { - window.__TAURI__!.core.invoke("set_hotkey_config", { config: config.asJson() }); + window.__TAURI__!.core.invoke("set_hotkey_config", { + config: config.asJson(), + }); if (this.hotkeySystem != null) { this.hotkeySystem.setConfig(config); } else { @@ -28,7 +33,9 @@ class GlobalHotkeys implements HotkeyImplementation { } setConfigJson(configJson: unknown): void { - window.__TAURI__!.core.invoke("set_hotkey_config", { config: configJson }); + window.__TAURI__!.core.invoke("set_hotkey_config", { + config: configJson, + }); if (this.hotkeySystem != null) { const config = HotkeyConfig.parseJson(configJson); if (config != null) { @@ -38,12 +45,16 @@ class GlobalHotkeys implements HotkeyImplementation { } public activate(): void { - window.__TAURI__!.core.invoke("set_hotkey_activation", { active: true }); + window.__TAURI__!.core.invoke("set_hotkey_activation", { + active: true, + }); this.hotkeySystem?.activate(); } public deactivate(): void { - window.__TAURI__!.core.invoke("set_hotkey_activation", { active: false }); + window.__TAURI__!.core.invoke("set_hotkey_activation", { + active: false, + }); this.hotkeySystem?.deactivate(); } @@ -52,7 +63,10 @@ class GlobalHotkeys implements HotkeyImplementation { } } -export function createHotkeys(commandSink: CommandSinkRef, configJson: unknown): HotkeyImplementation { +export function createHotkeys( + commandSink: CommandSinkRef, + configJson: unknown, +): HotkeyImplementation { let hotkeySystem: HotkeySystem | null = null; const tauri = window.__TAURI__ != null; @@ -63,7 +77,9 @@ export function createHotkeys(commandSink: CommandSinkRef, configJson: unknown): if (config !== null) { hotkeySystem = HotkeySystem.withConfig(commandSink, config); } - } catch (_) { /* Looks like the storage has no valid data */ } + } catch (_) { + /* Looks like the storage has no valid data */ + } if (hotkeySystem == null) { hotkeySystem = expect( diff --git a/src/storage/index.tsx b/src/storage/index.tsx index 0916bd8..cdb2184 100644 --- a/src/storage/index.tsx +++ b/src/storage/index.tsx @@ -1,7 +1,10 @@ import { openDB, IDBPDatabase } from "idb"; import { Option, assert } from "../util/OptionUtil"; import { RunRef, Run, TimingMethod } from "../livesplit-core"; -import { GeneralSettings, MANUAL_GAME_TIME_SETTINGS_DEFAULT } from "../ui/MainSettings"; +import { + GeneralSettings, + MANUAL_GAME_TIME_SETTINGS_DEFAULT, +} from "../ui/MainSettings"; import { FRAME_RATE_AUTOMATIC } from "../util/FrameRate"; export type HotkeyConfigSettings = unknown; @@ -13,10 +16,10 @@ const DEFAULT_LAYOUT_HEIGHT = 500; let db: Option>> = null; export interface SplitsInfo { - game: string, - category: string, - realTime?: number, - gameTime?: number, + game: string; + category: string; + realTime?: number; + gameTime?: number; } function getSplitsInfo(run: RunRef): SplitsInfo { @@ -90,7 +93,10 @@ function getDb(): Promise> { const hotkeys = localStorage.getItem("settings"); if (hotkeys) { - settingsStore.put(JSON.parse(hotkeys).hotkeys, "hotkeys"); + settingsStore.put( + JSON.parse(hotkeys).hotkeys, + "hotkeys", + ); } const layoutWidth = localStorage.getItem("layoutWidth"); @@ -105,13 +111,13 @@ function getDb(): Promise> { return db; } -export async function storeRunWithoutDisposing(run: RunRef, key: number | undefined) { - await storeSplits( - (callback) => { - callback(run, run.saveAsLssBytes()); - }, - key, - ); +export async function storeRunWithoutDisposing( + run: RunRef, + key: number | undefined, +) { + await storeSplits((callback) => { + callback(run, run.saveAsLssBytes()); + }, key); } export async function storeRunAndDispose(run: Run, key: number | undefined) { @@ -219,8 +225,8 @@ export async function loadLayoutDims(): Promise<[number, number]> { const db = await getDb(); return [ - await db.get("settings", "layoutWidth") ?? DEFAULT_LAYOUT_WIDTH, - await db.get("settings", "layoutHeight") ?? DEFAULT_LAYOUT_HEIGHT, + (await db.get("settings", "layoutWidth")) ?? DEFAULT_LAYOUT_WIDTH, + (await db.get("settings", "layoutHeight")) ?? DEFAULT_LAYOUT_HEIGHT, ]; } @@ -245,7 +251,7 @@ export async function storeGeneralSettings(generalSettings: GeneralSettings) { export async function loadGeneralSettings(): Promise { const db = await getDb(); - const generalSettings = await db.get("settings", "generalSettings") ?? {}; + const generalSettings = (await db.get("settings", "generalSettings")) ?? {}; const isTauri = window.__TAURI__ != null; @@ -260,7 +266,8 @@ export async function loadGeneralSettings(): Promise { saveOnReset: generalSettings.saveOnReset ?? false, speedrunComIntegration: generalSettings.speedrunComIntegration ?? true, serverUrl: generalSettings.serverUrl, - alwaysOnTop: generalSettings.alwaysOnTop ?? (isTauri ? true : undefined), + alwaysOnTop: + generalSettings.alwaysOnTop ?? (isTauri ? true : undefined), }; } @@ -273,7 +280,7 @@ export async function storeTimingMethod(timingMethod: TimingMethod) { export async function loadTimingMethod(): Promise { const db = await getDb(); - return await db.get("settings", "timingMethod") ?? TimingMethod.RealTime; + return (await db.get("settings", "timingMethod")) ?? TimingMethod.RealTime; } export async function storeComparison(comparison: string) { diff --git a/src/type-definitions/images.d.ts b/src/type-definitions/images.d.ts index 63199ce..dd4ab7e 100644 --- a/src/type-definitions/images.d.ts +++ b/src/type-definitions/images.d.ts @@ -1 +1 @@ -declare module '*.svg' +declare module "*.svg"; diff --git a/src/type-definitions/scss.d.ts b/src/type-definitions/scss.d.ts index 3c187f6..fbb911b 100644 --- a/src/type-definitions/scss.d.ts +++ b/src/type-definitions/scss.d.ts @@ -1,2 +1,2 @@ -declare module '*.scss' -declare module '*.css' +declare module "*.scss"; +declare module "*.css"; diff --git a/src/type-definitions/tauri.d.ts b/src/type-definitions/tauri.d.ts index 1f013c9..b849355 100644 --- a/src/type-definitions/tauri.d.ts +++ b/src/type-definitions/tauri.d.ts @@ -12,7 +12,10 @@ declare interface CoreModule { } declare interface TauriEventModule { - listen(eventName: string, callback: (event: TauriEvent) => void): Promise; + listen( + eventName: string, + callback: (event: TauriEvent) => void, + ): Promise; } declare interface TauriNotificationModule { @@ -35,4 +38,4 @@ declare interface TauriEvent { payload: unknown; } -declare interface ListenHandle { } +declare interface ListenHandle {} diff --git a/src/type-definitions/wasm.d.ts b/src/type-definitions/wasm.d.ts index d2a6ba4..26c59c0 100644 --- a/src/type-definitions/wasm.d.ts +++ b/src/type-definitions/wasm.d.ts @@ -1 +1 @@ -declare module '*.wasm' +declare module "*.wasm"; diff --git a/src/type-definitions/webpack-globals.d.ts b/src/type-definitions/webpack-globals.d.ts index f49f5d7..bcfec39 100644 --- a/src/type-definitions/webpack-globals.d.ts +++ b/src/type-definitions/webpack-globals.d.ts @@ -4,12 +4,12 @@ declare const CONTRIBUTORS_LIST: Contributor[]; declare const CHANGELOG: ChangelogEntry[]; declare interface Contributor { - id: string, - name: string, + id: string; + name: string; } declare interface ChangelogEntry { - id: string, - message: string, - date: string, + id: string; + message: string; + date: string; } diff --git a/src/ui/About.tsx b/src/ui/About.tsx index 0958cb0..eba0e6a 100644 --- a/src/ui/About.tsx +++ b/src/ui/About.tsx @@ -8,12 +8,15 @@ import { ArrowLeft } from "lucide-react"; import "../css/About.scss"; export interface Props { - callbacks: Callbacks, + callbacks: Callbacks; } interface Callbacks { - renderViewWithSidebar(renderedView: React.JSX.Element, sidebarContent: React.JSX.Element): React.JSX.Element, - openTimerView(): void, + renderViewWithSidebar( + renderedView: React.JSX.Element, + sidebarContent: React.JSX.Element, + ): React.JSX.Element; + openTimerView(): void; } const contributorAvatarSize = parseFloat(variables.contributorAvatarSize); @@ -22,11 +25,16 @@ export class About extends React.Component { public render() { const renderedView = this.renderView(); const sidebarContent = this.renderSidebarContent(); - return this.props.callbacks.renderViewWithSidebar(renderedView, sidebarContent); + return this.props.callbacks.renderViewWithSidebar( + renderedView, + sidebarContent, + ); } private renderView() { - const idealAvatarResolution = Math.round(devicePixelRatio * contributorAvatarSize); + const idealAvatarResolution = Math.round( + devicePixelRatio * contributorAvatarSize, + ); return (
@@ -38,43 +46,56 @@ export class About extends React.Component {
LiveSplit One

- + Version: {BUILD_DATE}

-

LiveSplit One is a multiplatform version of LiveSplit, the sleek, - highly-customizable timer for speedrunners.

- + LiveSplit One is a multiplatform version of LiveSplit, + the sleek, highly-customizable timer for speedrunners. +

+

+ View Source Code on GitHub

Recent Changes

- { - CHANGELOG.map((change) => ( - <> - - {change.date} - - - - )) - } + {CHANGELOG.map((change) => ( + <> + + {change.date} + + + + ))}

Contributors

- { - CONTRIBUTORS_LIST.map((contributor) => ( - - (e.target as any).remove()} - /> - {contributor.name} - - )) - } + {CONTRIBUTORS_LIST.map((contributor) => ( + + (e.target as any).remove()} + /> + {contributor.name} + + ))}
diff --git a/src/ui/ColorPicker.tsx b/src/ui/ColorPicker.tsx index 4175605..1ede999 100644 --- a/src/ui/ColorPicker.tsx +++ b/src/ui/ColorPicker.tsx @@ -17,7 +17,13 @@ function colorToCss(color: Color): string { return `rgba(${r},${g},${b},${a})`; } -export default function ColorPicker({ color, setColor }: { color: Color, setColor: (color: Color) => void }) { +export default function ColorPicker({ + color, + setColor, +}: { + color: Color; + setColor: (color: Color) => void; +}) { const [isShowing, setIsShowing] = useState(false); return (
@@ -27,17 +33,31 @@ export default function ColorPicker({ color, setColor }: { color: Color, setColo onClick={() => setIsShowing(true)} />
- {isShowing && setIsShowing(false)} />} + {isShowing && ( + setIsShowing(false)} + /> + )}
); } -function ColorPickerDialog({ color, setColor, close }: { color: Color, setColor: (color: Color) => void, close: () => void }) { +function ColorPickerDialog({ + color, + setColor, + close, +}: { + color: Color; + setColor: (color: Color) => void; + close: () => void; +}) { return ( <>
-
+

@@ -52,7 +72,13 @@ function Hr() { return
; } -function GradientSelector({ color, setColor }: { color: Color, setColor: (color: Color) => void }) { +function GradientSelector({ + color, + setColor, +}: { + color: Color; + setColor: (color: Color) => void; +}) { const [r, g, b, a] = color; const [h, s, v] = rgbToHsv(r, g, b); const [r1, g1, b1] = hsvToRgb(h, 1, 1); @@ -98,9 +124,14 @@ function GradientSelector({ color, setColor }: { color: Color, setColor: (color: }} >
-
+
@@ -115,7 +146,13 @@ function nextMode(mode: Mode): Mode { return mode === "Rgb" ? "Hsv" : mode === "Hsv" ? "Hex" : "Rgb"; } -function ControlPanel({ color, setColor }: { color: Color, setColor: (color: Color) => void }) { +function ControlPanel({ + color, + setColor, +}: { + color: Color; + setColor: (color: Color) => void; +}) { const [r, g, b, a] = color; const [h, s, v] = rgbToHsv(r, g, b); const [mode, setMode] = useState("Hex"); @@ -151,7 +188,9 @@ function ControlPanel({ color, setColor }: { color: Color, setColor: (color: Col
@@ -189,29 +228,32 @@ function parseHex(hex: string): [number, number, number] | undefined { return; } if (hex.length === 6) { - const r = ((num >> 16) & 0xFF) / 255; - const g = ((num >> 8) & 0xFF) / 255; - const b = (num & 0xFF) / 255; + const r = ((num >> 16) & 0xff) / 255; + const g = ((num >> 8) & 0xff) / 255; + const b = (num & 0xff) / 255; return [r, g, b]; } else if (hex.length === 3) { - const r = ((num >> 8) & 0xF) * 0x11 / 255; - const g = ((num >> 4) & 0xF) * 0x11 / 255; - const b = (num & 0xF) * 0x11 / 255; + const r = (((num >> 8) & 0xf) * 0x11) / 255; + const g = (((num >> 4) & 0xf) * 0x11) / 255; + const b = ((num & 0xf) * 0x11) / 255; return [r, g, b]; } else { return; } } -function ColorPreview({ color, setColor }: { color: Color, setColor: (color: Color) => void }) { +function ColorPreview({ + color, + setColor, +}: { + color: Color; + setColor: (color: Color) => void; +}) { const [r, g, b, a] = color; return ( <> {hasEyeDropper && ( -
+
)} -
+
@@ -247,7 +288,13 @@ function ColorPreview({ color, setColor }: { color: Color, setColor: (color: Col ); } -function Hsva({ color, setColor }: { color: Color, setColor: (color: Color) => void }) { +function Hsva({ + color, + setColor, +}: { + color: Color; + setColor: (color: Color) => void; +}) { const [r, g, b, a] = color; const [h, s, v] = rgbToHsv(r, g, b); @@ -296,7 +343,13 @@ function Hsva({ color, setColor }: { color: Color, setColor: (color: Color) => v ); } -function Rgba({ color, setColor }: { color: Color, setColor: (color: Color) => void }) { +function Rgba({ + color, + setColor, +}: { + color: Color; + setColor: (color: Color) => void; +}) { const [r, g, b, a] = color; return ( @@ -341,7 +394,13 @@ function Rgba({ color, setColor }: { color: Color, setColor: (color: Color) => v ); } -function Hex({ color, setColor }: { color: Color, setColor: (color: Color) => void }) { +function Hex({ + color, + setColor, +}: { + color: Color; + setColor: (color: Color) => void; +}) { const [r, g, b, a] = color; return ( @@ -349,7 +408,15 @@ function Hex({ color, setColor }: { color: Color, setColor: (color: Color) => vo
`#${Math.round(255 * r).toString(16).padStart(2, '0')}${Math.round(255 * g).toString(16).padStart(2, '0')}${Math.round(255 * b).toString(16).padStart(2, '0')}`.toUpperCase()} + format={([r, g, b, _]) => + `#${Math.round(255 * r) + .toString(16) + .padStart(2, "0")}${Math.round(255 * g) + .toString(16) + .padStart(2, "0")}${Math.round(255 * b) + .toString(16) + .padStart(2, "0")}`.toUpperCase() + } parse={(value) => { const parsed = parseHex(value); if (parsed) { @@ -374,11 +441,16 @@ function Hex({ color, setColor }: { color: Color, setColor: (color: Color) => vo ); } -function FormattedInput({ value, format, parse, setValue }: { - value: T, - format: (value: T) => string, - parse: (value: string) => T | undefined, - setValue: (value: T) => void, +function FormattedInput({ + value, + format, + parse, + setValue, +}: { + value: T; + format: (value: T) => string; + parse: (value: string) => T | undefined; + setValue: (value: T) => void; }) { const [formatted, setFormatted] = useState(() => format(value)); const [isValid, setIsValid] = useState(true); @@ -411,26 +483,55 @@ function FormattedInput({ value, format, parse, setValue }: { value={formatted} onChange={handleInputChange} onBlur={() => { - setFormatted(format(value)) + setFormatted(format(value)); setIsValid(true); }} /> - ) + ); } type Format = "Degree" | "Percent" | "Byte"; -function ColorComponent({ title, short, kind, value, setValue }: { title: string, short: string, kind: Format, value: number, setValue: (value: number) => void }) { +function ColorComponent({ + title, + short, + kind, + value, + setValue, +}: { + title: string; + short: string; + kind: Format; + value: number; + setValue: (value: number) => void; +}) { return (
kind === "Degree" ? `${value.toFixed(0)}°` : kind === "Percent" ? `${(100 * value).toFixed(0)}%` : `${(255 * value).toFixed(0)}`} + format={(value) => + kind === "Degree" + ? `${value.toFixed(0)}°` + : kind === "Percent" + ? `${(100 * value).toFixed(0)}%` + : `${(255 * value).toFixed(0)}` + } parse={(value) => { - const newValue = parseFloat(value.replace(/[^0-9.]/g, '')); - const max = kind === "Degree" ? 360 : kind === "Percent" ? 100 : 255; - const scale = kind === "Degree" ? 1 : kind === "Percent" ? 0.01 : 1 / 255; - if (newValue < 0 || newValue > max || isNaN(newValue)) return undefined; + const newValue = parseFloat(value.replace(/[^0-9.]/g, "")); + const max = + kind === "Degree" + ? 360 + : kind === "Percent" + ? 100 + : 255; + const scale = + kind === "Degree" + ? 1 + : kind === "Percent" + ? 0.01 + : 1 / 255; + if (newValue < 0 || newValue > max || isNaN(newValue)) + return undefined; return newValue * scale; }} setValue={setValue} @@ -476,7 +577,11 @@ function PredefinedColors({ setColor }: { setColor: (color: Color) => void }) { {predefinedColors.map((hsv, index) => (
{hsv.map((color, i) => ( - + ))}
))} @@ -484,7 +589,13 @@ function PredefinedColors({ setColor }: { setColor: (color: Color) => void }) { ); } -function PredefinedColor({ color: [title, r, g, b], setColor }: { color: [string, number, number, number], setColor: (color: Color) => void }) { +function PredefinedColor({ + color: [title, r, g, b], + setColor, +}: { + color: [string, number, number, number]; + setColor: (color: Color) => void; +}) { return ( ; - }) + return ( + { + dialogElement = element; + }} + onKeyDown={(e) => { + if (e?.key === "ArrowLeft") { + e.preventDefault(); + ( + document.activeElement + ?.previousElementSibling as any + )?.focus(); + } else if (e?.key === "ArrowRight") { + e.preventDefault(); + ( + document.activeElement?.nextElementSibling as any + )?.focus(); } + }} + > +
+

{this.state.options.title}

+

{this.state.options.description}

+ {this.state.options.textInput && ( + + this.setState({ input: e.target.value }) + } + onKeyDown={(e) => { + if (e?.key === "Enter") { + e.preventDefault(); + this.close(0); + } + }} + /> + )} +
+ {this.state.options.buttons.map((button, i) => { + return ( + + ); + })} +
-
- ; + + ); } private close(i: number) { diff --git a/src/ui/DragUpload.tsx b/src/ui/DragUpload.tsx index 59b584c..b9c290e 100644 --- a/src/ui/DragUpload.tsx +++ b/src/ui/DragUpload.tsx @@ -4,15 +4,17 @@ import { toast } from "react-toastify"; import "../css/DragUpload.scss"; export interface Props { - children: React.ReactNode, - importLayout?: (file: File) => Promise, - importSplits(file: File): Promise, + children: React.ReactNode; + importLayout?: (file: File) => Promise; + importSplits(file: File): Promise; } export default class DragUpload extends React.Component { public componentDidMount() { const dropZone = document.getElementById("upload-drop-zone"); - const dropZoneOverlay = document.getElementById("upload-drop-zone-overlay"); + const dropZoneOverlay = document.getElementById( + "upload-drop-zone-overlay", + ); const importLayout = this.props.importLayout; const importSplits = this.props.importSplits; @@ -30,7 +32,8 @@ export default class DragUpload extends React.Component { }); dropZone.addEventListener("dragleave", (event) => { - if (dropZoneOverlay && + if ( + dropZoneOverlay && (event.pageX < 10 || event.pageY < 10 || window.innerWidth - event.pageX < 10 || @@ -77,9 +80,7 @@ export default class DragUpload extends React.Component { return (
-
- Waiting for drop... -
+
Waiting for drop...
{this.props.children}
diff --git a/src/ui/Embed.tsx b/src/ui/Embed.tsx index b77732f..e640f31 100644 --- a/src/ui/Embed.tsx +++ b/src/ui/Embed.tsx @@ -10,7 +10,13 @@ export function resolveEmbed(uri: string): Option { if (twitch != null) { return twitch; } - return

{uri}

; + return ( +

+ + {uri} + +

+ ); } function tryYoutubeFromUri(uri: string): Option { @@ -56,10 +62,14 @@ function videoIframe(videoSource: string): React.JSX.Element { } function resolveYoutube(videoId: string): React.JSX.Element { - return videoIframe(`https://www.youtube.com/embed/${videoId}?wmode=transparent`); + return videoIframe( + `https://www.youtube.com/embed/${videoId}?wmode=transparent`, + ); } function resolveTwitch(videoId: string): React.JSX.Element { const domain = window.location.hostname; - return videoIframe(`https://player.twitch.tv/?video=${videoId}&parent=${domain}&autoplay=false`); + return videoIframe( + `https://player.twitch.tv/?video=${videoId}&parent=${domain}&autoplay=false`, + ); } diff --git a/src/ui/HotkeyButton.tsx b/src/ui/HotkeyButton.tsx index a739963..ca23b15 100644 --- a/src/ui/HotkeyButton.tsx +++ b/src/ui/HotkeyButton.tsx @@ -6,18 +6,21 @@ import { Circle, Trash } from "lucide-react"; import "../css/HotkeyButton.scss"; function resolveKey(keyCode: string): Promise | string { - return expect(hotkeySystem, "The Hotkey System should always be initialized.").resolve(keyCode); + return expect( + hotkeySystem, + "The Hotkey System should always be initialized.", + ).resolve(keyCode); } export interface Props { - value: Option, - setValue: (value: Option) => void, + value: Option; + setValue: (value: Option) => void; } export interface State { - listener: Option, - intervalHandle: Option, - resolvedKey: Option, + listener: Option; + intervalHandle: Option; + resolvedKey: Option; } export default class HotkeyButton extends React.Component { @@ -56,27 +59,35 @@ export default class HotkeyButton extends React.Component { if (this.props.value != null) { buttonText = this.state.resolvedKey; } else if (this.state.listener != null) { - buttonText = ; + buttonText = ( + + ); } return (
- { - map(this.props.value, () => ( - this.props.setValue(null)} /> - )) - } - {this.state.listener != null && + {map(this.props.value, () => ( + this.props.setValue(null)} + /> + ))} + {this.state.listener != null && (
{ }} onClick={() => this.blurButton()} /> - } + )}
); } @@ -104,22 +115,38 @@ export default class HotkeyButton extends React.Component { return; } let text = ""; - if (ev.ctrlKey && ev.code !== "ControlLeft" && ev.code !== "ControlRight") { + if ( + ev.ctrlKey && + ev.code !== "ControlLeft" && + ev.code !== "ControlRight" + ) { text += "Ctrl + "; } - if (ev.altKey && ev.code !== "AltLeft" && ev.code !== "AltRight") { + if ( + ev.altKey && + ev.code !== "AltLeft" && + ev.code !== "AltRight" + ) { text += "Alt + "; } - if (ev.metaKey && ev.code !== "MetaLeft" && ev.code !== "MetaRight") { + if ( + ev.metaKey && + ev.code !== "MetaLeft" && + ev.code !== "MetaRight" + ) { text += "Meta + "; } - if (ev.shiftKey && ev.code !== "ShiftLeft" && ev.code !== "ShiftRight") { + if ( + ev.shiftKey && + ev.code !== "ShiftLeft" && + ev.code !== "ShiftRight" + ) { text += "Shift + "; } text += ev.code; this.props.setValue(text); ev.preventDefault(); - } + }, }; window.addEventListener("keydown", listener); @@ -139,12 +166,15 @@ export default class HotkeyButton extends React.Component { if (gamepad !== null) { let buttonIdx = 0; for (const button of gamepad.buttons) { - const oldState = oldButtonState[gamepadIdx]?.[buttonIdx] ?? false; + const oldState = + oldButtonState[gamepadIdx]?.[buttonIdx] ?? + false; if (button.pressed && !oldState) { this.props.setValue(`Gamepad${buttonIdx}`); } - oldButtonState[gamepadIdx][buttonIdx] = button.pressed; + oldButtonState[gamepadIdx][buttonIdx] = + button.pressed; buttonIdx++; } diff --git a/src/ui/LSOCommandSink.ts b/src/ui/LSOCommandSink.ts index dc44da1..43fc5b8 100644 --- a/src/ui/LSOCommandSink.ts +++ b/src/ui/LSOCommandSink.ts @@ -1,16 +1,34 @@ -import { CommandError, CommandResult, CommandSink, CommandSinkRef, Event, ImageCacheRefMut, LayoutEditorRefMut, LayoutRefMut, LayoutStateRefMut, Run, RunRef, TimeRef, TimeSpan, TimeSpanRef, Timer, TimerPhase, TimingMethod, isEvent } from "../livesplit-core"; +import { + CommandError, + CommandResult, + CommandSink, + CommandSinkRef, + Event, + ImageCacheRefMut, + LayoutEditorRefMut, + LayoutRefMut, + LayoutStateRefMut, + Run, + RunRef, + TimeRef, + TimeSpan, + TimeSpanRef, + Timer, + TimerPhase, + TimingMethod, + isEvent, +} from "../livesplit-core"; import { WebCommandSink } from "../livesplit-core/livesplit_core"; import { assert } from "../util/OptionUtil"; import { showDialog } from "./Dialog"; interface Callbacks { - handleEvent(event: Event): void, - runChanged(): void, - runNotModifiedAnymore(): void, - encounteredCustomVariable(name: string): void, + handleEvent(event: Event): void; + runChanged(): void; + runNotModifiedAnymore(): void; + encounteredCustomVariable(name: string): void; } - export class LSOCommandSink { private commandSink: CommandSink; // We don't want to the timer to be interacted with while we are in menus @@ -24,7 +42,9 @@ export class LSOCommandSink { private timer: Timer, private callbacks: Callbacks, ) { - this.commandSink = new CommandSink(new WebCommandSink(this).intoGeneric()); + this.commandSink = new CommandSink( + new WebCommandSink(this).intoGeneric(), + ); } public [Symbol.dispose](): void { @@ -100,7 +120,8 @@ export class LSOCommandSink { if (this.timer.currentAttemptHasNewBestTimes()) { const [result] = await showDialog({ title: "Save Best Times?", - description: "You have beaten some of your best times. Do you want to update them?", + description: + "You have beaten some of your best times. Do you want to update them?", buttons: ["Yes", "No", "Don't Reset"], }); if (result === 2) { @@ -233,7 +254,9 @@ export class LSOCommandSink { return CommandError.Busy; } - const result = this.timer.setCurrentComparison(comparison) as CommandResult; + const result = this.timer.setCurrentComparison( + comparison, + ) as CommandResult; if (isEvent(result)) { this.callbacks.handleEvent(result); diff --git a/src/ui/LayoutEditor.tsx b/src/ui/LayoutEditor.tsx index 6026664..197aa86 100644 --- a/src/ui/LayoutEditor.tsx +++ b/src/ui/LayoutEditor.tsx @@ -12,29 +12,32 @@ import { ArrowDown, ArrowUp, Check, Copy, Plus, Trash, X } from "lucide-react"; import "../css/LayoutEditor.scss"; export interface Props { - editor: LiveSplit.LayoutEditor, - layoutState: LiveSplit.LayoutStateRefMut, - layoutEditorUrlCache: UrlCache, - layoutUrlCache: UrlCache, - layoutWidth: number, - layoutHeight: number, - generalSettings: GeneralSettings, - allComparisons: string[], - allVariables: Set, - isDesktop: boolean, - commandSink: LSOCommandSink, - renderer: WebRenderer, - callbacks: Callbacks, + editor: LiveSplit.LayoutEditor; + layoutState: LiveSplit.LayoutStateRefMut; + layoutEditorUrlCache: UrlCache; + layoutUrlCache: UrlCache; + layoutWidth: number; + layoutHeight: number; + generalSettings: GeneralSettings; + allComparisons: string[]; + allVariables: Set; + isDesktop: boolean; + commandSink: LSOCommandSink; + renderer: WebRenderer; + callbacks: Callbacks; } export interface State { - editor: LiveSplit.LayoutEditorStateJson, - showComponentSettings: boolean, + editor: LiveSplit.LayoutEditorStateJson; + showComponentSettings: boolean; } interface Callbacks { - onResize(width: number, height: number): void, - renderViewWithSidebar(renderedView: React.JSX.Element, sidebarContent: React.JSX.Element): React.JSX.Element, - closeLayoutEditor(save: boolean): void, + onResize(width: number, height: number): void; + renderViewWithSidebar( + renderedView: React.JSX.Element, + sidebarContent: React.JSX.Element, + ): React.JSX.Element; + closeLayoutEditor(save: boolean): void; } export class LayoutEditor extends React.Component { @@ -42,7 +45,9 @@ export class LayoutEditor extends React.Component { super(props); this.state = { - editor: props.editor.stateAsJson(props.layoutEditorUrlCache.imageCache), + editor: props.editor.stateAsJson( + props.layoutEditorUrlCache.imageCache, + ), showComponentSettings: true, }; @@ -52,7 +57,10 @@ export class LayoutEditor extends React.Component { public render() { const renderedView = this.renderView(); const sidebarContent = this.renderSidebarContent(); - return this.props.callbacks.renderViewWithSidebar(renderedView, sidebarContent); + return this.props.callbacks.renderViewWithSidebar( + renderedView, + sidebarContent, + ); } private renderView() { @@ -62,7 +70,8 @@ export class LayoutEditor extends React.Component { className += " selected"; } return ( - this.selectComponent(i)} draggable onDragStart={(e) => { @@ -85,45 +94,42 @@ export class LayoutEditor extends React.Component { return false; }} > - - {c} - - + {c} + ); }); - const settings = this.state.showComponentSettings - ? ( - { - this.props.editor.setComponentSettingsValue(index, value); - this.update(); - }} - /> - ) : ( - { - this.props.editor.setGeneralSettingsValue( - index, - value, - this.props.layoutEditorUrlCache.imageCache, - ); - this.update(); - }} - /> - ); + const settings = this.state.showComponentSettings ? ( + { + this.props.editor.setComponentSettingsValue(index, value); + this.update(); + }} + /> + ) : ( + { + this.props.editor.setGeneralSettingsValue( + index, + value, + this.props.layoutEditorUrlCache.imageCache, + ); + this.update(); + }} + /> + ); return (
@@ -151,31 +157,34 @@ export class LayoutEditor extends React.Component {
- - {components} - + {components}
-
- {settings} -
+
{settings}
{ height={this.props.layoutHeight} generalSettings={this.props.generalSettings} renderer={this.props.renderer} - onResize={(width, height) => this.props.callbacks.onResize(width, height)} + onResize={(width, height) => + this.props.callbacks.onResize(width, height) + } />
@@ -235,13 +245,17 @@ export class LayoutEditor extends React.Component {
@@ -252,8 +266,11 @@ export class LayoutEditor extends React.Component { private update(showComponentSettings?: boolean) { this.setState({ - editor: this.props.editor.stateAsJson(this.props.layoutEditorUrlCache.imageCache), - showComponentSettings: showComponentSettings ?? this.state.showComponentSettings, + editor: this.props.editor.stateAsJson( + this.props.layoutEditorUrlCache.imageCache, + ), + showComponentSettings: + showComponentSettings ?? this.state.showComponentSettings, }); this.props.layoutEditorUrlCache.collect(); } @@ -316,125 +333,262 @@ function AddComponentButton({ {position && ( - setPosition(null)}> - addComponent(LiveSplit.TitleComponent)}> + setPosition(null)} + > + addComponent(LiveSplit.TitleComponent)} + > Title - Shows the name of the game and the category that is being run. Additionally, the game icon, the attempt count, and the total number of successfully finished runs can be shown. + Shows the name of the game and the category that is + being run. Additionally, the game icon, the attempt + count, and the total number of successfully finished + runs can be shown. - addComponent(LiveSplit.GraphComponent)}> + addComponent(LiveSplit.GraphComponent)} + > Graph - Visualizes how far the current run has been ahead or behind the chosen comparison throughout the whole run. All the individual deltas are shown as points on the graph. + Visualizes how far the current run has been ahead or + behind the chosen comparison throughout the whole + run. All the individual deltas are shown as points + on the graph. - addComponent(LiveSplit.SplitsComponent)}> + addComponent(LiveSplit.SplitsComponent)} + > Splits - The main component for visualizing all the split times. Each segment is shown in a tabular fashion showing the segment icon, segment name, the delta compared to the chosen comparison, and the split time. The list provides scrolling functionality, so not every segment needs to be shown all the time. + The main component for visualizing all the split + times. Each segment is shown in a tabular fashion + showing the segment icon, segment name, the delta + compared to the chosen comparison, and the split + time. The list provides scrolling functionality, so + not every segment needs to be shown all the time. - addComponent(LiveSplit.DetailedTimerComponent)}> + + addComponent(LiveSplit.DetailedTimerComponent) + } + > Detailed Timer - Shows two timers, one for the total time of the current run and one showing the time of just the current segment. Other information, like segment times of up to two comparisons, the segment icon, and the segment name, can also be shown. + Shows two timers, one for the total time of the + current run and one showing the time of just the + current segment. Other information, like segment + times of up to two comparisons, the segment icon, + and the segment name, can also be shown. - addComponent(LiveSplit.TimerComponent)}> + addComponent(LiveSplit.TimerComponent)} + > Timer - Shows the total time of the current run as a digital clock. The color of the time shown is based on a how well the current run is doing compared to the chosen comparison. + Shows the total time of the current run as a digital + clock. The color of the time shown is based on a how + well the current run is doing compared to the chosen + comparison. - addComponent(LiveSplit.CurrentComparisonComponent)}> + + addComponent(LiveSplit.CurrentComparisonComponent) + } + > Current Comparison - Shows the name of the comparison that the timer is currently comparing against. + Shows the name of the comparison that the timer is + currently comparing against. - addComponent(LiveSplit.CurrentPaceComponent)}> + + addComponent(LiveSplit.CurrentPaceComponent) + } + > Current Pace - Shows a prediction for the current run's final time. The remainder of the run is predicted based on the chosen comparison for the component. For example, the "Best Segments" comparison can be chosen to show the best possible final time for the current run based on the Sum of Best Segments. + Shows a prediction for the current run's final time. + The remainder of the run is predicted based on the + chosen comparison for the component. For example, + the "Best Segments" comparison can be chosen to show + the best possible final time for the current run + based on the Sum of Best Segments. - addComponent(LiveSplit.DeltaComponent)}> + addComponent(LiveSplit.DeltaComponent)} + > Delta - Shows how far ahead or behind the current run is compared to the chosen comparison. + Shows how far ahead or behind the current run is + compared to the chosen comparison. - addComponent(LiveSplit.PbChanceComponent)}> + + addComponent(LiveSplit.PbChanceComponent) + } + > PB Chance - Shows how likely it is for the active run to beat the personal best. If there is no active run, it shows the general chance of beating the personal best. During a run, it actively changes based on how well the run is going. + Shows how likely it is for the active run to beat + the personal best. If there is no active run, it + shows the general chance of beating the personal + best. During a run, it actively changes based on how + well the run is going. - addComponent(LiveSplit.PossibleTimeSaveComponent)}> + + addComponent(LiveSplit.PossibleTimeSaveComponent) + } + > Possible Time Save - Shows how much time you can save on the current segment compared to the chosen comparison, based on the best segment time of the segment. This component also allows showing the "Total Possible Time Save" for the remainder of the current run. + Shows how much time you can save on the current + segment compared to the chosen comparison, based on + the best segment time of the segment. This component + also allows showing the "Total Possible Time Save" + for the remainder of the current run. - addComponent(LiveSplit.PreviousSegmentComponent)}> + + addComponent(LiveSplit.PreviousSegmentComponent) + } + > Previous Segment - Shows how much time was saved or lost during the previous segment based on the chosen comparison. Additionally, the potential time save for the previous segment can be displayed. This component switches to a "Live Segment" view that shows the active time loss whenever you are losing time on the current segment. + Shows how much time was saved or lost during the + previous segment based on the chosen comparison. + Additionally, the potential time save for the + previous segment can be displayed. This component + switches to a "Live Segment" view that shows the + active time loss whenever you are losing time on the + current segment. - addComponent(LiveSplit.SegmentTimeComponent)}> + + addComponent(LiveSplit.SegmentTimeComponent) + } + > Segment Time - Shows the time for the current segment for the chosen comparison. If no comparison is specified it uses the timer's current comparison. + Shows the time for the current segment for the + chosen comparison. If no comparison is specified it + uses the timer's current comparison. - addComponent(LiveSplit.SumOfBestComponent)}> + + addComponent(LiveSplit.SumOfBestComponent) + } + > Sum of Best Segments - Shows the fastest possible time to complete a run of the current category, based on information collected from all the previous runs. This often matches up with the sum of the best segment times of all the segments, but that may not always be the case, as skipped segments may introduce combined segments that may be faster than the actual sum of their best segment times. The name is therefore a bit misleading, but sticks around for historical reasons. + Shows the fastest possible time to complete a run of + the current category, based on information collected + from all the previous runs. This often matches up + with the sum of the best segment times of all the + segments, but that may not always be the case, as + skipped segments may introduce combined segments + that may be faster than the actual sum of their best + segment times. The name is therefore a bit + misleading, but sticks around for historical + reasons. - addComponent(LiveSplit.TextComponent)}> + addComponent(LiveSplit.TextComponent)} + > Text - Shows the text that you specify. This can either be a single centered text, or split up into a left and right text, which is suitable for a situation where you have a label and a value. There is also the option of showing a custom variable that you specify in the splits editor. + Shows the text that you specify. This can either be + a single centered text, or split up into a left and + right text, which is suitable for a situation where + you have a label and a value. There is also the + option of showing a custom variable that you specify + in the splits editor. - addComponent(LiveSplit.TotalPlaytimeComponent)}> + + addComponent(LiveSplit.TotalPlaytimeComponent) + } + > Total Playtime - Shows the total amount of time that the current category has been played for. + Shows the total amount of time that the current + category has been played for. - { - allVariables.size > 0 && - } - { - allVariables.size > 0 && Array.from(allVariables).map((name) => { + {allVariables.size > 0 && } + {allVariables.size > 0 && + Array.from(allVariables).map((name) => { return ( - addVariable(name)}> + addVariable(name)} + > {name} - Creates a text component that shows the value of the custom variable "{name}". + Creates a text component that shows the + value of the custom variable "{name}". ); - }) - } + })} - addComponent(LiveSplit.BlankSpaceComponent)}> + + addComponent(LiveSplit.BlankSpaceComponent) + } + > Blank Space - An empty component that doesn't show anything other than a background. It mostly serves as padding between other components. + An empty component that doesn't show anything other + than a background. It mostly serves as padding + between other components. - addComponent(LiveSplit.SeparatorComponent)}> + + addComponent(LiveSplit.SeparatorComponent) + } + > Separator - A simple component that just renders a separator between components. + A simple component that just renders a separator + between components. diff --git a/src/ui/LayoutView.tsx b/src/ui/LayoutView.tsx index 3dd8080..cc30843 100644 --- a/src/ui/LayoutView.tsx +++ b/src/ui/LayoutView.tsx @@ -1,5 +1,10 @@ import * as React from "react"; -import { Layout, LayoutStateRefMut, TimerPhase, TimingMethod } from "../livesplit-core"; +import { + Layout, + LayoutStateRefMut, + TimerPhase, + TimingMethod, +} from "../livesplit-core"; import { TimerView } from "./TimerView"; import { UrlCache } from "../util/UrlCache"; import { WebRenderer } from "../livesplit-core/livesplit_core"; @@ -7,76 +12,92 @@ import { GeneralSettings } from "./MainSettings"; import { LiveSplitServer } from "../api/LiveSplitServer"; import { Option } from "../util/OptionUtil"; import { LSOCommandSink } from "./LSOCommandSink"; -import { ArrowLeft, Circle, Download, ListRestart, Save, SquarePen, Upload } from "lucide-react"; +import { + ArrowLeft, + Circle, + Download, + ListRestart, + Save, + SquarePen, + Upload, +} from "lucide-react"; export interface Props { - isDesktop: boolean, - layout: Layout, - layoutState: LayoutStateRefMut, - layoutUrlCache: UrlCache, - layoutWidth: number, - layoutHeight: number, - generalSettings: GeneralSettings, - renderWithSidebar: boolean, - sidebarOpen: boolean, - commandSink: LSOCommandSink, - renderer: WebRenderer, - serverConnection: Option, - callbacks: Callbacks, - currentComparison: string, - currentTimingMethod: TimingMethod, - currentPhase: TimerPhase, - currentSplitIndex: number, - allComparisons: string[], - splitsModified: boolean, - layoutModified: boolean, + isDesktop: boolean; + layout: Layout; + layoutState: LayoutStateRefMut; + layoutUrlCache: UrlCache; + layoutWidth: number; + layoutHeight: number; + generalSettings: GeneralSettings; + renderWithSidebar: boolean; + sidebarOpen: boolean; + commandSink: LSOCommandSink; + renderer: WebRenderer; + serverConnection: Option; + callbacks: Callbacks; + currentComparison: string; + currentTimingMethod: TimingMethod; + currentPhase: TimerPhase; + currentSplitIndex: number; + allComparisons: string[]; + splitsModified: boolean; + layoutModified: boolean; } interface Callbacks { - exportLayout(): void, - importLayout(): void, - importLayoutFromFile(file: File): Promise, - importSplitsFromFile(file: File): Promise, - loadDefaultLayout(): void, - onResize(width: number, height: number): void, - openAboutView(): void, - openLayoutEditor(): void, - openLayoutView(): void, - openSplitsView(): void, - openMainSettings(): void, - openTimerView(): void, - renderViewWithSidebar(renderedView: React.JSX.Element, sidebarContent: React.JSX.Element): React.JSX.Element, - saveLayout(): void, - onServerConnectionOpened(serverConnection: LiveSplitServer): void, - onServerConnectionClosed(): void, + exportLayout(): void; + importLayout(): void; + importLayoutFromFile(file: File): Promise; + importSplitsFromFile(file: File): Promise; + loadDefaultLayout(): void; + onResize(width: number, height: number): void; + openAboutView(): void; + openLayoutEditor(): void; + openLayoutView(): void; + openSplitsView(): void; + openMainSettings(): void; + openTimerView(): void; + renderViewWithSidebar( + renderedView: React.JSX.Element, + sidebarContent: React.JSX.Element, + ): React.JSX.Element; + saveLayout(): void; + onServerConnectionOpened(serverConnection: LiveSplitServer): void; + onServerConnectionClosed(): void; } export class LayoutView extends React.Component { public render() { - const renderedView = ; + const renderedView = ( + + ); const sidebarContent = this.renderSidebarContent(); - return this.props.callbacks.renderViewWithSidebar(renderedView, sidebarContent); + return this.props.callbacks.renderViewWithSidebar( + renderedView, + sidebarContent, + ); } private renderSidebarContent() { @@ -84,17 +105,23 @@ export class LayoutView extends React.Component {

Layout


- -
diff --git a/src/ui/LiveSplit.tsx b/src/ui/LiveSplit.tsx index bd1b2ef..b6573a9 100644 --- a/src/ui/LiveSplit.tsx +++ b/src/ui/LiveSplit.tsx @@ -1,16 +1,39 @@ import * as React from "react"; import Sidebar from "react-sidebar"; import { - Layout, LayoutEditor, Run, RunEditor, Segment, - Timer, HotkeyConfig, LayoutState, LayoutStateJson, - TimingMethod, TimerPhase, + Layout, + LayoutEditor, + Run, + RunEditor, + Segment, + Timer, + HotkeyConfig, + LayoutState, + LayoutStateJson, + TimingMethod, + TimerPhase, Event, } from "../livesplit-core"; -import { FILE_EXT_LAYOUTS, convertFileToArrayBuffer, convertFileToString, exportFile, openFileAsString } from "../util/FileUtil"; -import { Option, assertNull, expect, maybeDisposeAndThen, panic } from "../util/OptionUtil"; +import { + FILE_EXT_LAYOUTS, + convertFileToArrayBuffer, + convertFileToString, + exportFile, + openFileAsString, +} from "../util/FileUtil"; +import { + Option, + assertNull, + expect, + maybeDisposeAndThen, + panic, +} from "../util/OptionUtil"; import { LayoutEditor as LayoutEditorComponent } from "./LayoutEditor"; import { RunEditor as RunEditorComponent } from "./RunEditor"; -import { GeneralSettings, MainSettings as SettingsEditorComponent } from "./MainSettings"; +import { + GeneralSettings, + MainSettings as SettingsEditorComponent, +} from "./MainSettings"; import { TimerView } from "./TimerView"; import { About } from "./About"; import { SplitsSelection, EditingInfo } from "./SplitsSelection"; @@ -55,55 +78,55 @@ function isMenuLocked(menuKind: MenuKind) { } type Menu = - { kind: MenuKind.Timer } | - { kind: MenuKind.Splits } | - { kind: MenuKind.RunEditor, editor: RunEditor, splitsKey?: number } | - { kind: MenuKind.Layout } | - { kind: MenuKind.LayoutEditor, editor: LayoutEditor } | - { kind: MenuKind.MainSettings, config: HotkeyConfig } | - { kind: MenuKind.About }; + | { kind: MenuKind.Timer } + | { kind: MenuKind.Splits } + | { kind: MenuKind.RunEditor; editor: RunEditor; splitsKey?: number } + | { kind: MenuKind.Layout } + | { kind: MenuKind.LayoutEditor; editor: LayoutEditor } + | { kind: MenuKind.MainSettings; config: HotkeyConfig } + | { kind: MenuKind.About }; export interface Props { - splits?: Uint8Array, - layout?: Storage.LayoutSettings, - comparison?: string, - timingMethod: TimingMethod, - hotkeys?: Storage.HotkeyConfigSettings, - layoutWidth: number, - layoutHeight: number, - generalSettings: GeneralSettings, - splitsKey?: number, + splits?: Uint8Array; + layout?: Storage.LayoutSettings; + comparison?: string; + timingMethod: TimingMethod; + hotkeys?: Storage.HotkeyConfigSettings; + layoutWidth: number; + layoutHeight: number; + generalSettings: GeneralSettings; + splitsKey?: number; } export interface State { - hotkeySystem: HotkeyImplementation, - isBrowserSource: boolean, - isDesktop: boolean, - layout: Layout, - layoutState: LayoutState, - layoutUrlCache: UrlCache, - runEditorUrlCache: UrlCache, - layoutEditorUrlCache: UrlCache, - layoutWidth: number, - layoutHeight: number, - menu: Menu, - openedSplitsKey?: number, - sidebarOpen: boolean, - sidebarTransitionsEnabled: boolean, - storedLayoutWidth: number, - storedLayoutHeight: number, - commandSink: LSOCommandSink, - renderer: WebRenderer, - generalSettings: GeneralSettings, - serverConnection: Option, - currentComparison: string, - currentTimingMethod: TimingMethod, - currentPhase: TimerPhase, - currentSplitIndex: number, - allComparisons: string[], - allVariables: Set, - splitsModified: boolean, - layoutModified: boolean, + hotkeySystem: HotkeyImplementation; + isBrowserSource: boolean; + isDesktop: boolean; + layout: Layout; + layoutState: LayoutState; + layoutUrlCache: UrlCache; + runEditorUrlCache: UrlCache; + layoutEditorUrlCache: UrlCache; + layoutWidth: number; + layoutHeight: number; + menu: Menu; + openedSplitsKey?: number; + sidebarOpen: boolean; + sidebarTransitionsEnabled: boolean; + storedLayoutWidth: number; + storedLayoutHeight: number; + commandSink: LSOCommandSink; + renderer: WebRenderer; + generalSettings: GeneralSettings; + serverConnection: Option; + currentComparison: string; + currentTimingMethod: TimingMethod; + currentPhase: TimerPhase; + currentSplitIndex: number; + allComparisons: string[]; + allVariables: Set; + splitsModified: boolean; + layoutModified: boolean; } export let hotkeySystem: Option = null; @@ -112,7 +135,10 @@ export class LiveSplit extends React.Component { public static async loadStoredData() { // FIXME: We should probably request all of these concurrently. const splitsKey = await Storage.loadSplitsKey(); - const splits = splitsKey !== undefined ? await Storage.loadSplits(splitsKey) : undefined; + const splits = + splitsKey !== undefined + ? await Storage.loadSplits(splitsKey) + : undefined; const layout = await Storage.loadLayout(); const comparison = await Storage.loadComparison(); const timingMethod = await Storage.loadTimingMethod(); @@ -147,12 +173,12 @@ export class LiveSplit extends React.Component { "The Default Run should be a valid Run", ); - const commandSink = new LSOCommandSink( - timer, - this, - ); + const commandSink = new LSOCommandSink(timer, this); - hotkeySystem = createHotkeys(commandSink.getCommandSink(), props.hotkeys); + hotkeySystem = createHotkeys( + commandSink.getCommandSink(), + props.hotkeys, + ); if (props.splits !== undefined) { using result = Run.parseArray(props.splits, ""); @@ -173,7 +199,9 @@ export class LiveSplit extends React.Component { if (data !== undefined) { layout = Layout.parseJson(data); } - } catch (_) { /* Looks like the storage has no valid data */ } + } catch (_) { + /* Looks like the storage has no valid data */ + } if (layout === null) { layout = Layout.defaultLayout(); } @@ -182,7 +210,9 @@ export class LiveSplit extends React.Component { const isBrowserSource = !!(window as any).obsstudio; const renderer = new WebRenderer(); - renderer.element().setAttribute("style", "width: inherit; height: inherit;"); + renderer + .element() + .setAttribute("style", "width: inherit; height: inherit;"); this.state = { isDesktop: isDesktop && !isBrowserSource, @@ -217,7 +247,10 @@ export class LiveSplit extends React.Component { window.__TAURI__?.event.listen("command", (event) => { const payloadString = JSON.stringify(event.payload); - ServerProtocol.handleCommand(payloadString, commandSink.getCommandSink().ptr); + ServerProtocol.handleCommand( + payloadString, + commandSink.getCommandSink().ptr, + ); }); this.updateTauriSettings(props.generalSettings); @@ -232,7 +265,7 @@ export class LiveSplit extends React.Component { if (serviceWorker && serviceWorker.controller) { // Don't prompt for update when service worker gets removed toast.warn( - 'A new version of LiveSplit One is available! Click here to reload.', + "A new version of LiveSplit One is available! Click here to reload.", { closeOnClick: true, onClick: () => window.location.reload(), @@ -244,7 +277,9 @@ export class LiveSplit extends React.Component { public componentDidMount() { this.scrollEvent = { handleEvent: (e: WheelEvent) => this.onScroll(e) }; window.addEventListener("wheel", this.scrollEvent); - this.rightClickEvent = { handleEvent: (e: any) => this.onRightClick(e) }; + this.rightClickEvent = { + handleEvent: (e: any) => this.onRightClick(e), + }; window.addEventListener("contextmenu", this.rightClickEvent, false); this.resizeEvent = { handleEvent: () => this.handleAutomaticResize() }; window.addEventListener("resize", this.resizeEvent, false); @@ -270,7 +305,10 @@ export class LiveSplit extends React.Component { const { serviceWorker } = navigator; if (serviceWorker && serviceWorker.controller) { // Don't prompt for update when there was no service worker previously installed - serviceWorker.addEventListener('controllerchange', this.notifyAboutUpdate); + serviceWorker.addEventListener( + "controllerchange", + this.notifyAboutUpdate, + ); } } @@ -295,128 +333,151 @@ export class LiveSplit extends React.Component { this.state.layout[Symbol.dispose](); this.state.layoutState[Symbol.dispose](); - // This is bound in the constructor - // eslint-disable-next-line @typescript-eslint/unbound-method - this.isDesktopQuery.removeEventListener("change", this.mediaQueryChanged); + this.isDesktopQuery.removeEventListener( + "change", + // This is bound in the constructor + // eslint-disable-next-line @typescript-eslint/unbound-method + this.mediaQueryChanged, + ); const { serviceWorker } = navigator; if (serviceWorker) { - serviceWorker.removeEventListener('controllerchange', this.notifyAboutUpdate); + serviceWorker.removeEventListener( + "controllerchange", + this.notifyAboutUpdate, + ); } } public render() { let view; if (this.state.menu.kind === MenuKind.RunEditor) { - view = ; + view = ( + + ); } else if (this.state.menu.kind === MenuKind.LayoutEditor) { - view = ; + view = ( + + ); } else if (this.state.menu.kind === MenuKind.MainSettings) { - view = ; + view = ( + + ); } else if (this.state.menu.kind === MenuKind.About) { view = ; } else if (this.state.menu.kind === MenuKind.Splits) { - view = ; + view = ( + + ); } else if (this.state.menu.kind === MenuKind.Timer) { - view = ; + view = ( + + ); } else if (this.state.menu.kind === MenuKind.Layout) { - view = ; + view = ( + + ); } - return <> - {view} - this.lockTimerInteraction()} - onClose={() => this.unlockTimerInteraction()} - /> - - ; + return ( + <> + {view} + this.lockTimerInteraction()} + onClose={() => this.unlockTimerInteraction()} + /> + + + ); } - public renderViewWithSidebar(renderedView: React.JSX.Element, sidebarContent: React.JSX.Element) { + public renderViewWithSidebar( + renderedView: React.JSX.Element, + sidebarContent: React.JSX.Element, + ) { return (
{ contentClassName="livesplit-container" overlayClassName="sidebar-overlay" > - { - !this.state.isDesktop && - !this.state.sidebarOpen && + {!this.state.isDesktop && !this.state.sidebarOpen && ( - } -
- {renderedView} -
+ )} +
{renderedView}
); @@ -632,7 +689,10 @@ export class LiveSplit extends React.Component { }); } - public async closeMainSettings(save: boolean, generalSettings: GeneralSettings) { + public async closeMainSettings( + save: boolean, + generalSettings: GeneralSettings, + ) { const menu = this.state.menu; if (menu.kind !== MenuKind.MainSettings) { @@ -690,7 +750,9 @@ export class LiveSplit extends React.Component { private handleAutomaticResize() { if (!this.state.isDesktop) { - const layoutDirection = (this.state.layoutState.asJson() as LayoutStateJson).direction; + const layoutDirection = ( + this.state.layoutState.asJson() as LayoutStateJson + ).direction; if (layoutDirection !== "Vertical") { return; } @@ -702,11 +764,14 @@ export class LiveSplit extends React.Component { }); } - const showControlButtons = this.state.generalSettings.showControlButtons; - const showManualGameTime = this.state.generalSettings.showManualGameTime; + const showControlButtons = + this.state.generalSettings.showControlButtons; + const showManualGameTime = + this.state.generalSettings.showManualGameTime; let newHeight = window.innerHeight - largeMargin; if (showControlButtons && showManualGameTime) { - newHeight -= 2 * buttonHeight + manualGameTimeHeight + 3 * largeMargin; + newHeight -= + 2 * buttonHeight + manualGameTimeHeight + 3 * largeMargin; } else if (showControlButtons) { newHeight -= 2 * buttonHeight + 2 * largeMargin; } else if (showManualGameTime) { @@ -722,11 +787,16 @@ export class LiveSplit extends React.Component { } private mediaQueryChanged() { - const isDesktop = this.isDesktopQuery.matches && !this.state.isBrowserSource; + const isDesktop = + this.isDesktopQuery.matches && !this.state.isBrowserSource; this.setState({ isDesktop, - layoutWidth: isDesktop ? this.state.storedLayoutWidth : this.state.layoutWidth, - layoutHeight: isDesktop ? this.state.storedLayoutHeight : this.state.layoutHeight, + layoutWidth: isDesktop + ? this.state.storedLayoutWidth + : this.state.layoutWidth, + layoutHeight: isDesktop + ? this.state.storedLayoutHeight + : this.state.layoutHeight, sidebarTransitionsEnabled: false, }); } @@ -735,7 +805,9 @@ export class LiveSplit extends React.Component { let layout = null; try { layout = Layout.parseJson(JSON.parse(file)); - } catch (_) { /* Failed to load the layout */ } + } catch (_) { + /* Failed to load the layout */ + } if (layout === null) { layout = Layout.parseOriginalLivesplitString(file); } @@ -743,7 +815,9 @@ export class LiveSplit extends React.Component { this.setLayout(layout); return; } - throw Error("The layout could not be loaded. This may not be a valid LiveSplit or LiveSplit One Layout."); + throw Error( + "The layout could not be loaded. This may not be a valid LiveSplit or LiveSplit One Layout.", + ); } private setLayout(layout: Layout) { @@ -758,10 +832,7 @@ export class LiveSplit extends React.Component { } private setRun(run: Run, callback: () => void) { - maybeDisposeAndThen( - this.state.commandSink.setRun(run), - callback, - ); + maybeDisposeAndThen(this.state.commandSink.setRun(run), callback); this.setSplitsKey(undefined); } @@ -770,7 +841,9 @@ export class LiveSplit extends React.Component { using result = Run.parseArray(new Uint8Array(file), ""); if (result.parsedSuccessfully()) { const run = result.unwrap(); - this.setRun(run, () => { throw Error("Empty Splits are not supported."); }); + this.setRun(run, () => { + throw Error("Empty Splits are not supported."); + }); } else { throw Error("Couldn't parse the splits."); } @@ -801,13 +874,13 @@ export class LiveSplit extends React.Component { async saveSplits() { try { - const openedSplitsKey = await Storage.storeSplits( - (callback) => { - callback(this.state.commandSink.getRun(), this.state.commandSink.saveAsLssBytes()); - this.state.commandSink.markAsUnmodified(); - }, - this.state.openedSplitsKey, - ); + const openedSplitsKey = await Storage.storeSplits((callback) => { + callback( + this.state.commandSink.getRun(), + this.state.commandSink.saveAsLssBytes(), + ); + this.state.commandSink.markAsUnmodified(); + }, this.state.openedSplitsKey); if (this.state.openedSplitsKey !== openedSplitsKey) { this.setSplitsKey(openedSplitsKey); } @@ -910,7 +983,8 @@ export class LiveSplit extends React.Component { private currentComparisonChanged(): void { if (this.state != null) { - const currentComparison = this.state.commandSink.currentComparison(); + const currentComparison = + this.state.commandSink.currentComparison(); (async () => { try { @@ -926,7 +1000,8 @@ export class LiveSplit extends React.Component { private currentTimingMethodChanged(): void { if (this.state != null) { - const currentTimingMethod = this.state.commandSink.currentTimingMethod(); + const currentTimingMethod = + this.state.commandSink.currentTimingMethod(); (async () => { try { diff --git a/src/ui/MainSettings.tsx b/src/ui/MainSettings.tsx index 3470876..4e3ff96 100644 --- a/src/ui/MainSettings.tsx +++ b/src/ui/MainSettings.tsx @@ -1,10 +1,18 @@ import * as React from "react"; import { JsonSettingValueFactory, SettingsComponent } from "./Settings"; -import { SettingsDescriptionJson, SettingValue, HotkeyConfig } from "../livesplit-core"; +import { + SettingsDescriptionJson, + SettingValue, + HotkeyConfig, +} from "../livesplit-core"; import { toast } from "react-toastify"; import { UrlCache } from "../util/UrlCache"; -import { FRAME_RATE_AUTOMATIC as FRAME_RATE_BATTERY_AWARE, FRAME_RATE_MATCH_SCREEN as FRAME_RATE_MATCH_SCREEN, FrameRateSetting } from "../util/FrameRate"; +import { + FRAME_RATE_AUTOMATIC as FRAME_RATE_BATTERY_AWARE, + FRAME_RATE_MATCH_SCREEN as FRAME_RATE_MATCH_SCREEN, + FrameRateSetting, +} from "../util/FrameRate"; import { LiveSplitServer } from "../api/LiveSplitServer"; import { Option } from "../util/OptionUtil"; import { LSOCommandSink } from "./LSOCommandSink"; @@ -13,17 +21,17 @@ import { Check, FlaskConical, X } from "lucide-react"; import "../css/SettingsEditor.scss"; export interface GeneralSettings { - frameRate: FrameRateSetting, - showControlButtons: boolean, - showManualGameTime: ManualGameTimeSettings | false, - saveOnReset: boolean, - speedrunComIntegration: boolean, - serverUrl?: string, - alwaysOnTop?: boolean, + frameRate: FrameRateSetting; + showControlButtons: boolean; + showManualGameTime: ManualGameTimeSettings | false; + saveOnReset: boolean; + speedrunComIntegration: boolean; + serverUrl?: string; + alwaysOnTop?: boolean; } export interface ManualGameTimeSettings { - mode: string, + mode: string; } export const MANUAL_GAME_TIME_MODE_SEGMENT_TIMES = "Segment Times"; @@ -33,27 +41,30 @@ export const MANUAL_GAME_TIME_SETTINGS_DEFAULT: ManualGameTimeSettings = { }; export interface Props { - generalSettings: GeneralSettings, - hotkeyConfig: HotkeyConfig, - urlCache: UrlCache, - callbacks: Callbacks, - serverConnection: Option, - commandSink: LSOCommandSink, - allComparisons: string[], - allVariables: Set, + generalSettings: GeneralSettings; + hotkeyConfig: HotkeyConfig; + urlCache: UrlCache; + callbacks: Callbacks; + serverConnection: Option; + commandSink: LSOCommandSink; + allComparisons: string[]; + allVariables: Set; } export interface State { - settings: SettingsDescriptionJson, - generalSettings: GeneralSettings, + settings: SettingsDescriptionJson; + generalSettings: GeneralSettings; } interface Callbacks { - renderViewWithSidebar(renderedView: React.JSX.Element, sidebarContent: React.JSX.Element): React.JSX.Element, - closeMainSettings(save: boolean, newGeneralSettings: GeneralSettings): void, - onServerConnectionOpened(serverConnection: LiveSplitServer): void, - onServerConnectionClosed(): void, - forceUpdate(): void, + renderViewWithSidebar( + renderedView: React.JSX.Element, + sidebarContent: React.JSX.Element, + ): React.JSX.Element; + closeMainSettings(save: boolean, newGeneralSettings: GeneralSettings): void; + onServerConnectionOpened(serverConnection: LiveSplitServer): void; + onServerConnectionClosed(): void; + forceUpdate(): void; } export class MainSettings extends React.Component { @@ -69,38 +80,62 @@ export class MainSettings extends React.Component { public render() { const renderedView = this.renderView(); const sidebarContent = this.renderSidebarContent(); - return this.props.callbacks.renderViewWithSidebar(renderedView, sidebarContent); + return this.props.callbacks.renderViewWithSidebar( + renderedView, + sidebarContent, + ); } private renderView() { const generalFields = [ { text: "Frame Rate", - tooltip: "Determines the frame rate at which to display the timer. \"Battery Aware\" tries determining the type of device and charging status to select a good frame rate. \"Match Screen\" makes the timer match the screen's refresh rate.", + tooltip: + 'Determines the frame rate at which to display the timer. "Battery Aware" tries determining the type of device and charging status to select a good frame rate. "Match Screen" makes the timer match the screen\'s refresh rate.', value: { CustomCombobox: { - value: this.state.generalSettings.frameRate === FRAME_RATE_MATCH_SCREEN ? FRAME_RATE_MATCH_SCREEN : this.state.generalSettings.frameRate === FRAME_RATE_BATTERY_AWARE ? FRAME_RATE_BATTERY_AWARE : this.state.generalSettings.frameRate.toString() + " FPS", - list: [FRAME_RATE_BATTERY_AWARE, "30 FPS", "60 FPS", "120 FPS", FRAME_RATE_MATCH_SCREEN], + value: + this.state.generalSettings.frameRate === + FRAME_RATE_MATCH_SCREEN + ? FRAME_RATE_MATCH_SCREEN + : this.state.generalSettings.frameRate === + FRAME_RATE_BATTERY_AWARE + ? FRAME_RATE_BATTERY_AWARE + : this.state.generalSettings.frameRate.toString() + + " FPS", + list: [ + FRAME_RATE_BATTERY_AWARE, + "30 FPS", + "60 FPS", + "120 FPS", + FRAME_RATE_MATCH_SCREEN, + ], mandatory: true, - } + }, }, }, { text: "Save On Reset", - tooltip: "Determines whether to automatically save the splits when resetting the timer.", + tooltip: + "Determines whether to automatically save the splits when resetting the timer.", value: { Bool: this.state.generalSettings.saveOnReset, }, }, { text: "Show Control Buttons", - tooltip: "Determines whether to show buttons beneath the timer that allow controlling it. When disabled, you have to use the hotkeys instead.", + tooltip: + "Determines whether to show buttons beneath the timer that allow controlling it. When disabled, you have to use the hotkeys instead.", value: { Bool: this.state.generalSettings.showControlButtons }, }, { text: "Show Manual Game Time Input", - tooltip: "Shows a text box beneath the timer that allows you to manually input the game time. You start the timer and do splits by pressing the Enter key in the text box. Make sure to compare against \"Game Time\".", - value: { Bool: this.state.generalSettings.showManualGameTime !== false }, + tooltip: + 'Shows a text box beneath the timer that allows you to manually input the game time. You start the timer and do splits by pressing the Enter key in the text box. Make sure to compare against "Game Time".', + value: { + Bool: + this.state.generalSettings.showManualGameTime !== false, + }, }, ]; @@ -109,13 +144,18 @@ export class MainSettings extends React.Component { manualGameTimeModeIndex = generalFields.length; generalFields.push({ text: "Manual Game Time Mode", - tooltip: "Determines whether to input the manual game time as segment times or split times.", + tooltip: + "Determines whether to input the manual game time as segment times or split times.", value: { CustomCombobox: { - value: this.state.generalSettings.showManualGameTime.mode, - list: [MANUAL_GAME_TIME_MODE_SEGMENT_TIMES, MANUAL_GAME_TIME_MODE_SPLIT_TIMES], + value: this.state.generalSettings.showManualGameTime + .mode, + list: [ + MANUAL_GAME_TIME_MODE_SEGMENT_TIMES, + MANUAL_GAME_TIME_MODE_SPLIT_TIMES, + ], mandatory: false, - } + }, }, }); } @@ -165,11 +205,19 @@ export class MainSettings extends React.Component { this.setState({ generalSettings: { ...this.state.generalSettings, - frameRate: value.String === FRAME_RATE_MATCH_SCREEN - ? FRAME_RATE_MATCH_SCREEN - : value.String === FRAME_RATE_BATTERY_AWARE - ? FRAME_RATE_BATTERY_AWARE - : parseInt(value.String.split(' ')[0], 10) as FrameRateSetting, + frameRate: + value.String === + FRAME_RATE_MATCH_SCREEN + ? FRAME_RATE_MATCH_SCREEN + : value.String === + FRAME_RATE_BATTERY_AWARE + ? FRAME_RATE_BATTERY_AWARE + : (parseInt( + value.String.split( + " ", + )[0], + 10, + ) as FrameRateSetting), }, }); } @@ -199,21 +247,28 @@ export class MainSettings extends React.Component { this.setState({ generalSettings: { ...this.state.generalSettings, - showManualGameTime: value.Bool ? - MANUAL_GAME_TIME_SETTINGS_DEFAULT : false, + showManualGameTime: value.Bool + ? MANUAL_GAME_TIME_SETTINGS_DEFAULT + : false, }, }); } break; default: - if (index === alwaysOnTopIndex && "Bool" in value) { + if ( + index === alwaysOnTopIndex && + "Bool" in value + ) { this.setState({ generalSettings: { ...this.state.generalSettings, alwaysOnTop: value.Bool, }, }); - } else if (index === manualGameTimeModeIndex && "String" in value) { + } else if ( + index === manualGameTimeModeIndex && + "String" in value + ) { this.setState({ generalSettings: { ...this.state.generalSettings, @@ -235,27 +290,51 @@ export class MainSettings extends React.Component { fields: [ { text: "Speedrun.com Integration", - tooltip: "Queries the list of games, categories, and the leaderboards from speedrun.com.", - value: { Bool: this.state.generalSettings.speedrunComIntegration }, + tooltip: + "Queries the list of games, categories, and the leaderboards from speedrun.com.", + value: { + Bool: this.state.generalSettings + .speedrunComIntegration, + }, }, { - text:
- Server Connection -
, - tooltip: <> - Allows you to connect to a WebSocket server that can control the timer by sending various commands. The commands are currently a subset of the commands the original LiveSplit supports.

- This feature is experimental and the protocol will likely change in the future. - , + text: ( +
+ Server Connection{" "} + +
+ ), + tooltip: ( + <> + Allows you to connect to a WebSocket + server that can control the timer by + sending various commands. The commands + are currently a subset of the commands + the original LiveSplit supports. +
+
+ This feature is experimental and + the protocol will likely change in the + future. + + ), value: { ServerConnection: { - url: this.props.generalSettings.serverUrl, + url: this.props.generalSettings + .serverUrl, connection: this.props.serverConnection, }, - } + }, }, ], }} @@ -277,12 +356,15 @@ export class MainSettings extends React.Component { case 1: if ("String" in value) { try { - this.props.callbacks.onServerConnectionOpened(new LiveSplitServer( - value.String, - () => this.forceUpdate(), - () => this.props.callbacks.onServerConnectionClosed(), - this.props.commandSink, - )); + this.props.callbacks.onServerConnectionOpened( + new LiveSplitServer( + value.String, + () => this.forceUpdate(), + () => + this.props.callbacks.onServerConnectionClosed(), + this.props.commandSink, + ), + ); } catch { // It's fine if it fails. } @@ -309,13 +391,23 @@ export class MainSettings extends React.Component {
diff --git a/src/ui/RunEditor.tsx b/src/ui/RunEditor.tsx index 27635c0..0fd88ca 100644 --- a/src/ui/RunEditor.tsx +++ b/src/ui/RunEditor.tsx @@ -1,12 +1,27 @@ import * as React from "react"; import * as LiveSplit from "../livesplit-core"; -import { FILE_EXT_IMAGES, FILE_EXT_SPLITS, openFileAsArrayBuffer } from "../util/FileUtil"; +import { + FILE_EXT_IMAGES, + FILE_EXT_SPLITS, + openFileAsArrayBuffer, +} from "../util/FileUtil"; import { TextBox } from "./TextBox"; import { toast } from "react-toastify"; import { - downloadGameList, searchGames, getCategories, downloadCategories, - downloadLeaderboard, getLeaderboard, downloadPlatformList, getPlatforms, - downloadRegionList, getRegions, downloadGameInfo, getGameInfo, downloadGameInfoByGameId, downloadCategoriesByGameId, + downloadGameList, + searchGames, + getCategories, + downloadCategories, + downloadLeaderboard, + getLeaderboard, + downloadPlatformList, + getPlatforms, + downloadRegionList, + getRegions, + downloadGameInfo, + getGameInfo, + downloadGameInfoByGameId, + downloadCategoriesByGameId, gameListLength, platformListLength, regionListLength, @@ -16,7 +31,9 @@ import { Option, expect, map } from "../util/OptionUtil"; import { formatLeaderboardTime } from "../util/TimeUtil"; import { resolveEmbed } from "./Embed"; import { - SettingsComponent, JsonSettingValueFactory, ExtendedSettingsDescriptionFieldJson, + SettingsComponent, + JsonSettingValueFactory, + ExtendedSettingsDescriptionFieldJson, ExtendedSettingsDescriptionValueJson, } from "./Settings"; import { Markdown, replaceFlag } from "../util/Markdown"; @@ -30,38 +47,41 @@ import { Check, X } from "lucide-react"; import "../css/RunEditor.scss"; export interface Props { - editor: LiveSplit.RunEditor, - callbacks: Callbacks, - runEditorUrlCache: UrlCache, - allComparisons: string[], - allVariables: Set, - generalSettings: GeneralSettings, + editor: LiveSplit.RunEditor; + callbacks: Callbacks; + runEditorUrlCache: UrlCache; + allComparisons: string[]; + allVariables: Set; + generalSettings: GeneralSettings; } export interface State { - editor: LiveSplit.RunEditorStateJson, - foundGames: string[], - offsetIsValid: boolean, - attemptCountIsValid: boolean, - rowState: RowState, - tab: Tab, - abortController: AbortController, + editor: LiveSplit.RunEditorStateJson; + foundGames: string[]; + offsetIsValid: boolean; + attemptCountIsValid: boolean; + rowState: RowState; + tab: Tab; + abortController: AbortController; } interface Callbacks { - renderViewWithSidebar(renderedView: React.JSX.Element, sidebarContent: React.JSX.Element): React.JSX.Element, - closeRunEditor(save: boolean): void, + renderViewWithSidebar( + renderedView: React.JSX.Element, + sidebarContent: React.JSX.Element, + ): React.JSX.Element; + closeRunEditor(save: boolean): void; } interface RowState { - splitTime: string, - splitTimeChanged: boolean, - segmentTime: string, - segmentTimeChanged: boolean, - bestSegmentTime: string, - bestSegmentTimeChanged: boolean, - comparisonTimes: string[], - comparisonTimesChanged: boolean[], - index: number, + splitTime: string; + splitTimeChanged: boolean; + segmentTime: string; + segmentTimeChanged: boolean; + bestSegmentTime: string; + bestSegmentTimeChanged: boolean; + comparisonTimes: string[]; + comparisonTimesChanged: boolean[]; + index: number; } enum Tab { @@ -73,11 +93,11 @@ enum Tab { } interface Filters { - region?: string, - platform?: string, - isEmulated?: boolean, - showObsolete: boolean, - variables: Map, + region?: string; + platform?: string; + isEmulated?: boolean; + showObsolete: boolean; + variables: Map; } export class RunEditor extends React.Component { @@ -88,7 +108,9 @@ export class RunEditor extends React.Component { constructor(props: Props) { super(props); - const state: LiveSplit.RunEditorStateJson = props.editor.stateAsJson(props.runEditorUrlCache.imageCache) as LiveSplit.RunEditorStateJson; + const state: LiveSplit.RunEditorStateJson = props.editor.stateAsJson( + props.runEditorUrlCache.imageCache, + ) as LiveSplit.RunEditorStateJson; const foundGames = searchGames(state.game); props.runEditorUrlCache.collect(); @@ -108,7 +130,10 @@ export class RunEditor extends React.Component { splitTime: "", splitTimeChanged: false, }, - tab: state.timing_method === "RealTime" ? Tab.RealTime : Tab.GameTime, + tab: + state.timing_method === "RealTime" + ? Tab.RealTime + : Tab.GameTime, abortController: new AbortController(), }; @@ -125,7 +150,10 @@ export class RunEditor extends React.Component { public render() { const renderedView = this.renderView(); const sidebarContent = this.renderSidebarContent(); - return this.props.callbacks.renderViewWithSidebar(renderedView, sidebarContent); + return this.props.callbacks.renderViewWithSidebar( + renderedView, + sidebarContent, + ); } private renderView() { @@ -140,7 +168,9 @@ export class RunEditor extends React.Component {
this.changeGameIcon()} downloadBoxArt={() => this.downloadBoxArt()} downloadIcon={() => this.downloadIcon()} @@ -164,7 +194,9 @@ export class RunEditor extends React.Component { this.handleCategoryChange(e)} + onChange={(e) => + this.handleCategoryChange(e) + } label="Category" list={[ "run-editor-category-list", @@ -189,7 +221,9 @@ export class RunEditor extends React.Component { this.handleAttemptsChange(e)} + onChange={(e) => + this.handleAttemptsChange(e) + } onBlur={(_) => this.handleAttemptsBlur()} small invalid={!this.state.attemptCountIsValid} @@ -210,7 +244,7 @@ export class RunEditor extends React.Component { {this.renderTab(tab, category)}
-
+
); } @@ -251,7 +285,9 @@ export class RunEditor extends React.Component { [Tab.Leaderboard]: "Leaderboard", }; - const visibleTabs = Object.values(Tab).filter((tab) => this.shouldShowTab(tab as Tab)); + const visibleTabs = Object.values(Tab).filter((tab) => + this.shouldShowTab(tab as Tab), + ); return visibleTabs.map((tab, index) => { let toggleClassName = "toggle-middle"; if (index === 0) { @@ -262,12 +298,14 @@ export class RunEditor extends React.Component { const buttonClassName = currentTab === tab ? " button-pressed" : ""; - return (); + return ( + + ); }); } @@ -316,8 +354,12 @@ export class RunEditor extends React.Component { private renderAssociateRunButton(): React.JSX.Element { if (this.props.generalSettings.speedrunComIntegration) { return ( - ); } else { @@ -327,9 +369,7 @@ export class RunEditor extends React.Component { private renderRulesButtons(): React.JSX.Element { return ( -
- {this.renderAssociateRunButton()} -
+
{this.renderAssociateRunButton()}
); } @@ -347,7 +387,8 @@ export class RunEditor extends React.Component { private async addCustomVariable() { const [result, variableName] = await showDialog({ title: "Add Variable", - description: "Specify the name of the custom variable you want to add:", + description: + "Specify the name of the custom variable you want to add:", textInput: true, buttons: ["OK", "Cancel"], }); @@ -357,7 +398,10 @@ export class RunEditor extends React.Component { } } - private renderSideButtons(tab: Tab, category: Option): React.JSX.Element { + private renderSideButtons( + tab: Tab, + category: Option, + ): React.JSX.Element { switch (tab) { case Tab.RealTime: case Tab.GameTime: @@ -385,7 +429,9 @@ export class RunEditor extends React.Component { } } - private renderLeaderboardButtons(category: Option): React.JSX.Element { + private renderLeaderboardButtons( + category: Option, + ): React.JSX.Element { const gameInfo = getGameInfo(this.state.editor.game); if (gameInfo === undefined) { return this.renderRulesButtons(); @@ -418,7 +464,11 @@ export class RunEditor extends React.Component { } if (regionList.length > 2) { - filterList.push(Region:); + filterList.push( + + Region: + , + ); filterList.push( @@ -432,7 +482,9 @@ export class RunEditor extends React.Component { this.updateFilters(); }} > - {regionList.map((v) => )} + {regionList.map((v) => ( + + ))} , @@ -440,7 +492,11 @@ export class RunEditor extends React.Component { } if (platformList.length > 2) { - filterList.push(Platform:); + filterList.push( + + Platform: + , + ); filterList.push( @@ -454,7 +510,9 @@ export class RunEditor extends React.Component { this.updateFilters(); }} > - {platformList.map((v) => )} + {platformList.map((v) => ( + + ))} , @@ -462,7 +520,11 @@ export class RunEditor extends React.Component { } if (gameInfo.ruleset["emulators-allowed"]) { - filterList.push(Emulator:); + filterList.push( + + Emulator: + , + ); filterList.push( @@ -471,8 +533,8 @@ export class RunEditor extends React.Component { this.filters.isEmulated === true ? "Yes" : this.filters.isEmulated === false - ? "No" - : "" + ? "No" + : "" } style={{ width: "100%", @@ -489,28 +551,46 @@ export class RunEditor extends React.Component { this.updateFilters(); }} > - {["", "Yes", "No"].map((v) => )} + {["", "Yes", "No"].map((v) => ( + + ))} , ); } - const variables = expect(gameInfo.variables, "We need the variables to be embedded"); + const variables = expect( + gameInfo.variables, + "We need the variables to be embedded", + ); for (const variable of variables.data) { if (this.variableIsValidForCategory(variable, category)) { if (variable["is-subcategory"]) { - let currentFilterValue = this.filters.variables.get(variable.name); + let currentFilterValue = this.filters.variables.get( + variable.name, + ); if (currentFilterValue === undefined) { - const runValue = this.state.editor.metadata.speedrun_com_variables[variable.name]; + const runValue = + this.state.editor.metadata.speedrun_com_variables[ + variable.name + ]; if (runValue !== undefined) { currentFilterValue = runValue; - this.filters.variables.set(variable.name, currentFilterValue); + this.filters.variables.set( + variable.name, + currentFilterValue, + ); } else { const defaultValueId = variable.values.default; if (defaultValueId != null) { - currentFilterValue = variable.values.values[defaultValueId].label; - this.filters.variables.set(variable.name, currentFilterValue); + currentFilterValue = + variable.values.values[defaultValueId] + .label; + this.filters.variables.set( + variable.name, + currentFilterValue, + ); } } } @@ -522,43 +602,70 @@ export class RunEditor extends React.Component { - {Object.values(variable.values.values).map(({ label }) => { - const isSelected = currentFilterValue === label; - return ( - - { - this.filters.variables.set(variable.name, isSelected ? "" : label); - this.updateFilters(); - }} - > - {label} - - - ); - })} + {Object.values(variable.values.values).map( + ({ label }) => { + const isSelected = + currentFilterValue === label; + return ( + + { + this.filters.variables.set( + variable.name, + isSelected + ? "" + : label, + ); + this.updateFilters(); + }} + > + {label} + + + ); + }, + )} , ); } else { - filterList.push({variable.name}:); + filterList.push( + + {variable.name}: + , + ); filterList.push( @@ -568,7 +675,11 @@ export class RunEditor extends React.Component { } } - filterList.push(Obsolete Runs:); + filterList.push( + + Obsolete Runs: + , + ); filterList.push( @@ -583,7 +694,9 @@ export class RunEditor extends React.Component { this.updateFilters(); }} > - {["Shown", "Hidden"].map((v) => )} + {["Shown", "Hidden"].map((v) => ( + + ))} , @@ -594,15 +707,22 @@ export class RunEditor extends React.Component { - {subcategoryBoxes} @@ -611,17 +731,21 @@ export class RunEditor extends React.Component { - - {filterList} - + {filterList}
Filters
); } - private variableIsValidForCategory(variable: Variable, category: Option) { - return (variable.category == null || variable.category === category?.id) && - (variable.scope.type === "full-game" || variable.scope.type === "global"); + private variableIsValidForCategory( + variable: Variable, + category: Option, + ) { + return ( + (variable.category == null || variable.category === category?.id) && + (variable.scope.type === "full-game" || + variable.scope.type === "global") + ); } private updateFilters() { @@ -659,16 +783,28 @@ export class RunEditor extends React.Component { } } } - const variables = expect(gameInfo.variables, "We need the variables to be embedded"); + const variables = expect( + gameInfo.variables, + "We need the variables to be embedded", + ); for (const variable of variables.data) { if (this.variableIsValidForCategory(variable, category)) { speedrunComVariables.push({ text: variable.name, - tooltip: "A variable on speedrun.com specific to the game.", + tooltip: + "A variable on speedrun.com specific to the game.", value: { CustomCombobox: { - value: metadata.speedrun_com_variables[variable.name] || "", - list: ["", ...Object.values(variable.values.values).map((v) => v.label)], + value: + metadata.speedrun_com_variables[ + variable.name + ] || "", + list: [ + "", + ...Object.values( + variable.values.values, + ).map((v) => v.label), + ], mandatory: variable.mandatory, }, }, @@ -708,7 +844,8 @@ export class RunEditor extends React.Component { emulatorOffset = fields.length; fields.push({ text: "Uses Emulator", - tooltip: "Whether an emulator is being used to play the game.", + tooltip: + "Whether an emulator is being used to play the game.", value: { Bool: metadata.uses_emulator, }, @@ -716,12 +853,16 @@ export class RunEditor extends React.Component { } } - for (const customVariableName of Object.keys(metadata.custom_variables)) { - const customVariableValue = metadata.custom_variables[customVariableName]; + for (const customVariableName of Object.keys( + metadata.custom_variables, + )) { + const customVariableValue = + metadata.custom_variables[customVariableName]; if (customVariableValue && customVariableValue.is_permanent) { customVariables.push({ text: customVariableName, - tooltip: "A custom variable specified by you. These can be displayed with the text component.", + tooltip: + "A custom variable specified by you. These can be displayed with the text component.", value: { RemovableString: customVariableValue.value, }, @@ -737,18 +878,23 @@ export class RunEditor extends React.Component { return (
- { - fields.length === 0 && + {fields.length === 0 && ( - + + +

- {"There are currently no"} - {this.props.generalSettings.speedrunComIntegration && " Speedrun.com variables or"} - {" custom variables for this game."} -

+

+ {"There are currently no"} + {this.props.generalSettings + .speedrunComIntegration && + " Speedrun.com variables or"} + {" custom variables for this game."} +

+
- } + )} { allComparisons={this.props.allComparisons} allVariables={this.props.allVariables} setValue={(index, value) => { - function unwrapString(value: ExtendedSettingsDescriptionValueJson): string { + function unwrapString( + value: ExtendedSettingsDescriptionValueJson, + ): string { if ("String" in value) { return value.String; } else { - throw new Error("Expected Setting value to be a string."); + throw new Error( + "Expected Setting value to be a string.", + ); } } - function unwrapRemovableString(value: ExtendedSettingsDescriptionValueJson): string | null { + function unwrapRemovableString( + value: ExtendedSettingsDescriptionValueJson, + ): string | null { if ("RemovableString" in value) { return value.RemovableString; } else { - throw new Error("Expected Setting value to be a string."); + throw new Error( + "Expected Setting value to be a string.", + ); } } - function unwrapBool(value: ExtendedSettingsDescriptionValueJson): boolean { + function unwrapBool( + value: ExtendedSettingsDescriptionValueJson, + ): boolean { if ("Bool" in value) { return value.Bool; } else { - throw new Error("Expected Setting value to be a boolean."); + throw new Error( + "Expected Setting value to be a boolean.", + ); } } if (index === regionOffset) { @@ -789,17 +947,29 @@ export class RunEditor extends React.Component { this.props.editor.setEmulatorUsage(emulatorUsage); } else if (index < customVariablesOffset) { const stringValue = unwrapString(value); - const key = speedrunComVariables[index - speedrunComVariablesOffset].text as string; + const key = speedrunComVariables[ + index - speedrunComVariablesOffset + ].text as string; if (stringValue !== "") { - this.props.editor.setSpeedrunComVariable(key, stringValue); + this.props.editor.setSpeedrunComVariable( + key, + stringValue, + ); } else { - this.props.editor.removeSpeedrunComVariable(key); + this.props.editor.removeSpeedrunComVariable( + key, + ); } } else { - const key = customVariables[index - customVariablesOffset].text as string; + const key = customVariables[ + index - customVariablesOffset + ].text as string; const stringValue = unwrapRemovableString(value); if (stringValue !== null) { - this.props.editor.setCustomVariable(key, stringValue); + this.props.editor.setCustomVariable( + key, + stringValue, + ); } else { this.props.editor.removeCustomVariable(key); } @@ -834,17 +1004,22 @@ export class RunEditor extends React.Component { let uniqueCount = 0; let lastTime = ""; - function isFiltered(value: string | undefined, filter: string | undefined): boolean { - return filter !== undefined - && filter !== "" - && value !== filter; + function isFiltered( + value: string | undefined, + filter: string | undefined, + ): boolean { + return filter !== undefined && filter !== "" && value !== filter; } const uniquenessSet = new Set(); const allVariables = gameInfo?.variables; - const variables = allVariables?.data.filter((variable) => this.variableIsValidForCategory(variable, category)); - const variableColumns = variables?.filter((variable) => !this.filters.variables.get(variable.name)); + const variables = allVariables?.data.filter((variable) => + this.variableIsValidForCategory(variable, category), + ); + const variableColumns = variables?.filter( + (variable) => !this.filters.variables.get(variable.name), + ); return ( @@ -853,7 +1028,9 @@ export class RunEditor extends React.Component { - {variableColumns?.map((variable) => )} + {variableColumns?.map((variable) => ( + + ))} @@ -863,12 +1040,17 @@ export class RunEditor extends React.Component { return null; } - const region = map(run.system.region, (r) => regionList.get(r)); + const region = map(run.system.region, (r) => + regionList.get(r), + ); if (isFiltered(region, this.filters.region)) { return null; } - if (this.filters.isEmulated !== undefined && run.system.emulated !== this.filters.isEmulated) { + if ( + this.filters.isEmulated !== undefined && + run.system.emulated !== this.filters.isEmulated + ) { return null; } @@ -876,11 +1058,28 @@ export class RunEditor extends React.Component { if (variables !== undefined) { for (const variable of variables) { - if (this.variableIsValidForCategory(variable, category)) { - const variableValueId = run.values[variable.id]; - const variableValue = map(variableValueId, (i) => variable.values.values[i]); - const filterValue = this.filters.variables.get(variable.name); - if (isFiltered(variableValue?.label, filterValue)) { + if ( + this.variableIsValidForCategory( + variable, + category, + ) + ) { + const variableValueId = + run.values[variable.id]; + const variableValue = map( + variableValueId, + (i) => variable.values.values[i], + ); + const filterValue = + this.filters.variables.get( + variable.name, + ); + if ( + isFiltered( + variableValue?.label, + filterValue, + ) + ) { return null; } } @@ -892,19 +1091,24 @@ export class RunEditor extends React.Component { const valueId = run.values[variable.id]; let valueName; if (valueId) { - const value = Object.entries(variable.values.values).find( - ([listValueId]) => listValueId === valueId, + const value = Object.entries( + variable.values.values, + ).find( + ([listValueId]) => + listValueId === valueId, ); valueName = map(value, (v) => v[1].label); } renderedVariables.push( - , + , ); } } - const uniquenessKeys = run.players.data.map( - (p) => p.rel === "guest" ? `guest:${p.name}` : p.id, + const uniquenessKeys = run.players.data.map((p) => + p.rel === "guest" ? `guest:${p.name}` : p.id, ); const uniquenessKey = JSON.stringify(uniquenessKeys); @@ -915,52 +1119,80 @@ export class RunEditor extends React.Component { uniquenessSet.add(uniquenessKey); const rowIndex = visibleRowCount; - const evenOdd = rowIndex % 2 === 0 ? "table-row-odd" : "table-row-even"; + const evenOdd = + rowIndex % 2 === 0 + ? "table-row-odd" + : "table-row-even"; let expandedRow = null; - if (this.expandedLeaderboardRows.get(rowIndex) === true) { + if ( + this.expandedLeaderboardRows.get(rowIndex) === true + ) { let embed = null; - if (run.videos != null && run.videos.links != null && run.videos.links.length > 0) { - const videoUri = run.videos.links[run.videos.links.length - 1].uri; + if ( + run.videos != null && + run.videos.links != null && + run.videos.links.length > 0 + ) { + const videoUri = + run.videos.links[ + run.videos.links.length - 1 + ].uri; embed = resolveEmbed(videoUri); } const comment = run.comment ?? ""; - expandedRow = - - + - ; + + ); } visibleRowCount += 1; @@ -977,53 +1209,66 @@ export class RunEditor extends React.Component { key={run.id} title={run.comment ?? ""} className={`leaderboard-row ${evenOdd}`} - onClick={(_) => this.toggleExpandLeaderboardRow(rowIndex)} + onClick={(_) => + this.toggleExpandLeaderboardRow(rowIndex) + } style={{ cursor: "pointer", }} > - + {renderedVariables} @@ -1067,18 +1315,34 @@ export class RunEditor extends React.Component { additionalRules.push("require video proof"); } if (additionalRules.length !== 0) { - gameRules = + gameRules = (

Runs of this game {additionalRules.join(" and ")}. -

; +

+ ); } - const variables = expect(gameInfo.variables, "We need the variables to be embedded"); + const variables = expect( + gameInfo.variables, + "We need the variables to be embedded", + ); for (const variable of variables.data) { - if (this.variableIsValidForCategory(variable, category) && variable["is-subcategory"]) { - const currentValue = this.state.editor.metadata.speedrun_com_variables[variable.name]; - const foundValue = Object.values(variable.values.values).find((v) => v.label === currentValue); + if ( + this.variableIsValidForCategory(variable, category) && + variable["is-subcategory"] + ) { + const currentValue = + this.state.editor.metadata.speedrun_com_variables[ + variable.name + ]; + const foundValue = Object.values( + variable.values.values, + ).find((v) => v.label === currentValue); if (foundValue?.rules != null) { - subcategoryRules.push(); + subcategoryRules.push( + , + ); } } } @@ -1086,7 +1350,11 @@ export class RunEditor extends React.Component { return (
-
{gameRules}{rules}{subcategoryRules}
+
+ {gameRules} + {rules} + {subcategoryRules} +
); } @@ -1101,13 +1369,16 @@ export class RunEditor extends React.Component {
- { - this.state.editor.comparison_names.map((comparison, comparisonIndex) => { + {this.state.editor.comparison_names.map( + (comparison, comparisonIndex) => { return ( { - e.dataTransfer.setData("text/plain", ""); + e.dataTransfer.setData( + "text/plain", + "", + ); this.dragIndex = comparisonIndex; }} onDragEnd={(_) => this.update()} @@ -1115,122 +1386,191 @@ export class RunEditor extends React.Component { if (e.stopPropagation) { e.stopPropagation(); } - this.props.editor.moveComparison(this.dragIndex, comparisonIndex); + this.props.editor.moveComparison( + this.dragIndex, + comparisonIndex, + ); return false; }} - renameComparison={() => this.renameComparison(comparison)} - copyComparison={() => this.copyComparison(comparison)} - removeComparison={() => this.removeComparison(comparison)} + renameComparison={() => + this.renameComparison(comparison) + } + copyComparison={() => + this.copyComparison(comparison) + } + removeComparison={() => + this.removeComparison(comparison) + } /> ); - }) - } + }, + )} - { - this.state.editor.segments.map((s, segmentIndex) => { - const segmentIcon = this.getSegmentIconUrl(segmentIndex); + {this.state.editor.segments.map((s, segmentIndex) => { + const segmentIcon = + this.getSegmentIconUrl(segmentIndex); - return ( - + this.changeSegmentSelection(e, segmentIndex) + } + > + + this.changeSegmentIcon(segmentIndex) } - onClick={(e) => this.changeSegmentSelection(e, segmentIndex)} - > - this.changeSegmentIcon(segmentIndex)} - removeSegmentIcon={() => this.removeSegmentIcon(segmentIndex)} + removeSegmentIcon={() => + this.removeSegmentIcon(segmentIndex) + } + /> + - - - - { - this - .state - .editor - .segments[segmentIndex] - .comparison_times - .map((comparisonTime, comparisonIndex) => ( - - )) - } - - ); - }) - } + : s.split_time + } + onFocus={(_) => + this.focusSegment(segmentIndex) + } + onChange={(e) => + this.handleSplitTimeChange(e) + } + onBlur={(_) => + this.handleSplitTimeBlur() + } + /> + + + + {this.state.editor.segments[ + segmentIndex + ].comparison_times.map( + (comparisonTime, comparisonIndex) => ( + + ), + )} + + ); + })}
Rank Player Time{variable.name}{variable.name}
{valueName || ""} + {valueName || ""} +
+ expandedRow = ( +
{embed} -
+
- + - {map( - region, - (r) => - - - - , - )} - {map( - platform, - (p) => - - - - , - )} + {map(region, (r) => ( + + + + + ))} + {map(platform, (p) => ( + + + + + ))}
Date:{run.date?.split("-").join("/") ?? ""} + {run.date + ?.split("-") + .join("/") ?? ""} +
Region:{r}
Platform:{p}{run.system.emulated && " Emulator"}
Region:{r}
Platform: + {p} + {run.system + .emulated && + " Emulator"} +
{isUnique ? rank : "—"} + {isUnique ? rank : "—"} + - { - run.players.data.map((p, i) => { - if (p.rel === "user") { - const style = p["name-style"]; - let color; - if (style.style === "gradient") { - color = style["color-from"].dark; - } else { - color = style.color.dark; - } - const flag = map( - p.location, - (l) => replaceFlag(l.country.code), - ); - return [ - i !== 0 ? ", " : null, - e.stopPropagation()} - > - {flag}{p.names.international} - , - ]; + {run.players.data.map((p, i) => { + if (p.rel === "user") { + const style = p["name-style"]; + let color; + if (style.style === "gradient") { + color = + style["color-from"].dark; } else { - const possibleMatch = /^\[([a-z]+)\](.+)$/.exec(p.name); - let name = p.name; - let flag; - if (possibleMatch !== null) { - flag = replaceFlag(possibleMatch[1]); - name = possibleMatch[2]; - } - return [ - i !== 0 ? ", " : null, - {flag}{name} - ]; + color = style.color.dark; } - }) - } + const flag = map(p.location, (l) => + replaceFlag(l.country.code), + ); + return [ + i !== 0 ? ", " : null, + + e.stopPropagation() + } + > + {flag} + {p.names.international} + , + ]; + } else { + const possibleMatch = + /^\[([a-z]+)\](.+)$/.exec( + p.name, + ); + let name = p.name; + let flag; + if (possibleMatch !== null) { + flag = replaceFlag( + possibleMatch[1], + ); + name = possibleMatch[2]; + } + return [ + i !== 0 ? ", " : null, + + {flag} + {name} + , + ]; + } + })} { style={{ color: "white" }} onClick={(e) => e.stopPropagation()} > - {formatLeaderboardTime(run.times.primary_t, hideMilliseconds)} + {formatLeaderboardTime( + run.times.primary_t, + hideMilliseconds, + )} Split Time Segment Time Best Segment
+ + this.focusSegment(segmentIndex) + } + onChange={(e) => + this.handleSegmentNameChange(e) + } /> - - this.focusSegment(segmentIndex)} - onChange={(e) => this.handleSegmentNameChange(e)} - /> - - + + this.focusSegment(segmentIndex)} - onChange={(e) => this.handleSplitTimeChange(e)} - onBlur={(_) => this.handleSplitTimeBlur()} - /> - - this.focusSegment(segmentIndex)} - onChange={(e) => this.handleSegmentTimeChange(e)} - onBlur={(_) => this.handleSegmentTimeBlur()} - /> - - this.focusSegment(segmentIndex)} - onChange={(e) => this.handleBestSegmentTimeChange(e)} - onBlur={(_) => this.handleBestSegmentTimeBlur()} - /> - - this.focusSegment(segmentIndex)} - onChange={(e) => - this.handleComparisonTimeChange(e, comparisonIndex) - } - onBlur={(_) => - this.handleComparisonTimeBlur(comparisonIndex) - } - /> -
+ + this.focusSegment(segmentIndex) + } + onChange={(e) => + this.handleSegmentTimeChange(e) + } + onBlur={(_) => + this.handleSegmentTimeBlur() + } + /> + + + this.focusSegment(segmentIndex) + } + onChange={(e) => + this.handleBestSegmentTimeChange(e) + } + onBlur={(_) => + this.handleBestSegmentTimeBlur() + } + /> + + + this.focusSegment( + segmentIndex, + ) + } + onChange={(e) => + this.handleComparisonTimeChange( + e, + comparisonIndex, + ) + } + onBlur={(_) => + this.handleComparisonTimeBlur( + comparisonIndex, + ) + } + /> +
); @@ -1250,7 +1590,9 @@ export class RunEditor extends React.Component { const categoryList = getCategories(this.state.editor.game); if (categoryList !== undefined) { categoryNames = categoryList.map((c) => c.name); - const categoryIndex = categoryNames.indexOf(this.state.editor.category); + const categoryIndex = categoryNames.indexOf( + this.state.editor.category, + ); if (categoryIndex >= 0) { category = categoryList[categoryIndex]; } @@ -1273,7 +1615,9 @@ export class RunEditor extends React.Component { if (this.props.editor.parseAndGenerateGoalComparison(goalTime)) { this.update(); } else { - toast.error("Failed generating the goal comparison. Make sure to specify a valid time."); + toast.error( + "Failed generating the goal comparison. Make sure to specify a valid time.", + ); } } } @@ -1283,7 +1627,8 @@ export class RunEditor extends React.Component { if (comparison === undefined) { const [result, comparisonName] = await showDialog({ title: "Copy Comparison", - description: "Specify the name of the comparison you want to copy:", + description: + "Specify the name of the comparison you want to copy:", textInput: true, buttons: ["Copy", "Cancel"], }); @@ -1295,7 +1640,10 @@ export class RunEditor extends React.Component { let newName: string | undefined; if (comparison.endsWith(" Copy")) { - const before = comparison.substring(0, comparison.length - " Copy".length); + const before = comparison.substring( + 0, + comparison.length - " Copy".length, + ); newName = `${before} Copy 2`; } else { const regexMatch = /^(.* Copy )(\d+)$/.exec(comparison); @@ -1310,7 +1658,9 @@ export class RunEditor extends React.Component { if (this.props.editor.copyComparison(comparison, newName)) { this.update(); } else { - toast.error("Failed copying the comparison. The comparison may not exist."); + toast.error( + "Failed copying the comparison. The comparison may not exist.", + ); } } @@ -1373,7 +1723,8 @@ export class RunEditor extends React.Component { using run = result.unwrap(); const [dialogResult, comparisonName] = await showDialog({ title: "Import Comparison", - description: "Specify the name of the comparison you want to import:", + description: + "Specify the name of the comparison you want to import:", textInput: true, buttons: ["Import", "Cancel"], defaultText: file.name.replace(/\.[^/.]+$/, ""), @@ -1383,7 +1734,9 @@ export class RunEditor extends React.Component { } const valid = this.props.editor.importComparison(run, comparisonName); if (!valid) { - toast.error("The comparison could not be added. It may be a duplicate or a reserved name."); + toast.error( + "The comparison could not be added. It may be a duplicate or a reserved name.", + ); } else { this.update(); } @@ -1402,7 +1755,9 @@ export class RunEditor extends React.Component { if (valid) { this.update(); } else { - toast.error("The comparison could not be added. It may be a duplicate or a reserved name."); + toast.error( + "The comparison could not be added. It may be a duplicate or a reserved name.", + ); } } } @@ -1417,11 +1772,16 @@ export class RunEditor extends React.Component { }); if (result === 0) { - const valid = this.props.editor.renameComparison(comparison, newName); + const valid = this.props.editor.renameComparison( + comparison, + newName, + ); if (valid) { this.update(); } else { - toast.error("The comparison could not be renamed. It may be a duplicate or a reserved name."); + toast.error( + "The comparison could not be renamed. It may be a duplicate or a reserved name.", + ); } } } @@ -1452,7 +1812,9 @@ export class RunEditor extends React.Component { } private getSegmentIconUrl(index: number): string | undefined { - return this.props.runEditorUrlCache.cache(this.state.editor.segments[index].icon); + return this.props.runEditorUrlCache.cache( + this.state.editor.segments[index].icon, + ); } private async changeGameIcon() { @@ -1485,21 +1847,22 @@ export class RunEditor extends React.Component { * the leaderboards. We don't want to update the editor if it has been * disposed in the meantime. */ - private maybeUpdate(options: { switchTab?: Tab, search?: boolean } = {}) { + private maybeUpdate(options: { switchTab?: Tab; search?: boolean } = {}) { if (this.props.editor.ptr === 0) { return; } this.update(options); } - private update(options: { switchTab?: Tab, search?: boolean } = {}) { + private update(options: { switchTab?: Tab; search?: boolean } = {}) { const intendedTab = options.switchTab ?? this.state.tab; const shouldShowTab = this.shouldShowTab(intendedTab); const newActiveTab = shouldShowTab ? intendedTab : Tab.RealTime; - const state: LiveSplit.RunEditorStateJson = this.props.editor.stateAsJson( - this.props.runEditorUrlCache.imageCache, - ); + const state: LiveSplit.RunEditorStateJson = + this.props.editor.stateAsJson( + this.props.runEditorUrlCache.imageCache, + ); if (options.search) { this.setState({ foundGames: searchGames(state.game) }); } @@ -1516,7 +1879,10 @@ export class RunEditor extends React.Component { if (this.props.generalSettings.speedrunComIntegration) { this.refreshGameInfo(event.target.value); this.refreshCategoryList(event.target.value); - this.refreshLeaderboard(event.target.value, this.state.editor.category); + this.refreshLeaderboard( + event.target.value, + this.state.editor.category, + ); this.resetTotalLeaderboardState(); } this.update({ search: true }); @@ -1545,14 +1911,18 @@ export class RunEditor extends React.Component { private handleOffsetBlur() { this.setState({ - editor: this.props.editor.stateAsJson(this.props.runEditorUrlCache.imageCache), + editor: this.props.editor.stateAsJson( + this.props.runEditorUrlCache.imageCache, + ), offsetIsValid: true, }); this.props.runEditorUrlCache.collect(); } private handleAttemptsChange(event: any) { - const valid = this.props.editor.parseAndSetAttemptCount(event.target.value); + const valid = this.props.editor.parseAndSetAttemptCount( + event.target.value, + ); this.setState({ attemptCountIsValid: valid, editor: { @@ -1565,7 +1935,9 @@ export class RunEditor extends React.Component { private handleAttemptsBlur() { this.setState({ attemptCountIsValid: true, - editor: this.props.editor.stateAsJson(this.props.runEditorUrlCache.imageCache), + editor: this.props.editor.stateAsJson( + this.props.runEditorUrlCache.imageCache, + ), }); this.props.runEditorUrlCache.collect(); } @@ -1573,9 +1945,10 @@ export class RunEditor extends React.Component { private focusSegment(i: number) { this.props.editor.selectOnly(i); - const editor: LiveSplit.RunEditorStateJson = this.props.editor.stateAsJson( - this.props.runEditorUrlCache.imageCache, - ); + const editor: LiveSplit.RunEditorStateJson = + this.props.editor.stateAsJson( + this.props.runEditorUrlCache.imageCache, + ); this.props.runEditorUrlCache.collect(); const comparisonTimes = editor.segments[i].comparison_times; @@ -1634,7 +2007,9 @@ export class RunEditor extends React.Component { const comparisonTimes = { ...this.state.rowState.comparisonTimes }; comparisonTimes[comparisonIndex] = event.target.value; - const comparisonTimesChanged = { ...this.state.rowState.comparisonTimesChanged }; + const comparisonTimesChanged = { + ...this.state.rowState.comparisonTimesChanged, + }; comparisonTimesChanged[comparisonIndex] = true; this.setState({ @@ -1648,11 +2023,15 @@ export class RunEditor extends React.Component { private handleSplitTimeBlur() { if (this.state.rowState.splitTimeChanged) { - this.props.editor.activeParseAndSetSplitTime(this.state.rowState.splitTime); + this.props.editor.activeParseAndSetSplitTime( + this.state.rowState.splitTime, + ); } this.setState({ - editor: this.props.editor.stateAsJson(this.props.runEditorUrlCache.imageCache), + editor: this.props.editor.stateAsJson( + this.props.runEditorUrlCache.imageCache, + ), rowState: { ...this.state.rowState, splitTimeChanged: false, @@ -1664,11 +2043,15 @@ export class RunEditor extends React.Component { private handleSegmentTimeBlur() { if (this.state.rowState.segmentTimeChanged) { - this.props.editor.activeParseAndSetSegmentTime(this.state.rowState.segmentTime); + this.props.editor.activeParseAndSetSegmentTime( + this.state.rowState.segmentTime, + ); } this.setState({ - editor: this.props.editor.stateAsJson(this.props.runEditorUrlCache.imageCache), + editor: this.props.editor.stateAsJson( + this.props.runEditorUrlCache.imageCache, + ), rowState: { ...this.state.rowState, segmentTimeChanged: false, @@ -1680,11 +2063,15 @@ export class RunEditor extends React.Component { private handleBestSegmentTimeBlur() { if (this.state.rowState.bestSegmentTimeChanged) { - this.props.editor.activeParseAndSetBestSegmentTime(this.state.rowState.bestSegmentTime); + this.props.editor.activeParseAndSetBestSegmentTime( + this.state.rowState.bestSegmentTime, + ); } this.setState({ - editor: this.props.editor.stateAsJson(this.props.runEditorUrlCache.imageCache), + editor: this.props.editor.stateAsJson( + this.props.runEditorUrlCache.imageCache, + ), rowState: { ...this.state.rowState, bestSegmentTimeChanged: false, @@ -1695,16 +2082,25 @@ export class RunEditor extends React.Component { } private handleComparisonTimeBlur(comparisonIndex: number) { - const comparisonTimesChanged = { ...this.state.rowState.comparisonTimesChanged }; + const comparisonTimesChanged = { + ...this.state.rowState.comparisonTimesChanged, + }; if (comparisonTimesChanged[comparisonIndex]) { - const comparisonName = this.state.editor.comparison_names[comparisonIndex]; - const comparisonTime = this.state.rowState.comparisonTimes[comparisonIndex]; - this.props.editor.activeParseAndSetComparisonTime(comparisonName, comparisonTime); + const comparisonName = + this.state.editor.comparison_names[comparisonIndex]; + const comparisonTime = + this.state.rowState.comparisonTimes[comparisonIndex]; + this.props.editor.activeParseAndSetComparisonTime( + comparisonName, + comparisonTime, + ); } comparisonTimesChanged[comparisonIndex] = false; this.setState({ - editor: this.props.editor.stateAsJson(this.props.runEditorUrlCache.imageCache), + editor: this.props.editor.stateAsJson( + this.props.runEditorUrlCache.imageCache, + ), rowState: { ...this.state.rowState, comparisonTimesChanged, @@ -1751,11 +2147,15 @@ export class RunEditor extends React.Component { private switchTab(tab: Tab) { switch (tab) { case Tab.RealTime: { - this.props.editor.selectTimingMethod(LiveSplit.TimingMethod.RealTime); + this.props.editor.selectTimingMethod( + LiveSplit.TimingMethod.RealTime, + ); break; } case Tab.GameTime: { - this.props.editor.selectTimingMethod(LiveSplit.TimingMethod.GameTime); + this.props.editor.selectTimingMethod( + LiveSplit.TimingMethod.GameTime, + ); break; } } @@ -1764,7 +2164,11 @@ export class RunEditor extends React.Component { } private shouldShowTab(tab: Tab) { - if (tab === Tab.RealTime || tab === Tab.GameTime || tab === Tab.Variables) { + if ( + tab === Tab.RealTime || + tab === Tab.GameTime || + tab === Tab.Variables + ) { return true; } @@ -1804,12 +2208,17 @@ export class RunEditor extends React.Component { const game = getGameInfo(gameName); if (game !== undefined) { const uri = game.assets["cover-medium"].uri; - if (uri.startsWith("https://") && uri !== "https://www.speedrun.com/images/blankcover.png") { + if ( + uri.startsWith("https://") && + uri !== "https://www.speedrun.com/images/blankcover.png" + ) { const buffer = await corsBustingFetch(uri, signal); if (this.props.editor.ptr === 0) { return; } - this.props.editor.setGameIconFromArray(new Uint8Array(buffer)); + this.props.editor.setGameIconFromArray( + new Uint8Array(buffer), + ); this.maybeUpdate(); } else { toast.error("The game doesn't have a box art."); @@ -1833,12 +2242,17 @@ export class RunEditor extends React.Component { const game = getGameInfo(gameName); if (game !== undefined) { const uri = game.assets.icon.uri; - if (uri.startsWith("https://") && uri !== "https://www.speedrun.com/images/1st.png") { + if ( + uri.startsWith("https://") && + uri !== "https://www.speedrun.com/images/1st.png" + ) { const buffer = await corsBustingFetch(uri, signal); if (this.props.editor.ptr === 0) { return; } - this.props.editor.setGameIconFromArray(new Uint8Array(buffer)); + this.props.editor.setGameIconFromArray( + new Uint8Array(buffer), + ); this.maybeUpdate(); } else { toast.error("The game doesn't have an icon."); @@ -1920,13 +2334,19 @@ export class RunEditor extends React.Component { if (gameInfo === undefined) { continue; } - const variables = expect(gameInfo.variables, "We need the variables to be embedded"); + const variables = expect( + gameInfo.variables, + "We need the variables to be embedded", + ); for (const variable of variables.data) { if ( - variable.category === category?.id - && (variable.scope.type === "full-game" || variable.scope.type === "global") + variable.category === category?.id && + (variable.scope.type === "full-game" || + variable.scope.type === "global") ) { - this.props.editor.removeSpeedrunComVariable(variable.name); + this.props.editor.removeSpeedrunComVariable( + variable.name, + ); } } break; @@ -1937,7 +2357,10 @@ export class RunEditor extends React.Component { private async interactiveAssociateRunOrOpenPage() { const currentRunId = this.state.editor.metadata.run_id; if (currentRunId !== "") { - window.open(`https://www.speedrun.com/run/${currentRunId}`, "_blank"); + window.open( + `https://www.speedrun.com/run/${currentRunId}`, + "_blank", + ); return; } @@ -1951,7 +2374,8 @@ export class RunEditor extends React.Component { if (result !== 0) { return; } - const pattern = /^(?:(?:https?:\/\/)?(?:www\.)?speedrun\.com\/(?:\w+\/)?run\/)?(\w+)$/; + const pattern = + /^(?:(?:https?:\/\/)?(?:www\.)?speedrun\.com\/(?:\w+\/)?run\/)?(\w+)$/; const matches = pattern.exec(idOrUrl); if (matches === null) { toast.error("Invalid speedrun.com ID or URL."); @@ -1969,12 +2393,7 @@ export class RunEditor extends React.Component { const gameName = gameInfo.names.international; const categoryName = category.name; - associateRun( - this.props.editor, - gameName, - categoryName, - run, - ); + associateRun(this.props.editor, gameName, categoryName, run); this.refreshLeaderboard(gameName, categoryName); this.resetTotalLeaderboardState(); @@ -2050,47 +2469,62 @@ function GameIcon({ className="game-icon-container" onClick={(e) => setPosition({ x: e.clientX, y: e.clientY })} > - { - gameIcon !== undefined && - - } + {gameIcon !== undefined && ( + + )}
{position && ( - setPosition(null)}> - + setPosition(null)} + > + Set Icon - Allows you to choose an image file to set as the game's icon. Certain file formats may not work everywhere. + Allows you to choose an image file to set as the + game's icon. Certain file formats may not work + everywhere. - { - speedrunComIntegration && <> - + {speedrunComIntegration && ( + <> + Download Box Art - Attempts to download the box art of the game from speedrun.com, to set as the game's icon. + Attempts to download the box art of the game + from speedrun.com, to set as the game's + icon. - + Download Icon - Attempts to download the icon of the game from speedrun.com. + Attempts to download the icon of the game + from speedrun.com. - } - { - gameIcon !== undefined && - + )} + {gameIcon !== undefined && ( + Remove Icon Removes the icon of the game. - } + )} )} @@ -2110,27 +2544,55 @@ function CleaningButton({ return ( <> - {position && ( - setPosition(null)}> - + setPosition(null)} + > + Clear Only History - Splits store the entire history of all runs, including every segment time. This information is used by various components. You can clear the history with this. The personal best, the best segment times, and the comparisons will not be affected. + Splits store the entire history of all runs, + including every segment time. This information is + used by various components. You can clear the + history with this. The personal best, the best + segment times, and the comparisons will not be + affected. - + Clear All Times - This removes all the times from the splits, including all the history, such that the splits are completely empty, as if they were just created. + This removes all the times from the splits, + including all the history, such that the splits are + completely empty, as if they were just created. - + Clean Sum of Best - Allows you to interactively remove potential issues in the segment history that lead to an inaccurate Sum of Best. If you skip a split, whenever you will do the next split, the combined segment time might be faster than the sum of the individual best segments. This will point out all such occurrences and allow you to delete them individually if any of them seem wrong. + Allows you to interactively remove potential issues + in the segment history that lead to an inaccurate + Sum of Best. If you skip a split, whenever you will + do the next split, the combined segment time might + be faster than the sum of the individual best + segments. This will point out all such occurrences + and allow you to delete them individually if any of + them seem wrong. @@ -2154,33 +2616,69 @@ function ComparisonsButton({ return ( <> - {position && ( - setPosition(null)}> - + setPosition(null)} + > + Add Comparison - Adds a new custom comparison where you can store any times that you would like. + Adds a new custom comparison where you can store any + times that you would like. - + Import Comparison - Imports the Personal Best of a splits file you provide as a comparison. + Imports the Personal Best of a splits file you + provide as a comparison. - + Generate Goal Comparison - Generates a custom goal comparison based on a goal time that you can specify. The comparison's times are automatically balanced based on the segment history such that it roughly represents what the split times for the goal time would look like. Since it is populated by the segment history, the goal times are capped to a range between the sum of the best segments and the sum of the worst segments. The comparison is only populated for the selected timing method. The other timing method's comparison times are not modified by this, so you can generate it again with the other timing method to generate the comparison times for both timing methods. + Generates a custom goal comparison based on a goal + time that you can specify. The comparison's times + are automatically balanced based on the segment + history such that it roughly represents what the + split times for the goal time would look like. Since + it is populated by the segment history, the goal + times are capped to a range between the sum of the + best segments and the sum of the worst segments. The + comparison is only populated for the selected timing + method. The other timing method's comparison times + are not modified by this, so you can generate it + again with the other timing method to generate the + comparison times for both timing methods. - + Copy Comparison - Copies any existing comparison, including the Personal Best or even any other automatically provided comparison as a new custom comparison. You could for example use this to keep the Latest Run around as a comparison that exists for as long as you want it to. + Copies any existing comparison, including the + Personal Best or even any other automatically + provided comparison as a new custom comparison. You + could for example use this to keep the Latest Run + around as a comparison that exists for as long as + you want it to. @@ -2214,19 +2712,27 @@ function SegmentIcon({ } }} > - { - segmentIcon !== undefined && - - } + {segmentIcon !== undefined && } {position && ( - setPosition(null)}> - + setPosition(null)} + > + Set Icon - Allows you to choose an image file to set as the segment's icon. Certain file formats may not work everywhere. + Allows you to choose an image file to set as the + segment's icon. Certain file formats may not work + everywhere. - + Remove Icon Removes the segment's icon. @@ -2280,20 +2786,34 @@ function CustomComparison({ > {comparison} {position && ( - setPosition(null)}> - + setPosition(null)} + > + Rename - Choose a new name for the custom comparison. There are reserved names that can't be used. You also can't have duplicate names. + Choose a new name for the custom comparison. There + are reserved names that can't be used. You also + can't have duplicate names. - + Copy Creates a copy of the custom comparison. - + Remove Removes the custom comparison. diff --git a/src/ui/Settings.tsx b/src/ui/Settings.tsx index 480169b..47467b3 100644 --- a/src/ui/Settings.tsx +++ b/src/ui/Settings.tsx @@ -16,34 +16,34 @@ import "../css/Tooltip.scss"; import "../css/LiveSplitServerButton.scss"; export interface Props { - context: string, - setValue: (index: number, value: T) => void, - state: ExtendedSettingsDescriptionJson, - factory: SettingValueFactory, - editorUrlCache: UrlCache, - allComparisons: string[], - allVariables: Set, + context: string; + setValue: (index: number, value: T) => void; + state: ExtendedSettingsDescriptionJson; + factory: SettingValueFactory; + editorUrlCache: UrlCache; + allComparisons: string[]; + allVariables: Set; } export interface ExtendedSettingsDescriptionJson { - fields: ExtendedSettingsDescriptionFieldJson[], + fields: ExtendedSettingsDescriptionFieldJson[]; } export interface ExtendedSettingsDescriptionFieldJson { - text: string | React.JSX.Element, - tooltip: string | React.JSX.Element, - value: ExtendedSettingsDescriptionValueJson, + text: string | React.JSX.Element; + tooltip: string | React.JSX.Element; + value: ExtendedSettingsDescriptionValueJson; } export type ExtendedSettingsDescriptionValueJson = - SettingsDescriptionValueJson | - { RemovableString: string | null } | - { - ServerConnection: { - url: string | undefined, - connection: Option, - } - }; + | SettingsDescriptionValueJson + | { RemovableString: string | null } + | { + ServerConnection: { + url: string | undefined; + connection: Option; + }; + }; export interface SettingValueFactory { fromBool(v: boolean): T; @@ -63,16 +63,34 @@ export interface SettingValueFactory { fromOptionalEmptyColor(): T; fromTransparentGradient(): T; fromVerticalGradient( - r1: number, g1: number, b1: number, a1: number, - r2: number, g2: number, b2: number, a2: number, + r1: number, + g1: number, + b1: number, + a1: number, + r2: number, + g2: number, + b2: number, + a2: number, ): T; fromHorizontalGradient( - r1: number, g1: number, b1: number, a1: number, - r2: number, g2: number, b2: number, a2: number, + r1: number, + g1: number, + b1: number, + a1: number, + r2: number, + g2: number, + b2: number, + a2: number, ): T; fromAlternatingGradient( - r1: number, g1: number, b1: number, a1: number, - r2: number, g2: number, b2: number, a2: number, + r1: number, + g1: number, + b1: number, + a1: number, + r2: number, + g2: number, + b2: number, + a2: number, ): T; fromAlignment(value: string): T | null; fromColumnKind(value: string): T | null; @@ -80,7 +98,12 @@ export interface SettingValueFactory { fromColumnUpdateWith(value: string): T | null; fromColumnUpdateTrigger(value: string): T | null; fromLayoutDirection(value: string): T | null; - fromFont(name: string, style: string, weight: string, stretch: string): T | null; + fromFont( + name: string, + style: string, + weight: string, + stretch: string, + ): T | null; fromEmptyFont(): T; fromDeltaGradient(value: string): T | null; fromBackgroundImage( @@ -91,7 +114,9 @@ export interface SettingValueFactory { ): T | null; } -export class JsonSettingValueFactory implements SettingValueFactory { +export class JsonSettingValueFactory + implements SettingValueFactory +{ public fromBool(v: boolean): ExtendedSettingsDescriptionValueJson { return { Bool: v }; } @@ -110,19 +135,27 @@ export class JsonSettingValueFactory implements SettingValueFactory extends React.Component> { public render() { const settingsRows: React.JSX.Element[] = []; @@ -208,7 +254,7 @@ export class SettingsComponent extends React.Component> { setIsChecked={(value) => { this.props.setValue( valueIndex, - factory.fromBool(value) + factory.fromBool(value), ); }} /> @@ -250,35 +296,49 @@ export class SettingsComponent extends React.Component> { } else if ("String" in value) { // FIXME: This is a hack that we need for now until the way // settings are represented is refactored. - if (typeof (field.text) === "string" && /^Variable/.test(field.text)) { + if ( + typeof field.text === "string" && + /^Variable/.test(field.text) + ) { if (this.props.allVariables.size === 0) { - component =
- - No variables available - - Custom variables can be defined in the Variables tab when - editing splits. Additional custom variables can be provided - automatically by auto splitters. + component = ( +
+ + No variables available + + Custom variables can be defined in the + Variables tab when editing splits. + Additional custom variables can be + provided automatically by auto + splitters. + - -
; +
+ ); } else { - component =
- -
; + component = ( +
+ +
+ ); } } else { component = ( @@ -299,30 +359,37 @@ export class SettingsComponent extends React.Component> { } else if ("OptionalString" in value) { // FIXME: This is a hack that we need for now until the way // settings are represented is refactored. - if (typeof (field.text) === "string" && /^Comparison( \d)?$/.test(field.text)) { - component =
- -
; + if ( + typeof field.text === "string" && + /^Comparison( \d)?$/.test(field.text) + ) { + component = ( +
+ +
+ ); } else { const children = [ extends React.Component> { onChange={(e) => { this.props.setValue( valueIndex, - factory.fromOptionalString(e.target.value), + factory.fromOptionalString( + e.target.value, + ), ); }} />, @@ -374,23 +443,30 @@ export class SettingsComponent extends React.Component> { if (factory.fromRemovableString) { this.props.setValue( valueIndex, - factory.fromRemovableString(e.target.value), + factory.fromRemovableString( + e.target.value, + ), + ); + } else { + throw Error("Method is not implemented"); + } + }} + /> + { + if (factory.fromRemovableEmptyString) { + this.props.setValue( + valueIndex, + factory.fromRemovableEmptyString(), ); } else { throw Error("Method is not implemented"); } }} /> - { - if (factory.fromRemovableEmptyString) { - this.props.setValue( - valueIndex, - factory.fromRemovableEmptyString(), - ); - } else { - throw Error("Method is not implemented"); - } - }} />
); } else if ("Accuracy" in value) { @@ -424,7 +500,9 @@ export class SettingsComponent extends React.Component> { this.props.setValue( valueIndex, expect( - factory.fromDigitsFormat(e.target.value), + factory.fromDigitsFormat( + e.target.value, + ), "Unexpected Digits Format", ), ); @@ -488,7 +566,12 @@ export class SettingsComponent extends React.Component> { if (value) { this.props.setValue( valueIndex, - factory.fromOptionalColor(1.0, 1.0, 1.0, 1.0), + factory.fromOptionalColor( + 1.0, + 1.0, + 1.0, + 1.0, + ), ); } else { this.props.setValue( @@ -534,17 +617,32 @@ export class SettingsComponent extends React.Component> { return factory.fromTransparentGradient(); case "Plain": return factory.fromColor( - color1[0], color1[1], color1[2], color1[3], + color1[0], + color1[1], + color1[2], + color1[3], ); case "Vertical": return factory.fromVerticalGradient( - color1[0], color1[1], color1[2], color1[3], - color2[0], color2[1], color2[2], color2[3], + color1[0], + color1[1], + color1[2], + color1[3], + color2[0], + color2[1], + color2[2], + color2[3], ); case "Horizontal": return factory.fromHorizontalGradient( - color1[0], color1[1], color1[2], color1[3], - color2[0], color2[1], color2[2], color2[3], + color1[0], + color1[1], + color1[2], + color1[3], + color2[0], + color2[1], + color2[2], + color2[3], ); default: throw new Error("Unexpected Gradient Type"); @@ -610,9 +708,7 @@ export class SettingsComponent extends React.Component> { ); } else { component = ( -
- {children} -
+
{children}
); } } else if ("ListGradient" in value) { @@ -654,22 +750,43 @@ export class SettingsComponent extends React.Component> { return factory.fromTransparentGradient(); case "Plain": return factory.fromColor( - color1[0], color1[1], color1[2], color1[3], + color1[0], + color1[1], + color1[2], + color1[3], ); case "Vertical": return factory.fromVerticalGradient( - color1[0], color1[1], color1[2], color1[3], - color2[0], color2[1], color2[2], color2[3], + color1[0], + color1[1], + color1[2], + color1[3], + color2[0], + color2[1], + color2[2], + color2[3], ); case "Horizontal": return factory.fromHorizontalGradient( - color1[0], color1[1], color1[2], color1[3], - color2[0], color2[1], color2[2], color2[3], + color1[0], + color1[1], + color1[2], + color1[3], + color2[0], + color2[1], + color2[2], + color2[3], ); case "Alternating": return factory.fromAlternatingGradient( - color1[0], color1[1], color1[2], color1[3], - color2[0], color2[1], color2[2], color2[3], + color1[0], + color1[1], + color1[2], + color1[3], + color2[0], + color2[1], + color2[2], + color2[3], ); default: throw new Error("Unexpected Gradient Type"); @@ -736,9 +853,7 @@ export class SettingsComponent extends React.Component> { ); } else { component = ( -
- {children} -
+
{children}
); } } else if ("OptionalTimingMethod" in value) { @@ -750,7 +865,9 @@ export class SettingsComponent extends React.Component> { this.props.setValue( valueIndex, expect( - factory.fromOptionalTimingMethod("RealTime"), + factory.fromOptionalTimingMethod( + "RealTime", + ), "Unexpected Optional Timing Method", ), ); @@ -772,7 +889,9 @@ export class SettingsComponent extends React.Component> { this.props.setValue( valueIndex, expect( - factory.fromOptionalTimingMethod(e.target.value), + factory.fromOptionalTimingMethod( + e.target.value, + ), "Unexpected Optional Timing Method", ), ); @@ -839,16 +958,24 @@ export class SettingsComponent extends React.Component> { this.props.setValue( valueIndex, expect( - factory.fromColumnStartWith(e.target.value), + factory.fromColumnStartWith( + e.target.value, + ), "Unexpected Column Start With value", ), ); }} > - - - + + +
); @@ -861,7 +988,9 @@ export class SettingsComponent extends React.Component> { this.props.setValue( valueIndex, expect( - factory.fromColumnUpdateWith(e.target.value), + factory.fromColumnUpdateWith( + e.target.value, + ), "Unexpected Column Update With value", ), ); @@ -870,10 +999,16 @@ export class SettingsComponent extends React.Component> { - + - - + +
); @@ -886,21 +1021,28 @@ export class SettingsComponent extends React.Component> { this.props.setValue( valueIndex, expect( - factory.fromColumnUpdateTrigger(e.target.value), + factory.fromColumnUpdateTrigger( + e.target.value, + ), "Unexpected Column Update Trigger value", ), ); }} > - + - +
); } else if ("CustomCombobox" in value) { - const isError = value.CustomCombobox.mandatory - && !value.CustomCombobox.value; + const isError = + value.CustomCombobox.mandatory && + !value.CustomCombobox.value; component = (
); @@ -951,7 +1095,9 @@ export class SettingsComponent extends React.Component> { this.props.setValue( valueIndex, expect( - factory.fromLayoutDirection(e.target.value), + factory.fromLayoutDirection( + e.target.value, + ), "Unexpected Layout Direction", ), ); @@ -971,7 +1117,12 @@ export class SettingsComponent extends React.Component> { this.props.setValue( valueIndex, expect( - factory.fromFont("", "normal", "normal", "normal"), + factory.fromFont( + "", + "normal", + "normal", + "normal", + ), "Unexpected Font", ), ); @@ -1003,19 +1154,24 @@ export class SettingsComponent extends React.Component> { this.props.setValue( valueIndex, expect( - factory.fromFont(e.target.value, style, weight, stretch), + factory.fromFont( + e.target.value, + style, + weight, + stretch, + ), "Unexpected Font", ), ); }} > - { - FontList.knownFamilies.map((n) => - - ) - } - + {FontList.knownFamilies.map((n) => ( + + ))} + , ); } else { children.push( @@ -1026,12 +1182,17 @@ export class SettingsComponent extends React.Component> { this.props.setValue( valueIndex, expect( - factory.fromFont(e.target.value, style, weight, stretch), + factory.fromFont( + e.target.value, + style, + weight, + stretch, + ), "Unexpected Font", ), ); }} - /> + />, ); } @@ -1043,7 +1204,12 @@ export class SettingsComponent extends React.Component> { this.props.setValue( valueIndex, expect( - factory.fromFont(family, e.target.value, weight, stretch), + factory.fromFont( + family, + e.target.value, + weight, + stretch, + ), "Unexpected Font", ), ); @@ -1059,19 +1225,29 @@ export class SettingsComponent extends React.Component> { this.props.setValue( valueIndex, expect( - factory.fromFont(family, style, e.target.value, stretch), + factory.fromFont( + family, + style, + e.target.value, + stretch, + ), "Unexpected Font", ), ); }} > - { - FontList.FONT_WEIGHTS.map(([_, value, name]) => ( - - )) - } + {FontList.FONT_WEIGHTS.map(([_, value, name]) => ( + + ))} , <>Stretch, , ); } @@ -1135,17 +1321,32 @@ export class SettingsComponent extends React.Component> { return factory.fromTransparentGradient(); case "Plain": return factory.fromColor( - color1[0], color1[1], color1[2], color1[3], + color1[0], + color1[1], + color1[2], + color1[3], ); case "Vertical": return factory.fromVerticalGradient( - color1[0], color1[1], color1[2], color1[3], - color2[0], color2[1], color2[2], color2[3], + color1[0], + color1[1], + color1[2], + color1[3], + color2[0], + color2[1], + color2[2], + color2[3], ); case "Horizontal": return factory.fromHorizontalGradient( - color1[0], color1[1], color1[2], color1[3], - color2[0], color2[1], color2[2], color2[3], + color1[0], + color1[1], + color1[2], + color1[3], + color2[0], + color2[1], + color2[2], + color2[3], ); default: return expect( @@ -1171,7 +1372,9 @@ export class SettingsComponent extends React.Component> { - + , ]; @@ -1217,16 +1420,15 @@ export class SettingsComponent extends React.Component> { ); } else { component = ( -
- {children} -
+
{children}
); } } else if ("LayoutBackground" in value) { let type: string; let color1: Option = null; let color2: Option = null; - let imageId = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; + let imageId = + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; let brightness = 100; let opacity = 100; let blur = 0; @@ -1246,21 +1448,41 @@ export class SettingsComponent extends React.Component> { return factory.fromTransparentGradient(); case "Plain": return factory.fromColor( - color1[0], color1[1], color1[2], color1[3], + color1[0], + color1[1], + color1[2], + color1[3], ); case "Vertical": return factory.fromVerticalGradient( - color1[0], color1[1], color1[2], color1[3], - color2[0], color2[1], color2[2], color2[3], + color1[0], + color1[1], + color1[2], + color1[3], + color2[0], + color2[1], + color2[2], + color2[3], ); case "Horizontal": return factory.fromHorizontalGradient( - color1[0], color1[1], color1[2], color1[3], - color2[0], color2[1], color2[2], color2[3], + color1[0], + color1[1], + color1[2], + color1[3], + color2[0], + color2[1], + color2[2], + color2[3], ); default: return expect( - factory.fromBackgroundImage(imageId, brightness / 100, opacity / 100, blur / 100), + factory.fromBackgroundImage( + imageId, + brightness / 100, + opacity / 100, + blur / 100, + ), "Unexpected layout background", ); } @@ -1280,47 +1502,64 @@ export class SettingsComponent extends React.Component> { opacity = 100 * gradient.opacity; blur = 100 * gradient.blur; type = "Image"; - const imageUrl = this.props.editorUrlCache.cache(imageId); + const imageUrl = + this.props.editorUrlCache.cache(imageId); children.push(
{ - const maybeFile = await openFileAsArrayBuffer(FILE_EXT_IMAGES); + const maybeFile = + await openFileAsArrayBuffer( + FILE_EXT_IMAGES, + ); if (maybeFile === undefined) { return; } if (maybeFile instanceof Error) { - toast.error(`Failed to read the file: ${maybeFile.message}`); + toast.error( + `Failed to read the file: ${maybeFile.message}`, + ); return; } const [file] = maybeFile; - const imageId = this.props.editorUrlCache.imageCache.cacheFromArray( - new Uint8Array(file), - true, - ); + const imageId = + this.props.editorUrlCache.imageCache.cacheFromArray( + new Uint8Array(file), + true, + ); this.props.editorUrlCache.cache(imageId); const value = expect( - factory.fromBackgroundImage(imageId, brightness / 100, opacity / 100, blur / 100), + factory.fromBackgroundImage( + imageId, + brightness / 100, + opacity / 100, + blur / 100, + ), "Unexpected layout background", ); this.props.setValue(valueIndex, value); }} />, -
+
Brightness { brightness = Number(e.target.value); @@ -1333,7 +1572,8 @@ export class SettingsComponent extends React.Component> { Opacity { opacity = Number(e.target.value); @@ -1346,7 +1586,8 @@ export class SettingsComponent extends React.Component> { Blur { blur = Number(e.target.value); @@ -1356,7 +1597,7 @@ export class SettingsComponent extends React.Component> { ); }} /> -
+
, ); } else { assertNever(gradient); @@ -1365,7 +1606,9 @@ export class SettingsComponent extends React.Component> { type = gradient; } - children.splice(0, 0, + children.splice( + 0, + 0, setIsChecked(e.target.checked)} /> + setIsChecked(e.target.checked)} + />
diff --git a/src/ui/TextBox.tsx b/src/ui/TextBox.tsx index 26b59b9..1bb86fe 100644 --- a/src/ui/TextBox.tsx +++ b/src/ui/TextBox.tsx @@ -1,14 +1,14 @@ import * as React from "react"; export interface Props { - className?: string, - value?: any, - onChange?: React.EventHandler>, - onBlur?: React.EventHandler>, - label: string, - invalid?: boolean, - small?: boolean, - list?: [string, string[]], + className?: string; + value?: any; + onChange?: React.EventHandler>; + onBlur?: React.EventHandler>; + label: string; + invalid?: boolean; + small?: boolean; + list?: [string, string[]]; } export class TextBox extends React.Component { @@ -24,9 +24,13 @@ export class TextBox extends React.Component { let list; if (this.props.list !== undefined) { name = this.props.list[0]; - list = - {this.props.list[1].map((n, i) => ; + list = ( + + {this.props.list[1].map((n, i) => ( + + ); } return ( diff --git a/src/ui/TimerView.tsx b/src/ui/TimerView.tsx index f9bd9c6..5448987 100644 --- a/src/ui/TimerView.tsx +++ b/src/ui/TimerView.tsx @@ -1,57 +1,79 @@ import * as React from "react"; -import { TimingMethod, TimeSpan, LayoutStateRefMut, TimerPhase } from "../livesplit-core"; +import { + TimingMethod, + TimeSpan, + LayoutStateRefMut, + TimerPhase, +} from "../livesplit-core"; import * as LiveSplit from "../livesplit-core"; import { Option, expect } from "../util/OptionUtil"; import DragUpload from "./DragUpload"; import Layout from "../layout/Layout"; import { UrlCache } from "../util/UrlCache"; import { WebRenderer } from "../livesplit-core/livesplit_core"; -import { GeneralSettings, MANUAL_GAME_TIME_MODE_SEGMENT_TIMES } from "./MainSettings"; +import { + GeneralSettings, + MANUAL_GAME_TIME_MODE_SEGMENT_TIMES, +} from "./MainSettings"; import { LiveSplitServer } from "../api/LiveSplitServer"; import { LSOCommandSink } from "./LSOCommandSink"; -import { ArrowDown, ArrowUp, Circle, Info, Layers, List, Pause, Play, Settings, X } from "lucide-react"; +import { + ArrowDown, + ArrowUp, + Circle, + Info, + Layers, + List, + Pause, + Play, + Settings, + X, +} from "lucide-react"; import LiveSplitIcon from "../assets/icon.svg"; import "../css/TimerView.scss"; export interface Props { - isDesktop: boolean, - layout: LiveSplit.Layout, - layoutState: LayoutStateRefMut, - layoutUrlCache: UrlCache, - layoutWidth: number, - layoutHeight: number, - generalSettings: GeneralSettings, - renderWithSidebar: boolean, - sidebarOpen: boolean, - commandSink: LSOCommandSink, - renderer: WebRenderer, - callbacks: Callbacks, - serverConnection: Option, - currentComparison: string, - currentTimingMethod: TimingMethod, - currentPhase: TimerPhase, - currentSplitIndex: number, - allComparisons: string[], - splitsModified: boolean, - layoutModified: boolean, + isDesktop: boolean; + layout: LiveSplit.Layout; + layoutState: LayoutStateRefMut; + layoutUrlCache: UrlCache; + layoutWidth: number; + layoutHeight: number; + generalSettings: GeneralSettings; + renderWithSidebar: boolean; + sidebarOpen: boolean; + commandSink: LSOCommandSink; + renderer: WebRenderer; + callbacks: Callbacks; + serverConnection: Option; + currentComparison: string; + currentTimingMethod: TimingMethod; + currentPhase: TimerPhase; + currentSplitIndex: number; + allComparisons: string[]; + splitsModified: boolean; + layoutModified: boolean; } export interface State { - manualGameTime: string, + manualGameTime: string; } interface Callbacks { - importLayoutFromFile(file: File): Promise, - importSplitsFromFile(file: File): Promise, - onResize(width: number, height: number): void, - openAboutView(): void, - openLayoutView(): void, - openSplitsView(): void, - openMainSettings(): void, - renderViewWithSidebar(renderedView: React.JSX.Element, sidebarContent: React.JSX.Element): React.JSX.Element, - onServerConnectionClosed(): void, - onServerConnectionOpened(serverConnection: LiveSplitServer): void, + importLayoutFromFile(file: File): Promise; + importSplitsFromFile(file: File): Promise; + onResize(width: number, height: number): void; + openAboutView(): void; + openLayoutView(): void; + openSplitsView(): void; + openMainSettings(): void; + renderViewWithSidebar( + renderedView: React.JSX.Element, + sidebarContent: React.JSX.Element, + ): React.JSX.Element; + onServerConnectionClosed(): void; + onServerConnectionOpened(serverConnection: LiveSplitServer): void; } export class TimerView extends React.Component { @@ -67,145 +89,207 @@ export class TimerView extends React.Component { const renderedView = this.renderView(); if (this.props.renderWithSidebar) { const sidebarContent = this.renderSidebarContent(); - return this.props.callbacks.renderViewWithSidebar(renderedView, sidebarContent); + return this.props.callbacks.renderViewWithSidebar( + renderedView, + sidebarContent, + ); } else { return renderedView; } } private renderView() { - const showManualGameTime = this.props.generalSettings.showManualGameTime; + const showManualGameTime = + this.props.generalSettings.showManualGameTime; - return this.props.callbacks.importLayoutFromFile(file)} - importSplits={(file) => this.props.callbacks.importSplitsFromFile(file)} - > -
-
{ - if (this.props.generalSettings.showControlButtons) { - this.props.commandSink.splitOrStart(); - } - }} - style={{ - display: "inline-block", - cursor: this.props.generalSettings.showControlButtons ? "pointer" : undefined, - }} - > - { - // The drag upload above causes the layout to be - // dropped. We need to wait for it to be replaced - // with the new layout. - if (this.props.layout.ptr !== 0) { - this.props.commandSink.updateLayoutState( - this.props.layout, - this.props.layoutState, - this.props.layoutUrlCache.imageCache, - ); - this.props.layoutUrlCache.collect(); + return ( + + this.props.callbacks.importLayoutFromFile(file) + } + importSplits={(file) => + this.props.callbacks.importSplitsFromFile(file) + } + > +
+
{ + if (this.props.generalSettings.showControlButtons) { + this.props.commandSink.splitOrStart(); } - return this.props.layoutState; }} - layoutUrlCache={this.props.layoutUrlCache} - allowResize={this.props.isDesktop} - width={this.props.layoutWidth} - height={this.props.layoutHeight} - generalSettings={this.props.generalSettings} - renderer={this.props.renderer} - onResize={(width, height) => this.props.callbacks.onResize(width, height)} - /> -
-
- { - this.props.generalSettings.showControlButtons &&
-
- - - - + />
- } - { - showManualGameTime &&
- { - this.setState({ - manualGameTime: e.target.value, - }); - }} - onKeyDown={(e) => { - if (e.key === "Enter") { - const timer = this.props.commandSink; - if (timer.currentPhase() === LiveSplit.TimerPhase.NotRunning) { - timer.start(); - timer.pauseGameTime(); - using gameTime = TimeSpan.parse(this.state.manualGameTime) - ?? expect(TimeSpan.parse("0"), "Failed to parse TimeSpan"); - timer.setGameTimeInner(gameTime); - this.setState({ manualGameTime: "" }); - } else { - using gameTime = TimeSpan.parse(this.state.manualGameTime); - if (gameTime !== null) { - if (showManualGameTime.mode === MANUAL_GAME_TIME_MODE_SEGMENT_TIMES) { - const currentGameTime = timer.currentTime().gameTime(); - if (currentGameTime !== null) { - gameTime.addAssign(currentGameTime); - } - } + {this.props.generalSettings.showControlButtons && ( +
+
+ + + + +
+
+ )} + {showManualGameTime && ( +
+ { + this.setState({ + manualGameTime: e.target.value, + }); + }} + onKeyDown={(e) => { + if (e.key === "Enter") { + const timer = this.props.commandSink; + if ( + timer.currentPhase() === + LiveSplit.TimerPhase.NotRunning + ) { + timer.start(); + timer.pauseGameTime(); + using gameTime = + TimeSpan.parse( + this.state.manualGameTime, + ) ?? + expect( + TimeSpan.parse("0"), + "Failed to parse TimeSpan", + ); timer.setGameTimeInner(gameTime); - timer.split(); this.setState({ manualGameTime: "" }); + } else { + using gameTime = TimeSpan.parse( + this.state.manualGameTime, + ); + if (gameTime !== null) { + if ( + showManualGameTime.mode === + MANUAL_GAME_TIME_MODE_SEGMENT_TIMES + ) { + const currentGameTime = timer + .currentTime() + .gameTime(); + if (currentGameTime !== null) { + gameTime.addAssign( + currentGameTime, + ); + } + } + timer.setGameTimeInner(gameTime); + timer.split(); + this.setState({ + manualGameTime: "", + }); + } } } - } - }} - /> -
- } - ; + }} + /> +
+ )} +
+ ); } private renderSidebarContent() { @@ -222,50 +306,72 @@ export class TimerView extends React.Component { Splits - { - this.props.splitsModified && - - } + {this.props.splitsModified && ( + + )}

Compare Against