mirror of
https://github.com/ApfelTeeSaft/LiveSplitOne.git
synced 2026-08-26 19:23:40 +00:00
Refactor and Split Up Some Code
This refactors some code to use CSS modules, functional React components and splits up the Settings component into roughly one component per setting.
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
@use "mobile.module.scss";
|
||||
@use "variables.icss";
|
||||
|
||||
$icon-size: 40px;
|
||||
$title-font-size: 40px;
|
||||
$build-version-font-size: 12px;
|
||||
$link-color: #56b0ff;
|
||||
|
||||
.about {
|
||||
max-width: 700px;
|
||||
|
||||
@include mobile.mobile {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.aboutInnerContainer {
|
||||
background-color: variables.$light-row-color;
|
||||
padding: variables.$ui-large-margin;
|
||||
border: 1px solid variables.$border-color;
|
||||
width: fit-content;
|
||||
|
||||
h2 {
|
||||
margin-bottom: variables.$ui-large-margin;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $link-color;
|
||||
}
|
||||
|
||||
@include mobile.mobile {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
.livesplitTitle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: variables.$ui-margin;
|
||||
}
|
||||
|
||||
.livesplitIcon {
|
||||
height: $icon-size;
|
||||
}
|
||||
|
||||
.titleText {
|
||||
font-weight: bold;
|
||||
font-size: $title-font-size;
|
||||
}
|
||||
|
||||
.buildVersion {
|
||||
font-size: $build-version-font-size;
|
||||
}
|
||||
|
||||
.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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
@use "mobile";
|
||||
@use "variables.icss";
|
||||
|
||||
$icon-size: 40px;
|
||||
$title-font-size: 40px;
|
||||
$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;
|
||||
|
||||
.livesplit-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.livesplit-icon {
|
||||
height: $icon-size;
|
||||
margin-right: variables.$ui-margin;
|
||||
|
||||
img {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include mobile.mobile {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
max-width: 700px;
|
||||
|
||||
@include mobile.mobile {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
@use "variables.icss";
|
||||
|
||||
.is-mobile + dialog > .dialog {
|
||||
:global(.is-mobile) + dialog > .dialog {
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
@@ -20,13 +20,19 @@ dialog {
|
||||
border: 2px solid rgba(255, 255, 255, 0.25);
|
||||
border-radius: 10px;
|
||||
min-width: 225px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: variables.$ui-large-margin;
|
||||
|
||||
max-width: 400px;
|
||||
padding: variables.$ui-large-margin;
|
||||
|
||||
h1 {
|
||||
font-size: 20px;
|
||||
margin: 5px 0;
|
||||
font-size: larger;
|
||||
}
|
||||
|
||||
>* {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
@@ -49,7 +55,6 @@ dialog {
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
border: none;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.25);
|
||||
background: transparent;
|
||||
@@ -1,14 +1,14 @@
|
||||
@use "mobile";
|
||||
@use "mobile.module.scss";
|
||||
@use "variables.icss";
|
||||
|
||||
#upload-drop-zone {
|
||||
.uploadDropZone {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
|
||||
#upload-drop-zone-overlay {
|
||||
.overlay {
|
||||
position: absolute;
|
||||
width: calc(100% + 2 * #{variables.$main-content-margin});
|
||||
height: calc(100% + 2 * #{variables.$main-content-margin});
|
||||
@@ -27,7 +27,7 @@
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.overlay-text {
|
||||
.overlayText {
|
||||
font-size: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
@@ -1,23 +1,32 @@
|
||||
@use "variables.icss";
|
||||
|
||||
.hotkey-box {
|
||||
.hotkeyBox {
|
||||
button {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
min-height: 22px;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.hotkey-button.focused {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.trash {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 20px;
|
||||
column-gap: variables.$ui-margin;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.focused {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.trash {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
z-index: 5;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
@use "Font";
|
||||
|
||||
.resizableLayout {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: inherit;
|
||||
height: inherit;
|
||||
|
||||
:global(.react-resizable) {
|
||||
width: 0 !important;
|
||||
height: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.handleEast {
|
||||
cursor: e-resize;
|
||||
right: -10px;
|
||||
bottom: 10px;
|
||||
top: 0;
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.handleSouth {
|
||||
cursor: s-resize;
|
||||
bottom: -10px;
|
||||
left: 0;
|
||||
right: 10px;
|
||||
position: absolute;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.handleSouthEast {
|
||||
cursor: se-resize;
|
||||
bottom: -10px;
|
||||
right: -10px;
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
@use "Font";
|
||||
|
||||
.layout {
|
||||
.resizable-layout {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: inherit;
|
||||
height: inherit;
|
||||
|
||||
.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-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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,6 @@
|
||||
$tab-bar-height: 30px;
|
||||
$tab-width: 625px;
|
||||
$button-width: 160px;
|
||||
$label-size: 14px;
|
||||
$segment-icon-size: 19px;
|
||||
|
||||
$mobile-game-icon-size: 100px;
|
||||
@@ -326,86 +325,6 @@ $small-button-padding: 1px 3px 1px 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.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%;
|
||||
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
@use "sass:math";
|
||||
|
||||
@use "variables.icss";
|
||||
|
||||
$label-size: 14px;
|
||||
|
||||
.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: inherit;
|
||||
}
|
||||
|
||||
>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;
|
||||
}
|
||||
|
||||
>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;
|
||||
}
|
||||
|
||||
>.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%;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
@mixin mobile {
|
||||
:global(.is-mobile) & {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
@@ -44,7 +44,7 @@ import { UrlCache } from "../util/UrlCache";
|
||||
import { ServerProtocol, WebRenderer } from "../livesplit-core/livesplit_core";
|
||||
import { LiveSplitServer } from "../api/LiveSplitServer";
|
||||
import { LSOCommandSink } from "./LSOCommandSink";
|
||||
import DialogContainer from "./components/Dialog";
|
||||
import { DialogContainer } from "./components/Dialog";
|
||||
import { createHotkeys, HotkeyImplementation } from "../platform/Hotkeys";
|
||||
import { Menu } from "lucide-react";
|
||||
|
||||
@@ -350,7 +350,7 @@ export class LiveSplit extends React.Component<Props, State> {
|
||||
}
|
||||
|
||||
public render() {
|
||||
let view;
|
||||
let view: React.JSX.Element | undefined;
|
||||
if (this.state.menu.kind === MenuKind.RunEditor) {
|
||||
view = (
|
||||
<RunEditorComponent
|
||||
|
||||
@@ -17,7 +17,7 @@ function colorToCss(color: Color): string {
|
||||
return `rgba(${r},${g},${b},${a})`;
|
||||
}
|
||||
|
||||
export default function ColorPicker({
|
||||
export function ColorPicker({
|
||||
color,
|
||||
setColor,
|
||||
}: {
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import * as React from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
|
||||
import "../../css/Dialog.scss";
|
||||
|
||||
export interface Props {
|
||||
onShow: () => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
import * as classes from "../../css/Dialog.module.scss";
|
||||
|
||||
export interface Options {
|
||||
title: string | React.JSX.Element;
|
||||
@@ -21,7 +16,7 @@ export interface State {
|
||||
}
|
||||
|
||||
let dialogElement: HTMLDialogElement | null = null;
|
||||
let setState: ((options: Options) => void) | undefined;
|
||||
let setStateFn: ((options: Options) => void) | undefined;
|
||||
let resolveFn: ((_: [number, string]) => void) | undefined;
|
||||
let onCloseFn: (() => void) | undefined;
|
||||
let alreadyClosed = false;
|
||||
@@ -39,99 +34,91 @@ export function showDialog(options: Options): Promise<[number, string]> {
|
||||
}
|
||||
};
|
||||
}
|
||||
setState?.(options);
|
||||
setStateFn?.(options);
|
||||
return new Promise((resolve) => (resolveFn = resolve));
|
||||
}
|
||||
|
||||
export default class DialogContainer extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
export function DialogContainer({
|
||||
onShow,
|
||||
onClose,
|
||||
}: {
|
||||
onShow: () => void;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const [options, setOptions] = useState<Options>({
|
||||
title: "",
|
||||
description: "",
|
||||
buttons: [],
|
||||
});
|
||||
const [input, setInput] = useState("");
|
||||
|
||||
onCloseFn = props.onClose;
|
||||
useEffect(() => {
|
||||
onCloseFn = onClose;
|
||||
}, [onClose]);
|
||||
|
||||
this.state = {
|
||||
options: {
|
||||
title: "",
|
||||
description: "",
|
||||
buttons: [],
|
||||
},
|
||||
input: "",
|
||||
useEffect(() => {
|
||||
setStateFn = (options) => {
|
||||
onShow();
|
||||
setOptions(options);
|
||||
setInput(options.defaultText ?? "");
|
||||
};
|
||||
}
|
||||
}, [onShow]);
|
||||
|
||||
public componentDidMount(): void {
|
||||
setState = (options) => {
|
||||
this.props.onShow();
|
||||
this.setState({
|
||||
options,
|
||||
input: options.defaultText ?? "",
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
);
|
||||
}
|
||||
|
||||
private close(i: number) {
|
||||
const handleClose = (i: number) => {
|
||||
alreadyClosed = true;
|
||||
dialogElement?.close();
|
||||
resolveFn?.([i, this.state.input]);
|
||||
this.props.onClose();
|
||||
}
|
||||
resolveFn?.([i, input]);
|
||||
onClose();
|
||||
};
|
||||
|
||||
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={classes.dialog}>
|
||||
<h1>{options.title}</h1>
|
||||
<p>{options.description}</p>
|
||||
{options.textInput && (
|
||||
<input
|
||||
type="text"
|
||||
value={input}
|
||||
autoFocus={true}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e?.key === "Enter") {
|
||||
e.preventDefault();
|
||||
handleClose(0);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div className={classes.buttons}>
|
||||
{options.buttons.map((button, i) => (
|
||||
<button
|
||||
key={i}
|
||||
autoFocus={i === 0 && !options.textInput}
|
||||
onClick={() => handleClose(i)}
|
||||
>
|
||||
{button}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,37 +1,38 @@
|
||||
import * as React from "react";
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
import "../../css/DragUpload.scss";
|
||||
import * as classes from "../../css/DragUpload.module.scss";
|
||||
|
||||
export interface Props {
|
||||
export function DragUpload({
|
||||
children,
|
||||
importLayout,
|
||||
importSplits,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
importLayout?: (file: File) => Promise<void>;
|
||||
importSplits(file: File): Promise<void>;
|
||||
}
|
||||
importSplits: (file: File) => Promise<void>;
|
||||
}) {
|
||||
const dropZoneRef = useRef<HTMLDivElement>(null);
|
||||
const dropZoneOverlayRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
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 importLayout = this.props.importLayout;
|
||||
const importSplits = this.props.importSplits;
|
||||
useEffect(() => {
|
||||
const dropZone = dropZoneRef.current;
|
||||
const dropZoneOverlay = dropZoneOverlayRef.current;
|
||||
|
||||
if (dropZone === null) {
|
||||
if (!dropZone) {
|
||||
return;
|
||||
}
|
||||
|
||||
dropZone.addEventListener("dragenter", (event) => {
|
||||
const handleDragEnter = (event: DragEvent) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
if (dropZoneOverlay) {
|
||||
dropZoneOverlay.style.visibility = "visible";
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
dropZone.addEventListener("dragleave", (event) => {
|
||||
const handleDragLeave = (event: DragEvent) => {
|
||||
if (
|
||||
dropZoneOverlay &&
|
||||
(event.pageX < 10 ||
|
||||
@@ -41,14 +42,14 @@ export default class DragUpload extends React.Component<Props> {
|
||||
) {
|
||||
dropZoneOverlay.style.visibility = "hidden";
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
dropZone.addEventListener("dragover", (event) => {
|
||||
const handleDragOver = (event: DragEvent) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
});
|
||||
};
|
||||
|
||||
dropZone.addEventListener("drop", (event) => {
|
||||
const handleDrop = (event: DragEvent) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
@@ -73,17 +74,27 @@ export default class DragUpload extends React.Component<Props> {
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<div id="upload-drop-zone">
|
||||
<div id="upload-drop-zone-overlay">
|
||||
<div className="overlay-text">Waiting for drop...</div>
|
||||
</div>
|
||||
{this.props.children}
|
||||
dropZone.addEventListener("dragenter", handleDragEnter);
|
||||
dropZone.addEventListener("dragleave", handleDragLeave);
|
||||
dropZone.addEventListener("dragover", handleDragOver);
|
||||
dropZone.addEventListener("drop", handleDrop);
|
||||
|
||||
return () => {
|
||||
dropZone.removeEventListener("dragenter", handleDragEnter);
|
||||
dropZone.removeEventListener("dragleave", handleDragLeave);
|
||||
dropZone.removeEventListener("dragover", handleDragOver);
|
||||
dropZone.removeEventListener("drop", handleDrop);
|
||||
};
|
||||
}, [importLayout, importSplits]);
|
||||
|
||||
return (
|
||||
<div ref={dropZoneRef} className={classes.uploadDropZone}>
|
||||
<div ref={dropZoneOverlayRef} className={classes.overlay}>
|
||||
<div className={classes.overlayText}>Waiting for drop...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import * as React from "react";
|
||||
import { Option, map, expect } from "../../util/OptionUtil";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { Option, expect } from "../../util/OptionUtil";
|
||||
import { hotkeySystem } from "../LiveSplit";
|
||||
import { Circle, Trash } from "lucide-react";
|
||||
|
||||
import "../../css/HotkeyButton.scss";
|
||||
import * as classes from "../../css/HotkeyButton.module.scss";
|
||||
|
||||
function resolveKey(keyCode: string): Promise<string> | string {
|
||||
return expect(
|
||||
@@ -12,104 +12,45 @@ function resolveKey(keyCode: string): Promise<string> | string {
|
||||
).resolve(keyCode);
|
||||
}
|
||||
|
||||
export interface Props {
|
||||
export function HotkeyButton({
|
||||
value,
|
||||
setValue,
|
||||
}: {
|
||||
value: Option<string>;
|
||||
setValue: (value: Option<string>) => void;
|
||||
}
|
||||
}) {
|
||||
const [listener, setListener] = useState<Option<EventListenerObject>>(null);
|
||||
const [intervalHandle, setIntervalHandle] = useState<Option<number>>(null);
|
||||
const [resolvedKey, setResolvedKey] = useState<Option<string>>(null);
|
||||
|
||||
export interface State {
|
||||
listener: Option<EventListenerObject>;
|
||||
intervalHandle: Option<number>;
|
||||
resolvedKey: Option<string>;
|
||||
}
|
||||
|
||||
export default class HotkeyButton extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
listener: null,
|
||||
intervalHandle: null,
|
||||
resolvedKey: null,
|
||||
};
|
||||
|
||||
this.updateResolvedKey(props.value);
|
||||
}
|
||||
public componentDidUpdate(previousProps: Props) {
|
||||
if (previousProps.value !== this.props.value) {
|
||||
this.updateResolvedKey(this.props.value);
|
||||
}
|
||||
}
|
||||
|
||||
private async updateResolvedKey(value: Option<string>): Promise<void> {
|
||||
let resolvedKey = "";
|
||||
if (value != null) {
|
||||
const matches = value.match(/(.+)\+\s*(.+)$/);
|
||||
if (matches != null) {
|
||||
resolvedKey = `${matches[1]}+ ${await resolveKey(matches[2])}`;
|
||||
} else {
|
||||
resolvedKey = await resolveKey(value);
|
||||
const updateResolvedKey = useCallback(
|
||||
async (value: Option<string>): Promise<void> => {
|
||||
let resolvedKey = "";
|
||||
if (value != null) {
|
||||
const matches = value.match(/(.+)\+\s*(.+)$/);
|
||||
if (matches != null) {
|
||||
resolvedKey = `${matches[1]}+ ${await resolveKey(
|
||||
matches[2],
|
||||
)}`;
|
||||
} else {
|
||||
resolvedKey = await resolveKey(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.setState({ resolvedKey });
|
||||
}
|
||||
setResolvedKey(resolvedKey);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
public render() {
|
||||
let buttonText: Option<string> | React.JSX.Element = null;
|
||||
if (this.props.value != null) {
|
||||
buttonText = this.state.resolvedKey;
|
||||
} else if (this.state.listener != null) {
|
||||
buttonText = (
|
||||
<Circle strokeWidth={0} size={16} fill="currentColor" />
|
||||
);
|
||||
}
|
||||
useEffect(() => {
|
||||
updateResolvedKey(value);
|
||||
}, [value, updateResolvedKey]);
|
||||
|
||||
return (
|
||||
<div className="hotkey-box">
|
||||
<button
|
||||
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.
|
||||
</span>
|
||||
</button>
|
||||
{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",
|
||||
left: "0px",
|
||||
position: "fixed",
|
||||
right: "0px",
|
||||
top: "0px",
|
||||
zIndex: 5,
|
||||
}}
|
||||
onClick={() => this.blurButton()}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const focusButton = () => {
|
||||
let newListener = listener;
|
||||
let newIntervalHandle = intervalHandle;
|
||||
|
||||
private focusButton() {
|
||||
let listener = this.state.listener;
|
||||
let intervalHandle = this.state.intervalHandle;
|
||||
|
||||
if (listener === null) {
|
||||
listener = {
|
||||
if (newListener == null) {
|
||||
newListener = {
|
||||
handleEvent: (ev: KeyboardEvent) => {
|
||||
if (ev.repeat) {
|
||||
return;
|
||||
@@ -144,17 +85,17 @@ export default class HotkeyButton extends React.Component<Props, State> {
|
||||
text += "Shift + ";
|
||||
}
|
||||
text += ev.code;
|
||||
this.props.setValue(text);
|
||||
setValue(text);
|
||||
ev.preventDefault();
|
||||
},
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", listener);
|
||||
window.addEventListener("keydown", newListener);
|
||||
}
|
||||
|
||||
if (intervalHandle === null) {
|
||||
if (newIntervalHandle == null) {
|
||||
const oldButtonState: boolean[][] = [];
|
||||
intervalHandle = window.setInterval(() => {
|
||||
newIntervalHandle = window.setInterval(() => {
|
||||
const gamepads = navigator.getGamepads();
|
||||
|
||||
let gamepadIdx = 0;
|
||||
@@ -163,14 +104,14 @@ export default class HotkeyButton extends React.Component<Props, State> {
|
||||
oldButtonState[gamepadIdx] = [];
|
||||
}
|
||||
|
||||
if (gamepad !== null) {
|
||||
if (gamepad != null) {
|
||||
let buttonIdx = 0;
|
||||
for (const button of gamepad.buttons) {
|
||||
const oldState =
|
||||
oldButtonState[gamepadIdx]?.[buttonIdx] ??
|
||||
false;
|
||||
if (button.pressed && !oldState) {
|
||||
this.props.setValue(`Gamepad${buttonIdx}`);
|
||||
setValue(`Gamepad${buttonIdx}`);
|
||||
}
|
||||
|
||||
oldButtonState[gamepadIdx][buttonIdx] =
|
||||
@@ -185,22 +126,52 @@ export default class HotkeyButton extends React.Component<Props, State> {
|
||||
}, 1000 / 60.0);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
listener,
|
||||
intervalHandle,
|
||||
});
|
||||
setListener(newListener);
|
||||
setIntervalHandle(newIntervalHandle);
|
||||
};
|
||||
|
||||
const blurButton = () => {
|
||||
if (listener != null) {
|
||||
window.removeEventListener("keydown", listener);
|
||||
}
|
||||
if (intervalHandle != null) {
|
||||
window.clearInterval(intervalHandle);
|
||||
}
|
||||
setListener(null);
|
||||
setIntervalHandle(null);
|
||||
};
|
||||
|
||||
let buttonText: Option<string> | React.JSX.Element = null;
|
||||
if (value != null) {
|
||||
buttonText = resolvedKey;
|
||||
} else if (listener != null) {
|
||||
buttonText = <Circle strokeWidth={0} size={16} fill="currentColor" />;
|
||||
}
|
||||
|
||||
private blurButton() {
|
||||
if (this.state.listener != null) {
|
||||
window.removeEventListener("keydown", this.state.listener);
|
||||
}
|
||||
if (this.state.intervalHandle != null) {
|
||||
window.clearTimeout(this.state.intervalHandle);
|
||||
}
|
||||
this.setState({
|
||||
listener: null,
|
||||
intervalHandle: null,
|
||||
});
|
||||
}
|
||||
return (
|
||||
<div className={classes.hotkeyBox}>
|
||||
<button
|
||||
className={`tooltip ${listener != null ? classes.focused : ""}`}
|
||||
onClick={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.
|
||||
</span>
|
||||
</button>
|
||||
{value && (
|
||||
<Trash
|
||||
className={classes.trash}
|
||||
strokeWidth={2.5}
|
||||
size={20}
|
||||
onClick={() => setValue(null)}
|
||||
/>
|
||||
)}
|
||||
{listener != null && (
|
||||
<div className={classes.overlay} onClick={blurButton} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,9 +6,18 @@ import AutoRefresh from "../../util/AutoRefresh";
|
||||
import { UrlCache } from "../../util/UrlCache";
|
||||
import { GeneralSettings } from "../views/MainSettings";
|
||||
|
||||
import "../../css/Layout.scss";
|
||||
import * as classes from "../../css/Layout.module.scss";
|
||||
|
||||
export interface Props {
|
||||
export default function Layout({
|
||||
getState,
|
||||
layoutUrlCache,
|
||||
allowResize,
|
||||
width,
|
||||
height,
|
||||
generalSettings,
|
||||
renderer,
|
||||
onResize,
|
||||
}: {
|
||||
getState: () => LayoutStateRef;
|
||||
layoutUrlCache: UrlCache;
|
||||
allowResize: boolean;
|
||||
@@ -16,100 +25,78 @@ export interface Props {
|
||||
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(
|
||||
onResize: (width: number, height: number) => void;
|
||||
}) {
|
||||
const update = () => {
|
||||
const layoutState = getState();
|
||||
const newDims = renderer.render(
|
||||
layoutState.ptr,
|
||||
this.props.layoutUrlCache.imageCache.ptr,
|
||||
layoutUrlCache.imageCache.ptr,
|
||||
);
|
||||
if (newDims !== undefined) {
|
||||
this.props.onResize(newDims[0], newDims[1]);
|
||||
if (newDims != null) {
|
||||
onResize(newDims[0], newDims[1]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<AutoRefresh
|
||||
frameRate={this.props.generalSettings.frameRate}
|
||||
update={() => this.refreshLayout()}
|
||||
>
|
||||
return (
|
||||
<AutoRefresh frameRate={generalSettings.frameRate} update={update}>
|
||||
<div style={{ width, height }}>
|
||||
<div
|
||||
className="layout"
|
||||
style={{
|
||||
width: this.props.width,
|
||||
height: this.props.height,
|
||||
style={{ width, height }}
|
||||
ref={(element) => {
|
||||
element?.appendChild(renderer.element());
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{ width: "inherit", height: "inherit" }}
|
||||
ref={(element) => {
|
||||
element?.appendChild(this.props.renderer.element());
|
||||
}}
|
||||
/>
|
||||
{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,
|
||||
)
|
||||
}
|
||||
/>
|
||||
<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,
|
||||
)
|
||||
}
|
||||
/>
|
||||
<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,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</AutoRefresh>
|
||||
);
|
||||
}
|
||||
/>
|
||||
{allowResize && (
|
||||
<div className={classes.resizableLayout}>
|
||||
<ResizableBox
|
||||
axis="x"
|
||||
width={width}
|
||||
height={height}
|
||||
minConstraints={[100, 40]}
|
||||
handle={
|
||||
<div
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className={classes.handleEast}
|
||||
/>
|
||||
}
|
||||
onResize={(_event, data) =>
|
||||
onResize(data.size.width, data.size.height)
|
||||
}
|
||||
/>
|
||||
<ResizableBox
|
||||
axis="y"
|
||||
width={width}
|
||||
height={height}
|
||||
minConstraints={[100, 40]}
|
||||
handle={
|
||||
<div
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className={classes.handleSouth}
|
||||
/>
|
||||
}
|
||||
onResize={(_event, data) =>
|
||||
onResize(data.size.width, data.size.height)
|
||||
}
|
||||
/>
|
||||
<ResizableBox
|
||||
axis="both"
|
||||
width={width}
|
||||
height={height}
|
||||
minConstraints={[100, 40]}
|
||||
handle={
|
||||
<div
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className={classes.handleSouthEast}
|
||||
/>
|
||||
}
|
||||
onResize={(_event, data) =>
|
||||
onResize(data.size.width, data.size.height)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</AutoRefresh>
|
||||
);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,35 @@
|
||||
import * as React from "react";
|
||||
import { expect } from "../../../util/OptionUtil";
|
||||
import { AccuracyJson } from "../../../livesplit-core";
|
||||
import { SettingValueFactory } from ".";
|
||||
|
||||
export function Accuracy<T>({
|
||||
value,
|
||||
setValue,
|
||||
factory,
|
||||
}: {
|
||||
value: AccuracyJson;
|
||||
setValue: (value: T) => void;
|
||||
factory: SettingValueFactory<T>;
|
||||
}) {
|
||||
return (
|
||||
<div className="settings-value-box">
|
||||
<select
|
||||
value={value}
|
||||
onChange={(e) =>
|
||||
setValue(
|
||||
expect(
|
||||
factory.fromAccuracy(e.target.value),
|
||||
"Unexpected Accuracy",
|
||||
),
|
||||
)
|
||||
}
|
||||
>
|
||||
<option value="Seconds">Seconds</option>
|
||||
<option value="Tenths">Tenths</option>
|
||||
<option value="Hundredths">Hundredths</option>
|
||||
<option value="Milliseconds">Milliseconds</option>
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import * as React from "react";
|
||||
import { expect } from "../../../util/OptionUtil";
|
||||
import { Alignment } from "../../../livesplit-core";
|
||||
import { SettingValueFactory } from ".";
|
||||
|
||||
export function Alignment<T>({
|
||||
value,
|
||||
setValue,
|
||||
factory,
|
||||
}: {
|
||||
value: Alignment;
|
||||
setValue: (value: T) => void;
|
||||
factory: SettingValueFactory<T>;
|
||||
}) {
|
||||
return (
|
||||
<div className="settings-value-box">
|
||||
<select
|
||||
value={value}
|
||||
onChange={(e) =>
|
||||
setValue(
|
||||
expect(
|
||||
factory.fromAlignment(e.target.value),
|
||||
"Unexpected Alignment",
|
||||
),
|
||||
)
|
||||
}
|
||||
>
|
||||
<option value="Auto">Automatic</option>
|
||||
<option value="Left">Left</option>
|
||||
<option value="Center">Center</option>
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import * as React from "react";
|
||||
import { Color } from "../../../livesplit-core";
|
||||
import { SettingValueFactory } from ".";
|
||||
import { ColorPicker } from "../ColorPicker";
|
||||
import { Switch } from "../Switch";
|
||||
|
||||
export function Color<T>({
|
||||
value,
|
||||
setValue,
|
||||
factory,
|
||||
}: {
|
||||
value: Color;
|
||||
setValue: (value: T) => void;
|
||||
factory: SettingValueFactory<T>;
|
||||
}) {
|
||||
return (
|
||||
<div className="settings-value-box">
|
||||
<ColorPicker
|
||||
color={value}
|
||||
setColor={(color) =>
|
||||
setValue(
|
||||
factory.fromColor(
|
||||
color[0],
|
||||
color[1],
|
||||
color[2],
|
||||
color[3],
|
||||
),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function OptionalColor<T>({
|
||||
value,
|
||||
setValue,
|
||||
factory,
|
||||
}: {
|
||||
value: Color | null;
|
||||
setValue: (value: T) => void;
|
||||
factory: SettingValueFactory<T>;
|
||||
}) {
|
||||
const children: React.ReactNode[] = [];
|
||||
|
||||
if (value !== null) {
|
||||
children.push(
|
||||
<ColorPicker
|
||||
color={value}
|
||||
setColor={(color) =>
|
||||
setValue(
|
||||
factory.fromOptionalColor(
|
||||
color[0],
|
||||
color[1],
|
||||
color[2],
|
||||
color[3],
|
||||
),
|
||||
)
|
||||
}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="settings-value-box optional-value">
|
||||
<Switch
|
||||
checked={value !== null}
|
||||
setIsChecked={(checked) => {
|
||||
if (checked) {
|
||||
setValue(factory.fromOptionalColor(1, 1, 1, 1));
|
||||
} else {
|
||||
setValue(factory.fromOptionalEmptyColor());
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
import * as React from "react";
|
||||
import { expect } from "../../../util/OptionUtil";
|
||||
import {
|
||||
ColumnKind,
|
||||
ColumnStartWith,
|
||||
ColumnUpdateWith,
|
||||
ColumnUpdateTrigger,
|
||||
} from "../../../livesplit-core";
|
||||
import { SettingValueFactory } from ".";
|
||||
|
||||
export function ColumnKind<T>({
|
||||
value,
|
||||
setValue,
|
||||
factory,
|
||||
}: {
|
||||
value: ColumnKind;
|
||||
setValue: (value: T) => void;
|
||||
factory: SettingValueFactory<T>;
|
||||
}) {
|
||||
return (
|
||||
<div className="settings-value-box">
|
||||
<select
|
||||
value={value}
|
||||
onChange={(e) =>
|
||||
setValue(
|
||||
expect(
|
||||
factory.fromColumnKind(e.target.value),
|
||||
"Unexpected Column Kind value",
|
||||
),
|
||||
)
|
||||
}
|
||||
>
|
||||
<option value="Time">Time</option>
|
||||
<option value="Variable">Variable</option>
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ColumnStartWith<T>({
|
||||
value,
|
||||
setValue,
|
||||
factory,
|
||||
}: {
|
||||
value: ColumnStartWith;
|
||||
setValue: (value: T) => void;
|
||||
factory: SettingValueFactory<T>;
|
||||
}) {
|
||||
return (
|
||||
<div className="settings-value-box">
|
||||
<select
|
||||
value={value}
|
||||
onChange={(e) =>
|
||||
setValue(
|
||||
expect(
|
||||
factory.fromColumnStartWith(e.target.value),
|
||||
"Unexpected Column Start With value",
|
||||
),
|
||||
)
|
||||
}
|
||||
>
|
||||
<option value="Empty">Empty</option>
|
||||
<option value="ComparisonTime">Comparison Time</option>
|
||||
<option value="ComparisonSegmentTime">
|
||||
Comparison Segment Time
|
||||
</option>
|
||||
<option value="PossibleTimeSave">Possible Time Save</option>
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ColumnUpdateWith<T>({
|
||||
value,
|
||||
setValue,
|
||||
factory,
|
||||
}: {
|
||||
value: ColumnUpdateWith;
|
||||
setValue: (value: T) => void;
|
||||
factory: SettingValueFactory<T>;
|
||||
}) {
|
||||
return (
|
||||
<div className="settings-value-box">
|
||||
<select
|
||||
value={value}
|
||||
onChange={(e) =>
|
||||
setValue(
|
||||
expect(
|
||||
factory.fromColumnUpdateWith(e.target.value),
|
||||
"Unexpected Column Update With value",
|
||||
),
|
||||
)
|
||||
}
|
||||
>
|
||||
<option value="DontUpdate">Don't Update</option>
|
||||
<option value="SplitTime">Split Time</option>
|
||||
<option value="Delta">Time Ahead / Behind</option>
|
||||
<option value="DeltaWithFallback">
|
||||
Time Ahead / Behind or Split Time If Empty
|
||||
</option>
|
||||
<option value="SegmentTime">Segment Time</option>
|
||||
<option value="SegmentDelta">Time Saved / Lost</option>
|
||||
<option value="SegmentDeltaWithFallback">
|
||||
Time Saved / Lost or Segment Time If Empty
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ColumnUpdateTrigger<T>({
|
||||
value,
|
||||
setValue,
|
||||
factory,
|
||||
}: {
|
||||
value: ColumnUpdateTrigger;
|
||||
setValue: (value: T) => void;
|
||||
factory: SettingValueFactory<T>;
|
||||
}) {
|
||||
return (
|
||||
<div className="settings-value-box">
|
||||
<select
|
||||
value={value}
|
||||
onChange={(e) =>
|
||||
setValue(
|
||||
expect(
|
||||
factory.fromColumnUpdateTrigger(e.target.value),
|
||||
"Unexpected Column Update Trigger value",
|
||||
),
|
||||
)
|
||||
}
|
||||
>
|
||||
<option value="OnStartingSegment">On Starting Segment</option>
|
||||
<option value="Contextual">Contextual</option>
|
||||
<option value="OnEndingSegment">On Ending Segment</option>
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import * as React from "react";
|
||||
import { expect } from "../../../util/OptionUtil";
|
||||
import { DigitsFormatJson } from "../../../livesplit-core";
|
||||
import { SettingValueFactory } from ".";
|
||||
|
||||
export function DigitsFormat<T>({
|
||||
value,
|
||||
setValue,
|
||||
factory,
|
||||
}: {
|
||||
value: DigitsFormatJson;
|
||||
setValue: (value: T) => void;
|
||||
factory: SettingValueFactory<T>;
|
||||
}) {
|
||||
return (
|
||||
<div className="settings-value-box">
|
||||
<select
|
||||
value={value}
|
||||
onChange={(e) =>
|
||||
setValue(
|
||||
expect(
|
||||
factory.fromDigitsFormat(e.target.value),
|
||||
"Unexpected Digits Format",
|
||||
),
|
||||
)
|
||||
}
|
||||
>
|
||||
<option value="SingleDigitSeconds">1</option>
|
||||
<option value="DoubleDigitSeconds">01</option>
|
||||
<option value="SingleDigitMinutes">0:01</option>
|
||||
<option value="DoubleDigitMinutes">00:01</option>
|
||||
<option value="SingleDigitHours">0:00:01</option>
|
||||
<option value="DoubleDigitHours">00:00:01</option>
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
import * as React from "react";
|
||||
import { expect } from "../../../util/OptionUtil";
|
||||
import { Font } from "../../../livesplit-core";
|
||||
import { SettingValueFactory } from ".";
|
||||
import { Switch } from "../Switch";
|
||||
import * as FontList from "../../../util/FontList";
|
||||
|
||||
export function Font<T>({
|
||||
value,
|
||||
setValue,
|
||||
factory,
|
||||
loadedCallback,
|
||||
}: {
|
||||
value: Font | null;
|
||||
setValue: (value: T) => void;
|
||||
factory: SettingValueFactory<T>;
|
||||
loadedCallback: () => void;
|
||||
}) {
|
||||
const children = [
|
||||
<Switch
|
||||
checked={value !== null}
|
||||
setIsChecked={(value) => {
|
||||
if (value) {
|
||||
setValue(
|
||||
expect(
|
||||
factory.fromFont("", "normal", "normal", "normal"),
|
||||
"Unexpected Font",
|
||||
),
|
||||
);
|
||||
} else {
|
||||
setValue(factory.fromEmptyFont());
|
||||
}
|
||||
}}
|
||||
/>,
|
||||
];
|
||||
|
||||
if (value !== null) {
|
||||
// FIXME: We should do a proper promise hook.
|
||||
FontList.load(loadedCallback);
|
||||
|
||||
const { family, style, weight, stretch } = value;
|
||||
|
||||
const styles = FontList.knownStyles.get(family);
|
||||
|
||||
if (FontList.knownFamilies.length > 0) {
|
||||
children.push(
|
||||
<select
|
||||
style={{
|
||||
fontFamily: family,
|
||||
}}
|
||||
value={family}
|
||||
onChange={(e) =>
|
||||
setValue(
|
||||
expect(
|
||||
factory.fromFont(
|
||||
e.target.value,
|
||||
style,
|
||||
weight,
|
||||
stretch,
|
||||
),
|
||||
"Unexpected Font",
|
||||
),
|
||||
)
|
||||
}
|
||||
>
|
||||
<option value=""></option>
|
||||
{FontList.knownFamilies.map((n) => (
|
||||
<option value={n} style={{ fontFamily: n }}>
|
||||
{n}
|
||||
</option>
|
||||
))}
|
||||
</select>,
|
||||
);
|
||||
} else {
|
||||
children.push(
|
||||
<input
|
||||
className="text-box"
|
||||
value={family}
|
||||
onChange={(e) =>
|
||||
setValue(
|
||||
expect(
|
||||
factory.fromFont(
|
||||
e.target.value,
|
||||
style,
|
||||
weight,
|
||||
stretch,
|
||||
),
|
||||
"Unexpected Font",
|
||||
),
|
||||
)
|
||||
}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
children.push(
|
||||
<>Style</>,
|
||||
<select
|
||||
value={style}
|
||||
onChange={(e) =>
|
||||
setValue(
|
||||
expect(
|
||||
factory.fromFont(
|
||||
family,
|
||||
e.target.value,
|
||||
weight,
|
||||
stretch,
|
||||
),
|
||||
"Unexpected Font",
|
||||
),
|
||||
)
|
||||
}
|
||||
>
|
||||
<option value="normal">Normal</option>
|
||||
<option value="italic">Italic</option>
|
||||
</select>,
|
||||
<>Weight</>,
|
||||
<select
|
||||
value={weight}
|
||||
onChange={(e) =>
|
||||
setValue(
|
||||
expect(
|
||||
factory.fromFont(
|
||||
family,
|
||||
style,
|
||||
e.target.value,
|
||||
stretch,
|
||||
),
|
||||
"Unexpected Font",
|
||||
),
|
||||
)
|
||||
}
|
||||
>
|
||||
{FontList.FONT_WEIGHTS.map(([_, value, name]) => (
|
||||
<option
|
||||
style={{
|
||||
color: styles?.has(value) ? "white" : "grey",
|
||||
}}
|
||||
value={value}
|
||||
>
|
||||
{name}
|
||||
</option>
|
||||
))}
|
||||
</select>,
|
||||
<>Stretch</>,
|
||||
<select
|
||||
value={stretch}
|
||||
onChange={(e) =>
|
||||
setValue(
|
||||
expect(
|
||||
factory.fromFont(
|
||||
family,
|
||||
style,
|
||||
weight,
|
||||
e.target.value,
|
||||
),
|
||||
"Unexpected Font",
|
||||
),
|
||||
)
|
||||
}
|
||||
>
|
||||
{FontList.FONT_STRETCHES.map(([_, value, name]) => (
|
||||
<option
|
||||
style={{
|
||||
color: styles?.has(value) ? "white" : "grey",
|
||||
}}
|
||||
value={value}
|
||||
>
|
||||
{name}
|
||||
</option>
|
||||
))}
|
||||
</select>,
|
||||
);
|
||||
}
|
||||
|
||||
return <div className="settings-value-box optional-value">{children}</div>;
|
||||
}
|
||||
@@ -0,0 +1,384 @@
|
||||
import * as React from "react";
|
||||
import {
|
||||
Color,
|
||||
DeltaGradient,
|
||||
Gradient,
|
||||
ListGradient,
|
||||
} from "../../../livesplit-core";
|
||||
import { SettingValueFactory } from ".";
|
||||
import { assertNever, expect, Option } from "../../../util/OptionUtil";
|
||||
import { ColorPicker } from "../ColorPicker";
|
||||
|
||||
export function Gradient<T>({
|
||||
value,
|
||||
setValue,
|
||||
factory,
|
||||
}: {
|
||||
value: Gradient;
|
||||
setValue: (value: T) => void;
|
||||
factory: SettingValueFactory<T>;
|
||||
}) {
|
||||
let type: string | undefined;
|
||||
let color1: Color | undefined;
|
||||
let color2: Color | undefined;
|
||||
|
||||
if (value !== "Transparent") {
|
||||
type = Object.keys(value)[0];
|
||||
if ("Plain" in value) {
|
||||
color1 = value.Plain;
|
||||
} else if ("Vertical" in value) {
|
||||
[color1, color2] = value.Vertical;
|
||||
} else if ("Horizontal" in value) {
|
||||
[color1, color2] = value.Horizontal;
|
||||
} else {
|
||||
assertNever(value);
|
||||
}
|
||||
} else {
|
||||
type = "Transparent";
|
||||
}
|
||||
|
||||
const colorsToValue = (
|
||||
type: string,
|
||||
color1: Option<Color>,
|
||||
color2: Option<Color>,
|
||||
) => {
|
||||
color1 = color1 ?? [0, 0, 0, 0];
|
||||
color2 = color2 ?? color1;
|
||||
switch (type) {
|
||||
case "Transparent":
|
||||
return factory.fromTransparentGradient();
|
||||
case "Plain":
|
||||
return factory.fromColor(
|
||||
color1[0],
|
||||
color1[1],
|
||||
color1[2],
|
||||
color1[3],
|
||||
);
|
||||
case "Vertical":
|
||||
return factory.fromVerticalGradient(
|
||||
color1[0],
|
||||
color1[1],
|
||||
color1[2],
|
||||
color1[3],
|
||||
color2[0],
|
||||
color2[1],
|
||||
color2[2],
|
||||
color2[3],
|
||||
);
|
||||
case "Horizontal":
|
||||
return factory.fromHorizontalGradient(
|
||||
color1[0],
|
||||
color1[1],
|
||||
color1[2],
|
||||
color1[3],
|
||||
color2[0],
|
||||
color2[1],
|
||||
color2[2],
|
||||
color2[3],
|
||||
);
|
||||
default:
|
||||
throw new Error("Unexpected Gradient Type");
|
||||
}
|
||||
};
|
||||
|
||||
const children: React.JSX.Element[] = [
|
||||
<select
|
||||
value={type}
|
||||
onChange={(e) =>
|
||||
setValue(colorsToValue(e.target.value, color1, color2))
|
||||
}
|
||||
>
|
||||
<option value="Transparent">Transparent</option>
|
||||
<option value="Plain">Plain</option>
|
||||
<option value="Vertical">Vertical</option>
|
||||
<option value="Horizontal">Horizontal</option>
|
||||
</select>,
|
||||
];
|
||||
|
||||
if (color1) {
|
||||
children.push(
|
||||
<ColorPicker
|
||||
color={color1}
|
||||
setColor={(color) =>
|
||||
setValue(colorsToValue(type, color, color2))
|
||||
}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
if (color2) {
|
||||
children.push(
|
||||
<ColorPicker
|
||||
color={color2}
|
||||
setColor={(color) =>
|
||||
setValue(colorsToValue(type, color1, color))
|
||||
}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
if (color2) {
|
||||
return <div className="settings-value-box two-colors">{children}</div>;
|
||||
} else if (color1) {
|
||||
return <div className="settings-value-box one-color">{children}</div>;
|
||||
} else {
|
||||
return <div className="settings-value-box">{children}</div>;
|
||||
}
|
||||
}
|
||||
export function DeltaGradient<T>({
|
||||
value,
|
||||
setValue,
|
||||
factory,
|
||||
}: {
|
||||
value: DeltaGradient;
|
||||
setValue: (value: T) => void;
|
||||
factory: SettingValueFactory<T>;
|
||||
}) {
|
||||
let type: string | undefined;
|
||||
let color1: Color | undefined;
|
||||
let color2: Color | undefined;
|
||||
|
||||
if (typeof value !== "string") {
|
||||
[type] = Object.keys(value);
|
||||
if ("Plain" in value) {
|
||||
color1 = value.Plain;
|
||||
} else if ("Vertical" in value) {
|
||||
[color1, color2] = value.Vertical;
|
||||
} else if ("Horizontal" in value) {
|
||||
[color1, color2] = value.Horizontal;
|
||||
} else {
|
||||
assertNever(value);
|
||||
}
|
||||
} else {
|
||||
type = value;
|
||||
}
|
||||
|
||||
const colorsToValue = (
|
||||
type: string,
|
||||
color1: Option<Color>,
|
||||
color2: Option<Color>,
|
||||
) => {
|
||||
color1 = color1 ?? [0, 0, 0, 0];
|
||||
color2 = color2 ?? color1;
|
||||
switch (type) {
|
||||
case "Transparent":
|
||||
return factory.fromTransparentGradient();
|
||||
case "Plain":
|
||||
return factory.fromColor(
|
||||
color1[0],
|
||||
color1[1],
|
||||
color1[2],
|
||||
color1[3],
|
||||
);
|
||||
case "Vertical":
|
||||
return factory.fromVerticalGradient(
|
||||
color1[0],
|
||||
color1[1],
|
||||
color1[2],
|
||||
color1[3],
|
||||
color2[0],
|
||||
color2[1],
|
||||
color2[2],
|
||||
color2[3],
|
||||
);
|
||||
case "Horizontal":
|
||||
return factory.fromHorizontalGradient(
|
||||
color1[0],
|
||||
color1[1],
|
||||
color1[2],
|
||||
color1[3],
|
||||
color2[0],
|
||||
color2[1],
|
||||
color2[2],
|
||||
color2[3],
|
||||
);
|
||||
default:
|
||||
return expect(
|
||||
factory.fromDeltaGradient(type),
|
||||
"Unexpected Gradient Type",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const children: React.JSX.Element[] = [
|
||||
<select
|
||||
value={type}
|
||||
onChange={(e) =>
|
||||
setValue(colorsToValue(e.target.value, color1, color2))
|
||||
}
|
||||
>
|
||||
<option value="Transparent">Transparent</option>
|
||||
<option value="Plain">Plain</option>
|
||||
<option value="Vertical">Vertical</option>
|
||||
<option value="Horizontal">Horizontal</option>
|
||||
<option value="DeltaPlain">Plain Delta</option>
|
||||
<option value="DeltaVertical">Vertical Delta</option>
|
||||
<option value="DeltaHorizontal">Horizontal Delta</option>
|
||||
</select>,
|
||||
];
|
||||
|
||||
if (color1) {
|
||||
children.push(
|
||||
<ColorPicker
|
||||
color={color1}
|
||||
setColor={(color) =>
|
||||
setValue(colorsToValue(type, color, color2))
|
||||
}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
if (color2) {
|
||||
children.push(
|
||||
<ColorPicker
|
||||
color={color2}
|
||||
setColor={(color) =>
|
||||
setValue(colorsToValue(type, color1, color))
|
||||
}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
if (color2) {
|
||||
return <div className="settings-value-box two-colors">{children}</div>;
|
||||
} else if (color1) {
|
||||
return <div className="settings-value-box one-color">{children}</div>;
|
||||
} else {
|
||||
return <div className="settings-value-box">{children}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
export function ListGradient<T>({
|
||||
value,
|
||||
setValue,
|
||||
factory,
|
||||
}: {
|
||||
value: ListGradient;
|
||||
setValue: (value: T) => void;
|
||||
factory: SettingValueFactory<T>;
|
||||
}) {
|
||||
let type: string | undefined;
|
||||
let color1: Color | undefined;
|
||||
let color2: Color | undefined;
|
||||
|
||||
if ("Alternating" in value) {
|
||||
type = Object.keys(value)[0];
|
||||
[color1, color2] = value.Alternating;
|
||||
} else {
|
||||
const gradient = value.Same;
|
||||
if (gradient !== "Transparent") {
|
||||
type = Object.keys(gradient)[0];
|
||||
if ("Plain" in gradient) {
|
||||
color1 = gradient.Plain;
|
||||
} else if ("Vertical" in gradient) {
|
||||
[color1, color2] = gradient.Vertical;
|
||||
} else if ("Horizontal" in gradient) {
|
||||
[color1, color2] = gradient.Horizontal;
|
||||
} else {
|
||||
assertNever(gradient);
|
||||
}
|
||||
} else {
|
||||
type = "Transparent";
|
||||
}
|
||||
}
|
||||
|
||||
const colorsToValue = (
|
||||
type: string,
|
||||
color1: Option<Color>,
|
||||
color2: Option<Color>,
|
||||
) => {
|
||||
color1 = color1 ?? [0, 0, 0, 0];
|
||||
color2 = color2 ?? color1;
|
||||
switch (type) {
|
||||
case "Transparent":
|
||||
return factory.fromTransparentGradient();
|
||||
case "Plain":
|
||||
return factory.fromColor(
|
||||
color1[0],
|
||||
color1[1],
|
||||
color1[2],
|
||||
color1[3],
|
||||
);
|
||||
case "Vertical":
|
||||
return factory.fromVerticalGradient(
|
||||
color1[0],
|
||||
color1[1],
|
||||
color1[2],
|
||||
color1[3],
|
||||
color2[0],
|
||||
color2[1],
|
||||
color2[2],
|
||||
color2[3],
|
||||
);
|
||||
case "Horizontal":
|
||||
return factory.fromHorizontalGradient(
|
||||
color1[0],
|
||||
color1[1],
|
||||
color1[2],
|
||||
color1[3],
|
||||
color2[0],
|
||||
color2[1],
|
||||
color2[2],
|
||||
color2[3],
|
||||
);
|
||||
case "Alternating":
|
||||
return factory.fromAlternatingGradient(
|
||||
color1[0],
|
||||
color1[1],
|
||||
color1[2],
|
||||
color1[3],
|
||||
color2[0],
|
||||
color2[1],
|
||||
color2[2],
|
||||
color2[3],
|
||||
);
|
||||
default:
|
||||
throw new Error("Unexpected Gradient Type");
|
||||
}
|
||||
};
|
||||
|
||||
const children: React.JSX.Element[] = [
|
||||
<select
|
||||
value={type}
|
||||
onChange={(e) =>
|
||||
setValue(colorsToValue(e.target.value, color1, color2))
|
||||
}
|
||||
>
|
||||
<option value="Transparent">Transparent</option>
|
||||
<option value="Plain">Plain</option>
|
||||
<option value="Vertical">Vertical</option>
|
||||
<option value="Horizontal">Horizontal</option>
|
||||
<option value="Alternating">Alternating</option>
|
||||
</select>,
|
||||
];
|
||||
|
||||
if (color1) {
|
||||
children.push(
|
||||
<ColorPicker
|
||||
color={color1}
|
||||
setColor={(color) =>
|
||||
setValue(colorsToValue(type, color, color2))
|
||||
}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
if (color2) {
|
||||
children.push(
|
||||
<ColorPicker
|
||||
color={color2}
|
||||
setColor={(color) =>
|
||||
setValue(colorsToValue(type, color1, color))
|
||||
}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
if (color2) {
|
||||
return <div className="settings-value-box two-colors">{children}</div>;
|
||||
} else if (color1) {
|
||||
return <div className="settings-value-box one-color">{children}</div>;
|
||||
} else {
|
||||
return <div className="settings-value-box">{children}</div>;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
import * as React from "react";
|
||||
import { Option, assertNever, expect } from "../../../util/OptionUtil";
|
||||
import { Color, LayoutBackground } from "../../../livesplit-core";
|
||||
import { ColorPicker } from "../ColorPicker";
|
||||
import { SettingValueFactory } from ".";
|
||||
import { UrlCache } from "../../../util/UrlCache";
|
||||
import { toast } from "react-toastify";
|
||||
import { FILE_EXT_IMAGES, openFileAsArrayBuffer } from "../../../util/FileUtil";
|
||||
|
||||
export function LayoutBackground<T>({
|
||||
value,
|
||||
setValue,
|
||||
factory,
|
||||
editorUrlCache,
|
||||
}: {
|
||||
value: LayoutBackground;
|
||||
setValue: (value: T) => void;
|
||||
factory: SettingValueFactory<T>;
|
||||
editorUrlCache: UrlCache;
|
||||
}) {
|
||||
let type: string;
|
||||
let color1: Option<Color> = null;
|
||||
let color2: Option<Color> = null;
|
||||
let imageId =
|
||||
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
|
||||
let brightness = 100;
|
||||
let opacity = 100;
|
||||
let blur = 0;
|
||||
|
||||
const children: React.JSX.Element[] = [];
|
||||
|
||||
const colorsToValue = (
|
||||
type: string,
|
||||
color1: Option<Color>,
|
||||
color2: Option<Color>,
|
||||
) => {
|
||||
color1 = color1 ?? [0, 0, 0, 0];
|
||||
color2 = color2 ?? color1;
|
||||
switch (type) {
|
||||
case "Transparent":
|
||||
return factory.fromTransparentGradient();
|
||||
case "Plain":
|
||||
return factory.fromColor(
|
||||
color1[0],
|
||||
color1[1],
|
||||
color1[2],
|
||||
color1[3],
|
||||
);
|
||||
case "Vertical":
|
||||
return factory.fromVerticalGradient(
|
||||
color1[0],
|
||||
color1[1],
|
||||
color1[2],
|
||||
color1[3],
|
||||
color2[0],
|
||||
color2[1],
|
||||
color2[2],
|
||||
color2[3],
|
||||
);
|
||||
case "Horizontal":
|
||||
return factory.fromHorizontalGradient(
|
||||
color1[0],
|
||||
color1[1],
|
||||
color1[2],
|
||||
color1[3],
|
||||
color2[0],
|
||||
color2[1],
|
||||
color2[2],
|
||||
color2[3],
|
||||
);
|
||||
default:
|
||||
return expect(
|
||||
factory.fromBackgroundImage(
|
||||
imageId,
|
||||
brightness / 100,
|
||||
opacity / 100,
|
||||
blur / 100,
|
||||
),
|
||||
"Unexpected layout background",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if (typeof value !== "string") {
|
||||
[type] = Object.keys(value);
|
||||
if ("Plain" in value) {
|
||||
color1 = value.Plain;
|
||||
} else if ("Vertical" in value) {
|
||||
[color1, color2] = value.Vertical;
|
||||
} else if ("Horizontal" in value) {
|
||||
[color1, color2] = value.Horizontal;
|
||||
} else if ("image" in value) {
|
||||
imageId = value.image;
|
||||
brightness = 100 * value.brightness;
|
||||
opacity = 100 * value.opacity;
|
||||
blur = 100 * value.blur;
|
||||
type = "Image";
|
||||
const imageUrl = editorUrlCache.cache(imageId);
|
||||
children.push(
|
||||
<div
|
||||
className="color-picker-button"
|
||||
style={{
|
||||
background: imageUrl
|
||||
? `url("${imageUrl}") center / cover`
|
||||
: undefined,
|
||||
}}
|
||||
onClick={async (_) => {
|
||||
const maybeFile =
|
||||
await openFileAsArrayBuffer(FILE_EXT_IMAGES);
|
||||
if (maybeFile === undefined) {
|
||||
return;
|
||||
}
|
||||
if (maybeFile instanceof Error) {
|
||||
toast.error(
|
||||
`Failed to read the file: ${maybeFile.message}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
const [file] = maybeFile;
|
||||
const imageId =
|
||||
editorUrlCache.imageCache.cacheFromArray(
|
||||
new Uint8Array(file),
|
||||
true,
|
||||
);
|
||||
editorUrlCache.cache(imageId);
|
||||
const value = expect(
|
||||
factory.fromBackgroundImage(
|
||||
imageId,
|
||||
brightness / 100,
|
||||
opacity / 100,
|
||||
blur / 100,
|
||||
),
|
||||
"Unexpected layout background",
|
||||
);
|
||||
setValue(value);
|
||||
}}
|
||||
/>,
|
||||
<div
|
||||
style={{
|
||||
gridTemplateColumns: "max-content 1fr",
|
||||
columnGap: "8px",
|
||||
rowGap: "8px",
|
||||
alignItems: "center",
|
||||
display: "grid",
|
||||
gridColumn: "1 / 3",
|
||||
}}
|
||||
>
|
||||
Brightness
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
value={brightness}
|
||||
onChange={(e) => {
|
||||
brightness = Number(e.target.value);
|
||||
setValue(colorsToValue(type, color1, color2));
|
||||
}}
|
||||
/>
|
||||
Opacity
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
value={opacity}
|
||||
onChange={(e) => {
|
||||
opacity = Number(e.target.value);
|
||||
setValue(colorsToValue(type, color1, color2));
|
||||
}}
|
||||
/>
|
||||
Blur
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
value={blur}
|
||||
onChange={(e) => {
|
||||
blur = Number(e.target.value);
|
||||
setValue(colorsToValue(type, color1, color2));
|
||||
}}
|
||||
/>
|
||||
</div>,
|
||||
);
|
||||
} else {
|
||||
assertNever(value);
|
||||
}
|
||||
} else {
|
||||
type = value;
|
||||
}
|
||||
|
||||
children.splice(
|
||||
0,
|
||||
0,
|
||||
<select
|
||||
value={type}
|
||||
onChange={(e) => {
|
||||
setValue(colorsToValue(e.target.value, color1, color2));
|
||||
}}
|
||||
>
|
||||
<option value="Transparent">Transparent</option>
|
||||
<option value="Plain">Plain</option>
|
||||
<option value="Vertical">Vertical</option>
|
||||
<option value="Horizontal">Horizontal</option>
|
||||
<option value="Image">Image</option>
|
||||
</select>,
|
||||
);
|
||||
|
||||
if (color1) {
|
||||
children.push(
|
||||
<ColorPicker
|
||||
color={color1}
|
||||
setColor={(color) => {
|
||||
setValue(colorsToValue(type, color, color2));
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
if (color2) {
|
||||
children.push(
|
||||
<ColorPicker
|
||||
color={color2}
|
||||
setColor={(color) => {
|
||||
setValue(colorsToValue(type, color1, color));
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
if (color2) {
|
||||
return <div className="settings-value-box two-colors">{children}</div>;
|
||||
} else if (color1 || type === "Image") {
|
||||
return <div className="settings-value-box one-color">{children}</div>;
|
||||
} else {
|
||||
return <div className="settings-value-box">{children}</div>;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import * as React from "react";
|
||||
import { expect } from "../../../util/OptionUtil";
|
||||
import { LayoutDirection } from "../../../livesplit-core";
|
||||
import { SettingValueFactory } from ".";
|
||||
|
||||
export function LayoutDirection<T>({
|
||||
value,
|
||||
setValue,
|
||||
factory,
|
||||
}: {
|
||||
value: LayoutDirection;
|
||||
setValue: (value: T) => void;
|
||||
factory: SettingValueFactory<T>;
|
||||
}) {
|
||||
return (
|
||||
<div className="settings-value-box">
|
||||
<select
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
setValue(
|
||||
expect(
|
||||
factory.fromLayoutDirection(e.target.value),
|
||||
"Unexpected Layout Direction",
|
||||
),
|
||||
);
|
||||
}}
|
||||
>
|
||||
<option value="Vertical">Vertical</option>
|
||||
<option value="Horizontal">Horizontal</option>
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import * as React from "react";
|
||||
import { Option } from "../../../util/OptionUtil";
|
||||
import { LiveSplitServer } from "../../../api/LiveSplitServer";
|
||||
|
||||
import "../../../css/LiveSplitServerButton.scss";
|
||||
|
||||
export function ServerConnectionButton({
|
||||
value,
|
||||
connectOrDisconnect,
|
||||
}: {
|
||||
value: {
|
||||
url: string | undefined;
|
||||
connection: Option<LiveSplitServer>;
|
||||
};
|
||||
connectOrDisconnect: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="settings-value-box">
|
||||
<button
|
||||
className="livesplit-server-button"
|
||||
onClick={connectOrDisconnect}
|
||||
>
|
||||
{(() => {
|
||||
const connectionState =
|
||||
value.connection?.getConnectionState() ??
|
||||
WebSocket.CLOSED;
|
||||
switch (connectionState) {
|
||||
case WebSocket.OPEN:
|
||||
return <div>Disconnect</div>;
|
||||
case WebSocket.CLOSED:
|
||||
return <div>Connect</div>;
|
||||
case WebSocket.CONNECTING:
|
||||
return <div>Connecting...</div>;
|
||||
case WebSocket.CLOSING:
|
||||
return <div>Disconnecting...</div>;
|
||||
default:
|
||||
throw new Error("Unknown WebSocket State");
|
||||
}
|
||||
})()}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
import * as React from "react";
|
||||
import { SettingValueFactory } from ".";
|
||||
import { Trash } from "lucide-react";
|
||||
import { Switch } from "../Switch";
|
||||
|
||||
export function String<T>({
|
||||
value,
|
||||
setValue,
|
||||
factory,
|
||||
}: {
|
||||
value: string;
|
||||
setValue: (value: T) => void;
|
||||
factory: SettingValueFactory<T>;
|
||||
}) {
|
||||
return (
|
||||
<div className="settings-value-box">
|
||||
<input
|
||||
className="text-box"
|
||||
value={value}
|
||||
onChange={(e) => setValue(factory.fromString(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function OptionalString<T>({
|
||||
value,
|
||||
setValue,
|
||||
factory,
|
||||
}: {
|
||||
value: string | null;
|
||||
setValue: (value: T) => void;
|
||||
factory: SettingValueFactory<T>;
|
||||
}) {
|
||||
const children = [
|
||||
<Switch
|
||||
checked={value !== null}
|
||||
setIsChecked={(checked) => {
|
||||
if (checked) {
|
||||
setValue(factory.fromOptionalString(""));
|
||||
} else {
|
||||
setValue(factory.fromOptionalEmptyString());
|
||||
}
|
||||
}}
|
||||
/>,
|
||||
];
|
||||
|
||||
if (value !== null) {
|
||||
children.push(
|
||||
<input
|
||||
className="text-box"
|
||||
value={value}
|
||||
onChange={(e) =>
|
||||
setValue(factory.fromOptionalString(e.target.value))
|
||||
}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
return <div className="settings-value-box optional-value">{children}</div>;
|
||||
}
|
||||
|
||||
export function RemovableString<T>({
|
||||
value,
|
||||
setValue,
|
||||
factory,
|
||||
}: {
|
||||
value: string | null;
|
||||
setValue: (value: T) => void;
|
||||
factory: SettingValueFactory<T>;
|
||||
}) {
|
||||
return (
|
||||
<div className="settings-value-box removable-string">
|
||||
<input
|
||||
className="text-box"
|
||||
value={value ?? ""}
|
||||
onChange={(e) => {
|
||||
if (factory.fromRemovableString) {
|
||||
setValue(factory.fromRemovableString(e.target.value));
|
||||
} else {
|
||||
throw Error("Method is not implemented");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Trash
|
||||
className="trash"
|
||||
strokeWidth={2.5}
|
||||
size={20}
|
||||
onClick={() => {
|
||||
if (factory.fromRemovableEmptyString) {
|
||||
setValue(factory.fromRemovableEmptyString());
|
||||
} else {
|
||||
throw Error("Method is not implemented");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Comparison<T>({
|
||||
value,
|
||||
setValue,
|
||||
factory,
|
||||
allComparisons,
|
||||
}: {
|
||||
value: string | null;
|
||||
setValue: (value: T) => void;
|
||||
factory: SettingValueFactory<T>;
|
||||
allComparisons: string[];
|
||||
}) {
|
||||
return (
|
||||
<div className="settings-value-box">
|
||||
<select
|
||||
value={value ?? ""}
|
||||
onChange={(e) => {
|
||||
if (e.target.value !== "") {
|
||||
setValue(factory.fromOptionalString(e.target.value));
|
||||
} else {
|
||||
setValue(factory.fromOptionalEmptyString());
|
||||
}
|
||||
}}
|
||||
>
|
||||
<option value="">Current Comparison</option>
|
||||
{allComparisons.map((comparison) => (
|
||||
<option>{comparison}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function CustomVariable<T>({
|
||||
value,
|
||||
setValue,
|
||||
factory,
|
||||
allVariables,
|
||||
}: {
|
||||
value: string;
|
||||
setValue: (value: T) => void;
|
||||
factory: SettingValueFactory<T>;
|
||||
allVariables: Set<string>;
|
||||
}) {
|
||||
if (allVariables.size === 0) {
|
||||
return (
|
||||
<div className="settings-value-box">
|
||||
<span className="tooltip" style={{ textAlign: "center" }}>
|
||||
No variables available
|
||||
<span className="tooltip-text">
|
||||
Custom variables can be defined in the Variables tab
|
||||
when editing splits. Additional custom variables can be
|
||||
provided automatically by auto splitters.
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className="settings-value-box">
|
||||
<select
|
||||
value={value ?? ""}
|
||||
onChange={(e) =>
|
||||
setValue(factory.fromString(e.target.value))
|
||||
}
|
||||
>
|
||||
<option value="" />
|
||||
{Array.from(allVariables).map((variable) => (
|
||||
<option>{variable}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import * as React from "react";
|
||||
import { expect } from "../../../util/OptionUtil";
|
||||
import { TimingMethodJson } from "../../../livesplit-core";
|
||||
import { SettingValueFactory } from ".";
|
||||
import { Switch } from "../Switch";
|
||||
|
||||
export function OptionalTimingMethod<T>({
|
||||
value,
|
||||
setValue,
|
||||
factory,
|
||||
}: {
|
||||
value: TimingMethodJson | null;
|
||||
setValue: (value: T) => void;
|
||||
factory: SettingValueFactory<T>;
|
||||
}) {
|
||||
const children = [
|
||||
<Switch
|
||||
checked={value !== null}
|
||||
setIsChecked={(value) => {
|
||||
if (value) {
|
||||
setValue(
|
||||
expect(
|
||||
factory.fromOptionalTimingMethod("RealTime"),
|
||||
"Unexpected Optional Timing Method",
|
||||
),
|
||||
);
|
||||
} else {
|
||||
setValue(factory.fromOptionalEmptyTimingMethod());
|
||||
}
|
||||
}}
|
||||
/>,
|
||||
];
|
||||
|
||||
if (value !== null) {
|
||||
children.push(
|
||||
<select
|
||||
value={value}
|
||||
onChange={(e) =>
|
||||
setValue(
|
||||
expect(
|
||||
factory.fromOptionalTimingMethod(e.target.value),
|
||||
"Unexpected Optional Timing Method",
|
||||
),
|
||||
)
|
||||
}
|
||||
>
|
||||
<option value="RealTime">Real Time</option>
|
||||
<option value="GameTime">Game Time</option>
|
||||
</select>,
|
||||
);
|
||||
}
|
||||
|
||||
return <div className="settings-value-box optional-value">{children}</div>;
|
||||
}
|
||||
@@ -0,0 +1,645 @@
|
||||
import * as React from "react";
|
||||
import { SettingsDescriptionValueJson } from "../../../livesplit-core";
|
||||
import { assertNever, Option } from "../../../util/OptionUtil";
|
||||
import { HotkeyButton } from "../HotkeyButton";
|
||||
import { UrlCache } from "../../../util/UrlCache";
|
||||
import { LiveSplitServer } from "../../../api/LiveSplitServer";
|
||||
import { showDialog } from "../Dialog";
|
||||
import { Switch } from "../Switch";
|
||||
import { ServerConnectionButton } from "./ServerConnectionButton";
|
||||
import { LayoutBackground } from "./LayoutBackground";
|
||||
import { Font } from "./Font";
|
||||
|
||||
import "../../../css/Tooltip.scss";
|
||||
import { LayoutDirection } from "./LayoutDirection";
|
||||
import {
|
||||
ColumnKind,
|
||||
ColumnStartWith,
|
||||
ColumnUpdateWith,
|
||||
ColumnUpdateTrigger,
|
||||
} from "./Column";
|
||||
import { Alignment } from "./Alignment";
|
||||
import { OptionalTimingMethod } from "./TimingMethod";
|
||||
import { DeltaGradient, Gradient, ListGradient } from "./Gradient";
|
||||
import { Color, OptionalColor } from "./Color";
|
||||
import { DigitsFormat } from "./DigitsFormat";
|
||||
import { Accuracy } from "./Accuracy";
|
||||
import {
|
||||
Comparison,
|
||||
CustomVariable,
|
||||
OptionalString,
|
||||
RemovableString,
|
||||
String,
|
||||
} from "./String";
|
||||
|
||||
export interface Props<T> {
|
||||
context: string;
|
||||
setValue: (index: number, value: T) => void;
|
||||
state: ExtendedSettingsDescriptionJson;
|
||||
factory: SettingValueFactory<T>;
|
||||
editorUrlCache: UrlCache;
|
||||
allComparisons: string[];
|
||||
allVariables: Set<string>;
|
||||
}
|
||||
|
||||
export interface ExtendedSettingsDescriptionJson {
|
||||
fields: ExtendedSettingsDescriptionFieldJson[];
|
||||
}
|
||||
|
||||
export interface ExtendedSettingsDescriptionFieldJson {
|
||||
text: string | React.JSX.Element;
|
||||
tooltip: string | React.JSX.Element;
|
||||
value: ExtendedSettingsDescriptionValueJson;
|
||||
}
|
||||
|
||||
export type ExtendedSettingsDescriptionValueJson =
|
||||
| SettingsDescriptionValueJson
|
||||
| { RemovableString: string | null }
|
||||
| {
|
||||
ServerConnection: {
|
||||
url: string | undefined;
|
||||
connection: Option<LiveSplitServer>;
|
||||
};
|
||||
};
|
||||
|
||||
export interface SettingValueFactory<T> {
|
||||
fromBool(v: boolean): T;
|
||||
fromUint(value: number): T;
|
||||
fromInt(value: number): T;
|
||||
fromString(value: string): T;
|
||||
fromOptionalString(value: string): T;
|
||||
fromOptionalEmptyString(): T;
|
||||
fromRemovableString?(value: string): T;
|
||||
fromRemovableEmptyString?(): T;
|
||||
fromAccuracy(value: string): T | null;
|
||||
fromDigitsFormat(value: string): T | null;
|
||||
fromOptionalTimingMethod(value: string): T | null;
|
||||
fromOptionalEmptyTimingMethod(): T;
|
||||
fromColor(r: number, g: number, b: number, a: number): T;
|
||||
fromOptionalColor(r: number, g: number, b: number, a: number): T;
|
||||
fromOptionalEmptyColor(): T;
|
||||
fromTransparentGradient(): T;
|
||||
fromVerticalGradient(
|
||||
r1: number,
|
||||
g1: number,
|
||||
b1: number,
|
||||
a1: number,
|
||||
r2: number,
|
||||
g2: number,
|
||||
b2: number,
|
||||
a2: number,
|
||||
): T;
|
||||
fromHorizontalGradient(
|
||||
r1: number,
|
||||
g1: number,
|
||||
b1: number,
|
||||
a1: number,
|
||||
r2: number,
|
||||
g2: number,
|
||||
b2: number,
|
||||
a2: number,
|
||||
): T;
|
||||
fromAlternatingGradient(
|
||||
r1: number,
|
||||
g1: number,
|
||||
b1: number,
|
||||
a1: number,
|
||||
r2: number,
|
||||
g2: number,
|
||||
b2: number,
|
||||
a2: number,
|
||||
): T;
|
||||
fromAlignment(value: string): T | null;
|
||||
fromColumnKind(value: string): T | null;
|
||||
fromColumnStartWith(value: string): T | null;
|
||||
fromColumnUpdateWith(value: string): T | null;
|
||||
fromColumnUpdateTrigger(value: string): T | null;
|
||||
fromLayoutDirection(value: string): T | null;
|
||||
fromFont(
|
||||
name: string,
|
||||
style: string,
|
||||
weight: string,
|
||||
stretch: string,
|
||||
): T | null;
|
||||
fromEmptyFont(): T;
|
||||
fromDeltaGradient(value: string): T | null;
|
||||
fromBackgroundImage(
|
||||
imageId: string,
|
||||
brightness: number,
|
||||
opacity: number,
|
||||
blur: number,
|
||||
): T | null;
|
||||
}
|
||||
|
||||
export class JsonSettingValueFactory
|
||||
implements SettingValueFactory<ExtendedSettingsDescriptionValueJson>
|
||||
{
|
||||
public fromBool(v: boolean): ExtendedSettingsDescriptionValueJson {
|
||||
return { Bool: v };
|
||||
}
|
||||
public fromUint(_: number): ExtendedSettingsDescriptionValueJson {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
public fromInt(_: number): ExtendedSettingsDescriptionValueJson {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
public fromString(v: string): ExtendedSettingsDescriptionValueJson {
|
||||
return { String: v };
|
||||
}
|
||||
public fromOptionalString(_: string): ExtendedSettingsDescriptionValueJson {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
public fromOptionalEmptyString(): ExtendedSettingsDescriptionValueJson {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
public fromRemovableString(
|
||||
v: string,
|
||||
): ExtendedSettingsDescriptionValueJson {
|
||||
return { RemovableString: v };
|
||||
}
|
||||
public fromRemovableEmptyString(): ExtendedSettingsDescriptionValueJson {
|
||||
return { RemovableString: null };
|
||||
}
|
||||
public fromAccuracy(
|
||||
_: string,
|
||||
): ExtendedSettingsDescriptionValueJson | null {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
public fromDigitsFormat(
|
||||
_: string,
|
||||
): ExtendedSettingsDescriptionValueJson | null {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
public fromOptionalTimingMethod(
|
||||
_: string,
|
||||
): ExtendedSettingsDescriptionValueJson | null {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
public fromOptionalEmptyTimingMethod(): ExtendedSettingsDescriptionValueJson {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
public fromColor(): ExtendedSettingsDescriptionValueJson {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
public fromOptionalColor(): ExtendedSettingsDescriptionValueJson {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
public fromOptionalEmptyColor(): ExtendedSettingsDescriptionValueJson {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
public fromTransparentGradient(): ExtendedSettingsDescriptionValueJson {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
public fromVerticalGradient(): ExtendedSettingsDescriptionValueJson {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
public fromHorizontalGradient(): ExtendedSettingsDescriptionValueJson {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
public fromAlternatingGradient(): ExtendedSettingsDescriptionValueJson {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
public fromAlignment(
|
||||
_: string,
|
||||
): ExtendedSettingsDescriptionValueJson | null {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
public fromColumnKind(
|
||||
_: string,
|
||||
): ExtendedSettingsDescriptionValueJson | null {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
public fromColumnStartWith(
|
||||
_: string,
|
||||
): ExtendedSettingsDescriptionValueJson | null {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
public fromColumnUpdateWith(
|
||||
_: string,
|
||||
): ExtendedSettingsDescriptionValueJson | null {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
public fromColumnUpdateTrigger(
|
||||
_: string,
|
||||
): ExtendedSettingsDescriptionValueJson | null {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
public fromLayoutDirection(
|
||||
_: string,
|
||||
): ExtendedSettingsDescriptionValueJson | null {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
public fromFont(
|
||||
_name: string,
|
||||
_style: string,
|
||||
_weight: string,
|
||||
_stretch: string,
|
||||
): ExtendedSettingsDescriptionValueJson | null {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
public fromEmptyFont(): ExtendedSettingsDescriptionValueJson {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
public fromDeltaGradient(
|
||||
_: string,
|
||||
): ExtendedSettingsDescriptionValueJson | null {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
public fromBackgroundImage(
|
||||
_imageId: string,
|
||||
_brightness: number,
|
||||
_opacity: number,
|
||||
_blur: number,
|
||||
): ExtendedSettingsDescriptionValueJson | null {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
}
|
||||
|
||||
export class SettingsComponent<T> extends React.Component<Props<T>> {
|
||||
public render() {
|
||||
const settingsRows: React.JSX.Element[] = [];
|
||||
const { factory } = this.props;
|
||||
|
||||
this.props.state.fields.forEach((field, valueIndex) => {
|
||||
const { value } = field;
|
||||
let component;
|
||||
if ("Bool" in value) {
|
||||
component = (
|
||||
<div className="settings-value-box">
|
||||
<Switch
|
||||
checked={value.Bool}
|
||||
setIsChecked={(value) => {
|
||||
this.props.setValue(
|
||||
valueIndex,
|
||||
factory.fromBool(value),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else if ("UInt" in value) {
|
||||
component = (
|
||||
<div className="settings-value-box">
|
||||
<input
|
||||
type="number"
|
||||
className="number text-box"
|
||||
value={value.UInt}
|
||||
min="0"
|
||||
onChange={(e) => {
|
||||
this.props.setValue(
|
||||
valueIndex,
|
||||
factory.fromUint(e.target.valueAsNumber),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else if ("Int" in value) {
|
||||
component = (
|
||||
<div className="settings-value-box">
|
||||
<input
|
||||
type="number"
|
||||
className="number text-box"
|
||||
value={value.Int}
|
||||
onChange={(e) => {
|
||||
this.props.setValue(
|
||||
valueIndex,
|
||||
factory.fromInt(e.target.valueAsNumber),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else if ("String" in value) {
|
||||
// FIXME: This is a hack that we need for now until the way
|
||||
// settings are represented is refactored.
|
||||
if (
|
||||
typeof field.text === "string" &&
|
||||
/^Variable/.test(field.text)
|
||||
) {
|
||||
component = (
|
||||
<CustomVariable
|
||||
value={value.String}
|
||||
setValue={(value) =>
|
||||
this.props.setValue(valueIndex, value)
|
||||
}
|
||||
factory={this.props.factory}
|
||||
allVariables={this.props.allVariables}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
component = (
|
||||
<String
|
||||
value={value.String}
|
||||
setValue={(value) =>
|
||||
this.props.setValue(valueIndex, value)
|
||||
}
|
||||
factory={this.props.factory}
|
||||
/>
|
||||
);
|
||||
}
|
||||
} else if ("OptionalString" in value) {
|
||||
// FIXME: This is a hack that we need for now until the way
|
||||
// settings are represented is refactored.
|
||||
if (
|
||||
typeof field.text === "string" &&
|
||||
/^Comparison( \d)?$/.test(field.text)
|
||||
) {
|
||||
component = (
|
||||
<Comparison
|
||||
allComparisons={this.props.allComparisons}
|
||||
value={value.OptionalString}
|
||||
setValue={(value) =>
|
||||
this.props.setValue(valueIndex, value)
|
||||
}
|
||||
factory={this.props.factory}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
component = (
|
||||
<OptionalString
|
||||
value={value.OptionalString}
|
||||
setValue={(value) =>
|
||||
this.props.setValue(valueIndex, value)
|
||||
}
|
||||
factory={this.props.factory}
|
||||
/>
|
||||
);
|
||||
}
|
||||
} else if ("RemovableString" in value) {
|
||||
component = (
|
||||
<RemovableString
|
||||
value={value.RemovableString}
|
||||
setValue={(value) =>
|
||||
this.props.setValue(valueIndex, value)
|
||||
}
|
||||
factory={this.props.factory}
|
||||
/>
|
||||
);
|
||||
} else if ("Accuracy" in value) {
|
||||
component = (
|
||||
<Accuracy
|
||||
value={value.Accuracy}
|
||||
setValue={(value) =>
|
||||
this.props.setValue(valueIndex, value)
|
||||
}
|
||||
factory={this.props.factory}
|
||||
/>
|
||||
);
|
||||
} else if ("DigitsFormat" in value) {
|
||||
component = (
|
||||
<DigitsFormat
|
||||
value={value.DigitsFormat}
|
||||
setValue={(value) =>
|
||||
this.props.setValue(valueIndex, value)
|
||||
}
|
||||
factory={this.props.factory}
|
||||
/>
|
||||
);
|
||||
} else if ("Color" in value) {
|
||||
component = (
|
||||
<Color
|
||||
value={value.Color}
|
||||
setValue={(value) =>
|
||||
this.props.setValue(valueIndex, value)
|
||||
}
|
||||
factory={this.props.factory}
|
||||
/>
|
||||
);
|
||||
} else if ("OptionalColor" in value) {
|
||||
component = (
|
||||
<OptionalColor
|
||||
value={value.OptionalColor}
|
||||
setValue={(value) =>
|
||||
this.props.setValue(valueIndex, value)
|
||||
}
|
||||
factory={this.props.factory}
|
||||
/>
|
||||
);
|
||||
} else if ("Gradient" in value) {
|
||||
component = (
|
||||
<Gradient
|
||||
value={value.Gradient}
|
||||
setValue={(value) =>
|
||||
this.props.setValue(valueIndex, value)
|
||||
}
|
||||
factory={this.props.factory}
|
||||
/>
|
||||
);
|
||||
} else if ("ListGradient" in value) {
|
||||
component = (
|
||||
<ListGradient
|
||||
value={value.ListGradient}
|
||||
setValue={(value) =>
|
||||
this.props.setValue(valueIndex, value)
|
||||
}
|
||||
factory={this.props.factory}
|
||||
/>
|
||||
);
|
||||
} else if ("OptionalTimingMethod" in value) {
|
||||
component = (
|
||||
<OptionalTimingMethod
|
||||
value={value.OptionalTimingMethod}
|
||||
setValue={(value) =>
|
||||
this.props.setValue(valueIndex, value)
|
||||
}
|
||||
factory={this.props.factory}
|
||||
/>
|
||||
);
|
||||
} else if ("Alignment" in value) {
|
||||
component = (
|
||||
<Alignment
|
||||
value={value.Alignment}
|
||||
setValue={(value) =>
|
||||
this.props.setValue(valueIndex, value)
|
||||
}
|
||||
factory={this.props.factory}
|
||||
/>
|
||||
);
|
||||
} else if ("ColumnKind" in value) {
|
||||
component = (
|
||||
<ColumnKind
|
||||
value={value.ColumnKind}
|
||||
setValue={(value) =>
|
||||
this.props.setValue(valueIndex, value)
|
||||
}
|
||||
factory={this.props.factory}
|
||||
/>
|
||||
);
|
||||
} else if ("ColumnStartWith" in value) {
|
||||
component = (
|
||||
<ColumnStartWith
|
||||
value={value.ColumnStartWith}
|
||||
setValue={(value) =>
|
||||
this.props.setValue(valueIndex, value)
|
||||
}
|
||||
factory={this.props.factory}
|
||||
/>
|
||||
);
|
||||
} else if ("ColumnUpdateWith" in value) {
|
||||
component = (
|
||||
<ColumnUpdateWith
|
||||
value={value.ColumnUpdateWith}
|
||||
setValue={(value) =>
|
||||
this.props.setValue(valueIndex, value)
|
||||
}
|
||||
factory={this.props.factory}
|
||||
/>
|
||||
);
|
||||
} else if ("ColumnUpdateTrigger" in value) {
|
||||
component = (
|
||||
<ColumnUpdateTrigger
|
||||
value={value.ColumnUpdateTrigger}
|
||||
setValue={(value) =>
|
||||
this.props.setValue(valueIndex, value)
|
||||
}
|
||||
factory={this.props.factory}
|
||||
/>
|
||||
);
|
||||
} else if ("CustomCombobox" in value) {
|
||||
const isError =
|
||||
value.CustomCombobox.mandatory &&
|
||||
!value.CustomCombobox.value;
|
||||
component = (
|
||||
<div className="settings-value-box">
|
||||
<select
|
||||
value={value.CustomCombobox.value}
|
||||
onChange={(e) => {
|
||||
this.props.setValue(
|
||||
valueIndex,
|
||||
factory.fromString(e.target.value),
|
||||
);
|
||||
}}
|
||||
style={{
|
||||
border: isError
|
||||
? "1px solid rgb(255, 0, 0)"
|
||||
: undefined,
|
||||
}}
|
||||
>
|
||||
{value.CustomCombobox.list.map((v) => (
|
||||
<option value={v}>{v}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
} else if ("Hotkey" in value) {
|
||||
component = (
|
||||
<div className="settings-value-box">
|
||||
<HotkeyButton
|
||||
value={value.Hotkey}
|
||||
setValue={(value) => {
|
||||
if (value != null) {
|
||||
this.props.setValue(
|
||||
valueIndex,
|
||||
factory.fromOptionalString(value),
|
||||
);
|
||||
} else {
|
||||
this.props.setValue(
|
||||
valueIndex,
|
||||
factory.fromOptionalEmptyString(),
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else if ("LayoutDirection" in value) {
|
||||
component = (
|
||||
<LayoutDirection
|
||||
value={value.LayoutDirection}
|
||||
setValue={(value) =>
|
||||
this.props.setValue(valueIndex, value)
|
||||
}
|
||||
factory={this.props.factory}
|
||||
/>
|
||||
);
|
||||
} else if ("Font" in value) {
|
||||
component = (
|
||||
<Font
|
||||
value={value.Font}
|
||||
setValue={(value) =>
|
||||
this.props.setValue(valueIndex, value)
|
||||
}
|
||||
factory={this.props.factory}
|
||||
loadedCallback={() => this.setState({})}
|
||||
/>
|
||||
);
|
||||
} else if ("DeltaGradient" in value) {
|
||||
component = (
|
||||
<DeltaGradient
|
||||
value={value.DeltaGradient}
|
||||
setValue={(value) =>
|
||||
this.props.setValue(valueIndex, value)
|
||||
}
|
||||
factory={this.props.factory}
|
||||
/>
|
||||
);
|
||||
} else if ("LayoutBackground" in value) {
|
||||
component = (
|
||||
<LayoutBackground
|
||||
value={value.LayoutBackground}
|
||||
setValue={(value) =>
|
||||
this.props.setValue(valueIndex, value)
|
||||
}
|
||||
factory={this.props.factory}
|
||||
editorUrlCache={this.props.editorUrlCache}
|
||||
/>
|
||||
);
|
||||
} else if ("ServerConnection" in value) {
|
||||
component = (
|
||||
<ServerConnectionButton
|
||||
value={value.ServerConnection}
|
||||
connectOrDisconnect={() =>
|
||||
this.connectToServerOrDisconnect(
|
||||
valueIndex,
|
||||
value.ServerConnection.url,
|
||||
value.ServerConnection.connection,
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
assertNever(value);
|
||||
}
|
||||
|
||||
settingsRows.push(
|
||||
<tr key={`${this.props.context}$${valueIndex}`}>
|
||||
<td className="tooltip">
|
||||
{field.text}
|
||||
<span className="tooltip-text">{field.tooltip}</span>
|
||||
</td>
|
||||
<td>{component}</td>
|
||||
</tr>,
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<table className="table settings-table">
|
||||
<tbody className="table-body">{settingsRows}</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
// FIXME: Move to the component if possible.
|
||||
private async connectToServerOrDisconnect(
|
||||
valueIndex: number,
|
||||
serverUrl: string | undefined,
|
||||
connection: Option<LiveSplitServer>,
|
||||
) {
|
||||
if (connection) {
|
||||
connection.close();
|
||||
return;
|
||||
}
|
||||
const [result, url] = await showDialog({
|
||||
title: "Connect to Server",
|
||||
description: "Specify the WebSocket URL:",
|
||||
textInput: true,
|
||||
defaultText: serverUrl,
|
||||
buttons: ["Connect", "Cancel"],
|
||||
});
|
||||
if (result !== 0) {
|
||||
return;
|
||||
}
|
||||
this.props.setValue(valueIndex, this.props.factory.fromString(url));
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import React from "react";
|
||||
|
||||
import * as classes from "../../css/Switch.module.scss";
|
||||
|
||||
export default function Switch({
|
||||
export function Switch({
|
||||
checked,
|
||||
setIsChecked,
|
||||
}: {
|
||||
|
||||
@@ -1,53 +1,56 @@
|
||||
import * as React from "react";
|
||||
|
||||
export interface Props {
|
||||
import * as classes from "../../css/TextBox.module.scss";
|
||||
|
||||
export function TextBox({
|
||||
className,
|
||||
value,
|
||||
onChange,
|
||||
onBlur,
|
||||
label,
|
||||
invalid,
|
||||
list,
|
||||
}: {
|
||||
className?: string;
|
||||
value?: any;
|
||||
onChange?: React.EventHandler<React.ChangeEvent<HTMLInputElement>>;
|
||||
onBlur?: React.EventHandler<React.FocusEvent<HTMLInputElement>>;
|
||||
value?: string | number | readonly string[];
|
||||
onChange?: React.ChangeEventHandler<HTMLInputElement>;
|
||||
onBlur?: React.FocusEventHandler<HTMLInputElement>;
|
||||
label: string;
|
||||
invalid?: boolean;
|
||||
small?: boolean;
|
||||
list?: [string, string[]];
|
||||
}
|
||||
}) {
|
||||
let outerClassName = classes.group;
|
||||
if (invalid) {
|
||||
outerClassName += ` ${classes.invalid}`;
|
||||
}
|
||||
|
||||
export class TextBox extends React.Component<Props> {
|
||||
public render() {
|
||||
let className = "group";
|
||||
if (this.props.invalid) {
|
||||
className += " invalid";
|
||||
}
|
||||
if (this.props.small) {
|
||||
className += " small";
|
||||
}
|
||||
let name;
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<input
|
||||
list={name}
|
||||
type="text text-box"
|
||||
required
|
||||
className={this.props.className}
|
||||
value={this.props.value}
|
||||
onChange={this.props.onChange}
|
||||
onBlur={this.props.onBlur}
|
||||
/>
|
||||
{list}
|
||||
<span className="bar"></span>
|
||||
<label>{this.props.label}</label>
|
||||
</div>
|
||||
let name;
|
||||
let listElement;
|
||||
if (list !== undefined) {
|
||||
name = list[0];
|
||||
listElement = (
|
||||
<datalist id={name}>
|
||||
{list[1].map((n, i) => (
|
||||
<option key={i} value={n} />
|
||||
))}
|
||||
</datalist>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={outerClassName}>
|
||||
<input
|
||||
list={name}
|
||||
type="text text-box"
|
||||
required
|
||||
className={className}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
/>
|
||||
{listElement}
|
||||
<span className={classes.bar}></span>
|
||||
<label>{label}</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+79
-89
@@ -5,11 +5,7 @@ import { Markdown } from "../../util/Markdown";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
|
||||
import * as variables from "../../css/variables.icss.scss";
|
||||
import "../../css/About.scss";
|
||||
|
||||
export interface Props {
|
||||
callbacks: Callbacks;
|
||||
}
|
||||
import * as classes from "../../css/About.module.scss";
|
||||
|
||||
interface Callbacks {
|
||||
renderViewWithSidebar(
|
||||
@@ -21,96 +17,90 @@ interface Callbacks {
|
||||
|
||||
const contributorAvatarSize = parseFloat(variables.contributorAvatarSize);
|
||||
|
||||
export class About extends React.Component<Props> {
|
||||
public render() {
|
||||
const renderedView = this.renderView();
|
||||
const sidebarContent = this.renderSidebarContent();
|
||||
return this.props.callbacks.renderViewWithSidebar(
|
||||
renderedView,
|
||||
sidebarContent,
|
||||
);
|
||||
}
|
||||
export function About({ callbacks }: { callbacks: Callbacks }) {
|
||||
const renderedView = renderView();
|
||||
const sidebarContent = renderSidebarContent(callbacks);
|
||||
return callbacks.renderViewWithSidebar(renderedView, sidebarContent);
|
||||
}
|
||||
|
||||
private renderView() {
|
||||
const idealAvatarResolution = Math.round(
|
||||
devicePixelRatio * contributorAvatarSize,
|
||||
);
|
||||
function renderView() {
|
||||
const idealAvatarResolution = Math.round(
|
||||
devicePixelRatio * contributorAvatarSize,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="about">
|
||||
<div className="about-inner-container">
|
||||
<div className="livesplit-title">
|
||||
<span className="livesplit-icon">
|
||||
<img src={LiveSplitIcon} alt="LiveSplit Logo" />
|
||||
</span>
|
||||
<div className="title-text">LiveSplit One</div>
|
||||
</div>
|
||||
<p className="build-version">
|
||||
<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"
|
||||
>
|
||||
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}
|
||||
/>
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
<h2>Contributors</h2>
|
||||
<div className="contributors">
|
||||
{CONTRIBUTORS_LIST.map((contributor) => (
|
||||
return (
|
||||
<div className={classes.about}>
|
||||
<div className={classes.aboutInnerContainer}>
|
||||
<div className={classes.livesplitTitle}>
|
||||
<img
|
||||
className={classes.livesplitIcon}
|
||||
src={LiveSplitIcon}
|
||||
alt="LiveSplit Logo"
|
||||
/>
|
||||
<div className={classes.titleText}>LiveSplit One</div>
|
||||
</div>
|
||||
<p className={classes.buildVersion}>
|
||||
<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"
|
||||
>
|
||||
View Source Code on GitHub
|
||||
</a>
|
||||
</p>
|
||||
<h2>Recent Changes</h2>
|
||||
<div className={classes.changelog}>
|
||||
{CHANGELOG.map((change) => (
|
||||
<>
|
||||
<a
|
||||
href={`https://github.com/${contributor.name}`}
|
||||
href={`https://github.com/LiveSplit/LiveSplitOne/commit/${change.id}`}
|
||||
target="_blank"
|
||||
>
|
||||
<img
|
||||
src={`https://avatars.githubusercontent.com/u/${contributor.id}?s=${idealAvatarResolution}&v=4`}
|
||||
onError={(e) => (e.target as any).remove()}
|
||||
/>
|
||||
{contributor.name}
|
||||
{change.date}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
<Markdown markdown={change.message} unsafe={true} />
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
<h2>Contributors</h2>
|
||||
<div className={classes.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>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private renderSidebarContent() {
|
||||
return (
|
||||
<div className="sidebar-buttons">
|
||||
<h1>About</h1>
|
||||
<hr />
|
||||
<button onClick={(_) => this.props.callbacks.openTimerView()}>
|
||||
<ArrowLeft strokeWidth={2.5} /> Back
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderSidebarContent(callbacks: Callbacks) {
|
||||
return (
|
||||
<div className="sidebar-buttons">
|
||||
<h1>About</h1>
|
||||
<hr />
|
||||
<button onClick={(_) => callbacks.openTimerView()}>
|
||||
<ArrowLeft strokeWidth={2.5} /> Back
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -212,7 +212,6 @@ export class RunEditor extends React.Component<Props, State> {
|
||||
value={this.state.editor.offset}
|
||||
onChange={(e) => this.handleOffsetChange(e)}
|
||||
onBlur={(_) => this.handleOffsetBlur()}
|
||||
small
|
||||
invalid={!this.state.offsetIsValid}
|
||||
label="Start Timer At"
|
||||
/>
|
||||
@@ -225,7 +224,6 @@ export class RunEditor extends React.Component<Props, State> {
|
||||
this.handleAttemptsChange(e)
|
||||
}
|
||||
onBlur={(_) => this.handleAttemptsBlur()}
|
||||
small
|
||||
invalid={!this.state.attemptCountIsValid}
|
||||
label="Attempts"
|
||||
/>
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
FILE_EXT_SPLITS,
|
||||
} from "../../util/FileUtil";
|
||||
import { Option, bug, maybeDisposeAndThen } from "../../util/OptionUtil";
|
||||
import DragUpload from "../components/DragUpload";
|
||||
import { DragUpload } from "../components/DragUpload";
|
||||
import { GeneralSettings } from "./MainSettings";
|
||||
import { LSOCommandSink } from "../LSOCommandSink";
|
||||
import { showDialog } from "../components/Dialog";
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
} from "../../livesplit-core";
|
||||
import * as LiveSplit from "../../livesplit-core";
|
||||
import { Option, expect } from "../../util/OptionUtil";
|
||||
import DragUpload from "../components/DragUpload";
|
||||
import { DragUpload } from "../components/DragUpload";
|
||||
import Layout from "../components/Layout";
|
||||
import { UrlCache } from "../../util/UrlCache";
|
||||
import { WebRenderer } from "../../livesplit-core/livesplit_core";
|
||||
@@ -119,7 +119,7 @@ export class TimerView extends React.Component<Props, State> {
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
display: "inline-block",
|
||||
width: "fit-content",
|
||||
cursor: this.props.generalSettings
|
||||
.showControlButtons
|
||||
? "pointer"
|
||||
|
||||
+40
-32
@@ -5,55 +5,63 @@ import {
|
||||
batteryAwareFrameRate,
|
||||
} from "./FrameRate";
|
||||
|
||||
export interface Props {
|
||||
interface State {
|
||||
reqId?: number;
|
||||
previousTime: number;
|
||||
frameRate: FrameRateSetting;
|
||||
update(): void;
|
||||
children: React.ReactNode;
|
||||
update: () => void;
|
||||
}
|
||||
|
||||
export default class AutoRefresh extends React.Component<Props> {
|
||||
private reqId?: number;
|
||||
private previousTime: number = 0;
|
||||
export default function AutoRefresh({
|
||||
frameRate,
|
||||
update,
|
||||
children,
|
||||
}: {
|
||||
frameRate: FrameRateSetting;
|
||||
update: () => void;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const { current: state } = React.useRef<State>({
|
||||
previousTime: 0,
|
||||
frameRate,
|
||||
update,
|
||||
});
|
||||
state.frameRate = frameRate;
|
||||
state.update = update;
|
||||
|
||||
public componentDidMount() {
|
||||
this.startAnimation();
|
||||
}
|
||||
const animate = React.useCallback(() => {
|
||||
state.reqId = requestAnimationFrame(animate);
|
||||
|
||||
public componentWillUnmount() {
|
||||
if (this.reqId) {
|
||||
cancelAnimationFrame(this.reqId);
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
return this.props.children;
|
||||
}
|
||||
|
||||
private startAnimation() {
|
||||
this.previousTime = 0;
|
||||
this.animate();
|
||||
}
|
||||
|
||||
private animate() {
|
||||
this.reqId = requestAnimationFrame(() => this.animate());
|
||||
|
||||
let frameRate = this.props.frameRate;
|
||||
let frameRate = state.frameRate;
|
||||
if (frameRate === FRAME_RATE_AUTOMATIC) {
|
||||
frameRate = batteryAwareFrameRate;
|
||||
}
|
||||
|
||||
if (typeof frameRate === "number") {
|
||||
const currentTime = performance.now();
|
||||
const elapsed = currentTime - this.previousTime;
|
||||
const elapsed = currentTime - state.previousTime;
|
||||
|
||||
const refreshInterval = 1000 / frameRate;
|
||||
|
||||
if (elapsed < refreshInterval) {
|
||||
return;
|
||||
}
|
||||
this.previousTime = currentTime - (elapsed % refreshInterval);
|
||||
state.previousTime = currentTime - (elapsed % refreshInterval);
|
||||
}
|
||||
|
||||
this.props.update();
|
||||
}
|
||||
state.update();
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
state.previousTime = 0;
|
||||
animate();
|
||||
|
||||
return () => {
|
||||
if (state.reqId) {
|
||||
cancelAnimationFrame(state.reqId);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from "react";
|
||||
import * as React from "react";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
export type Option<T> = T | null | undefined;
|
||||
|
||||
Reference in New Issue
Block a user