mirror of
https://github.com/ApfelTeeSaft/LiveSplitOne.git
synced 2026-08-26 19:23:40 +00:00
Add indicators for unsaved changes (#912)
This adds indicators to various parts of the UI to indicate that there are unsaved changes. The indicators are mainly shown as white dots in the sidebar highlighting what is unsaved. The title is also updated to reflect this. The Badging API is also used to show a badge on the "app icon" if it's installed as a PWA. In addition to the splits, the layouts are also now properly tracked when it comes to unsaved changes. Changelog: There are now indicators in the user interface that remind you about unsaved changes to your splits and layouts.
This commit is contained in:
@@ -67,6 +67,12 @@ $h2-font-size: 22px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modified-icon {
|
||||
position: absolute;
|
||||
font-size: 10px;
|
||||
padding-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.choose-comparison {
|
||||
|
||||
@@ -11,6 +11,7 @@ export class LSOEventSink {
|
||||
private currentPhaseChanged: () => void,
|
||||
private currentSplitChanged: () => void,
|
||||
private comparisonListChanged: () => void,
|
||||
private splitsModifiedChanged: () => void,
|
||||
private onReset: () => void,
|
||||
) {
|
||||
this.eventSink = new EventSink(new WebEventSink(this).intoGeneric());
|
||||
@@ -30,6 +31,7 @@ export class LSOEventSink {
|
||||
|
||||
this.currentPhaseChanged();
|
||||
this.currentSplitChanged();
|
||||
this.splitsModifiedChanged();
|
||||
}
|
||||
|
||||
public split(): void {
|
||||
@@ -44,6 +46,7 @@ export class LSOEventSink {
|
||||
|
||||
this.currentPhaseChanged();
|
||||
this.currentSplitChanged();
|
||||
this.splitsModifiedChanged();
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
@@ -167,6 +170,7 @@ export class LSOEventSink {
|
||||
this.currentPhaseChanged();
|
||||
this.currentSplitChanged();
|
||||
this.comparisonListChanged();
|
||||
this.splitsModifiedChanged();
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -177,6 +181,7 @@ export class LSOEventSink {
|
||||
|
||||
public markAsUnmodified(): void {
|
||||
this.timer.markAsUnmodified();
|
||||
this.splitsModifiedChanged();
|
||||
}
|
||||
|
||||
public getRun(): RunRef {
|
||||
|
||||
@@ -27,6 +27,8 @@ export interface Props {
|
||||
currentPhase: TimerPhase,
|
||||
currentSplitIndex: number,
|
||||
allComparisons: string[],
|
||||
splitsModified: boolean,
|
||||
layoutModified: boolean,
|
||||
}
|
||||
|
||||
interface Callbacks {
|
||||
@@ -69,6 +71,8 @@ export class LayoutView extends React.Component<Props> {
|
||||
currentPhase={this.props.currentPhase}
|
||||
currentSplitIndex={this.props.currentSplitIndex}
|
||||
allComparisons={this.props.allComparisons}
|
||||
splitsModified={this.props.splitsModified}
|
||||
layoutModified={this.props.layoutModified}
|
||||
/>;
|
||||
const sidebarContent = this.renderSidebarContent();
|
||||
return this.props.callbacks.renderViewWithSidebar(renderedView, sidebarContent);
|
||||
@@ -84,6 +88,10 @@ export class LayoutView extends React.Component<Props> {
|
||||
</button>
|
||||
<button onClick={(_) => this.props.callbacks.saveLayout()}>
|
||||
<i className="fa fa-save" aria-hidden="true" /> Save
|
||||
{
|
||||
this.props.layoutModified &&
|
||||
<i className="fa fa-circle modified-icon" aria-hidden="true" />
|
||||
}
|
||||
</button>
|
||||
<button onClick={(_) => this.props.callbacks.importLayout()}>
|
||||
<i className="fa fa-download" aria-hidden="true" /> Import
|
||||
|
||||
+51
-16
@@ -85,6 +85,8 @@ export interface State {
|
||||
currentPhase: TimerPhase,
|
||||
currentSplitIndex: number,
|
||||
allComparisons: string[],
|
||||
splitsModified: boolean,
|
||||
layoutModified: boolean,
|
||||
}
|
||||
|
||||
export let hotkeySystem: Option<HotkeySystem> = null;
|
||||
@@ -131,6 +133,7 @@ export class LiveSplit extends React.Component<Props, State> {
|
||||
() => this.currentPhaseChanged(),
|
||||
() => this.currentSplitChanged(),
|
||||
() => this.comparisonsListChanged(),
|
||||
() => this.splitsModifiedChanged(),
|
||||
() => this.onReset(),
|
||||
);
|
||||
|
||||
@@ -211,6 +214,8 @@ export class LiveSplit extends React.Component<Props, State> {
|
||||
currentPhase: eventSink.currentPhase(),
|
||||
currentSplitIndex: eventSink.currentSplitIndex(),
|
||||
allComparisons: eventSink.getAllComparisons(),
|
||||
splitsModified: eventSink.hasBeenModified(),
|
||||
layoutModified: false,
|
||||
};
|
||||
|
||||
this.mediaQueryChanged = this.mediaQueryChanged.bind(this);
|
||||
@@ -234,13 +239,12 @@ export class LiveSplit extends React.Component<Props, State> {
|
||||
this.resizeEvent = { handleEvent: () => this.handleAutomaticResize() };
|
||||
window.addEventListener("resize", this.resizeEvent, false);
|
||||
|
||||
window.onbeforeunload = (e: BeforeUnloadEvent) => {
|
||||
const hasBeenModified = this.state.eventSink.hasBeenModified();
|
||||
if (hasBeenModified) {
|
||||
e.returnValue = "There are unsaved changes. Do you really want to close LiveSplit One?";
|
||||
return e.returnValue;
|
||||
window.onbeforeunload = () => {
|
||||
if (this.state.splitsModified || this.state.layoutModified) {
|
||||
return "There are unsaved changes. Do you really want to close LiveSplit One?";
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// This is bound in the constructor
|
||||
@@ -333,6 +337,7 @@ export class LiveSplit extends React.Component<Props, State> {
|
||||
eventSink={this.state.eventSink}
|
||||
openedSplitsKey={this.state.openedSplitsKey}
|
||||
callbacks={this}
|
||||
splitsModified={this.state.splitsModified}
|
||||
/>;
|
||||
} else if (this.state.menu.kind === MenuKind.Timer) {
|
||||
return <TimerView
|
||||
@@ -354,6 +359,8 @@ export class LiveSplit extends React.Component<Props, State> {
|
||||
currentPhase={this.state.currentPhase}
|
||||
currentSplitIndex={this.state.currentSplitIndex}
|
||||
allComparisons={this.state.allComparisons}
|
||||
splitsModified={this.state.splitsModified}
|
||||
layoutModified={this.state.layoutModified}
|
||||
/>;
|
||||
} else if (this.state.menu.kind === MenuKind.Layout) {
|
||||
return <LayoutView
|
||||
@@ -375,6 +382,8 @@ export class LiveSplit extends React.Component<Props, State> {
|
||||
currentPhase={this.state.currentPhase}
|
||||
currentSplitIndex={this.state.currentSplitIndex}
|
||||
allComparisons={this.state.allComparisons}
|
||||
splitsModified={this.state.splitsModified}
|
||||
layoutModified={this.state.layoutModified}
|
||||
/>;
|
||||
}
|
||||
// Only get here if the type is invalid
|
||||
@@ -412,9 +421,8 @@ export class LiveSplit extends React.Component<Props, State> {
|
||||
);
|
||||
}
|
||||
|
||||
public openTimerView(layout: Layout = this.state.layout) {
|
||||
public openTimerView() {
|
||||
this.setState({
|
||||
layout,
|
||||
menu: { kind: MenuKind.Timer },
|
||||
sidebarOpen: false,
|
||||
});
|
||||
@@ -452,7 +460,7 @@ export class LiveSplit extends React.Component<Props, State> {
|
||||
try {
|
||||
const layout = this.state.layout.settingsAsJson();
|
||||
await Storage.storeLayout(layout);
|
||||
toast.info("Layout saved successfully.");
|
||||
this.setState({ layoutModified: false }, () => this.updateBadge());
|
||||
} catch (_) {
|
||||
toast.error("Failed to save the layout.");
|
||||
}
|
||||
@@ -544,12 +552,11 @@ export class LiveSplit extends React.Component<Props, State> {
|
||||
const layoutEditor = this.state.menu.editor;
|
||||
const layout = layoutEditor.close();
|
||||
if (save) {
|
||||
this.state.layout[Symbol.dispose]();
|
||||
this.openTimerView(layout);
|
||||
this.setLayout(layout);
|
||||
} else {
|
||||
layout[Symbol.dispose]();
|
||||
this.openTimerView();
|
||||
}
|
||||
this.openTimerView();
|
||||
}
|
||||
|
||||
public openSettingsEditor() {
|
||||
@@ -669,9 +676,13 @@ export class LiveSplit extends React.Component<Props, State> {
|
||||
|
||||
private setLayout(layout: Layout) {
|
||||
this.state.layout[Symbol.dispose]();
|
||||
this.setState({
|
||||
layout,
|
||||
});
|
||||
this.setState(
|
||||
{
|
||||
layout,
|
||||
layoutModified: true,
|
||||
},
|
||||
() => this.updateBadge(),
|
||||
);
|
||||
}
|
||||
|
||||
private setRun(run: Run, callback: () => void) {
|
||||
@@ -737,7 +748,6 @@ export class LiveSplit extends React.Component<Props, State> {
|
||||
if (this.state.openedSplitsKey !== openedSplitsKey) {
|
||||
this.setSplitsKey(openedSplitsKey);
|
||||
}
|
||||
toast.info("Splits saved successfully.");
|
||||
} catch (_) {
|
||||
toast.error("Failed to save the splits.");
|
||||
}
|
||||
@@ -796,4 +806,29 @@ export class LiveSplit extends React.Component<Props, State> {
|
||||
this.saveSplits();
|
||||
}
|
||||
}
|
||||
|
||||
splitsModifiedChanged(): void {
|
||||
if (this.state != null) {
|
||||
const splitsModified = this.state.eventSink.hasBeenModified();
|
||||
this.setState({ splitsModified }, () => this.updateBadge());
|
||||
}
|
||||
}
|
||||
|
||||
private updateBadge(): void {
|
||||
if (this.state.splitsModified || this.state.layoutModified) {
|
||||
try {
|
||||
navigator?.setAppBadge();
|
||||
} catch {
|
||||
// It's fine if this fails.
|
||||
}
|
||||
document.title = "*LiveSplit One";
|
||||
} else {
|
||||
try {
|
||||
navigator?.clearAppBadge();
|
||||
} catch {
|
||||
// It's fine if this fails.
|
||||
}
|
||||
document.title = "LiveSplit One";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ export interface Props {
|
||||
openedSplitsKey?: number,
|
||||
callbacks: Callbacks,
|
||||
generalSettings: GeneralSettings,
|
||||
splitsModified: boolean,
|
||||
}
|
||||
|
||||
interface State {
|
||||
@@ -195,6 +196,10 @@ export class SplitsSelection extends React.Component<Props, State> {
|
||||
</button>
|
||||
<button onClick={(_) => this.saveSplits()}>
|
||||
<i className="fa fa-save" aria-hidden="true" /> Save
|
||||
{
|
||||
this.props.splitsModified &&
|
||||
<i className="fa fa-circle modified-icon" aria-hidden="true" />
|
||||
}
|
||||
</button>
|
||||
<button onClick={(_) => this.exportTimerSplits()}>
|
||||
<i className="fa fa-upload" aria-hidden="true" /> Export
|
||||
|
||||
@@ -33,6 +33,8 @@ export interface Props {
|
||||
currentPhase: TimerPhase,
|
||||
currentSplitIndex: number,
|
||||
allComparisons: string[],
|
||||
splitsModified: boolean,
|
||||
layoutModified: boolean,
|
||||
}
|
||||
export interface State {
|
||||
manualGameTime: string,
|
||||
@@ -209,9 +211,17 @@ export class TimerView extends React.Component<Props, State> {
|
||||
<hr className="livesplit-title-separator" />
|
||||
<button onClick={(_) => this.props.callbacks.openSplitsView()}>
|
||||
<i className="fa fa-list" aria-hidden="true" /> Splits
|
||||
{
|
||||
this.props.splitsModified &&
|
||||
<i className="fa fa-circle modified-icon" aria-hidden="true" />
|
||||
}
|
||||
</button>
|
||||
<button onClick={(_) => this.props.callbacks.openLayoutView()}>
|
||||
<i className="fa fa-layer-group" aria-hidden="true" /> Layout
|
||||
{
|
||||
this.props.layoutModified &&
|
||||
<i className="fa fa-circle modified-icon" aria-hidden="true" />
|
||||
}
|
||||
</button>
|
||||
<hr />
|
||||
<h2>Compare Against</h2>
|
||||
|
||||
Reference in New Issue
Block a user