diff --git a/replay.pokemonshowdown.com/.htaccess b/replay.pokemonshowdown.com/.htaccess
index 9d85282fe..f160c89a1 100644
--- a/replay.pokemonshowdown.com/.htaccess
+++ b/replay.pokemonshowdown.com/.htaccess
@@ -11,7 +11,7 @@ RewriteRule ^([A-Za-z0-9-]+)$ caches/index.php?name=$1 [L,QSA]
RewriteRule ^search\.json$ /api/replays/search.json [L,QSA]
RewriteRule ^recent\.json$ /api/replays/search.json [L,QSA]
RewriteRule ^download$ download.html [L,QSA]
-RewriteRule ^([A-Za-z0-9-]+)/manage$ replay-manage.php?name=$1&manage [L,QSA]
+RewriteRule ^([A-Za-z0-9-]+)/manage$ /$1?manage [R=302,L,QSA]
# RewriteRule ^([A-Za-z0-9-]+)\.log$ replay.log.php?name=$1 [L,QSA]
# RewriteRule ^([A-Za-z0-9-]+)\.inputlog$ replay.log.php?inputlog&name=$1 [L,QSA]
diff --git a/replay.pokemonshowdown.com/index.php b/replay.pokemonshowdown.com/index.php
index 041674b24..73b290c7d 100644
--- a/replay.pokemonshowdown.com/index.php
+++ b/replay.pokemonshowdown.com/index.php
@@ -180,6 +180,30 @@ https://replay.pokemonshowdown.com/gen7randomdoublesbattle-865046831.log
.mainbar.has-sidebar {
margin-left: 330px;
}
+ .bar-wrapper.has-sidebar:not(.short-viewport) .mainbar {
+ position: sticky;
+ top: 0;
+ }
+ .bar-wrapper.has-sidebar.short-viewport {
+ max-width: none;
+ }
+ .bar-wrapper.has-sidebar.short-viewport .sidebar {
+ box-sizing: border-box;
+ position: sticky;
+ top: 0;
+ max-height: 100vh;
+ overflow-y: auto;
+ }
+ @media (min-width: 1431px) and (max-width: 1510px) {
+ .bar-wrapper.has-sidebar.short-viewport .sidebar {
+ width: calc(50% - 395px);
+ padding-left: calc(50% - 715px);
+ }
+ .bar-wrapper.has-sidebar.short-viewport .mainbar {
+ margin-right: calc(50% - 715px);
+ margin-left: calc(50% - 385px);
+ }
+ }
@media (min-width: 1511px) {
.sidebar {
width: 400px;
@@ -190,6 +214,14 @@ https://replay.pokemonshowdown.com/gen7randomdoublesbattle-865046831.log
.mainbar.has-sidebar {
margin-left: 410px;
}
+ .bar-wrapper.has-sidebar.short-viewport .sidebar {
+ width: calc(50% - 355px);
+ padding-left: calc(50% - 755px);
+ }
+ .bar-wrapper.has-sidebar.short-viewport .mainbar {
+ margin-right: calc(50% - 755px);
+ margin-left: calc(50% - 345px);
+ }
}
.section.first-section {
margin-top: 9px;
diff --git a/replay.pokemonshowdown.com/replay-manage.php b/replay.pokemonshowdown.com/replay-manage.php
deleted file mode 100644
index 5cd111492..000000000
--- a/replay.pokemonshowdown.com/replay-manage.php
+++ /dev/null
@@ -1,223 +0,0 @@
-isLeader()) die("access denied");
-$csrfOk = !!$users->csrfCheck();
-$manage = true;
-header('Cache-Control: max-age=0, no-cache, no-store, must-revalidate');
-
-if (preg_match('/[^A-Za-z0-9-]/', $id)) die("access denied");
-
-$fullid = $id;
-if (substr($id, -2) === 'pw') {
- $dashpos = strrpos($id, '-');
- $password = substr($id, $dashpos + 1, -2);
- $id = substr($id, 0, $dashpos);
- // die($id . ' ' . $password);
-}
-
-$replay = null;
-$cached = false;
-
-// $forcecache = isset($_REQUEST['forcecache8723']);
-$forcecache = false;
-
-if (file_exists('caches/' . $id . '.inc.php')) {
- include 'caches/' . $id . '.inc.php';
- $replay['formatid'] = '';
- $cached = true;
-} else {
- require_once 'replays.lib.php';
- if (!$Replays->db && !$forcecache) {
- include '503.php';
- die();
- }
- $replay = $Replays->get($id, $forcecache);
-}
-if (!$replay || (@$replay['private'] === 3 && !$manage)) {
- include '404.php';
- die();
-}
-$fullid = $id . (@$replay['password'] ? '-' . $replay['password'] . 'pw' : '');
-
-if (@$replay['private']) {
- header('X-Robots-Tag: noindex');
-}
-if (@$replay['password']) {
- if (!$password && !$manage) {
- header('Cache-Control: max-age=0, no-cache, no-store, must-revalidate');
- require_once '../lib/ntbb-session.lib.php';
- if ($curuser['userid'] !== $p1id && $curuser['userid'] !== $p2id) {
- die("Access denied (you must be logged into " . $p1id . " or " . $p2id . ")");
- }
- $url = '/' . $id . '-' . $replay['password'] . 'pw';
- echo '
This private replay now has a new harder-to-guess URL:
';
- echo 'https://' . $psconfig['routes']['replays'] . $url . '
';
- die();
- }
- if ($password !== $replay['password'] && !$manage) {
- die("Access denied (please ask " . $p1id . " or " . $p2id . " for the password)");
- }
-}
-
-if ($forcecache) {
- file_put_contents('caches/' . $id . '.inc.php', 'userid($p1);
-$p2id = $users->userid($p2);
-
-$panels->setPageTitle($format.' replay: '.$p1.' vs. '.$p2);
-$panels->setPageDescription('Watch a replay of a Pokémon battle between ' . $p1 . ' and ' . $p2 . ' (' . $format . ')');
-$panels->setTab('replay');
-$panels->start();
-
-?>
-
-
-
-
-
-
- Play
-
-
-
-
Speed:
-
Hyperfast Fast Normal Slow Really Slow
-
-
-
Color scheme:
-
Light Dark
-
-
-
-
-
- THIS REPLAY IS PRIVATE - make sure you have the owner\'s permission to share
'; ?>
-
-
-
-
-
- Uploaded: = @$replay['rating'] ? ' | Rating: ' . $replay['rating'] : '' ?>
-
-
-
-edit($replay);
- echo '
Edited.
';
- }
-?>
- Change privacy:
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-output === 'normal') {
-?>
-
-
-
-
-
-
= $cached ? 'Other' : 'More' ?> replays
-
-
-end();
-
-?>
diff --git a/replay.pokemonshowdown.com/src/replay.js b/replay.pokemonshowdown.com/src/replay.js
deleted file mode 100644
index 8f9be18b0..000000000
--- a/replay.pokemonshowdown.com/src/replay.js
+++ /dev/null
@@ -1,243 +0,0 @@
-// preserved for replay management
-
-/* function updateProgress(done, a, b)
-{
- if (!battle.paused) return;
- //$('#battle').html('Play '+(done?'':' Buffering: '+parseInt(100*a/b)+'%')+'
');
-} */
-//setTimeout(function(){updateProgress(true)}, 10000);
-
-// Panels
-
-var Topbar = Panels.Topbar.extend({
- height: 51
-});
-
-var ReplaySidebarPanel = Panels.StaticPanel.extend({
- minWidth: 300,
- maxWidth: 600,
- events: {
- 'click button[name=moreResults]': 'moreResults'
- },
- moreResults: function(e) {
- var elem = e.currentTarget;
- elem.disabled = true;
- // var offset = elem.value;
- var page = Number(elem.value);
- if (!page) page = 2;
- var user = elem.dataset.user;
- var format = elem.dataset.format;
- var priv = !!elem.dataset.private;
- var self = this;
- elem.innerHTML = 'Loading... '
- $.get('/search', Object.assign({
- user: user,
- format: format,
- page: page,
- output: 'html'
- }, priv ? {private: 1} : {}), function (data) {
- self.$('ul.linklist').append(data);
- // var $nextOffset = self.$('input.offset');
- // var val = $nextOffset.val();
- // $nextOffset.remove();
- var $more = self.$('input.more');
- if (!$more.length) {
- $(elem).remove();
- return;
- } else {
- $more.remove();
- }
- elem.innerHTML = 'More '
- elem.disabled = false;
- elem.value = page + 1;
- });
- }
-});
-
-var ReplayPanel = Panels.StaticPanel.extend({
- minWidth: 960,
- maxWidth: 1180,
- moveTo: function() {
- Panels.Panel.prototype.moveTo.apply(this, arguments);
- this.$el.css('top', $(window).height() > 495 ? '51px' : '0');
- },
- events: {
- 'click .chooser button': 'clickChangeSetting',
- 'click .replayDownloadButton': 'clickReplayDownloadButton'
- },
- clickChangeSetting: function(e) {
- e.preventDefault();
- var $chooser = $(e.currentTarget).closest('.chooser');
- var value = e.currentTarget.value;
- this.changeSetting($chooser, value, $(e.currentTarget));
- },
- changeSetting: function(type, value, valueElem) {
- var $chooser;
- if (typeof type === 'string') {
- $chooser = this.$('.'+type+'chooser');
- } else {
- $chooser = type;
- type = '';
- if ($chooser.hasClass('colorchooser')) {
- type = 'color';
- } else if ($chooser.hasClass('soundchooser')) {
- type = 'sound';
- } else if ($chooser.hasClass('speedchooser')) {
- type = 'speed';
- }
- }
- if (!valueElem) valueElem = $chooser.find('button[value='+value+']');
-
- $chooser.find('button').removeClass('sel');
- valueElem.addClass('sel');
-
- switch (type) {
- case 'color':
- if (value === 'dark') {
- $(document.body).addClass('dark');
- } else {
- $(document.body).removeClass('dark');
- }
- break;
-
- case 'sound':
- // remember this is reversed: sound[off] === muted[true]
- this.muted = (value === 'off');
- this.battle.setMute(this.muted);
- this.$('.startsoundchooser').remove();
- break;
-
- case 'speed':
- var fadeTable = {
- hyperfast: 40,
- fast: 50,
- normal: 300,
- slow: 500,
- reallyslow: 1000
- };
- var delayTable = {
- hyperfast: 1,
- fast: 1,
- normal: 1,
- slow: 1000,
- reallyslow: 3000
- };
- this.battle.messageShownTime = delayTable[value];
- this.battle.messageFadeTime = fadeTable[value];
- this.battle.scene.updateAcceleration();
- break;
- }
- },
- battle: null,
- muted: null,
- updateContent: function() {
- this.$el.css('overflow-x', 'hidden');
- var $battle = this.$('.battle');
- if (!$battle.length) return;
-
- var replayid = this.$('input[name=replayid]').val() || '';
- var log = (this.$('script.log').text() || '').replace(/\\\//g,'/');
-
- var self = this;
- this.battle = new Battle({
- id: replayid,
- $frame: $battle,
- $logFrame: this.$('.battle-log'),
- log: log.split('\n'),
- isReplay: true,
- paused: true
- })
-
- this.$('.urlbox').css('margin-right', 120).before(' Download ');
-
- // this works around a WebKit/Blink bug relating to float layout
- var rc2 = this.$('.replay-controls-2')[0];
- if (rc2) rc2.innerHTML = rc2.innerHTML;
-
- if (window.HTMLAudioElement) this.$('.soundchooser, .startsoundchooser').show();
-
- this.battle.subscribe(function (state) { self.update(state); });
- this.update();
- },
- update: function (state) {
- if (state === 'error') {
- var m = /^([a-z0-9]+)-[a-z0-9]+-[0-9]+$/.exec(this.battle.id);
- if (m) {
- this.battle.log('This replay was uploaded from a third-party server (' + BattleLog.escapeHTML(m[1]) + '). It contains errors.
Replays uploaded from third-party servers can contain errors if the server is running custom code, or the server operator has otherwise incorrectly configured their server.
', true);
- }
- return;
- }
-
- if (BattleSound.muted && !this.muted) this.changeSetting('sound', 'off');
-
- if (this.battle.paused) {
- var resetDisabled = !this.battle.started ? ' disabled' : '';
- this.$('.replay-controls').html(' Play Reset Last turn Next turn Go to turn... Switch sides ');
- } else {
- this.$('.replay-controls').html(' Pause Reset Last turn Next turn Go to turn... Switch sides ');
- }
- },
- clickReplayDownloadButton: function (e) {
- var filename = (this.battle.tier || 'Battle').replace(/[^A-Za-z0-9]/g, '');
-
- // ladies and gentlemen, JavaScript dates
- var timestamp = parseInt(this.$('.uploaddate').data('timestamp'), 10) * 1000;
- var date = new Date(timestamp);
- filename += '-' + date.getFullYear();
- filename += (date.getMonth() >= 9 ? '-' : '-0') + (date.getMonth() + 1);
- filename += (date.getDate() >= 10 ? '-' : '-0') + date.getDate();
-
- filename += '-' + toID(this.battle.p1.name);
- filename += '-' + toID(this.battle.p2.name);
-
- e.currentTarget.href = BattleLog.createReplayFileHref(this);
- e.currentTarget.download = filename + '.html';
-
- e.stopPropagation();
- },
- pause: function() {
- this.battle.pause();
- },
- play: function() {
- this.battle.play();
- },
- reset: function() {
- this.battle.reset();
- },
- ff: function() {
- this.battle.skipTurn();
- },
- rewind: function() {
- this.battle.seekBy(-1);
- },
- ffto: function() {
- var turn = prompt('Turn?');
- if (!turn.trim()) return;
- if (turn === 'e' || turn === 'end' || turn === 'f' || turn === 'finish') turn = Infinity;
- turn = Number(turn);
- if (isNaN(turn) || turn < 0) alert("Invalid turn");
- this.battle.seekTurn(turn);
- },
- switchViewpoint: function() {
- this.battle.switchViewpoint();
- },
- remove: function() {
- this.battle.destroy();
- Panels.StaticPanel.prototype.remove.call(this);
- },
-});
-
-var App = Panels.App.extend({
- topbarView: Topbar,
- states: {
- '*path': ReplaySidebarPanel, // catch-all default
-
- ':replay': ReplayPanel,
- ':replay/manage': ReplayPanel,
- 'search': ReplaySidebarPanel,
- 'search/': ReplaySidebarPanel,
- 'search:query': ReplaySidebarPanel,
- 'search/:query': ReplaySidebarPanel
- },
-});
-var app = new App();
diff --git a/replay.pokemonshowdown.com/src/replays-battle.tsx b/replay.pokemonshowdown.com/src/replays-battle.tsx
index aa5f1cf35..93ebac422 100644
--- a/replay.pokemonshowdown.com/src/replays-battle.tsx
+++ b/replay.pokemonshowdown.com/src/replays-battle.tsx
@@ -56,7 +56,7 @@ class BattleLogDiv extends preact.Component {
}
}
-export class BattlePanel extends preact.Component<{ id: string }> {
+export class BattlePanel extends preact.Component<{ id: string, user: PSReplays['user'] }> {
result: {
uploadtime: number,
id: string,
@@ -66,9 +66,12 @@ export class BattlePanel extends preact.Component<{ id: string }> {
views: number,
rating: number,
private: number,
- password: string,
+ password: string | null,
} | null | undefined = undefined;
resultError = '';
+ privacySaving: number | null = null;
+ manageError = '';
+ manageOpen = false;
battle!: Battle | null;
/** debug purposes */
lastUsedKeyCode = '0';
@@ -81,17 +84,28 @@ export class BattlePanel extends preact.Component<{ id: string }> {
}
override componentWillReceiveProps(nextProps: this['props']) {
if (this.stripQuery(this.props.id) !== this.stripQuery(nextProps.id)) {
+ if (this.replayID(this.props.id) !== this.replayID(nextProps.id)) {
+ this.manageOpen = false;
+ }
this.loadBattle(nextProps.id);
}
}
stripQuery(id: string) {
return id.includes('?') ? id.slice(0, id.indexOf('?')) : id;
}
+ replayID(id: string) {
+ const fullid = this.stripQuery(id);
+ if (!fullid.endsWith('pw')) return fullid;
+ const passwordIndex = fullid.lastIndexOf('-');
+ return passwordIndex > 0 ? fullid.slice(0, passwordIndex) : fullid;
+ }
loadBattle(id: string) {
if (this.battle) this.battle.destroy();
this.battle = null;
this.result = undefined;
this.resultError = '';
+ this.privacySaving = null;
+ this.manageError = '';
this.forceUpdate();
const elem = document.getElementById(`replaydata-${id}`);
@@ -367,9 +381,97 @@ export class BattlePanel extends preact.Component<{ id: string }> {
e?.preventDefault();
this.forceUpdate();
};
- renderError(position: any) {
+ manageURL(extension: 'json' | 'log' | 'inputlog') {
+ return `${Net.defaultRoute}/${this.stripQuery(this.props.id)}.${extension}?manage`;
+ }
+ toggleManage = () => {
+ this.manageOpen = !this.manageOpen;
+ this.forceUpdate();
+ };
+ currentPrivacy() {
+ if (!this.result) return 0;
+ if (this.result.private === 1 && !this.result.password) return 2;
+ return this.result.private;
+ }
+ shareURL() {
+ if (!this.result) return '';
+ const fullid = this.result.id + (this.result.password ? `-${this.result.password}pw` : '');
+ return `https://psim.us/r/${fullid}`;
+ }
+ selectShareURL = (e: Event) => {
+ (e.currentTarget as HTMLInputElement).select();
+ };
+ changePrivacy = (e: Event) => {
+ const privateValue = Number((e.target as HTMLSelectElement).value);
+ if (!this.result || this.privacySaving !== null) return;
+ if (privateValue === 3 && !confirm("Delete this replay?")) return;
+
+ const replay = this.result;
+ this.privacySaving = privateValue;
+ this.manageError = '';
+ this.forceUpdate();
+
+ Net(`/api/replays/edit`).post({}, {
+ id: replay.id,
+ private: privateValue,
+ }).then(resultText => {
+ if (resultText.startsWith(']')) resultText = resultText.slice(1);
+ const result = JSON.parse(resultText);
+ if (result.actionerror) throw new Error(result.actionerror);
+ if (this.result !== replay) return;
+
+ replay.private = privateValue;
+ replay.password = result.password || null;
+ this.privacySaving = null;
+
+ const fullid = replay.id + (replay.password ? `-${replay.password}pw` : '');
+ PSRouter.replace(fullid);
+ PSRouter.update();
+ }).catch(error => {
+ if (this.result !== replay) return;
+ this.privacySaving = null;
+ this.manageError = error instanceof Error ? error.message : String(error);
+ this.forceUpdate();
+ });
+ };
+ renderManagement() {
+ if (!this.manageOpen) return null;
+ if (!this.props.user?.isLeader) {
+ return You do not have permission to manage this replay.
;
+ }
+ const privacy = this.currentPrivacy();
+ return
+
+ Close
+
+ Manage replay
+
+ Privacy: {}
+
+ Public
+
+
+ Private
+
+
+ Private (no password)
+
+
+ Deleted
+ {}
+ {this.privacySaving !== null && Saving... }
+
+ {this.manageError && {this.manageError}
}
+
+ JSON {}
+ Log {}
+ Input log
+
+ ;
+ }
+ renderError() {
if (this.resultError) {
- return
+ return
Error
@@ -383,7 +485,7 @@ export class BattlePanel extends preact.Component<{ id: string }> {
// never link to a nonexistent replay, but this might happen if e.g.
// a replay gets deleted or made private after you searched for it
// but before you clicked it.
- return
+ return
@@ -506,36 +608,48 @@ export class BattlePanel extends preact.Component<{ id: string }> {
{this.result ?
- {this.result.format} : {this.result.players.join(' vs. ')}
+ {this.result.format} : {}
+ {!!this.result.private && } {this.result.players.join(' vs. ')}
:
Loading...
}
+ {!!this.result?.private &&
+ PRIVATE - make sure you have the owner's permission to share
+
}
+
+
+ Short URL:
+
+
{this.result ?
-
- Download
-
+
+ {this.props.user?.isLeader &&
+ Manage
+ } {}
+
+ Download
+
+
{this.result.uploadtime ? new Date(this.result.uploadtime * 1000).toDateString() : "Unknown upload date"}
{this.result.rating ? [` | `, Rating: , ` ${this.result.rating}`] : ''}
{/* {} {this.keyCode} */}
:
}
+ {this.renderManagement()}
{!PSRouter.showingLeft() &&
More replays
}
;
}
override render() {
- let position: any = {};
- if (PSRouter.showingLeft()) {
- if (PSRouter.stickyRight) {
- position = { position: 'sticky', top: '0' };
- } else {
- position = { position: 'sticky', bottom: '0' };
- }
- }
+ if (this.result === null) return this.renderError();
- if (this.result === null) return this.renderError(position);
-
- return
+ return
diff --git a/replay.pokemonshowdown.com/src/replays-index.tsx b/replay.pokemonshowdown.com/src/replays-index.tsx
index 4a7e37e72..778b35fa8 100644
--- a/replay.pokemonshowdown.com/src/replays-index.tsx
+++ b/replay.pokemonshowdown.com/src/replays-index.tsx
@@ -1,6 +1,6 @@
/** @jsx preact.h */
import preact from '../../play.pokemonshowdown.com/js/lib/preact';
-import { PSRouter } from "./replays";
+import { type PSReplays, PSRouter } from "./replays";
import { Net } from './utils';
type ID = Lowercase
;
@@ -37,7 +37,7 @@ function ReplayLink(props: {
;
}
-export class SearchPanel extends preact.Component<{ id: string }> {
+export class SearchPanel extends preact.Component<{ id: string, user: PSReplays['user'] }> {
results: ReplayResult[] | null = null;
resultError: string | null = null;
format = '';
@@ -45,17 +45,8 @@ export class SearchPanel extends preact.Component<{ id: string }> {
isPrivate = false;
byRating = false;
page = 1;
- loggedInUser: string | null = null;
- loggedInUserIsSysop = false;
sort = 'date';
override componentDidMount() {
- if (!Net.defaultRoute) Net(`/api/replays/check-login`).get().then(result => {
- if (!result.startsWith(']')) return;
- const [userid, sysop] = result.slice(1).split(',');
- this.loggedInUser = userid;
- this.loggedInUserIsSysop = !!sysop;
- this.forceUpdate();
- });
this.updateSearch(Net.decodeQuery(this.props.id));
}
override componentDidUpdate(previousProps: this['props']) {
@@ -171,8 +162,8 @@ export class SearchPanel extends preact.Component<{ id: string }> {
this.search('', '');
};
searchLoggedIn = (e: Event) => {
- if (!this.loggedInUser) return; // shouldn't happen
- this.base!.querySelector('input[name=user]')!.value = this.loggedInUser;
+ if (!this.props.user) return; // shouldn't happen
+ this.base!.querySelector('input[name=user]')!.value = this.props.user.id;
this.submitForm(e);
};
url(replay: ReplayResult) {
@@ -214,8 +205,10 @@ export class SearchPanel extends preact.Component<{ id: string }> {
Username: (separate multiple usernames by commas)
{}
- {this.loggedInUser &&
- {this.loggedInUser}'s replays }
+ {this.props.user &&
+
+ {this.props.user.id}'s replays
+ }
diff --git a/replay.pokemonshowdown.com/src/replays.tsx b/replay.pokemonshowdown.com/src/replays.tsx
index 0a4983852..966b7e818 100644
--- a/replay.pokemonshowdown.com/src/replays.tsx
+++ b/replay.pokemonshowdown.com/src/replays.tsx
@@ -3,6 +3,7 @@ import preact from '../../play.pokemonshowdown.com/js/lib/preact';
import { Net, PSModel } from './utils';
import { BattlePanel } from './replays-battle';
import { SearchPanel } from './replays-index';
+import type { ID } from '../../play.pokemonshowdown.com/src/battle-dex';
declare const Config: any;
export const PSRouter = new class extends PSModel {
@@ -10,7 +11,7 @@ export const PSRouter = new class extends PSModel {
leftLoc: string | null = null;
rightLoc: string | null = null;
forceSinglePanel = false;
- stickyRight = true;
+ shortViewport = false;
constructor() {
super();
const baseLocSlashIndex = document.location.href.lastIndexOf('/');
@@ -39,15 +40,16 @@ export const PSRouter = new class extends PSModel {
}
setSinglePanel(init?: boolean) {
const singlePanel = window.innerWidth < 1300;
- const stickyRight = (window.innerHeight > 614);
- if (this.forceSinglePanel !== singlePanel || this.stickyRight !== stickyRight) {
+ const shortViewport = window.innerHeight <= 660;
+ if (this.forceSinglePanel !== singlePanel || this.shortViewport !== shortViewport) {
this.forceSinglePanel = singlePanel;
- this.stickyRight = stickyRight;
+ this.shortViewport = shortViewport;
if (!init) this.update();
}
}
push(href: string): boolean {
if (!href.startsWith(this.baseLoc)) return false;
+ if (href.includes('.json') || href.includes('.log') || href.includes('.inputlog')) return false;
if (this.go(href)) {
window.history?.pushState([this.leftLoc, this.rightLoc], '', href);
@@ -95,6 +97,7 @@ export const PSRouter = new class extends PSModel {
export class PSReplays extends preact.Component {
static darkMode: 'dark' | 'light' | 'auto' = 'auto';
+ user: { id: ID, isLeader: boolean } | null = null;
static updateDarkMode() {
let dark = this.darkMode === 'dark' ? 'dark' : '';
if (this.darkMode === 'auto') {
@@ -122,15 +125,21 @@ export class PSReplays extends preact.Component {
const data = JSON.parse(response);
Object.assign(Config.customcolors, data);
});
+ if (!Net.defaultRoute) Net(`/api/replays/check-login`).get().then(result => {
+ if (!result.startsWith(']')) return;
+ const [userid, isLeader] = result.slice(1).split(',');
+ this.user = { id: userid as ID, isLeader: !!isLeader };
+ this.forceUpdate();
+ });
}
override render() {
- const position = PSRouter.showingLeft() && PSRouter.showingRight() && !PSRouter.stickyRight ?
- { display: 'flex', flexDirection: 'column', justifyContent: 'flex-end' } : {};
- return
- {PSRouter.showingLeft() &&
}
- {PSRouter.showingRight() &&
}
+ const twoColumn = PSRouter.showingLeft() && PSRouter.showingRight();
+ const className = `bar-wrapper${twoColumn ? ' has-sidebar' : ''}${
+ twoColumn && PSRouter.shortViewport ? ' short-viewport' : ''
+ }`;
+ return
+ {PSRouter.showingLeft() &&
}
+ {PSRouter.showingRight() &&
}
;
}
diff --git a/replay.pokemonshowdown.com/testclient.html b/replay.pokemonshowdown.com/testclient.html
index ddb9f3282..2d0d59254 100644
--- a/replay.pokemonshowdown.com/testclient.html
+++ b/replay.pokemonshowdown.com/testclient.html
@@ -62,6 +62,30 @@
.mainbar.has-sidebar {
margin-left: 330px;
}
+ .bar-wrapper.has-sidebar:not(.short-viewport) .mainbar {
+ position: sticky;
+ top: 0;
+ }
+ .bar-wrapper.has-sidebar.short-viewport {
+ max-width: none;
+ }
+ .bar-wrapper.has-sidebar.short-viewport .sidebar {
+ box-sizing: border-box;
+ position: sticky;
+ top: 0;
+ max-height: 100vh;
+ overflow-y: auto;
+ }
+ @media (min-width: 1431px) and (max-width: 1510px) {
+ .bar-wrapper.has-sidebar.short-viewport .sidebar {
+ width: calc(50% - 395px);
+ padding-left: calc(50% - 715px);
+ }
+ .bar-wrapper.has-sidebar.short-viewport .mainbar {
+ margin-right: calc(50% - 715px);
+ margin-left: calc(50% - 385px);
+ }
+ }
@media (min-width: 1511px) {
.sidebar {
width: 400px;
@@ -72,6 +96,14 @@
.mainbar.has-sidebar {
margin-left: 410px;
}
+ .bar-wrapper.has-sidebar.short-viewport .sidebar {
+ width: calc(50% - 355px);
+ padding-left: calc(50% - 755px);
+ }
+ .bar-wrapper.has-sidebar.short-viewport .mainbar {
+ margin-right: calc(50% - 755px);
+ margin-left: calc(50% - 345px);
+ }
}
.section.first-section {
margin-top: 9px;
diff --git a/replay.pokemonshowdown.com/theme/panels.lib.php b/replay.pokemonshowdown.com/theme/panels.lib.php
deleted file mode 100644
index f1731eb73..000000000
--- a/replay.pokemonshowdown.com/theme/panels.lib.php
+++ /dev/null
@@ -1,77 +0,0 @@
-root = '/';
- $this->name = 'Showdown';
-
- switch (@$_REQUEST['output']) {
- case 'html':
- case 'json':
- $this->output = $_REQUEST['output'];
- break;
- }
- }
-
- var $pagetitle = false;
- function setPageTitle($title) {
- if ($this->pagetitle === false) {
- $this->pagetitle = $title;
- }
- }
-
- var $pagedescription = false;
- function setPageDescription($description) {
- if ($this->pagedescription === false) {
- $this->pagedescription = $description;
- }
- }
-
- function setTab($tab) {
- if ($this->tab === false) {
- $this->tab = $tab;
- }
- }
-
- var $depth = 0;
- var $scriptsIncluded = false;
- function start() {
- $this->depth++;
- if ($this->depth != 1) {
- return;
- }
- if ($this->output === 'normal') {
- ThemeHeaderTemplate();
- }
- }
- function noScripts() {
- $this->scriptsIncluded = true;
- }
- function scripts() {
- if ($this->output !== 'normal') return;
- if ($this->scriptsIncluded) return;
- $this->scriptsIncluded = true;
- ThemeScriptsTemplate();
- }
- function end() {
- $this->depth--;
- if ($this->depth != 0) {
- return;
- }
- if ($this->output === 'normal') {
- ThemeFooterTemplate();
- }
- }
-}
-
-$panels = new Panels();
diff --git a/replay.pokemonshowdown.com/theme/wrapper.inc.template.php b/replay.pokemonshowdown.com/theme/wrapper.inc.template.php
deleted file mode 100644
index 876f722e9..000000000
--- a/replay.pokemonshowdown.com/theme/wrapper.inc.template.php
+++ /dev/null
@@ -1,110 +0,0 @@
-
-
-
-
-
-
-
pagetitle) echo htmlspecialchars($panels->pagetitle).' - '; ?>Pokémon Showdown
-
-pagedescription) { ?>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-scripts(); ?>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-