Implement some operations in the view

This commit is contained in:
Christopher Serr
2020-04-18 12:57:34 -05:00
committed by Sergey Papushin
parent d4543238f8
commit f691836bc5
4 changed files with 201 additions and 39 deletions
+32 -9
View File
@@ -1,6 +1,6 @@
import { openDB, IDBPDatabase } from "idb";
import { Option } from "../util/OptionUtil";
import { RunRef, Run, TimerRefMut } from "../livesplit-core";
import { RunRef, Run } from "../livesplit-core";
export type HotkeyConfigSettings = unknown;
export type LayoutSettings = unknown;
@@ -102,16 +102,19 @@ async function getDb(): Promise<IDBPDatabase<unknown>> {
return db;
}
export async function storeSplits(accessTimer: (callback: (timer: TimerRefMut) => void) => void) {
export async function storeSplits(
callback: (callback: (run: RunRef, lssBytes: Uint8Array) => void) => void,
key: number | undefined,
) {
const db = await getDb();
const key = 1;
const tx = db.transaction(["splitsData", "splitsInfo"], "readwrite");
accessTimer((timer) => {
tx.objectStore("splitsInfo").put(getSplitsInfo(timer.getRun()), key);
tx.objectStore("splitsData").put(timer.saveAsLssBytes(), key);
timer.markAsUnmodified();
callback((run, lssBytes) => {
// We need to consume the bytes first as they are usually very
// short-living, because they directly point into the WebAssembly memory.
tx.objectStore("splitsData").put(lssBytes, key);
tx.objectStore("splitsInfo").put(getSplitsInfo(run), key);
});
await tx.done;
@@ -132,10 +135,30 @@ export async function getSplitsInfos(): Promise<Array<[number, SplitsInfo]>> {
return arr;
}
export async function loadSplits(): Promise<Uint8Array | undefined> {
export async function loadSplits(key: number): Promise<Uint8Array | undefined> {
const db = await getDb();
return await db.get("splitsData", 1);
return await db.get("splitsData", key);
}
export async function deleteSplits(key: number) {
const db = await getDb();
const tx = db.transaction(["splitsData", "splitsInfo"], "readwrite");
tx.objectStore("splitsData").delete(key);
tx.objectStore("splitsInfo").delete(key);
await tx.done;
}
export async function copySplits(key: number) {
const db = await getDb();
const tx = db.transaction(["splitsData", "splitsInfo"], "readwrite");
const splitsData = tx.objectStore("splitsData");
splitsData.put(await splitsData.get(key));
const splitsInfo = tx.objectStore("splitsInfo");
splitsInfo.put(await splitsInfo.get(key));
await tx.done;
}
export async function storeLayout(layout: LayoutSettings) {
+15 -5
View File
@@ -62,11 +62,13 @@ export interface State {
menu: Menu,
comparison: Option<string>,
timingMethod: Option<TimingMethod>,
splitsKey: number,
}
export class LiveSplit extends React.Component<Props, State> {
public static async loadStoredData() {
const splits = await Storage.loadSplits();
const splitsKey = 1; // TODO: Load this from the settings. Not sure if here or somewhere else.
const splits = await Storage.loadSplits(splitsKey);
const layout = await Storage.loadLayout();
const hotkeys = await Storage.loadHotkeys();
const layoutWidth = await Storage.loadLayoutWidth();
@@ -155,6 +157,7 @@ export class LiveSplit extends React.Component<Props, State> {
hotkeySystem,
comparison: null,
timingMethod: null,
splitsKey: 1, // TODO: Should be flexible
};
this.mediaQueryChanged = this.mediaQueryChanged.bind(this);
@@ -232,7 +235,11 @@ export class LiveSplit extends React.Component<Props, State> {
} else if (this.state.menu.kind === MenuKind.About) {
return <About callbacks={this} />;
} else if (this.state.menu.kind === MenuKind.Splits) {
return <SplitsSelection callbacks={this} />;
return <SplitsSelection
callbacks={this}
timer={this.state.timer}
originalOpenedSplitsKey={this.state.splitsKey}
/>;
} else {
const renderedView = this.renderTimerView();
const sidebarContent = this.renderTimerViewSidebarContent();
@@ -372,9 +379,12 @@ export class LiveSplit extends React.Component<Props, State> {
public async saveSplits() {
try {
await Storage.storeSplits((accessTimer) => {
this.writeWith(accessTimer);
});
await Storage.storeSplits((callback) => {
this.writeWith((timer) => {
callback(timer.getRun(), timer.saveAsLssBytes());
timer.markAsUnmodified();
});
}, this.state.splitsKey);
} catch (_) {
toast.error("Failed to save the splits.");
}
+153 -24
View File
@@ -1,12 +1,20 @@
import * as React from "react";
import { getSplitsInfos, SplitsInfo } from "../storage";
import { getSplitsInfos, SplitsInfo, storeSplits, deleteSplits, copySplits, loadSplits } from "../storage";
import { Run, Segment, SharedTimerRef } from "../livesplit-core";
import * as SplitsIO from "../util/SplitsIO";
import { toast } from "react-toastify";
import { openFileAsArrayBuffer } from "../util/FileUtil";
import { maybeDisposeAndThen } from "../util/OptionUtil";
export interface Props {
timer: SharedTimerRef,
originalOpenedSplitsKey?: number,
callbacks: Callbacks,
}
interface State {
splitsInfos?: Array<[number, SplitsInfo]>,
openedSplitsKey?: number,
}
interface Callbacks {
@@ -31,7 +39,9 @@ export class SplitsSelection extends React.Component<Props, State> {
super(props);
this.state = {};
this.state = {
openedSplitsKey: props.originalOpenedSplitsKey,
};
}
public render() {
@@ -52,23 +62,50 @@ export class SplitsSelection extends React.Component<Props, State> {
<button onClick={() => this.importSplits()}>
<i className="fa fa-download" aria-hidden="true" /> Import
</button>
<button onClick={() => this.importSplitsFromSplitsIo()}>
<i className="fa fa-download" aria-hidden="true" /> From Splits.io
</button>
</div>
<div>
<h2>Active Splits</h2>
{
this.state.splitsInfos.map(([key, info]) => {
return <div>
<button onClick={() => this.editSplits(key)}>
<i className="fa fa-edit" aria-hidden="true" />
</button>
<button onClick={() => this.copySplits(key)}>
<i className="fa fa-clone" aria-hidden="true" />
</button>
<button onClick={() => this.deleteSplits(key)}>
<i className="fa fa-trash" aria-hidden="true" />
</button>
{info.game} - {info.category}
</div>;
})
this.state.splitsInfos
.filter(([key]) => key === this.state.openedSplitsKey)
.map(([key, info]) => {
return <div key={key}>
<button onClick={() => this.editSplits(key)}>
<i className="fa fa-edit" aria-hidden="true" />
</button>
<button onClick={() => this.copySplits(key)}>
<i className="fa fa-clone" aria-hidden="true" />
</button>
{info.game} - {info.category}
</div>;
})
}
</div>
<div>
<h2>Other Splits</h2>
{
this.state.splitsInfos
.filter(([key]) => key !== this.state.openedSplitsKey)
.map(([key, info]) => {
return <div key={key}>
<button onClick={() => this.openSplits(key)}>
<i className="fa fa-folder-open" aria-hidden="true" />
</button>
<button onClick={() => this.editSplits(key)}>
<i className="fa fa-edit" aria-hidden="true" />
</button>
<button onClick={() => this.copySplits(key)}>
<i className="fa fa-clone" aria-hidden="true" />
</button>
<button onClick={() => this.deleteSplits(key)}>
<i className="fa fa-trash" aria-hidden="true" />
</button>
{info.game} - {info.category}
</div>;
})
}
</div>
</div>;
@@ -108,23 +145,115 @@ export class SplitsSelection extends React.Component<Props, State> {
);
}
private async refreshState() {
this.setState({
splitsInfos: await getSplitsInfos(),
});
}
private async openSplits(key: number) {
const splitsData = await loadSplits(key);
if (splitsData === undefined) {
// TODO: This should never happen. Maybe show an error / use expect?
return;
}
// TODO: Manually accessing the timer like this has two disadvantages:
// 1. This is copy paste from LiveSplit.tsx for the most part.
// 2. We don't communicate back the new splits key yet.
const result = Run.parseArray(new Uint8Array(splitsData), "", false);
try {
if (result.parsedSuccessfully()) {
const run = result.unwrap();
maybeDisposeAndThen(
this.props.timer.writeWith((timer) => timer.setRun(run)),
() => toast.error("The loaded splits are invalid."),
);
this.setState({ openedSplitsKey: key });
} else {
throw Error("Couldn't parse the splits.");
}
} finally {
result.dispose();
}
}
private editSplits(_key: number): void {
throw new Error("Method not implemented.");
}
private copySplits(_key: number): void {
throw new Error("Method not implemented.");
private async copySplits(key: number) {
await copySplits(key);
await this.refreshState();
}
private deleteSplits(_key: number): void {
throw new Error("Method not implemented.");
private async deleteSplits(key: number) {
// TODO: We probably want to ask for confirmation here.
await deleteSplits(key);
await this.refreshState();
}
private importSplits() {
throw new Error("Method not implemented.");
private async importSplits() {
const splits = await openFileAsArrayBuffer();
try {
await this.importSplitsFromArrayBuffer(splits);
} catch (err) {
toast.error(err.message);
}
}
private addNewSplits() {
console.log("hi");
private async importSplitsFromArrayBuffer(buffer: [ArrayBuffer, File]) {
const [file] = buffer;
const result = Run.parseArray(new Uint8Array(file), "", false);
try {
if (result.parsedSuccessfully()) {
await this.storeRun(result.unwrap());
} else {
throw Error("Couldn't parse the splits.");
}
} finally {
result.dispose();
}
}
private async importSplitsFromSplitsIo() {
let id = prompt("Specify the Splits.io URL or ID:");
if (!id) {
return;
}
if (id.indexOf("https://splits.io/") === 0) {
id = id.substr("https://splits.io/".length);
}
try {
const run = await SplitsIO.downloadById(id);
await this.storeRun(run);
} catch (_) {
toast.error("Failed to download the splits.");
}
}
private async addNewSplits() {
// TODO: This is mostly copy paste atm.
const run = Run.new();
run.pushSegment(Segment.new("Time"));
await this.storeRun(run);
}
private async storeRun(run: Run) {
try {
if (run.len() === 0) {
toast.error("Can't import empty splits.");
return;
}
await storeSplits(
(callback) => {
callback(run, run.saveAsLssBytes());
},
undefined,
);
await this.refreshState();
} finally {
run.dispose();
}
}
}