diff --git a/src/css/Loading.scss b/src/css/Loading.scss new file mode 100644 index 00000000..a7b72f2c --- /dev/null +++ b/src/css/Loading.scss @@ -0,0 +1,15 @@ +@import 'variables'; + +$loading-text-font-size: 45px; + +@mixin loading { + .loading { + display: flex; + width: fit-content; + font-size: $loading-text-font-size; + + .loading-text { + margin-left: $ui-margin; + } + } +} diff --git a/src/css/RunEditor.scss b/src/css/RunEditor.scss index 7a9d4ec1..35be7f5f 100644 --- a/src/css/RunEditor.scss +++ b/src/css/RunEditor.scss @@ -1,6 +1,7 @@ @use 'sass:math'; @import "ContextMenu"; +@import "Loading"; @import "Markdown"; @import "mobile"; @import "Table"; @@ -19,6 +20,7 @@ $small-button-padding: 1px 3px 1px 3px; .run-editor { @include context-menu; + @include loading; @include markdown; @include table; @include toggle; diff --git a/src/css/SplitsSelection.scss b/src/css/SplitsSelection.scss index b69649fa..4b313c2c 100644 --- a/src/css/SplitsSelection.scss +++ b/src/css/SplitsSelection.scss @@ -2,23 +2,15 @@ @import 'mobile'; @import 'variables'; +@import 'Loading'; @import 'Table'; @import 'ContextMenu'; -$loading-text-font-size: 40px; $splits-row-width: 500px; $splits-row-height: 40px; .splits-selection { - .loading { - display: flex; - width: fit-content; - font-size: $loading-text-font-size; - - .loading-text { - margin-left: $ui-margin; - } - } + @include loading; .splits-selection-container { display: flex; diff --git a/src/css/main.scss b/src/css/main.scss index e1c00716..93e446b6 100644 --- a/src/css/main.scss +++ b/src/css/main.scss @@ -2,6 +2,7 @@ @import 'browser_source'; @import 'variables'; +@import 'Loading'; @font-face { font-family: timer; @@ -147,9 +148,6 @@ button:disabled:active { display: flex; justify-content: center; align-items: center; - font-size: 50px; - .initial-load-text { - margin-left: 10px; - } + @include loading; } diff --git a/src/index.html b/src/index.html index 99f1247b..d38e1ac0 100644 --- a/src/index.html +++ b/src/index.html @@ -11,8 +11,10 @@
-
-
Loading...
+
+
+
Loading...
+
diff --git a/src/ui/LiveSplit.tsx b/src/ui/LiveSplit.tsx index 8eec519e..ae9dc4de 100644 --- a/src/ui/LiveSplit.tsx +++ b/src/ui/LiveSplit.tsx @@ -55,7 +55,7 @@ function isMenuLocked(menuKind: MenuKind) { type Menu = { kind: MenuKind.Timer } | { kind: MenuKind.Splits } | - { kind: MenuKind.RunEditor, editor: RunEditor, splitsKey?: number } | + { kind: MenuKind.RunEditor, editor: RunEditor, splitsKey?: number, persistChanges: boolean } | { kind: MenuKind.Layout } | { kind: MenuKind.LayoutEditor, editor: LayoutEditor } | { kind: MenuKind.SettingsEditor, config: HotkeyConfig } | @@ -576,28 +576,49 @@ export class LiveSplit extends React.Component { this.setLayout(layout); } - public openRunEditor({ splitsKey, run }: EditingInfo) { + public openRunEditor({ splitsKey, persistChanges, run }: EditingInfo) { const editor = expect( RunEditor.new(run), "The Run Editor should always be able to be opened.", ); - this.changeMenu({ kind: MenuKind.RunEditor, editor, splitsKey }); + this.changeMenu( + { kind: MenuKind.RunEditor, editor, splitsKey, persistChanges }, + ); + } + + public replaceRunEditor(editor: RunEditor) { + const { menu } = this.state; + + if (menu.kind === MenuKind.RunEditor) { + const { editor: oldEditor } = menu; + oldEditor[Symbol.dispose](); + } + + this.setState({ + menu: { + kind: MenuKind.RunEditor, + editor, + splitsKey: undefined, + persistChanges: true, + }, + sidebarOpen: false, + }); } public closeRunEditor(save: boolean) { if (this.state.menu.kind !== MenuKind.RunEditor) { panic("No Run Editor to close"); } - const { editor, splitsKey } = this.state.menu; + const { editor, persistChanges, splitsKey } = this.state.menu; const run = editor.close(); if (save) { - if (splitsKey == null) { + if (persistChanges) { + Storage.storeRunAndDispose(run, splitsKey); + } else { assertNull( this.state.eventSink.setRun(run), "The Run Editor should always return a valid Run.", ); - } else { - Storage.storeRunAndDispose(run, splitsKey); } } else { run[Symbol.dispose](); diff --git a/src/ui/RunEditor.tsx b/src/ui/RunEditor.tsx index dfade43e..ddb85a1e 100644 --- a/src/ui/RunEditor.tsx +++ b/src/ui/RunEditor.tsx @@ -37,6 +37,7 @@ export interface Props { } export interface State { editor: LiveSplit.RunEditorStateJson, + isLoading: boolean, foundGames: string[], offsetIsValid: boolean, attemptCountIsValid: boolean, @@ -47,6 +48,7 @@ export interface State { interface Callbacks { renderViewWithSidebar(renderedView: JSX.Element, sidebarContent: JSX.Element): JSX.Element, + replaceRunEditor(editor: LiveSplit.RunEditor): void, closeRunEditor(save: boolean): void, } @@ -93,6 +95,7 @@ export class RunEditor extends React.Component { this.state = { attemptCountIsValid: true, editor: state, + isLoading: false, foundGames, offsetIsValid: true, rowState: { @@ -127,6 +130,17 @@ export class RunEditor extends React.Component { } private renderView() { + if (this.state.isLoading) { + return ( +
+
+
+
Loading...
+
+
+ ); + } + const gameIcon = this.getGameIcon(); let gameIconContextTrigger: any = null; @@ -2118,6 +2132,18 @@ export class RunEditor extends React.Component { } private async downloadSplits(apiRun: Run, apiUri: string) { + // FIXME: Determine whether there are any unsaved changes in the Run Editor + if (!confirm( + "Are you sure you want to load these splits? Any unsaved changes will be lost.", + )) { + return; + } + + this.setState({ + ...this.state, + isLoading: true, + }); + const baseUri = "https://splits.io/api/v3/runs/"; assert(apiUri.startsWith(baseUri), "Unexpected Splits.io URL"); const splitsId = apiUri.slice(baseUri.length); @@ -2129,10 +2155,16 @@ export class RunEditor extends React.Component { const platformListDownload = downloadPlatformList(); const regionListDownload = downloadRegionList(); const gameInfoDownload = downloadGameInfo(gameName); + await gameInfoDownload; await platformListDownload; await regionListDownload; using run = await runDownload; + + if (this.props.editor.ptr === 0) { + // Old editor is already disposed, so do not continue + return; + } const newEditor = LiveSplit.RunEditor.new(run); if (newEditor !== null) { associateRun( @@ -2142,11 +2174,7 @@ export class RunEditor extends React.Component { apiRun, ); - // TODO Oh no, not internal pointer stuff - this.props.editor[Symbol.dispose](); - this.props.editor.ptr = newEditor.ptr; - - this.update(); + this.props.callbacks.replaceRunEditor(newEditor); } else { toast.error("The downloaded splits are not suitable for being edited."); } @@ -2156,6 +2184,12 @@ export class RunEditor extends React.Component { } toast.error("Failed to download the splits."); } + + this.setState({ + ...this.state, + isLoading: false, + }); + this.maybeUpdate({ switchTab: Tab.RealTime }); } private toggleExpandLeaderboardRow(rowIndex: number) { diff --git a/src/ui/SplitsSelection.tsx b/src/ui/SplitsSelection.tsx index 5b1b6f76..7f625e54 100644 --- a/src/ui/SplitsSelection.tsx +++ b/src/ui/SplitsSelection.tsx @@ -17,7 +17,8 @@ import { showDialog } from "./Dialog"; import "../css/SplitsSelection.scss"; export interface EditingInfo { - splitsKey?: number, + splitsKey: number | undefined, + persistChanges: boolean, run: Run, } @@ -191,7 +192,11 @@ export class SplitsSelection extends React.Component { return; } const run = this.props.eventSink.getRun().clone(); - this.props.callbacks.openRunEditor({ run }); + this.props.callbacks.openRunEditor({ + run, + splitsKey: this.props.openedSplitsKey, + persistChanges: false, + }); }}>