New Replays: Support pagination in search results

This commit is contained in:
Guangcong Luo 2023-10-29 03:38:59 +00:00
parent 6d26acef8d
commit 0dcff6c2c3
4 changed files with 375 additions and 313 deletions

View File

@ -91,6 +91,20 @@
.button {
vertical-align: middle;
}
.replay-controls {
padding-top: 10px;
}
.replay-controls h1 {
font-size: 16pt;
font-weight: normal;
color: #CCC;
}
.pagelink {
text-align: center;
}
.pagelink a {
width: 150px;
}
</style>
<div>
@ -134,4 +148,5 @@
<script defer src="//play.pokemonshowdown.com/js/battle.js?a7"></script>
<script defer src="js/utils.js?"></script>
<script defer src="js/replays-battle.js?"></script>
<script defer src="js/replays.js?"></script>

View File

@ -0,0 +1,311 @@
/** @jsx preact.h */
import preact from 'preact';
import $ from 'jquery';
import {Net} from './utils';
import {PSRouter} from './replays';
import {Battle} from '../../../src/battle';
import {BattleSound} from '../../../src/battle-sound';
function showAd(id: string) {
// @ts-expect-error
window.top.__vm_add = window.top.__vm_add || [];
//this is a x-browser way to make sure content has loaded.
(function (success) {
if (window.document.readyState !== "loading") {
success();
} else {
window.document.addEventListener("DOMContentLoaded", function () {
success();
});
}
})(function () {
var placement = document.createElement("div");
placement.setAttribute("class", "vm-placement");
if (window.innerWidth > 1000) {
//load desktop placement
placement.setAttribute("data-id", "6452680c0b35755a3f09b59b");
} else {
//load mobile placement
placement.setAttribute("data-id", "645268557bc7b571c2f06f62");
}
document.querySelector("#" + id)!.appendChild(placement);
// @ts-expect-error
window.top.__vm_add.push(placement);
});
}
export class BattleDiv extends preact.Component {
override shouldComponentUpdate() {
return false;
}
override render() {
return <div class="battle" style={{position: 'relative'}}></div>;
}
}
class BattleLogDiv extends preact.Component {
override shouldComponentUpdate() {
return false;
}
override render() {
return <div class="battle-log"></div>;
}
}
export class BattlePanel extends preact.Component<{id: string}> {
result: {
uploadtime: number;
id: string;
format: string;
p1: string;
p2: string;
log: string;
views: number;
p1id: string;
p2id: string;
rating: number;
private: number;
password: string;
} | null | undefined = undefined;
battle: Battle | null;
speed = 'normal';
override componentDidMount() {
this.loadBattle(this.props.id);
showAd('LeaderboardBTF');
}
override componentWillReceiveProps(nextProps) {
if (this.stripQuery(this.props.id) !== this.stripQuery(nextProps.id)) {
this.loadBattle(nextProps.id);
}
}
stripQuery(id: string) {
return id.includes('?') ? id.slice(0, id.indexOf('?')) : id;
}
loadBattle(id: string) {
if (this.battle) this.battle.destroy();
this.battle = null;
this.result = undefined;
Net(`https://replay.pokemonshowdown.com/${this.stripQuery(id)}.json`).get().then(result => {
const replay: NonNullable<BattlePanel['result']> = JSON.parse(result);
this.result = replay;
const $base = $(this.base!);
this.battle = new Battle({
id: replay.id,
$frame: $base.find('.battle'),
$logFrame: $base.find('.battle-log'),
log: replay.log.split('\n'),
isReplay: true,
paused: true,
autoresize: true,
});
// for ease of debugging
(window as any).battle = this.battle;
this.battle.subscribe(_ => {
this.forceUpdate();
});
if (id.includes('?p2')) {
this.battle.switchViewpoint();
}
this.forceUpdate();
}).catch(_ => {
this.result = null;
this.forceUpdate();
});
}
override componentWillUnmount(): void {
this.battle?.destroy();
(window as any).battle = null;
}
play = () => {
this.battle?.play();
};
replay = () => {
this.battle?.reset();
this.battle?.play();
this.forceUpdate();
};
pause = () => {
this.battle?.pause();
};
nextTurn = () => {
this.battle?.seekBy(1);
};
prevTurn = () => {
this.battle?.seekBy(-1);
};
firstTurn = () => {
this.battle?.seekTurn(0);
};
lastTurn = () => {
this.battle?.seekTurn(Infinity);
};
goToTurn = () => {
const turn = prompt('Turn?');
if (!turn?.trim()) return;
let turnNum = Number(turn);
if (turn === 'e' || turn === 'end' || turn === 'f' || turn === 'finish') turnNum = Infinity;
if (isNaN(turnNum) || turnNum < 0) alert("Invalid turn");
this.battle?.seekTurn(turnNum);
};
switchViewpoint = () => {
this.battle?.switchViewpoint();
if (this.battle?.viewpointSwitched) {
PSRouter.replace(this.stripQuery(this.props.id) + '?p2');
} else {
PSRouter.replace(this.stripQuery(this.props.id));
}
};
changeSpeed = (e: Event) => {
this.speed = (e.target as HTMLSelectElement).value;
const fadeTable = {
hyperfast: 40,
fast: 50,
normal: 300,
slow: 500,
reallyslow: 1000
};
const delayTable = {
hyperfast: 1,
fast: 1,
normal: 1,
slow: 1000,
reallyslow: 3000
};
if (!this.battle) return;
this.battle.messageShownTime = delayTable[this.speed];
this.battle.messageFadeTime = fadeTable[this.speed];
this.battle.scene.updateAcceleration();
};
changeSound = (e: Event) => {
const muted = (e.target as HTMLSelectElement).value;
this.battle?.setMute(muted === 'off');
};
renderNotFound() {
return <div class={PSRouter.showingLeft() ? 'mainbar has-sidebar' : 'mainbar'}><section class="section" style={{maxWidth: '200px'}}>
{/* <div style={{margin: '0 -20px'}}>
<img src="//play.pokemonshowdown.com/sprites/dex/unown-n.png" alt="" width={120} height={120} style={{marginRight: '-60px'}} />
<img src="//play.pokemonshowdown.com/sprites/dex/unown-o.png" alt="" width={120} height={120} style={{marginRight: '-60px'}} />
<img src="//play.pokemonshowdown.com/sprites/dex/unown-t.png" alt="" width={120} height={120} />
</div>
<div style={{margin: '-40px -20px 0'}}>
<img src="//play.pokemonshowdown.com/sprites/dex/unown-f.png" alt="" width={120} height={120} style={{marginRight: '-60px'}} />
<img src="//play.pokemonshowdown.com/sprites/dex/unown-o.png" alt="" width={120} height={120} style={{marginRight: '-60px'}} />
<img src="//play.pokemonshowdown.com/sprites/dex/unown-u.png" alt="" width={120} height={120} style={{marginRight: '-60px'}} />
<img src="//play.pokemonshowdown.com/sprites/dex/unown-n.png" alt="" width={120} height={120} style={{marginRight: '-60px'}} />
<img src="//play.pokemonshowdown.com/sprites/dex/unown-d.png" alt="" width={120} height={120} />
</div> */}
<div style={{textAlign: 'center'}}>
<img src="//play.pokemonshowdown.com/sprites/gen5ani/unown-n.gif" alt="" style={{imageRendering: 'pixelated'}} />
<img src="//play.pokemonshowdown.com/sprites/gen5ani/unown-o.gif" alt="" style={{imageRendering: 'pixelated'}} />
<img src="//play.pokemonshowdown.com/sprites/gen5ani/unown-t.gif" alt="" style={{imageRendering: 'pixelated'}} />
</div>
<div style={{textAlign: 'center'}}>
<img src="//play.pokemonshowdown.com/sprites/gen5ani/unown-f.gif" alt="" style={{imageRendering: 'pixelated'}} />
<img src="//play.pokemonshowdown.com/sprites/gen5ani/unown-o.gif" alt="" style={{imageRendering: 'pixelated'}} />
<img src="//play.pokemonshowdown.com/sprites/gen5ani/unown-u.gif" alt="" style={{imageRendering: 'pixelated'}} />
<img src="//play.pokemonshowdown.com/sprites/gen5ani/unown-n.gif" alt="" style={{imageRendering: 'pixelated'}} />
<img src="//play.pokemonshowdown.com/sprites/gen5ani/unown-d.gif" alt="" style={{imageRendering: 'pixelated'}} />
</div>
</section><section class="section">
<h1>Not Found</h1>
<p>
The battle you're looking for has expired. Battles expire after 15 minutes of inactivity unless they're saved.
</p>
<p>
In the future, remember to click <strong>Upload and share replay</strong> to save a replay permanently.
</p>
</section></div>;
}
override render() {
const atEnd = this.battle?.atQueueEnd;
const atStart = !this.battle?.started;
if (this.result === null) return this.renderNotFound();
let position: any = {};
if (PSRouter.showingLeft()) {
if (PSRouter.stickyRight) {
position = {position: 'sticky', top: '0'};
} else {
position = {position: 'sticky', bottom: '0'};
}
}
return <div class={PSRouter.showingLeft() ? 'mainbar has-sidebar' : 'mainbar'} style={position}><div style={{position: 'relative'}}>
<BattleDiv />
<BattleLogDiv />
<div class="replay-controls">
<p>
{atEnd ?
<button onClick={this.replay} class="button" style={{width: '5em'}}>
<i class="fa fa-undo"></i><br />Replay
</button>
: this.battle?.paused ?
<button onClick={this.play} class="button" style={{width: '5em'}}>
<i class="fa fa-play"></i><br />Play
</button>
:
<button onClick={this.pause} class="button" style={{width: '5em'}}>
<i class="fa fa-pause"></i><br />Pause
</button>
} {}
<button class={"button button-first" + (atStart ? " disabled" : "")} onClick={this.firstTurn}>
<i class="fa fa-fast-backward"></i><br />First turn
</button>
<button class={"button button-middle" + (atStart ? " disabled" : "")} onClick={this.prevTurn}>
<i class="fa fa-step-backward"></i><br />Prev turn
</button>
<button class={"button button-middle" + (atEnd ? " disabled" : "")} onClick={this.nextTurn}>
<i class="fa fa-step-forward"></i><br />Skip turn
</button>
<button class={"button button-last" + (atEnd ? " disabled" : "")} onClick={this.lastTurn}>
<i class="fa fa-fast-forward"></i><br />Skip to end
</button> {}
<button class="button" onClick={this.goToTurn}>
<i class="fa fa-repeat"></i> Skip to turn...
</button>
</p>
<p>
<label class="optgroup">
Speed<br />
<select name="speed" class="button" onChange={this.changeSpeed} value={this.speed}>
<option value="hyperfast">Hyperfast</option>
<option value="fast">Fast</option>
<option value="normal">Normal</option>
<option value="slow">Slow</option>
<option value="reallyslow">Really slow</option>
</select>
</label> {}
<label class="optgroup">
Sound<br />
<select name="speed" class="button" onChange={this.changeSound} value={BattleSound.muted ? 'off' : 'on'}>
<option value="on">On</option>
<option value="off">Muted</option>
</select>
</label> {}
<label class="optgroup">
Viewpoint<br />
<button onClick={this.switchViewpoint} class={this.battle ? 'button' : 'button disabled'}>
{(this.battle?.viewpointSwitched ? this.result?.p2 : this.result?.p1)} {}
<i class="fa fa-random" aria-label="Switch viewpoint"></i>
</button>
</label>
</p>
{this.result ? <h1>
<strong>{this.result.format}</strong>: {this.result.p1} vs. {this.result.p2}
</h1> : <h1>
<em>Loading...</em>
</h1>}
{this.result ? <p>
{this.result.uploadtime ? new Date(this.result.uploadtime * 1000).toDateString() : "Unknown upload date"}
{this.result.rating ? ` | Rating: ${this.result.rating}` : ''}
</p> : <p>&nbsp;</p>}
{!PSRouter.showingLeft() && <p>
<a href="." class="button"><i class="fa fa-caret-left"></i> More replays</a>
</p>}
</div>
<div id="LeaderboardBTF"></div>
</div></div>;
}
}

