Format everything with prettier (#1029)

We didn't have Prettier until now, so this adds a configuration and
reformats everything with it.
This commit is contained in:
Christopher Serr
2025-03-25 19:10:25 +00:00
committed by GitHub
parent 456ae3b36c
commit d503c39c32
64 changed files with 4969 additions and 3250 deletions
+7
View File
@@ -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
}
}
+39 -11
View File
@@ -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<string, Category[]> = new Map();
const gameIdToCategoriesPromises: Map<string, Promise<Category[]>> = new Map();
const gameIdToGameInfoMap: Map<string, Game> = new Map();
const gameIdToGameInfoPromises: Map<string, Promise<Game>> = new Map();
const gameAndCategoryToLeaderboardPromises: Map<string, Promise<void>> = new Map();
const gameAndCategoryToLeaderboardMap: Map<string, Array<Run<PlayersEmbedded>>> = new Map();
const gameAndCategoryToLeaderboardPromises: Map<
string,
Promise<void>
> = new Map();
const gameAndCategoryToLeaderboardMap: Map<
string,
Array<Run<PlayersEmbedded>>
> = new Map();
let gameListPromise: Option<Promise<void>> = null;
let platformListPromise: Option<Promise<void>> = null;
let regionListPromise: Option<Promise<void>> = null;
@@ -61,17 +70,24 @@ export function getRegions(): Map<string, string> {
return regionList;
}
export function getLeaderboard(gameName: string, categoryName: string): Array<Run<PlayersEmbedded>> | undefined {
export function getLeaderboard(
gameName: string,
categoryName: string,
): Array<Run<PlayersEmbedded>> | undefined {
const key = JSON.stringify({ gameName, categoryName });
return gameAndCategoryToLeaderboardMap.get(key);
}
export function downloadCategoriesByGameId(gameId: string): Promise<Category[]> {
export function downloadCategoriesByGameId(
gameId: string,
): Promise<Category[]> {
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<Category[]>
return categoryPromise;
}
export async function downloadCategories(gameName: string): Promise<Option<string>> {
export async function downloadCategories(
gameName: string,
): Promise<Option<string>> {
await downloadGameList();
const gameId = getGameId(gameName);
if (gameId !== undefined) {
@@ -163,7 +181,10 @@ export function downloadRegionList(): Promise<void> {
return regionListPromise;
}
export async function downloadLeaderboard(gameName: string, categoryName: string): Promise<void> {
export async function downloadLeaderboard(
gameName: string,
categoryName: string,
): Promise<void> {
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);
+7 -2
View File
@@ -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) {
+131 -116
View File
@@ -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<string>,
scope: VariableScope,
values: VariableValues,
mandatory: boolean,
"is-subcategory": boolean,
"id": string;
"name": string;
"category": Option<string>;
"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<string>,
values: { [id: string]: VariableValue };
default: Option<string>;
}
export interface VariableValue {
label: string,
rules?: Option<string>,
label: string;
rules?: Option<string>;
}
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<string>,
twitch?: Option<string>,
international: string;
japanese: Option<string>;
twitch?: Option<string>;
}
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<Asset>,
background: Asset,
foreground: Option<Asset>,
"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<Asset>;
"background": Asset;
"foreground": Option<Asset>;
}
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<string>,
id: string;
weblink: string;
name: string;
type: "per-game" | "per-level";
rules: Option<string>;
}
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<UserLocation>,
"id": string;
"names": Names;
"weblink": string;
"name-style": NameStyleSolid | NameStyleGradient;
"location": Option<UserLocation>;
}
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<PlayerUserRef | PlayerGuest>;
export interface PlayersEmbedded {
data: Array<PlayerUser | PlayerGuest>,
data: Array<PlayerUser | PlayerGuest>;
}
export interface Run<PlayerEmbedding = PlayersNotEmbedded> {
id: string,
weblink: string,
game: string,
category: string,
videos: Option<Videos>,
comment: Option<string>,
players: PlayerEmbedding,
date: Option<string>,
submitted: Option<string>,
times: Times,
system: RunSystem,
splits: Option<Splits>,
values: { [key: string]: string | undefined },
id: string;
weblink: string;
game: string;
category: string;
videos: Option<Videos>;
comment: Option<string>;
players: PlayerEmbedding;
date: Option<string>;
submitted: Option<string>;
times: Times;
system: RunSystem;
splits: Option<Splits>;
values: { [key: string]: string | undefined };
}
export interface RunSystem {
emulated: boolean,
platform: string,
region: Option<string>,
emulated: boolean;
platform: string;
region: Option<string>;
}
export interface Videos {
links: Option<Video[]>,
links: Option<Video[]>;
}
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<T> {
public constructor(
public elements: T[],
public next: Option<() => Promise<Page<T>>>,
) { }
) {}
public async evaluateAll(): Promise<T[]> {
const elements = this.elements;
@@ -276,7 +276,9 @@ export class Page<T> {
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<T>(uri: string): Promise<Page<T>> {
return new Page(data as T[], next);
}
export async function getGame(gameId: string, embeds?: Array<"variables">): Promise<Game> {
export async function getGame(
gameId: string,
embeds?: Array<"variables">,
): Promise<Game> {
const parameters = [];
if (embeds !== undefined) {
parameters.push(`embed=${embeds.join(",")}`);
@@ -337,7 +342,9 @@ export async function getGames(name?: string): Promise<Page<Game>> {
return executePaginatedRequest<Game>(uri);
}
export async function getGameHeaders(elementsPerPage: number = 1000): Promise<Page<GameHeader>> {
export async function getGameHeaders(
elementsPerPage: number = 1000,
): Promise<Page<GameHeader>> {
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<Leaderboard>(uri);
}
export async function getPlatforms(elementsPerPage?: number): Promise<Page<Platform>> {
export async function getPlatforms(
elementsPerPage?: number,
): Promise<Page<Platform>> {
const parameters = [];
if (elementsPerPage !== undefined) {
parameters.push(`max=${elementsPerPage}`);
@@ -372,7 +383,9 @@ export async function getPlatforms(elementsPerPage?: number): Promise<Page<Platf
return executePaginatedRequest<Platform>(uri);
}
export async function getRegions(elementsPerPage?: number): Promise<Page<Region>> {
export async function getRegions(
elementsPerPage?: number,
): Promise<Page<Region>> {
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<Run<PlayersEmbedded | PlayersNotEmbedded>>(uri);
return executePaginatedRequest<Run<PlayersEmbedded | PlayersNotEmbedded>>(
uri,
);
}
export async function getRun(runId: string, embeds?: never[]): Promise<Run> {
+71 -70
View File
@@ -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%;
}
}
+187 -175
View File
@@ -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;
}
}
+6 -6
View File
@@ -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;
}
+5 -5
View File
@@ -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;
+28 -28
View File
@@ -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;
}
}
}
}
+17 -17
View File
@@ -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;
}
+34 -34
View File
@@ -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;
}
}
}
}
+63 -63
View File
@@ -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;
}
}
}
+30 -28
View File
@@ -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}
);
}
}
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
@use 'variables.icss';
@use "variables.icss";
.livesplit-server-button {
margin: 0;
+33 -33
View File
@@ -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;
}
}
+431 -427
View File
@@ -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});
}
}
}
+11 -11
View File
@@ -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%;
}
}
}
}
}
+87 -87
View File
@@ -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;
}
}
+106 -106
View File
@@ -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;
}
}
}
}
+6 -6
View File
@@ -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);
}
+150 -146
View File
@@ -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%;
}
}
}
}
+35 -34
View File
@@ -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;
}
}
+21 -21
View File
@@ -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;
}
}
+4 -4
View File
@@ -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;
+13 -13
View File
@@ -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;
}
}
+85 -85
View File
@@ -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;
}
}
+3 -3
View File
@@ -1,5 +1,5 @@
@mixin mobile {
.is-mobile & {
@content;
}
.is-mobile & {
@content;
}
}
+15 -9
View File
@@ -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;
}
+20 -16
View File
@@ -1,19 +1,23 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="description"
content="A version of LiveSplit that works on a lot of platforms."
/>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=5.0"
/>
<title>LiveSplit One</title>
</head>
<head>
<meta charset="UTF-8" />
<meta name="description" content="A version of LiveSplit that works on a lot of platforms.">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0">
<title>LiveSplit One</title>
</head>
<body>
<div id="base">
<div class="initial-load">
<div class="initial-load-text">Loading...</div>
</div>
</div>
</body>
<body>
<div id="base">
<div class="initial-load">
<div class="initial-load-text">Loading...</div>
</div>
</div>
</body>
</html>
+11 -7
View File
@@ -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<FontFace>)) {
if (fontFace.family === 'timer' || fontFace.family === 'fira') {
for (const fontFace of document.fonts as any as Iterable<FontFace>) {
if (fontFace.family === "timer" || fontFace.family === "fira") {
promises.push(fontFace.load());
}
}
+61 -20
View File
@@ -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<Props, unknown> {
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<Props, unknown> {
frameRate={this.props.generalSettings.frameRate}
update={() => this.refreshLayout()}
>
<div className="layout" style={{ width: this.props.width, height: this.props.height }}>
<div
className="layout"
style={{
width: this.props.width,
height: this.props.height,
}}
>
<div
style={{ width: "inherit", height: "inherit" }}
ref={(element) => { element?.appendChild(this.props.renderer.element()); }}
ref={(element) => {
element?.appendChild(this.props.renderer.element());
}}
/>
{
this.props.allowResize && <div className="resizable-layout">
{this.props.allowResize && (
<div className="resizable-layout">
<ResizableBox
axis="x"
width={this.props.width}
height={this.props.height}
minConstraints={[100, 40]}
handle={<div onClick={(e) => e.stopPropagation()} className="resizable-handle-east" />}
onResize={(_event, data) => this.props.onResize(data.size.width, data.size.height)}
handle={
<div
onClick={(e) => e.stopPropagation()}
className="resizable-handle-east"
/>
}
onResize={(_event, data) =>
this.props.onResize(
data.size.width,
data.size.height,
)
}
/>
<ResizableBox
axis="y"
width={this.props.width}
height={this.props.height}
minConstraints={[100, 40]}
handle={<div onClick={(e) => e.stopPropagation()} className="resizable-handle-south" />}
onResize={(_event, data) => this.props.onResize(data.size.width, data.size.height)}
handle={
<div
onClick={(e) => e.stopPropagation()}
className="resizable-handle-south"
/>
}
onResize={(_event, data) =>
this.props.onResize(
data.size.width,
data.size.height,
)
}
/>
<ResizableBox
axis="both"
width={this.props.width}
height={this.props.height}
minConstraints={[100, 40]}
handle={<div onClick={(e) => e.stopPropagation()} className="resizable-handle-south-east" />}
onResize={(_event, data) => this.props.onResize(data.size.width, data.size.height)}
handle={
<div
onClick={(e) => e.stopPropagation()}
className="resizable-handle-south-east"
/>
}
onResize={(_event, data) =>
this.props.onResize(
data.size.width,
data.size.height,
)
}
/>
</div>
}
)}
</div>
</AutoRefresh>
);
+4 -1
View File
@@ -1,4 +1,7 @@
export async function corsBustingFetch(url: string, signal?: AbortSignal): Promise<ArrayBuffer> {
export async function corsBustingFetch(
url: string,
signal?: AbortSignal,
): Promise<ArrayBuffer> {
let response: Response | undefined;
if (window.__TAURI__ != null) {
response = await window.__TAURI__.http.fetch(url, { signal });
+31 -15
View File
@@ -2,24 +2,29 @@ import { CommandSinkRef, HotkeyConfig, HotkeySystem } from "../livesplit-core";
import { expect } from "../util/OptionUtil";
export interface HotkeyImplementation {
config(): Promise<HotkeyConfig> | HotkeyConfig,
setConfig(config: HotkeyConfig): void,
activate(): void,
deactivate(): void,
resolve(keyCode: string): Promise<string> | string,
config(): Promise<HotkeyConfig> | HotkeyConfig;
setConfig(config: HotkeyConfig): void;
activate(): void;
deactivate(): void;
resolve(keyCode: string): Promise<string> | string;
}
class GlobalHotkeys implements HotkeyImplementation {
constructor(private hotkeySystem?: HotkeySystem) { }
constructor(private hotkeySystem?: HotkeySystem) {}
public async config(): Promise<HotkeyConfig> {
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(
+25 -18
View File
@@ -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<Promise<IDBPDatabase<unknown>>> = 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<IDBPDatabase<unknown>> {
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<IDBPDatabase<unknown>> {
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<GeneralSettings> {
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<GeneralSettings> {
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<TimingMethod> {
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) {
+1 -1
View File
@@ -1 +1 @@
declare module '*.svg'
declare module "*.svg";
+2 -2
View File
@@ -1,2 +1,2 @@
declare module '*.scss'
declare module '*.css'
declare module "*.scss";
declare module "*.css";
+5 -2
View File
@@ -12,7 +12,10 @@ declare interface CoreModule {
}
declare interface TauriEventModule {
listen(eventName: string, callback: (event: TauriEvent) => void): Promise<ListenHandle>;
listen(
eventName: string,
callback: (event: TauriEvent) => void,
): Promise<ListenHandle>;
}
declare interface TauriNotificationModule {
@@ -35,4 +38,4 @@ declare interface TauriEvent {
payload: unknown;
}
declare interface ListenHandle { }
declare interface ListenHandle {}
+1 -1
View File
@@ -1 +1 @@
declare module '*.wasm'
declare module "*.wasm";
+5 -5
View File
@@ -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;
}
+51 -30
View File
@@ -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<Props> {
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 (
<div className="about">
@@ -38,43 +46,56 @@ export class About extends React.Component<Props> {
<div className="title-text">LiveSplit One</div>
</div>
<p className="build-version">
<a href={`https://github.com/LiveSplit/LiveSplitOne/commit/${COMMIT_HASH}`} target="_blank">
<a
href={`https://github.com/LiveSplit/LiveSplitOne/commit/${COMMIT_HASH}`}
target="_blank"
>
Version: {BUILD_DATE}
</a>
</p>
<p>LiveSplit One is a multiplatform version of LiveSplit, the sleek,
highly-customizable timer for speedrunners.</p>
<p>
<a href="https://github.com/LiveSplit/LiveSplitOne" target="_blank">
LiveSplit One is a multiplatform version of LiveSplit,
the sleek, highly-customizable timer for speedrunners.
</p>
<p>
<a
href="https://github.com/LiveSplit/LiveSplitOne"
target="_blank"
>
View Source Code on GitHub
</a>
</p>
<h2>Recent Changes</h2>
<div className="changelog">
{
CHANGELOG.map((change) => (
<>
<a href={`https://github.com/LiveSplit/LiveSplitOne/commit/${change.id}`} target="_blank">
{change.date}
</a>
<Markdown markdown={change.message} unsafe={true} />
</>
))
}
{CHANGELOG.map((change) => (
<>
<a
href={`https://github.com/LiveSplit/LiveSplitOne/commit/${change.id}`}
target="_blank"
>
{change.date}
</a>
<Markdown
markdown={change.message}
unsafe={true}
/>
</>
))}
</div>
<h2>Contributors</h2>
<div className="contributors">
{
CONTRIBUTORS_LIST.map((contributor) => (
<a href={`https://github.com/${contributor.name}`} target="_blank">
<img
src={`https://avatars.githubusercontent.com/u/${contributor.id}?s=${idealAvatarResolution}&v=4`}
onError={(e) => (e.target as any).remove()}
/>
{contributor.name}
</a>
))
}
{CONTRIBUTORS_LIST.map((contributor) => (
<a
href={`https://github.com/${contributor.name}`}
target="_blank"
>
<img
src={`https://avatars.githubusercontent.com/u/${contributor.id}?s=${idealAvatarResolution}&v=4`}
onError={(e) => (e.target as any).remove()}
/>
{contributor.name}
</a>
))}
</div>
</div>
</div>
+178 -57
View File
@@ -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 (
<div>
@@ -27,17 +33,31 @@ export default function ColorPicker({ color, setColor }: { color: Color, setColo
onClick={() => setIsShowing(true)}
/>
<div className={classes.colorPickerDialogPositioning}>
{isShowing && <ColorPickerDialog color={color} setColor={setColor} close={() => setIsShowing(false)} />}
{isShowing && (
<ColorPickerDialog
color={color}
setColor={setColor}
close={() => setIsShowing(false)}
/>
)}
</div>
</div>
);
}
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 (
<>
<div className={classes.overlay} onClick={close} />
<div className={classes.glassPanel} >
<div className={classes.glassPanel}>
<GradientSelector color={color} setColor={setColor} />
<Hr />
<ControlPanel color={color} setColor={setColor} />
@@ -52,7 +72,13 @@ function Hr() {
return <hr className={classes.hr} />;
}
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:
}}
>
<div className={classes.whiteGradient}>
<div className={classes.cursor} style={{
transform: `translate(${s * 225 - 6}px, ${125 - v * 125 - 6}px)`,
}}>
<div
className={classes.cursor}
style={{
transform: `translate(${s * 225 - 6}px, ${
125 - v * 125 - 6
}px)`,
}}
>
<div />
</div>
<div className={classes.blackGradient} />
@@ -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<Mode>("Hex");
@@ -151,7 +188,9 @@ function ControlPanel({ color, setColor }: { color: Color, setColor: (color: Col
<div
style={{
height: "18px",
background: `linear-gradient(to right, transparent 0%, rgb(${255 * r}, ${255 * g}, ${255 * b}) 100%)`,
background: `linear-gradient(to right, transparent 0%, rgb(${
255 * r
}, ${255 * g}, ${255 * b}) 100%)`,
}}
/>
</div>
@@ -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 && (
<div
style={{ cursor: "pointer" }}
title="Eye Dropper"
>
<div style={{ cursor: "pointer" }} title="Eye Dropper">
<Pipette
size={20}
aria-hidden="true"
@@ -225,20 +267,19 @@ function ColorPreview({ color, setColor }: { color: Color, setColor: (color: Col
setColor([...parsed, 1]);
}
}
} catch { }
} catch {}
}}
/>
</div>
)}
<div
className={classes.colorPreview}
title="Color Preview"
>
<div className={classes.colorPreview} title="Color Preview">
<div className={classes.checker}>
<div
className={classes.colorPreviewInner}
style={{
backgroundColor: `rgba(${255 * r}, ${255 * g}, ${255 * b}, ${a})`,
backgroundColor: `rgba(${255 * r}, ${255 * g}, ${
255 * b
}, ${a})`,
}}
/>
</div>
@@ -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
<div className={classes.colorInput} title="Hexadecimal">
<FormattedInput
value={color}
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()}
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<T>({ value, format, parse, setValue }: {
value: T,
format: (value: T) => string,
parse: (value: string) => T | undefined,
setValue: (value: T) => void,
function FormattedInput<T>({
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<T>({ 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 (
<div className={classes.colorInput} title={title}>
<FormattedInput
value={value}
format={(value) => 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) => (
<div key={index} className={classes.predefinedColorsRow}>
{hsv.map((color, i) => (
<PredefinedColor key={i} color={color} setColor={setColor} />
<PredefinedColor
key={i}
color={color}
setColor={setColor}
/>
))}
</div>
))}
@@ -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 (
<button
className={classes.predefinedColor}
@@ -496,14 +607,20 @@ function PredefinedColor({ color: [title, r, g, b], setColor }: { color: [string
}
function hsvToRgb(h: number, s: number, v: number) {
const xDivC = 1 - Math.abs((h / 60) % 2 - 1);
const xDivC = 1 - Math.abs(((h / 60) % 2) - 1);
const [rc, rx, gc, gx, bc, bx] = h < 60 ? [1, 0, 0, 1, 0, 0] :
h < 120 ? [0, 1, 1, 0, 0, 0] :
h < 180 ? [0, 0, 1, 0, 0, 1] :
h < 240 ? [0, 0, 0, 1, 1, 0] :
h < 300 ? [0, 1, 0, 0, 1, 0] :
[1, 0, 0, 0, 0, 1];
const [rc, rx, gc, gx, bc, bx] =
h < 60
? [1, 0, 0, 1, 0, 0]
: h < 120
? [0, 1, 1, 0, 0, 0]
: h < 180
? [0, 0, 1, 0, 0, 1]
: h < 240
? [0, 0, 0, 1, 1, 0]
: h < 300
? [0, 1, 0, 0, 1, 0]
: [1, 0, 0, 0, 0, 1];
const c = v * s;
const x = xDivC * c;
@@ -521,10 +638,14 @@ function rgbToHsv(r: number, g: number, b: number) {
const min = Math.min(r, g, b);
const delta = max - min;
const h = delta == 0 ? 0 :
max == r ? 60 * ((g - b) / delta % 6) + (g < b ? 360 : 0) :
max == g ? 60 * ((b - r) / delta + 2) :
60 * ((r - g) / delta + 4);
const h =
delta == 0
? 0
: max == r
? 60 * (((g - b) / delta) % 6) + (g < b ? 360 : 0)
: max == g
? 60 * ((b - r) / delta + 2)
: 60 * ((r - g) / delta + 4);
const s = max == 0 ? 0 : delta / max;
const v = max;
+8 -5
View File
@@ -8,7 +8,9 @@ export interface Position {
y: number;
}
const InfoContext = createContext<{ onClose: () => void } | undefined>(undefined);
const InfoContext = createContext<{ onClose: () => void } | undefined>(
undefined,
);
export function ContextMenu({
position,
@@ -30,9 +32,7 @@ export function ContextMenu({
}}
>
<div className={classes.overlay} onClick={onClose} />
<div className={classes.panel}>
{children}
</div>
<div className={classes.panel}>{children}</div>
</nav>
</InfoContext.Provider>
);
@@ -47,7 +47,10 @@ export function MenuItem({
onClick: () => void;
className?: string;
}) {
const { onClose } = expect(useContext(InfoContext), "MenuItem must be used within a ContextMenu");
const { onClose } = expect(
useContext(InfoContext),
"MenuItem must be used within a ContextMenu",
);
return (
<div
+64 -51
View File
@@ -3,21 +3,21 @@ import * as React from "react";
import "../css/Dialog.scss";
export interface Props {
onShow: () => void,
onClose: () => void,
onShow: () => void;
onClose: () => void;
}
export interface Options {
title: string | React.JSX.Element,
description: string | React.JSX.Element,
textInput?: boolean,
defaultText?: string,
buttons: string[],
title: string | React.JSX.Element;
description: string | React.JSX.Element;
textInput?: boolean;
defaultText?: string;
buttons: string[];
}
export interface State {
options: Options,
input: string,
options: Options;
input: string;
}
let dialogElement: HTMLDialogElement | null = null;
@@ -40,7 +40,7 @@ export function showDialog(options: Options): Promise<[number, string]> {
};
}
setState?.(options);
return new Promise((resolve) => resolveFn = resolve);
return new Promise((resolve) => (resolveFn = resolve));
}
export default class DialogContainer extends React.Component<Props, State> {
@@ -70,49 +70,62 @@ export default class DialogContainer extends React.Component<Props, State> {
}
public render() {
return <dialog
ref={(element) => { 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();
}
}}
>
<div className="dialog">
<h1>{this.state.options.title}</h1>
<p>{this.state.options.description}</p>
{
this.state.options.textInput && <input
type="text"
value={this.state.input}
autoFocus={true}
onChange={(e) => this.setState({ input: e.target.value })}
onKeyDown={(e) => {
if (e?.key === "Enter") {
e.preventDefault();
this.close(0);
}
}}
/>
}
<div className="buttons">
{
this.state.options.buttons.map((button, i) => {
return <button
autoFocus={i === 0 && !this.state.options.textInput}
onClick={() => this.close(i)}
>
{button}
</button>;
})
return (
<dialog
ref={(element) => {
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();
}
}}
>
<div className="dialog">
<h1>{this.state.options.title}</h1>
<p>{this.state.options.description}</p>
{this.state.options.textInput && (
<input
type="text"
value={this.state.input}
autoFocus={true}
onChange={(e) =>
this.setState({ input: e.target.value })
}
onKeyDown={(e) => {
if (e?.key === "Enter") {
e.preventDefault();
this.close(0);
}
}}
/>
)}
<div className="buttons">
{this.state.options.buttons.map((button, i) => {
return (
<button
autoFocus={
i === 0 && !this.state.options.textInput
}
onClick={() => this.close(i)}
>
{button}
</button>
);
})}
</div>
</div>
</div>
</dialog>;
</dialog>
);
}
private close(i: number) {
+9 -8
View File
@@ -4,15 +4,17 @@ import { toast } from "react-toastify";
import "../css/DragUpload.scss";
export interface Props {
children: React.ReactNode,
importLayout?: (file: File) => Promise<void>,
importSplits(file: File): Promise<void>,
children: React.ReactNode;
importLayout?: (file: File) => Promise<void>;
importSplits(file: File): Promise<void>;
}
export default class DragUpload extends React.Component<Props> {
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<Props> {
});
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<Props> {
return (
<div id="upload-drop-zone">
<div id="upload-drop-zone-overlay">
<div className="overlay-text">
Waiting for drop...
</div>
<div className="overlay-text">Waiting for drop...</div>
</div>
{this.props.children}
</div>
+13 -3
View File
@@ -10,7 +10,13 @@ export function resolveEmbed(uri: string): Option<React.JSX.Element> {
if (twitch != null) {
return twitch;
}
return <p><a href={uri} target="_blank">{uri}</a></p>;
return (
<p>
<a href={uri} target="_blank">
{uri}
</a>
</p>
);
}
function tryYoutubeFromUri(uri: string): Option<React.JSX.Element> {
@@ -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`,
);
}
+54 -24
View File
@@ -6,18 +6,21 @@ import { Circle, Trash } from "lucide-react";
import "../css/HotkeyButton.scss";
function resolveKey(keyCode: string): Promise<string> | 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<string>,
setValue: (value: Option<string>) => void,
value: Option<string>;
setValue: (value: Option<string>) => void;
}
export interface State {
listener: Option<EventListenerObject>,
intervalHandle: Option<number>,
resolvedKey: Option<string>,
listener: Option<EventListenerObject>;
intervalHandle: Option<number>;
resolvedKey: Option<string>;
}
export default class HotkeyButton extends React.Component<Props, State> {
@@ -56,27 +59,35 @@ export default class HotkeyButton extends React.Component<Props, State> {
if (this.props.value != null) {
buttonText = this.state.resolvedKey;
} else if (this.state.listener != null) {
buttonText = <Circle strokeWidth={0} size={16} fill="currentColor" />;
buttonText = (
<Circle strokeWidth={0} size={16} fill="currentColor" />
);
}
return (
<div className="hotkey-box">
<button
className={`hotkey-button tooltip ${this.state.listener != null ? "focused" : ""}`}
className={`hotkey-button tooltip ${
this.state.listener != null ? "focused" : ""
}`}
onClick={() => this.focusButton()}
>
{buttonText}
<span className="tooltip-text">
Click to record a hotkey. You may also use buttons on a gamepad.
Global hotkeys are currently not possible. Gamepad buttons work globally.
Click to record a hotkey. You may also use buttons on a
gamepad. Global hotkeys are currently not possible.
Gamepad buttons work globally.
</span>
</button>
{
map(this.props.value, () => (
<Trash className="trash" strokeWidth={2.5} size={20} onClick={() => this.props.setValue(null)} />
))
}
{this.state.listener != null &&
{map(this.props.value, () => (
<Trash
className="trash"
strokeWidth={2.5}
size={20}
onClick={() => this.props.setValue(null)}
/>
))}
{this.state.listener != null && (
<div
style={{
bottom: "0px",
@@ -88,7 +99,7 @@ export default class HotkeyButton extends React.Component<Props, State> {
}}
onClick={() => this.blurButton()}
/>
}
)}
</div>
);
}
@@ -104,22 +115,38 @@ export default class HotkeyButton extends React.Component<Props, State> {
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<Props, State> {
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++;
}
+32 -9
View File
@@ -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);
+276 -122
View File
@@ -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<string>,
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<string>;
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<Props, State> {
@@ -42,7 +45,9 @@ export class LayoutEditor extends React.Component<Props, State> {
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<Props, State> {
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<Props, State> {
className += " selected";
}
return (
<tr key={i}
<tr
key={i}
onClick={(_) => this.selectComponent(i)}
draggable
onDragStart={(e) => {
@@ -85,45 +94,42 @@ export class LayoutEditor extends React.Component<Props, State> {
return false;
}}
>
<td className={className}>
{c}
</td>
</tr >
<td className={className}>{c}</td>
</tr>
);
});
const settings = this.state.showComponentSettings
? (
<SettingsComponent
context={`component-settings$${this.state.editor.selected_component}`}
factory={LiveSplit.SettingValue}
state={this.state.editor.component_settings}
editorUrlCache={this.props.layoutEditorUrlCache}
allComparisons={this.props.allComparisons}
allVariables={this.props.allVariables}
setValue={(index, value) => {
this.props.editor.setComponentSettingsValue(index, value);
this.update();
}}
/>
) : (
<SettingsComponent
context={`layout-settings`}
factory={LiveSplit.SettingValue}
state={this.state.editor.general_settings}
editorUrlCache={this.props.layoutEditorUrlCache}
allComparisons={this.props.allComparisons}
allVariables={this.props.allVariables}
setValue={(index, value) => {
this.props.editor.setGeneralSettingsValue(
index,
value,
this.props.layoutEditorUrlCache.imageCache,
);
this.update();
}}
/>
);
const settings = this.state.showComponentSettings ? (
<SettingsComponent
context={`component-settings$${this.state.editor.selected_component}`}
factory={LiveSplit.SettingValue}
state={this.state.editor.component_settings}
editorUrlCache={this.props.layoutEditorUrlCache}
allComparisons={this.props.allComparisons}
allVariables={this.props.allVariables}
setValue={(index, value) => {
this.props.editor.setComponentSettingsValue(index, value);
this.update();
}}
/>
) : (
<SettingsComponent
context={`layout-settings`}
factory={LiveSplit.SettingValue}
state={this.state.editor.general_settings}
editorUrlCache={this.props.layoutEditorUrlCache}
allComparisons={this.props.allComparisons}
allVariables={this.props.allVariables}
setValue={(index, value) => {
this.props.editor.setGeneralSettingsValue(
index,
value,
this.props.layoutEditorUrlCache.imageCache,
);
this.update();
}}
/>
);
return (
<div className="layout-editor-outer">
@@ -151,31 +157,34 @@ export class LayoutEditor extends React.Component<Props, State> {
<button
aria-label="Move Component Up"
onClick={(_) => this.moveComponentUp()}
disabled={!this.state.editor.buttons.can_move_up}
disabled={
!this.state.editor.buttons.can_move_up
}
>
<ArrowUp strokeWidth={2.5} />
</button>
<button
aria-label="Move Component Down"
onClick={(_) => this.moveComponentDown()}
disabled={!this.state.editor.buttons.can_move_down}
disabled={
!this.state.editor.buttons.can_move_down
}
>
<ArrowDown strokeWidth={2.5} />
</button>
</div>
<table className="layout-editor-component-list table">
<tbody className="table-body">
{components}
</tbody>
<tbody className="table-body">{components}</tbody>
</table>
</div>
<div className="tab-bar layout-editor-tabs">
<button
className={"toggle-left" + (
!this.state.showComponentSettings
className={
"toggle-left" +
(!this.state.showComponentSettings
? " button-pressed"
: ""
)}
: "")
}
onClick={(_) => {
this.setState({
showComponentSettings: false,
@@ -185,11 +194,12 @@ export class LayoutEditor extends React.Component<Props, State> {
Layout
</button>
<button
className={"toggle-right" + (
this.state.showComponentSettings
className={
"toggle-right" +
(this.state.showComponentSettings
? " button-pressed"
: ""
)}
: "")
}
onClick={(_) => {
this.setState({
showComponentSettings: true,
@@ -199,9 +209,7 @@ export class LayoutEditor extends React.Component<Props, State> {
Component
</button>
</div>
<div>
{settings}
</div>
<div>{settings}</div>
</div>
<div className="layout-container">
<Layout
@@ -220,7 +228,9 @@ export class LayoutEditor extends React.Component<Props, State> {
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)
}
/>
</div>
</div>
@@ -235,13 +245,17 @@ export class LayoutEditor extends React.Component<Props, State> {
<div className="small">
<button
className="toggle-left"
onClick={(_) => this.props.callbacks.closeLayoutEditor(true)}
onClick={(_) =>
this.props.callbacks.closeLayoutEditor(true)
}
>
<Check strokeWidth={2.5} /> OK
</button>
<button
className="toggle-right"
onClick={(_) => this.props.callbacks.closeLayoutEditor(false)}
onClick={(_) =>
this.props.callbacks.closeLayoutEditor(false)
}
>
<X strokeWidth={2.5} /> Cancel
</button>
@@ -252,8 +266,11 @@ export class LayoutEditor extends React.Component<Props, State> {
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({
<Plus strokeWidth={2.5} />
</button>
{position && (
<ContextMenu position={position} onClose={() => setPosition(null)}>
<MenuItem className="contextmenu-item tooltip" onClick={() => addComponent(LiveSplit.TitleComponent)}>
<ContextMenu
position={position}
onClose={() => setPosition(null)}
>
<MenuItem
className="contextmenu-item tooltip"
onClick={() => addComponent(LiveSplit.TitleComponent)}
>
Title
<span className="tooltip-text">
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.
</span>
</MenuItem>
<MenuItem className="contextmenu-item tooltip" onClick={() => addComponent(LiveSplit.GraphComponent)}>
<MenuItem
className="contextmenu-item tooltip"
onClick={() => addComponent(LiveSplit.GraphComponent)}
>
Graph
<span className="tooltip-text">
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.
</span>
</MenuItem>
<MenuItem className="contextmenu-item tooltip" onClick={() => addComponent(LiveSplit.SplitsComponent)}>
<MenuItem
className="contextmenu-item tooltip"
onClick={() => addComponent(LiveSplit.SplitsComponent)}
>
Splits
<span className="tooltip-text">
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.
</span>
</MenuItem>
<Separator />
<MenuItem className="contextmenu-item tooltip" onClick={() => addComponent(LiveSplit.DetailedTimerComponent)}>
<MenuItem
className="contextmenu-item tooltip"
onClick={() =>
addComponent(LiveSplit.DetailedTimerComponent)
}
>
Detailed Timer
<span className="tooltip-text">
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.
</span>
</MenuItem>
<MenuItem className="contextmenu-item tooltip" onClick={() => addComponent(LiveSplit.TimerComponent)}>
<MenuItem
className="contextmenu-item tooltip"
onClick={() => addComponent(LiveSplit.TimerComponent)}
>
Timer
<span className="tooltip-text">
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.
</span>
</MenuItem>
<Separator />
<MenuItem className="contextmenu-item tooltip" onClick={() => addComponent(LiveSplit.CurrentComparisonComponent)}>
<MenuItem
className="contextmenu-item tooltip"
onClick={() =>
addComponent(LiveSplit.CurrentComparisonComponent)
}
>
Current Comparison
<span className="tooltip-text">
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.
</span>
</MenuItem>
<MenuItem className="contextmenu-item tooltip" onClick={() => addComponent(LiveSplit.CurrentPaceComponent)}>
<MenuItem
className="contextmenu-item tooltip"
onClick={() =>
addComponent(LiveSplit.CurrentPaceComponent)
}
>
Current Pace
<span className="tooltip-text">
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.
</span>
</MenuItem>
<MenuItem className="contextmenu-item tooltip" onClick={() => addComponent(LiveSplit.DeltaComponent)}>
<MenuItem
className="contextmenu-item tooltip"
onClick={() => addComponent(LiveSplit.DeltaComponent)}
>
Delta
<span className="tooltip-text">
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.
</span>
</MenuItem>
<MenuItem className="contextmenu-item tooltip" onClick={() => addComponent(LiveSplit.PbChanceComponent)}>
<MenuItem
className="contextmenu-item tooltip"
onClick={() =>
addComponent(LiveSplit.PbChanceComponent)
}
>
PB Chance
<span className="tooltip-text">
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.
</span>
</MenuItem>
<MenuItem className="contextmenu-item tooltip" onClick={() => addComponent(LiveSplit.PossibleTimeSaveComponent)}>
<MenuItem
className="contextmenu-item tooltip"
onClick={() =>
addComponent(LiveSplit.PossibleTimeSaveComponent)
}
>
Possible Time Save
<span className="tooltip-text">
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.
</span>
</MenuItem>
<MenuItem className="contextmenu-item tooltip" onClick={() => addComponent(LiveSplit.PreviousSegmentComponent)}>
<MenuItem
className="contextmenu-item tooltip"
onClick={() =>
addComponent(LiveSplit.PreviousSegmentComponent)
}
>
Previous Segment
<span className="tooltip-text">
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.
</span>
</MenuItem>
<MenuItem className="contextmenu-item tooltip" onClick={() => addComponent(LiveSplit.SegmentTimeComponent)}>
<MenuItem
className="contextmenu-item tooltip"
onClick={() =>
addComponent(LiveSplit.SegmentTimeComponent)
}
>
Segment Time
<span className="tooltip-text">
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.
</span>
</MenuItem>
<MenuItem className="contextmenu-item tooltip" onClick={() => addComponent(LiveSplit.SumOfBestComponent)}>
<MenuItem
className="contextmenu-item tooltip"
onClick={() =>
addComponent(LiveSplit.SumOfBestComponent)
}
>
Sum of Best Segments
<span className="tooltip-text">
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.
</span>
</MenuItem>
<MenuItem className="contextmenu-item tooltip" onClick={() => addComponent(LiveSplit.TextComponent)}>
<MenuItem
className="contextmenu-item tooltip"
onClick={() => addComponent(LiveSplit.TextComponent)}
>
Text
<span className="tooltip-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.
</span>
</MenuItem>
<MenuItem className="contextmenu-item tooltip" onClick={() => addComponent(LiveSplit.TotalPlaytimeComponent)}>
<MenuItem
className="contextmenu-item tooltip"
onClick={() =>
addComponent(LiveSplit.TotalPlaytimeComponent)
}
>
Total Playtime
<span className="tooltip-text">
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.
</span>
</MenuItem>
{
allVariables.size > 0 && <Separator />
}
{
allVariables.size > 0 && Array.from(allVariables).map((name) => {
{allVariables.size > 0 && <Separator />}
{allVariables.size > 0 &&
Array.from(allVariables).map((name) => {
return (
<MenuItem className="contextmenu-item tooltip" key={name} onClick={() => addVariable(name)}>
<MenuItem
className="contextmenu-item tooltip"
key={name}
onClick={() => addVariable(name)}
>
{name}
<span className="tooltip-text">
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}".
</span>
</MenuItem>
);
})
}
})}
<Separator />
<MenuItem className="contextmenu-item tooltip" onClick={() => addComponent(LiveSplit.BlankSpaceComponent)}>
<MenuItem
className="contextmenu-item tooltip"
onClick={() =>
addComponent(LiveSplit.BlankSpaceComponent)
}
>
Blank Space
<span className="tooltip-text">
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.
</span>
</MenuItem>
<MenuItem className="contextmenu-item tooltip" onClick={() => addComponent(LiveSplit.SeparatorComponent)}>
<MenuItem
className="contextmenu-item tooltip"
onClick={() =>
addComponent(LiveSplit.SeparatorComponent)
}
>
Separator
<span className="tooltip-text">
A simple component that just renders a separator between components.
A simple component that just renders a separator
between components.
</span>
</MenuItem>
</ContextMenu>
+96 -67
View File
@@ -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<LiveSplitServer>,
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<LiveSplitServer>;
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<void>,
importSplitsFromFile(file: File): Promise<void>,
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<void>;
importSplitsFromFile(file: File): Promise<void>;
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<Props> {
public render() {
const renderedView = <TimerView
layout={this.props.layout}
layoutState={this.props.layoutState}
layoutUrlCache={this.props.layoutUrlCache}
layoutWidth={this.props.layoutWidth}
layoutHeight={this.props.layoutHeight}
generalSettings={this.props.generalSettings}
isDesktop={this.props.isDesktop}
renderWithSidebar={false}
sidebarOpen={this.props.sidebarOpen}
commandSink={this.props.commandSink}
renderer={this.props.renderer}
serverConnection={this.props.serverConnection}
callbacks={this.props.callbacks}
currentComparison={this.props.currentComparison}
currentTimingMethod={this.props.currentTimingMethod}
currentPhase={this.props.currentPhase}
currentSplitIndex={this.props.currentSplitIndex}
allComparisons={this.props.allComparisons}
splitsModified={this.props.splitsModified}
layoutModified={this.props.layoutModified}
/>;
const renderedView = (
<TimerView
layout={this.props.layout}
layoutState={this.props.layoutState}
layoutUrlCache={this.props.layoutUrlCache}
layoutWidth={this.props.layoutWidth}
layoutHeight={this.props.layoutHeight}
generalSettings={this.props.generalSettings}
isDesktop={this.props.isDesktop}
renderWithSidebar={false}
sidebarOpen={this.props.sidebarOpen}
commandSink={this.props.commandSink}
renderer={this.props.renderer}
serverConnection={this.props.serverConnection}
callbacks={this.props.callbacks}
currentComparison={this.props.currentComparison}
currentTimingMethod={this.props.currentTimingMethod}
currentPhase={this.props.currentPhase}
currentSplitIndex={this.props.currentSplitIndex}
allComparisons={this.props.allComparisons}
splitsModified={this.props.splitsModified}
layoutModified={this.props.layoutModified}
/>
);
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<Props> {
<div className="sidebar-buttons">
<h1>Layout</h1>
<hr />
<button onClick={(_) => this.props.callbacks.openLayoutEditor()}>
<button
onClick={(_) => this.props.callbacks.openLayoutEditor()}
>
<SquarePen strokeWidth={2.5} /> Edit
</button>
<button onClick={(_) => this.props.callbacks.saveLayout()}>
<Save strokeWidth={2.5} />
<span>
Save
{
this.props.layoutModified &&
<Circle strokeWidth={0} size={12} fill="currentColor" className="modified-icon" />
}
{this.props.layoutModified && (
<Circle
strokeWidth={0}
size={12}
fill="currentColor"
className="modified-icon"
/>
)}
</span>
</button>
<button onClick={(_) => this.props.callbacks.importLayout()}>
@@ -103,7 +130,9 @@ export class LayoutView extends React.Component<Props> {
<button onClick={(_) => this.props.callbacks.exportLayout()}>
<Upload strokeWidth={2.5} /> Export
</button>
<button onClick={(_) => this.props.callbacks.loadDefaultLayout()}>
<button
onClick={(_) => this.props.callbacks.loadDefaultLayout()}
>
<ListRestart strokeWidth={2.5} /> Default
</button>
<hr />
+270 -195
View File
@@ -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<LiveSplitServer>,
currentComparison: string,
currentTimingMethod: TimingMethod,
currentPhase: TimerPhase,
currentSplitIndex: number,
allComparisons: string[],
allVariables: Set<string>,
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<LiveSplitServer>;
currentComparison: string;
currentTimingMethod: TimingMethod;
currentPhase: TimerPhase;
currentSplitIndex: number;
allComparisons: string[];
allVariables: Set<string>;
splitsModified: boolean;
layoutModified: boolean;
}
export let hotkeySystem: Option<HotkeyImplementation> = null;
@@ -112,7 +135,10 @@ export class LiveSplit extends React.Component<Props, State> {
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<Props, State> {
"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<Props, State> {
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<Props, State> {
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<Props, State> {
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<Props, State> {
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<Props, State> {
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<Props, State> {
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<Props, State> {
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 = <RunEditorComponent
editor={this.state.menu.editor}
callbacks={this}
runEditorUrlCache={this.state.runEditorUrlCache}
allComparisons={this.state.allComparisons}
allVariables={this.state.allVariables}
generalSettings={this.state.generalSettings}
/>;
view = (
<RunEditorComponent
editor={this.state.menu.editor}
callbacks={this}
runEditorUrlCache={this.state.runEditorUrlCache}
allComparisons={this.state.allComparisons}
allVariables={this.state.allVariables}
generalSettings={this.state.generalSettings}
/>
);
} else if (this.state.menu.kind === MenuKind.LayoutEditor) {
view = <LayoutEditorComponent
editor={this.state.menu.editor}
layoutState={this.state.layoutState}
layoutEditorUrlCache={this.state.layoutEditorUrlCache}
layoutUrlCache={this.state.layoutUrlCache}
layoutWidth={this.state.layoutWidth}
layoutHeight={this.state.layoutHeight}
generalSettings={this.state.generalSettings}
allComparisons={this.state.allComparisons}
allVariables={this.state.allVariables}
isDesktop={this.state.isDesktop}
commandSink={this.state.commandSink}
renderer={this.state.renderer}
callbacks={this}
/>;
view = (
<LayoutEditorComponent
editor={this.state.menu.editor}
layoutState={this.state.layoutState}
layoutEditorUrlCache={this.state.layoutEditorUrlCache}
layoutUrlCache={this.state.layoutUrlCache}
layoutWidth={this.state.layoutWidth}
layoutHeight={this.state.layoutHeight}
generalSettings={this.state.generalSettings}
allComparisons={this.state.allComparisons}
allVariables={this.state.allVariables}
isDesktop={this.state.isDesktop}
commandSink={this.state.commandSink}
renderer={this.state.renderer}
callbacks={this}
/>
);
} else if (this.state.menu.kind === MenuKind.MainSettings) {
view = <SettingsEditorComponent
generalSettings={this.state.generalSettings}
hotkeyConfig={this.state.menu.config}
urlCache={this.state.layoutUrlCache}
callbacks={this}
commandSink={this.state.commandSink}
serverConnection={this.state.serverConnection}
allComparisons={this.state.allComparisons}
allVariables={this.state.allVariables}
/>;
view = (
<SettingsEditorComponent
generalSettings={this.state.generalSettings}
hotkeyConfig={this.state.menu.config}
urlCache={this.state.layoutUrlCache}
callbacks={this}
commandSink={this.state.commandSink}
serverConnection={this.state.serverConnection}
allComparisons={this.state.allComparisons}
allVariables={this.state.allVariables}
/>
);
} else if (this.state.menu.kind === MenuKind.About) {
view = <About callbacks={this} />;
} else if (this.state.menu.kind === MenuKind.Splits) {
view = <SplitsSelection
generalSettings={this.state.generalSettings}
commandSink={this.state.commandSink}
openedSplitsKey={this.state.openedSplitsKey}
callbacks={this}
splitsModified={this.state.splitsModified}
/>;
view = (
<SplitsSelection
generalSettings={this.state.generalSettings}
commandSink={this.state.commandSink}
openedSplitsKey={this.state.openedSplitsKey}
callbacks={this}
splitsModified={this.state.splitsModified}
/>
);
} else if (this.state.menu.kind === MenuKind.Timer) {
view = <TimerView
layout={this.state.layout}
layoutState={this.state.layoutState}
layoutUrlCache={this.state.layoutUrlCache}
layoutWidth={this.state.layoutWidth}
layoutHeight={this.state.layoutHeight}
generalSettings={this.state.generalSettings}
isDesktop={this.state.isDesktop}
renderWithSidebar={true}
sidebarOpen={this.state.sidebarOpen}
commandSink={this.state.commandSink}
renderer={this.state.renderer}
serverConnection={this.state.serverConnection}
callbacks={this}
currentComparison={this.state.currentComparison}
currentTimingMethod={this.state.currentTimingMethod}
currentPhase={this.state.currentPhase}
currentSplitIndex={this.state.currentSplitIndex}
allComparisons={this.state.allComparisons}
splitsModified={this.state.splitsModified}
layoutModified={this.state.layoutModified}
/>;
view = (
<TimerView
layout={this.state.layout}
layoutState={this.state.layoutState}
layoutUrlCache={this.state.layoutUrlCache}
layoutWidth={this.state.layoutWidth}
layoutHeight={this.state.layoutHeight}
generalSettings={this.state.generalSettings}
isDesktop={this.state.isDesktop}
renderWithSidebar={true}
sidebarOpen={this.state.sidebarOpen}
commandSink={this.state.commandSink}
renderer={this.state.renderer}
serverConnection={this.state.serverConnection}
callbacks={this}
currentComparison={this.state.currentComparison}
currentTimingMethod={this.state.currentTimingMethod}
currentPhase={this.state.currentPhase}
currentSplitIndex={this.state.currentSplitIndex}
allComparisons={this.state.allComparisons}
splitsModified={this.state.splitsModified}
layoutModified={this.state.layoutModified}
/>
);
} else if (this.state.menu.kind === MenuKind.Layout) {
view = <LayoutView
layout={this.state.layout}
layoutState={this.state.layoutState}
layoutUrlCache={this.state.layoutUrlCache}
layoutWidth={this.state.layoutWidth}
layoutHeight={this.state.layoutHeight}
generalSettings={this.state.generalSettings}
isDesktop={this.state.isDesktop}
renderWithSidebar={true}
sidebarOpen={this.state.sidebarOpen}
commandSink={this.state.commandSink}
renderer={this.state.renderer}
serverConnection={this.state.serverConnection}
callbacks={this}
currentComparison={this.state.currentComparison}
currentTimingMethod={this.state.currentTimingMethod}
currentPhase={this.state.currentPhase}
currentSplitIndex={this.state.currentSplitIndex}
allComparisons={this.state.allComparisons}
splitsModified={this.state.splitsModified}
layoutModified={this.state.layoutModified}
/>;
view = (
<LayoutView
layout={this.state.layout}
layoutState={this.state.layoutState}
layoutUrlCache={this.state.layoutUrlCache}
layoutWidth={this.state.layoutWidth}
layoutHeight={this.state.layoutHeight}
generalSettings={this.state.generalSettings}
isDesktop={this.state.isDesktop}
renderWithSidebar={true}
sidebarOpen={this.state.sidebarOpen}
commandSink={this.state.commandSink}
renderer={this.state.renderer}
serverConnection={this.state.serverConnection}
callbacks={this}
currentComparison={this.state.currentComparison}
currentTimingMethod={this.state.currentTimingMethod}
currentPhase={this.state.currentPhase}
currentSplitIndex={this.state.currentSplitIndex}
allComparisons={this.state.allComparisons}
splitsModified={this.state.splitsModified}
layoutModified={this.state.layoutModified}
/>
);
}
return <>
{view}
<DialogContainer
onShow={() => this.lockTimerInteraction()}
onClose={() => this.unlockTimerInteraction()}
/>
<ToastContainer
position="bottom-right"
toastClassName="toast-class"
className="toast-body"
theme="dark"
/>
</>;
return (
<>
{view}
<DialogContainer
onShow={() => this.lockTimerInteraction()}
onClose={() => this.unlockTimerInteraction()}
/>
<ToastContainer
position="bottom-right"
toastClassName="toast-class"
className="toast-body"
theme="dark"
/>
</>
);
}
public renderViewWithSidebar(renderedView: React.JSX.Element, sidebarContent: React.JSX.Element) {
public renderViewWithSidebar(
renderedView: React.JSX.Element,
sidebarContent: React.JSX.Element,
) {
return (
<div className={this.state.isDesktop ? "" : "is-mobile"}>
<Sidebar
@@ -429,9 +490,7 @@ export class LiveSplit extends React.Component<Props, State> {
contentClassName="livesplit-container"
overlayClassName="sidebar-overlay"
>
{
!this.state.isDesktop &&
!this.state.sidebarOpen &&
{!this.state.isDesktop && !this.state.sidebarOpen && (
<button
aria-label="Open Sidebar"
className="open-sidebar-button"
@@ -439,10 +498,8 @@ export class LiveSplit extends React.Component<Props, State> {
>
<Menu size={30} strokeWidth={2.5} />
</button>
}
<div className="view-container">
{renderedView}
</div>
)}
<div className="view-container">{renderedView}</div>
</Sidebar>
</div>
);
@@ -632,7 +689,10 @@ export class LiveSplit extends React.Component<Props, State> {
});
}
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<Props, State> {
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<Props, State> {
});
}
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<Props, State> {
}
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<Props, State> {
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<Props, State> {
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<Props, State> {
}
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<Props, State> {
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<Props, State> {
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<Props, State> {
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<Props, State> {
private currentTimingMethodChanged(): void {
if (this.state != null) {
const currentTimingMethod = this.state.commandSink.currentTimingMethod();
const currentTimingMethod =
this.state.commandSink.currentTimingMethod();
(async () => {
try {
+162 -70
View File
@@ -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<LiveSplitServer>,
commandSink: LSOCommandSink,
allComparisons: string[],
allVariables: Set<string>,
generalSettings: GeneralSettings;
hotkeyConfig: HotkeyConfig;
urlCache: UrlCache;
callbacks: Callbacks;
serverConnection: Option<LiveSplitServer>;
commandSink: LSOCommandSink;
allComparisons: string[];
allVariables: Set<string>;
}
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<Props, State> {
@@ -69,38 +80,62 @@ export class MainSettings extends React.Component<Props, State> {
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<Props, State> {
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<Props, State> {
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<Props, State> {
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<Props, State> {
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: <div style={{
display: "flex",
alignItems: "center",
gap: "0.25em",
}}>
Server Connection <FlaskConical size={16} color="#07bc0c" strokeWidth={2.5} />
</div>,
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.<br /><br />
This feature is <b>experimental</b> and the protocol will likely change in the future.
</>,
text: (
<div
style={{
display: "flex",
alignItems: "center",
gap: "0.25em",
}}
>
Server Connection{" "}
<FlaskConical
size={16}
color="#07bc0c"
strokeWidth={2.5}
/>
</div>
),
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.
<br />
<br />
This feature is <b>experimental</b> 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<Props, State> {
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<Props, State> {
<div className="small">
<button
className="toggle-left"
onClick={(_) => this.props.callbacks.closeMainSettings(true, this.state.generalSettings)}
onClick={(_) =>
this.props.callbacks.closeMainSettings(
true,
this.state.generalSettings,
)
}
>
<Check strokeWidth={2.5} /> OK
</button>
<button
className="toggle-right"
onClick={(_) => this.props.callbacks.closeMainSettings(false, this.state.generalSettings)}
onClick={(_) =>
this.props.callbacks.closeMainSettings(
false,
this.state.generalSettings,
)
}
>
<X strokeWidth={2.5} /> Cancel
</button>
+958 -438
View File
File diff suppressed because it is too large Load Diff
+479 -222
View File
File diff suppressed because it is too large Load Diff
+127 -68
View File
@@ -1,43 +1,67 @@
import * as React from "react";
import {
getSplitsInfos, SplitsInfo, deleteSplits, copySplits, loadSplits,
storeRunWithoutDisposing, storeSplitsKey,
getSplitsInfos,
SplitsInfo,
deleteSplits,
copySplits,
loadSplits,
storeRunWithoutDisposing,
storeSplitsKey,
} from "../storage";
import { Run, Segment, TimerPhase } from "../livesplit-core";
import { toast } from "react-toastify";
import { openFileAsArrayBuffer, exportFile, convertFileToArrayBuffer, FILE_EXT_SPLITS } from "../util/FileUtil";
import {
openFileAsArrayBuffer,
exportFile,
convertFileToArrayBuffer,
FILE_EXT_SPLITS,
} from "../util/FileUtil";
import { Option, bug, maybeDisposeAndThen } from "../util/OptionUtil";
import DragUpload from "./DragUpload";
import { GeneralSettings } from "./MainSettings";
import { LSOCommandSink } from "./LSOCommandSink";
import { showDialog } from "./Dialog";
import { ArrowLeft, Circle, Copy, Download, FolderOpen, Plus, Save, SquarePen, Trash, Upload } from "lucide-react";
import {
ArrowLeft,
Circle,
Copy,
Download,
FolderOpen,
Plus,
Save,
SquarePen,
Trash,
Upload,
} from "lucide-react";
import "../css/SplitsSelection.scss";
export interface EditingInfo {
splitsKey?: number,
run: Run,
splitsKey?: number;
run: Run;
}
export interface Props {
commandSink: LSOCommandSink,
openedSplitsKey?: number,
callbacks: Callbacks,
generalSettings: GeneralSettings,
splitsModified: boolean,
commandSink: LSOCommandSink;
openedSplitsKey?: number;
callbacks: Callbacks;
generalSettings: GeneralSettings;
splitsModified: boolean;
}
interface State {
splitsInfos?: Array<[number, SplitsInfo]>,
splitsInfos?: Array<[number, SplitsInfo]>;
}
interface Callbacks {
openRunEditor(editingInfo: EditingInfo): void,
setSplitsKey(newKey?: number): void,
openTimerView(): void,
renderViewWithSidebar(renderedView: React.JSX.Element, sidebarContent: React.JSX.Element): React.JSX.Element,
saveSplits(): Promise<void>,
openRunEditor(editingInfo: EditingInfo): void;
setSplitsKey(newKey?: number): void;
openTimerView(): void;
renderViewWithSidebar(
renderedView: React.JSX.Element,
sidebarContent: React.JSX.Element,
): React.JSX.Element;
saveSplits(): Promise<void>;
}
export class SplitsSelection extends React.Component<Props, State> {
@@ -51,7 +75,10 @@ export class SplitsSelection extends React.Component<Props, State> {
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() {
@@ -74,25 +101,23 @@ export class SplitsSelection extends React.Component<Props, State> {
<Download strokeWidth={2.5} /> Import
</button>
</div>
{
this.state.splitsInfos?.length > 0 &&
{this.state.splitsInfos?.length > 0 && (
<div className="splits-table">
<div className="splits-rows">
{
this.state.splitsInfos
.map(([key, info]) => this.renderSavedSplitsRow(key, info))
}
{this.state.splitsInfos.map(([key, info]) =>
this.renderSavedSplitsRow(key, info),
)}
</div>
</div>
}
)}
</div>
);
}
return <DragUpload
importSplits={this.importSplitsFromFile.bind(this)}
>
<div className="splits-selection">{content}</div>
</DragUpload>;
return (
<DragUpload importSplits={this.importSplitsFromFile.bind(this)}>
<div className="splits-selection">{content}</div>
</DragUpload>
);
}
private async refreshDb() {
@@ -106,28 +131,44 @@ export class SplitsSelection extends React.Component<Props, State> {
const isOpened = key === this.props.openedSplitsKey;
return (
<div className={isOpened ? "splits-row selected" : "splits-row"} key={key}>
<div
className={isOpened ? "splits-row selected" : "splits-row"}
key={key}
>
{this.splitsTitle(info)}
<div className="splits-row-buttons">
{
isOpened
? null
: <>
<button aria-label="Open Splits" onClick={() => this.openSplits(key)}>
<FolderOpen strokeWidth={2.5} />
</button>
<button aria-label="Edit Splits" onClick={() => this.editSplits(key)}>
<SquarePen strokeWidth={2.5} />
</button>
<button aria-label="Export Splits" onClick={() => this.exportSplits(key, info)}>
<Upload strokeWidth={2.5} />
</button>
</>
}
<button aria-label="Copy Splits" onClick={() => this.copySplits(key)}>
{isOpened ? null : (
<>
<button
aria-label="Open Splits"
onClick={() => this.openSplits(key)}
>
<FolderOpen strokeWidth={2.5} />
</button>
<button
aria-label="Edit Splits"
onClick={() => this.editSplits(key)}
>
<SquarePen strokeWidth={2.5} />
</button>
<button
aria-label="Export Splits"
onClick={() => this.exportSplits(key, info)}
>
<Upload strokeWidth={2.5} />
</button>
</>
)}
<button
aria-label="Copy Splits"
onClick={() => this.copySplits(key)}
>
<Copy strokeWidth={2.5} />
</button>
<button aria-label="Remove Splits" onClick={() => this.deleteSplits(key)}>
<button
aria-label="Remove Splits"
onClick={() => this.deleteSplits(key)}
>
<Trash strokeWidth={2.5} />
</button>
</div>
@@ -138,8 +179,12 @@ export class SplitsSelection extends React.Component<Props, State> {
private splitsTitle(info: SplitsInfo) {
return (
<div className="splits-title-text">
<div className="splits-text splits-game">{info.game || "Untitled"}</div>
<div className="splits-text splits-category">{info.category || "—"}</div>
<div className="splits-text splits-game">
{info.game || "Untitled"}
</div>
<div className="splits-text splits-category">
{info.category || "—"}
</div>
</div>
);
}
@@ -149,24 +194,35 @@ export class SplitsSelection extends React.Component<Props, State> {
<div className="sidebar-buttons">
<h1>Splits</h1>
<hr />
<button onClick={(_) => {
if (this.props.commandSink.currentPhase() !== TimerPhase.NotRunning) {
toast.error("You can't edit your splits while the timer is running.");
return;
}
const run = this.props.commandSink.getRun().clone();
this.props.callbacks.openRunEditor({ run });
}}>
<button
onClick={(_) => {
if (
this.props.commandSink.currentPhase() !==
TimerPhase.NotRunning
) {
toast.error(
"You can't edit your splits while the timer is running.",
);
return;
}
const run = this.props.commandSink.getRun().clone();
this.props.callbacks.openRunEditor({ run });
}}
>
<SquarePen strokeWidth={2.5} /> Edit
</button>
<button onClick={(_) => this.saveSplits()}>
<Save strokeWidth={2.5} />
<span>
Save
{
this.props.splitsModified &&
<Circle strokeWidth={0} size={12} fill="currentColor" className="modified-icon" />
}
{this.props.splitsModified && (
<Circle
strokeWidth={0}
size={12}
fill="currentColor"
className="modified-icon"
/>
)}
</span>
</button>
<button onClick={(_) => this.exportTimerSplits()}>
@@ -202,7 +258,8 @@ export class SplitsSelection extends React.Component<Props, State> {
if (isModified) {
const [result] = await showDialog({
title: "Discard Changes?",
description: "Your current splits are modified and have unsaved changes. Do you want to continue and discard those changes?",
description:
"Your current splits are modified and have unsaved changes. Do you want to continue and discard those changes?",
buttons: ["Yes", "No"],
});
if (result === 1) {
@@ -214,9 +271,8 @@ export class SplitsSelection extends React.Component<Props, State> {
if (run === undefined) {
return;
}
maybeDisposeAndThen(
this.props.commandSink.setRun(run),
() => toast.error("The loaded splits are invalid."),
maybeDisposeAndThen(this.props.commandSink.setRun(run), () =>
toast.error("The loaded splits are invalid."),
);
this.props.callbacks.setSplitsKey(key);
}
@@ -260,7 +316,8 @@ export class SplitsSelection extends React.Component<Props, State> {
private async deleteSplits(key: number) {
const [result] = await showDialog({
title: "Delete Splits?",
description: "Are you sure you want to delete the splits? This operation can not be undone.",
description:
"Are you sure you want to delete the splits? This operation can not be undone.",
buttons: ["Yes", "No"],
});
if (result !== 0) {
@@ -304,7 +361,9 @@ export class SplitsSelection extends React.Component<Props, State> {
}
}
private async importSplitsFromArrayBuffer(buffer: [ArrayBuffer, File]): Promise<Option<Error>> {
private async importSplitsFromArrayBuffer(
buffer: [ArrayBuffer, File],
): Promise<Option<Error>> {
const [file] = buffer;
using result = Run.parseArray(new Uint8Array(file), "");
if (result.parsedSuccessfully()) {
+11 -4
View File
@@ -2,14 +2,21 @@ import React from "react";
import * as classes from "../css/Switch.module.scss";
export default function Switch({ checked, setIsChecked }: {
checked: boolean,
setIsChecked: (checked: boolean) => void,
export default function Switch({
checked,
setIsChecked,
}: {
checked: boolean;
setIsChecked: (checked: boolean) => void;
}) {
return (
<label className={classes.label}>
<div className={classes.switch}>
<input type="checkbox" checked={checked} onChange={(e) => setIsChecked(e.target.checked)} />
<input
type="checkbox"
checked={checked}
onChange={(e) => setIsChecked(e.target.checked)}
/>
<span />
</div>
</label>
+15 -11
View File
@@ -1,14 +1,14 @@
import * as React from "react";
export interface Props {
className?: string,
value?: any,
onChange?: React.EventHandler<React.ChangeEvent<HTMLInputElement>>,
onBlur?: React.EventHandler<React.FocusEvent<HTMLInputElement>>,
label: string,
invalid?: boolean,
small?: boolean,
list?: [string, string[]],
className?: string;
value?: any;
onChange?: React.EventHandler<React.ChangeEvent<HTMLInputElement>>;
onBlur?: React.EventHandler<React.FocusEvent<HTMLInputElement>>;
label: string;
invalid?: boolean;
small?: boolean;
list?: [string, string[]];
}
export class TextBox extends React.Component<Props> {
@@ -24,9 +24,13 @@ export class TextBox extends React.Component<Props> {
let list;
if (this.props.list !== undefined) {
name = this.props.list[0];
list = <datalist id={name}>
{this.props.list[1].map((n, i) => <option key={i} value={n} />)}
</datalist>;
list = (
<datalist id={name}>
{this.props.list[1].map((n, i) => (
<option key={i} value={n} />
))}
</datalist>
);
}
return (
+279 -173
View File
@@ -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<LiveSplitServer>,
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<LiveSplitServer>;
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<void>,
importSplitsFromFile(file: File): Promise<void>,
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<void>;
importSplitsFromFile(file: File): Promise<void>;
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<Props, State> {
@@ -67,145 +89,207 @@ export class TimerView extends React.Component<Props, State> {
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 <DragUpload
importLayout={(file) => this.props.callbacks.importLayoutFromFile(file)}
importSplits={(file) => this.props.callbacks.importSplitsFromFile(file)}
>
<div>
<div
onClick={(_) => {
if (this.props.generalSettings.showControlButtons) {
this.props.commandSink.splitOrStart();
}
}}
style={{
display: "inline-block",
cursor: this.props.generalSettings.showControlButtons ? "pointer" : undefined,
}}
>
<Layout
getState={() => {
// 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 (
<DragUpload
importLayout={(file) =>
this.props.callbacks.importLayoutFromFile(file)
}
importSplits={(file) =>
this.props.callbacks.importSplitsFromFile(file)
}
>
<div>
<div
onClick={(_) => {
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)}
/>
</div>
</div>
{
this.props.generalSettings.showControlButtons && <div className="buttons" style={{ width: this.props.layoutWidth }}>
<div className="control-buttons">
<button
aria-label={this.props.currentPhase === TimerPhase.NotRunning
? "Start"
: this.props.currentPhase === TimerPhase.Paused
? "Resume"
: "Pause"
style={{
display: "inline-block",
cursor: this.props.generalSettings
.showControlButtons
? "pointer"
: undefined,
}}
>
<Layout
getState={() => {
// 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.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)
}
disabled={this.props.currentPhase === TimerPhase.Ended}
onClick={(_) => this.props.commandSink.togglePauseOrStart()}
>
{
(this.props.currentPhase === TimerPhase.NotRunning ||
this.props.currentPhase === TimerPhase.Paused)
? <Play fill="currentColor" strokeWidth={0} />
: <Pause fill="currentColor" strokeWidth={0} />
}
</button>
<button
aria-label="Undo Split"
disabled={this.props.currentSplitIndex <= 0}
onClick={(_) => this.props.commandSink.undoSplit()}
>
<ArrowUp strokeWidth={3.5} />
</button>
<button
aria-label="Reset"
disabled={this.props.currentPhase === TimerPhase.NotRunning}
onClick={(_) => this.props.commandSink.reset()}
>
<X strokeWidth={3.5} />
</button>
<button
aria-label="Skip Split"
disabled={
this.props.currentPhase === TimerPhase.NotRunning ||
this.props.currentSplitIndex + 1 >= this.props.commandSink.segmentsCount()
}
onClick={(_) => this.props.commandSink.skipSplit()}
>
<ArrowDown strokeWidth={3.5} />
</button>
/>
</div>
</div>
}
{
showManualGameTime && <div className="buttons" style={{ width: this.props.layoutWidth }}>
<input
type="text"
className="manual-game-time"
value={this.state.manualGameTime}
placeholder="Manual Game Time"
onChange={(e) => {
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 && (
<div
className="buttons"
style={{ width: this.props.layoutWidth }}
>
<div className="control-buttons">
<button
aria-label={
this.props.currentPhase ===
TimerPhase.NotRunning
? "Start"
: this.props.currentPhase ===
TimerPhase.Paused
? "Resume"
: "Pause"
}
disabled={
this.props.currentPhase === TimerPhase.Ended
}
onClick={(_) =>
this.props.commandSink.togglePauseOrStart()
}
>
{this.props.currentPhase ===
TimerPhase.NotRunning ||
this.props.currentPhase ===
TimerPhase.Paused ? (
<Play fill="currentColor" strokeWidth={0} />
) : (
<Pause
fill="currentColor"
strokeWidth={0}
/>
)}
</button>
<button
aria-label="Undo Split"
disabled={this.props.currentSplitIndex <= 0}
onClick={(_) =>
this.props.commandSink.undoSplit()
}
>
<ArrowUp strokeWidth={3.5} />
</button>
<button
aria-label="Reset"
disabled={
this.props.currentPhase ===
TimerPhase.NotRunning
}
onClick={(_) => this.props.commandSink.reset()}
>
<X strokeWidth={3.5} />
</button>
<button
aria-label="Skip Split"
disabled={
this.props.currentPhase ===
TimerPhase.NotRunning ||
this.props.currentSplitIndex + 1 >=
this.props.commandSink.segmentsCount()
}
onClick={(_) =>
this.props.commandSink.skipSplit()
}
>
<ArrowDown strokeWidth={3.5} />
</button>
</div>
</div>
)}
{showManualGameTime && (
<div
className="buttons"
style={{ width: this.props.layoutWidth }}
>
<input
type="text"
className="manual-game-time"
value={this.state.manualGameTime}
placeholder="Manual Game Time"
onChange={(e) => {
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: "",
});
}
}
}
}
}}
/>
</div>
}
</DragUpload>;
}}
/>
</div>
)}
</DragUpload>
);
}
private renderSidebarContent() {
@@ -222,50 +306,72 @@ export class TimerView extends React.Component<Props, State> {
<List strokeWidth={2.5} />
<span>
Splits
{
this.props.splitsModified &&
<Circle strokeWidth={0} size={12} fill="currentColor" className="modified-icon" />
}
{this.props.splitsModified && (
<Circle
strokeWidth={0}
size={12}
fill="currentColor"
className="modified-icon"
/>
)}
</span>
</button>
<button onClick={(_) => this.props.callbacks.openLayoutView()}>
<Layers strokeWidth={2.5} />
<span>
Layout
{
this.props.layoutModified &&
<Circle strokeWidth={0} size={12} fill="currentColor" className="modified-icon" />
}
{this.props.layoutModified && (
<Circle
strokeWidth={0}
size={12}
fill="currentColor"
className="modified-icon"
/>
)}
</span>
</button>
<hr />
<h2>Compare Against</h2>
<select
value={this.props.currentComparison}
onChange={(e) => this.props.commandSink.setCurrentComparison(e.target.value)}
onChange={(e) =>
this.props.commandSink.setCurrentComparison(
e.target.value,
)
}
className="choose-comparison"
>
{this.props.allComparisons.map((comparison) => <option>{comparison}</option>)}
{this.props.allComparisons.map((comparison) => (
<option>{comparison}</option>
))}
</select>
<div className="small">
<button
onClick={(_) => {
this.props.commandSink.setCurrentTimingMethod(TimingMethod.RealTime);
this.props.commandSink.setCurrentTimingMethod(
TimingMethod.RealTime,
);
}}
className={
(this.props.currentTimingMethod === TimingMethod.RealTime ? "button-pressed" : "") +
" toggle-left"
(this.props.currentTimingMethod ===
TimingMethod.RealTime
? "button-pressed"
: "") + " toggle-left"
}
>
Real Time
</button>
<button
onClick={(_) => {
this.props.commandSink.setCurrentTimingMethod(TimingMethod.GameTime);
this.props.commandSink.setCurrentTimingMethod(
TimingMethod.GameTime,
);
}}
className={
(this.props.currentTimingMethod === TimingMethod.GameTime ? "button-pressed" : "") +
" toggle-right"
(this.props.currentTimingMethod ===
TimingMethod.GameTime
? "button-pressed"
: "") + " toggle-right"
}
>
Game Time
+8 -4
View File
@@ -1,10 +1,14 @@
import * as React from "react";
import { FRAME_RATE_AUTOMATIC, FrameRateSetting, batteryAwareFrameRate } from "./FrameRate";
import {
FRAME_RATE_AUTOMATIC,
FrameRateSetting,
batteryAwareFrameRate,
} from "./FrameRate";
export interface Props {
frameRate: FrameRateSetting,
update(): void,
children: React.ReactNode,
frameRate: FrameRateSetting;
update(): void;
children: React.ReactNode;
}
export default class AutoRefresh extends React.Component<Props> {
+15 -5
View File
@@ -27,7 +27,9 @@ function openFile(accept: string): Promise<File | undefined> {
});
}
export async function convertFileToArrayBuffer(file: File): Promise<[ArrayBuffer, File] | Error> {
export async function convertFileToArrayBuffer(
file: File,
): Promise<[ArrayBuffer, File] | Error> {
return new Promise((resolve: (_: [ArrayBuffer, File] | Error) => void) => {
try {
const reader = new FileReader();
@@ -53,7 +55,9 @@ export async function convertFileToArrayBuffer(file: File): Promise<[ArrayBuffer
});
}
export async function openFileAsArrayBuffer(accept: string): Promise<[ArrayBuffer, File] | Error | undefined> {
export async function openFileAsArrayBuffer(
accept: string,
): Promise<[ArrayBuffer, File] | Error | undefined> {
const file = await openFile(accept);
if (file === undefined) {
return undefined;
@@ -61,7 +65,9 @@ export async function openFileAsArrayBuffer(accept: string): Promise<[ArrayBuffe
return convertFileToArrayBuffer(file);
}
export async function convertFileToString(file: File): Promise<[string, File] | Error> {
export async function convertFileToString(
file: File,
): Promise<[string, File] | Error> {
return new Promise((resolve: (_: [string, File] | Error) => void) => {
try {
const reader = new FileReader();
@@ -87,7 +93,9 @@ export async function convertFileToString(file: File): Promise<[string, File] |
});
}
export async function openFileAsString(accept: string): Promise<[string, File] | Error | undefined> {
export async function openFileAsString(
accept: string,
): Promise<[string, File] | Error | undefined> {
const file = await openFile(accept);
if (file === undefined) {
return undefined;
@@ -96,7 +104,9 @@ export async function openFileAsString(accept: string): Promise<[string, File] |
}
export function exportFile(filename: string, data: BlobPart) {
const url = URL.createObjectURL(new Blob([data], { type: "application/octet-stream" }));
const url = URL.createObjectURL(
new Blob([data], { type: "application/octet-stream" }),
);
try {
const element = document.createElement("a");
element.setAttribute("href", url);
+7 -2
View File
@@ -42,11 +42,16 @@ export function load(loadedCallback: () => void) {
try {
for (const font of availableFonts) {
if (!knownStyles.has(font.family)) {
knownStyles.set(font.family, new Set(["normal", "bold"]));
knownStyles.set(
font.family,
new Set(["normal", "bold"]),
);
}
const set = knownStyles.get(font.family)!;
const styles = (font.style as string).toLowerCase().split(" ");
const styles = (font.style as string)
.toLowerCase()
.split(" ");
for (const [keyword, value] of FONT_STRETCHES) {
if (styles.includes(keyword)) {
+21 -18
View File
@@ -22,23 +22,25 @@ export let batteryAwareFrameRate: FrameRate = batteryFrameRate;
let computePressure: FrameRate = FRAME_RATE_MATCH_SCREEN;
if ('PressureObserver' in window) {
if ("PressureObserver" in window) {
(async () => {
try {
const observer = new (window as any).PressureObserver((records: any) => {
const state = records[0].state;
switch (state) {
case "serious":
computePressure = FRAME_RATE_SERIOUS;
break;
case "critical":
computePressure = FRAME_RATE_CRITICAL;
break;
default:
computePressure = FRAME_RATE_MATCH_SCREEN;
}
updateBatteryAwareFrameRate();
});
const observer = new (window as any).PressureObserver(
(records: any) => {
const state = records[0].state;
switch (state) {
case "serious":
computePressure = FRAME_RATE_SERIOUS;
break;
case "critical":
computePressure = FRAME_RATE_CRITICAL;
break;
default:
computePressure = FRAME_RATE_MATCH_SCREEN;
}
updateBatteryAwareFrameRate();
},
);
await observer.observe("cpu", { sampleInterval: 2_000 });
} catch {
// The Compute Pressure API is not supported by every browser.
@@ -50,9 +52,10 @@ if ('PressureObserver' in window) {
try {
const batteryApi = await (navigator as any).getBattery();
batteryApi.onchargingchange = () => {
batteryFrameRate = batteryApi.charging === true
? FRAME_RATE_MATCH_SCREEN
: FRAME_RATE_LOW_POWER;
batteryFrameRate =
batteryApi.charging === true
? FRAME_RATE_MATCH_SCREEN
: FRAME_RATE_LOW_POWER;
updateBatteryAwareFrameRate();
};
} catch {
+20 -12
View File
@@ -5,20 +5,24 @@ import { emoteList } from "../api/EmoteList";
const UNSAFE = markdownit({ html: true, breaks: false, linkify: true });
const SAFE = markdownit({ html: false, breaks: true, linkify: true });
const unsafeDefault = UNSAFE.renderer.rules.link_open || function (tokens, idx, options, _env, self) {
return self.renderToken(tokens, idx, options);
};
const unsafeDefault =
UNSAFE.renderer.rules.link_open ||
function (tokens, idx, options, _env, self) {
return self.renderToken(tokens, idx, options);
};
UNSAFE.renderer.rules.link_open = function (tokens, idx, options, env, self) {
tokens[idx].attrSet('target', '_blank');
return unsafeDefault(tokens, idx, options, env, self);
tokens[idx].attrSet("target", "_blank");
return unsafeDefault(tokens, idx, options, env, self);
};
const safeDefault = SAFE.renderer.rules.link_open || function (tokens, idx, options, _env, self) {
return self.renderToken(tokens, idx, options);
};
const safeDefault =
SAFE.renderer.rules.link_open ||
function (tokens, idx, options, _env, self) {
return self.renderToken(tokens, idx, options);
};
SAFE.renderer.rules.link_open = function (tokens, idx, options, env, self) {
tokens[idx].attrSet('target', '_blank');
return safeDefault(tokens, idx, options, env, self);
tokens[idx].attrSet("target", "_blank");
return safeDefault(tokens, idx, options, env, self);
};
function replaceTwitchEmotes(text: string): string {
@@ -41,8 +45,12 @@ export function replaceFlag(countryCode: string): React.JSX.Element {
export const Markdown = React.memo(renderMarkdown);
export function renderMarkdown({ markdown, unsafe }: { markdown: string,
unsafe?: boolean,
export function renderMarkdown({
markdown,
unsafe,
}: {
markdown: string;
unsafe?: boolean;
}): React.JSX.Element {
const markdownWithEmotes = replaceTwitchEmotes(markdown);
const html = (unsafe ? UNSAFE : SAFE).render(markdownWithEmotes);
+16 -8
View File
@@ -11,7 +11,7 @@ export function expect<T>(obj: Option<T>, message: string): T {
}
interface Disposable {
[Symbol.dispose](): void,
[Symbol.dispose](): void;
}
export function panic(message: string): never {
@@ -23,11 +23,14 @@ export function bug(message: string): void {
toast.error(
<>
<b>You encountered a bug:</b>
<p><i>{message}</i></p>
Please report this issue <a
href="https://github.com/LiveSplit/LiveSplitOne"
target="_blank"
>here</a>.
<p>
<i>{message}</i>
</p>
Please report this issue{" "}
<a href="https://github.com/LiveSplit/LiveSplitOne" target="_blank">
here
</a>
.
</>,
{
autoClose: false,
@@ -36,7 +39,9 @@ export function bug(message: string): void {
);
}
export function assertNever(x: never): never { return x; }
export function assertNever(x: never): never {
return x;
}
export function assert(condition: boolean, message: string): asserts condition {
if (!condition) {
@@ -44,7 +49,10 @@ export function assert(condition: boolean, message: string): asserts condition {
}
}
export function assertNull<T>(obj: Option<T | Disposable>, message: string): asserts obj is null | undefined {
export function assertNull<T>(
obj: Option<T | Disposable>,
message: string,
): asserts obj is null | undefined {
if (obj != null) {
(obj as any)[Symbol.dispose]?.();
panic(message);
+8 -15
View File
@@ -1,4 +1,7 @@
export function formatLeaderboardTime(totalSeconds: number, hideMilliseconds: boolean): string {
export function formatLeaderboardTime(
totalSeconds: number,
hideMilliseconds: boolean,
): string {
const seconds = totalSeconds % 60;
const totalMinutes = Math.floor(totalSeconds / 60);
const minutes = totalMinutes % 60;
@@ -10,20 +13,10 @@ export function formatLeaderboardTime(totalSeconds: number, hideMilliseconds: bo
};
if (hours > 0) {
return `${
hours
}:${
minutes.toLocaleString("en-GB", {
minimumIntegerDigits: 2,
})
}:${
seconds.toLocaleString("en-GB", secondsOptions)
}`;
return `${hours}:${minutes.toLocaleString("en-GB", {
minimumIntegerDigits: 2,
})}:${seconds.toLocaleString("en-GB", secondsOptions)}`;
} else {
return `${
minutes
}:${
seconds.toLocaleString("en-GB", secondsOptions)
}`;
return `${minutes}:${seconds.toLocaleString("en-GB", secondsOptions)}`;
}
}