View File

@ -1,41 +1,9 @@
/** @jsx preact.h */
import preact from 'preact';
import {Net, PSModel} from './utils';
import {Battle} from '../../../src/battle';
import {BattleSound} from '../../../src/battle-sound';
import $ from 'jquery';
import {BattlePanel} from './replays-battle';
declare function toID(input: string): string;
function showAd(id: string) {
// @ts-expect-error
window.top.__vm_add = window.top.__vm_add || [];
//this is a x-browser way to make sure content has loaded.
(function (success) {
if (window.document.readyState !== "loading") {
success();
} else {
window.document.addEventListener("DOMContentLoaded", function () {
success();
});
}
})(function () {
var placement = document.createElement("div");
placement.setAttribute("class", "vm-placement");
if (window.innerWidth > 1000) {
//load desktop placement
placement.setAttribute("data-id", "6452680c0b35755a3f09b59b");
} else {
//load mobile placement
placement.setAttribute("data-id", "645268557bc7b571c2f06f62");
}
document.querySelector("#" + id)!.appendChild(placement);
// @ts-expect-error
window.top.__vm_add.push(placement);
});
}
interface ReplayResult {
uploadtime: number;
id: string;
@ -50,6 +18,8 @@ class SearchPanel extends preact.Component {
resultError: string | null = null;
format = '';
user = '';
isPrivate = false;
page = 1;
loggedInUser: string | null = null;
loggedInUserIsSysop = false;
sort = 'date';
@ -61,10 +31,18 @@ class SearchPanel extends preact.Component {
this.loggedInUserIsSysop = !!sysop;
this.forceUpdate();
});
this.updateSearch();
}
override componentDidUpdate() {
const page = parseInt(decodeURIComponent(/\bpage=([^&]*)/.exec(PSRouter.leftLoc || '')?.[1] || '1'));
if (page !== this.page) this.updateSearch();
}
updateSearch() {
const user = decodeURIComponent(/\buser=([^&]*)/.exec(PSRouter.leftLoc || '')?.[1] || '');
const format = decodeURIComponent(/\bformat=([^&]*)/.exec(PSRouter.leftLoc || '')?.[1] || '');
const page = parseInt(decodeURIComponent(/\bpage=([^&]*)/.exec(PSRouter.leftLoc || '')?.[1] || '1'));
const isPrivate = PSRouter.leftLoc!.includes('private=1');
this.search(user, format, isPrivate);
this.search(user, format, isPrivate, page);
}
parseResponse(response: string, isPrivate?: boolean) {
this.results = null;
@ -84,7 +62,7 @@ class SearchPanel extends preact.Component {
}
this.results = results;
}
search(user: string, format: string, isPrivate?: boolean) {
search(user: string, format: string, isPrivate?: boolean, page = 1) {
this.base!.querySelector<HTMLInputElement>('input[name=user]')!.value = user;
this.base!.querySelector<HTMLInputElement>('input[name=format]')!.value = format;
this.base!.querySelectorAll<HTMLInputElement>('input[name=private]')[isPrivate ? 1 : 0]!.checked = true;
@ -92,6 +70,8 @@ class SearchPanel extends preact.Component {
if (!format && !user) return this.recent();
this.user = user;
this.format = format;
this.isPrivate = !!isPrivate;
this.page = page;
this.results = null;
this.resultError = null;
@ -102,11 +82,12 @@ class SearchPanel extends preact.Component {
user: user || undefined,
format: format || undefined,
private: isPrivate ? '1' : undefined,
page: page === 1 ? undefined : page,
}));
}
this.forceUpdate();
Net(`/api/replays/${isPrivate ? 'searchprivate' : 'search'}`).get({
query: {username: this.user, format: this.format},
query: {username: this.user, format: this.format, page},
}).then(response => {
if (this.format !== format || this.user !== user) return;
this.parseResponse(response, true);
@ -117,6 +98,22 @@ class SearchPanel extends preact.Component {
this.forceUpdate();
});
}
prevPageLink() {
return './?' + Net.encodeQuery({
user: this.user || undefined,
format: this.format || undefined,
private: this.isPrivate ? '1' : undefined,
page: this.page - 1 === 1 ? undefined : this.page - 1,
});
}
nextPageLink() {
return './?' + Net.encodeQuery({
user: this.user || undefined,
format: this.format || undefined,
private: this.isPrivate ? '1' : undefined,
page: this.page + 1,
});
}
recent() {
this.format = '';
this.user = '';
@ -165,21 +162,23 @@ class SearchPanel extends preact.Component {
return formatid;
}
override render() {
const activelySearching = !!(this.format || this.user);
const hasNextPageLink = (this.results?.length || 0) > 50;
const results = hasNextPageLink ? this.results!.slice(0, 50) : this.results;
const searchResults = <ul class="linklist">
{(this.resultError && <li>
<strong class="message-error">{this.resultError}</strong>
</li>) ||
(!this.results && <li>
(!results && <li>
<em>Loading...</em>
</li>) ||
(this.results?.map(result => <li>
(results?.map(result => <li>
<a href={this.url(result)} class="blocklink">
<small>[{this.formatid(result)}]<br /></small>
<strong>{result.p1}</strong> vs. <strong>{result.p2}</strong>
</a>
</li>))}
</ul>;
const activelySearching = !!(this.format || this.user);
return <div class={PSRouter.showingRight() ? 'sidebar' : ''}><section class="section first-section">
<h1>Search replays</h1>
<form onSubmit={this.submitForm}>
@ -201,7 +200,13 @@ class SearchPanel extends preact.Component {
{activelySearching && <button class="button" onClick={this.cancelForm}>Cancel</button>}
</p>
{activelySearching && <h1 aria-label="Results"></h1>}
{activelySearching && this.page > 1 && <p class="pagelink">
<a href={this.prevPageLink()} class="button"><i class="fa fa-caret-up"></i><br />Page {this.page - 1}</a>
</p>}
{activelySearching && searchResults}
{activelySearching && (this.results?.length || 0) > 50 && <p class="pagelink">
<a href={this.nextPageLink()} class="button">Page {this.page + 1}<br /><i class="fa fa-caret-down"></i></a>
</p>}
</form>
</section>{!activelySearching && <FeaturedReplays />}{!activelySearching && <section class="section">
<h1>Recent replays</h1>
@ -322,280 +327,7 @@ class FeaturedReplays extends preact.Component {
}
}
class BattleDiv extends preact.Component {
override shouldComponentUpdate() {
return false;
}
override render() {
return <div class="battle" style={{position: 'relative'}}></div>;
}
}
class BattleLogDiv extends preact.Component {
override shouldComponentUpdate() {
return false;
}
override render() {
return <div class="battle-log"></div>;
}
}
class BattlePanel extends preact.Component<{id: string}> {
result: {
uploadtime: number;
id: string;
format: string;
p1: string;
p2: string;
log: string;
views: number;
p1id: string;
p2id: string;
rating: number;
private: number;
password: string;
} | null | undefined = undefined;
battle: Battle | null;
speed = 'normal';
override componentDidMount() {
this.loadBattle(this.props.id);
showAd('LeaderboardBTF');
}
override componentWillReceiveProps(nextProps) {
if (this.stripQuery(this.props.id) !== this.stripQuery(nextProps.id)) {
this.loadBattle(nextProps.id);
}
}
stripQuery(id: string) {
return id.includes('?') ? id.slice(0, id.indexOf('?')) : id;
}
loadBattle(id: string) {
if (this.battle) this.battle.destroy();
this.battle = null;
this.result = undefined;
Net(`https://replay.pokemonshowdown.com/${this.stripQuery(id)}.json`).get().then(result => {
const replay: NonNullable<BattlePanel['result']> = JSON.parse(result);
this.result = replay;
const $base = $(this.base!);
this.battle = new Battle({
id: replay.id,
$frame: $base.find('.battle'),
$logFrame: $base.find('.battle-log'),
log: replay.log.split('\n'),
isReplay: true,
paused: true,
autoresize: true,
});
// for ease of debugging
(window as any).battle = this.battle;
this.battle.subscribe(_ => {
this.forceUpdate();
});
if (id.includes('?p2')) {
this.battle.switchSides();
}
this.forceUpdate();
}).catch(_ => {
this.result = null;
this.forceUpdate();
});
}
override componentWillUnmount(): void {
this.battle?.destroy();
(window as any).battle = null;
}
play = () => {
this.battle?.play();
};
replay = () => {
this.battle?.reset();
this.battle?.play();
this.forceUpdate();
};
pause = () => {
this.battle?.pause();
};
nextTurn = () => {
this.battle?.seekBy(1);
};
prevTurn = () => {
this.battle?.seekBy(-1);
};
firstTurn = () => {
this.battle?.seekTurn(0);
};
lastTurn = () => {
this.battle?.seekTurn(Infinity);
};
goToTurn = () => {
const turn = prompt('Turn?');
if (!turn?.trim()) return;
let turnNum = Number(turn);
if (turn === 'e' || turn === 'end' || turn === 'f' || turn === 'finish') turnNum = Infinity;
if (isNaN(turnNum) || turnNum < 0) alert("Invalid turn");
this.battle?.seekTurn(turnNum);
};
switchSides = () => {
this.battle?.switchSides();
if (this.battle?.sidesSwitched) {
PSRouter.replace(this.stripQuery(this.props.id) + '?p2');
} else {
PSRouter.replace(this.stripQuery(this.props.id));
}
};
changeSpeed = (e: Event) => {
this.speed = (e.target as HTMLSelectElement).value;
const fadeTable = {
hyperfast: 40,
fast: 50,
normal: 300,
slow: 500,
reallyslow: 1000
};
const delayTable = {
hyperfast: 1,
fast: 1,
normal: 1,
slow: 1000,
reallyslow: 3000
};
if (!this.battle) return;
this.battle.messageShownTime = delayTable[this.speed];
this.battle.messageFadeTime = fadeTable[this.speed];
this.battle.scene.updateAcceleration();
};
changeSound = (e: Event) => {
const muted = (e.target as HTMLSelectElement).value;
this.battle?.setMute(muted === 'off');
};
renderNotFound() {
return <div class={PSRouter.showingLeft() ? 'mainbar has-sidebar' : 'mainbar'}><section class="section" style={{maxWidth: '200px'}}>
{/* <div style={{margin: '0 -20px'}}>
<img src="//play.pokemonshowdown.com/sprites/dex/unown-n.png" alt="" width={120} height={120} style={{marginRight: '-60px'}} />
<img src="//play.pokemonshowdown.com/sprites/dex/unown-o.png" alt="" width={120} height={120} style={{marginRight: '-60px'}} />
<img src="//play.pokemonshowdown.com/sprites/dex/unown-t.png" alt="" width={120} height={120} />
</div>
<div style={{margin: '-40px -20px 0'}}>
<img src="//play.pokemonshowdown.com/sprites/dex/unown-f.png" alt="" width={120} height={120} style={{marginRight: '-60px'}} />
<img src="//play.pokemonshowdown.com/sprites/dex/unown-o.png" alt="" width={120} height={120} style={{marginRight: '-60px'}} />
<img src="//play.pokemonshowdown.com/sprites/dex/unown-u.png" alt="" width={120} height={120} style={{marginRight: '-60px'}} />
<img src="//play.pokemonshowdown.com/sprites/dex/unown-n.png" alt="" width={120} height={120} style={{marginRight: '-60px'}} />
<img src="//play.pokemonshowdown.com/sprites/dex/unown-d.png" alt="" width={120} height={120} />
</div> */}
<div style={{textAlign: 'center'}}>
<img src="//play.pokemonshowdown.com/sprites/gen5ani/unown-n.gif" alt="" style={{imageRendering: 'pixelated'}} />
<img src="//play.pokemonshowdown.com/sprites/gen5ani/unown-o.gif" alt="" style={{imageRendering: 'pixelated'}} />
<img src="//play.pokemonshowdown.com/sprites/gen5ani/unown-t.gif" alt="" style={{imageRendering: 'pixelated'}} />
</div>
<div style={{textAlign: 'center'}}>
<img src="//play.pokemonshowdown.com/sprites/gen5ani/unown-f.gif" alt="" style={{imageRendering: 'pixelated'}} />
<img src="//play.pokemonshowdown.com/sprites/gen5ani/unown-o.gif" alt="" style={{imageRendering: 'pixelated'}} />
<img src="//play.pokemonshowdown.com/sprites/gen5ani/unown-u.gif" alt="" style={{imageRendering: 'pixelated'}} />
<img src="//play.pokemonshowdown.com/sprites/gen5ani/unown-n.gif" alt="" style={{imageRendering: 'pixelated'}} />
<img src="//play.pokemonshowdown.com/sprites/gen5ani/unown-d.gif" alt="" style={{imageRendering: 'pixelated'}} />
</div>
</section><section class="section">
<h1>Not Found</h1>
<p>
The battle you're looking for has expired. Battles expire after 15 minutes of inactivity unless they're saved.
</p>
<p>
In the future, remember to click <strong>Upload and share replay</strong> to save a replay permanently.
</p>
</section></div>;
}
override render() {
const atEnd = this.battle?.atQueueEnd;
const atStart = !this.battle?.started;
if (this.result === null) return this.renderNotFound();
let position: any = {};
if (PSRouter.showingLeft()) {
if (PSRouter.stickyRight) {
position = {position: 'sticky', top: '0'};
} else {
position = {position: 'sticky', bottom: '0'};
}
}
return <div class={PSRouter.showingLeft() ? 'mainbar has-sidebar' : 'mainbar'} style={position}><div style={{position: 'relative'}}>
<BattleDiv />
<BattleLogDiv />
<div style={{paddingTop: 10}}>
<p>
{atEnd ?
<button onClick={this.replay} class="button" style={{width: '5em'}}>
<i class="fa fa-undo"></i><br />Replay
</button>
: this.battle?.paused ?
<button onClick={this.play} class="button" style={{width: '5em'}}>
<i class="fa fa-play"></i><br />Play
</button>
:
<button onClick={this.pause} class="button" style={{width: '5em'}}>
<i class="fa fa-pause"></i><br />Pause
</button>
} {}
<button class={"button button-first" + (atStart ? " disabled" : "")} onClick={this.firstTurn}>
<i class="fa fa-fast-backward"></i><br />First turn
</button>
<button class={"button button-middle" + (atStart ? " disabled" : "")} onClick={this.prevTurn}>
<i class="fa fa-step-backward"></i><br />Prev turn
</button>
<button class={"button button-middle" + (atEnd ? " disabled" : "")} onClick={this.nextTurn}>
<i class="fa fa-step-forward"></i><br />Skip turn
</button>
<button class={"button button-last" + (atEnd ? " disabled" : "")} onClick={this.lastTurn}>
<i class="fa fa-fast-forward"></i><br />Skip to end
</button> {}
<button class="button" onClick={this.goToTurn}>
<i class="fa fa-repeat"></i> Skip to turn...
</button>
</p>
<p>
<label class="optgroup">
Speed<br />
<select name="speed" class="button" onChange={this.changeSpeed} value={this.speed}>
<option value="hyperfast">Hyperfast</option>
<option value="fast">Fast</option>
<option value="normal">Normal</option>
<option value="slow">Slow</option>
<option value="reallyslow">Really slow</option>
</select>
</label> {}
<label class="optgroup">
Sound<br />
<select name="speed" class="button" onChange={this.changeSound} value={BattleSound.muted ? 'off' : 'on'}>
<option value="on">On</option>
<option value="off">Muted</option>
</select>
</label> {}
<label class="optgroup">
Viewpoint<br />
<button onClick={this.switchSides} class={this.battle ? 'button' : 'button disabled'}>
{(this.battle?.sidesSwitched ? this.result?.p2 : this.result?.p1)} <i class="fa fa-random"></i>
</button>
</label>
</p>
{this.result ? <h1 style={{fontWeight: 'normal', fontSize: '18pt'}}>
<strong>{this.result.format}</strong>: {this.result.p1} vs. {this.result.p2}
</h1> : <h1 style={{fontSize: '18pt'}}>
<em>Loading...</em>
</h1>}
{this.result ? <p>
{this.result.uploadtime ? new Date(this.result.uploadtime * 1000).toDateString() : "Unknown upload date"}
{this.result.rating ? ` | Rating: ${this.result.rating}` : ''}
</p> : <p>&nbsp;</p>}
{!PSRouter.showingLeft() && <p>
<a href="." class="button"><i class="fa fa-caret-left"></i> More replays</a>
</p>}
</div>
<div id="LeaderboardBTF"></div>
</div></div>;
}
}
const PSRouter = new class extends PSModel {
export const PSRouter = new class extends PSModel {
baseLoc: string;
leftLoc: string | null = null;
rightLoc: string | null = null;

View File

@ -185,8 +185,12 @@ h1 {
margin: 0;
}
.dark h1 {
color: #CCC;
border-bottom-color: #888;
}
.dark h2, .dark h3 {
color: #CCC;
}
.section {
color: black;