Skip to content

Commit 357df4b

Browse files
committed
Allow Popping Out the Timer Into a Separate Window
This adds a new button to the sidebar that allows the user to pop out the entire layout into a separate child window. This makes it take up less of the screen and capture it more easily in OBS. I'm not sure if the button should be in the sidebar or not, but it's good enough for an initial implementation. Changelog: You can now pop out the timer into a separate window.
1 parent 88b874d commit 357df4b

File tree

16 files changed

+203
-47
lines changed

16 files changed

+203
-47
lines changed

src/css/variables.icss.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,5 @@ $selected-row-hover-color: linear-gradient(
4545
largeMargin: $ui-large-margin;
4646
manualGameTimeHeight: $manual-game-time-height;
4747
contributorAvatarSize: $contributor-avatar-size;
48+
mainBackgroundColor: $main-background-color;
4849
}

src/index.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,7 @@ try {
6969
// as otherwise information may be cached incorrectly.
7070
try {
7171
const promises = [];
72-
// TypeScript doesn't seem to know that the fonts are iterable.
73-
for (const fontFace of document.fonts as any as Iterable<FontFace>) {
72+
for (const fontFace of document.fonts) {
7473
if (fontFace.family === "timer" || fontFace.family === "fira") {
7574
promises.push(fontFace.load());
7675
}

src/platform/Hotkeys.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { CommandSinkRef, HotkeyConfig, HotkeySystem } from "../livesplit-core";
22
import { expect } from "../util/OptionUtil";
33

44
export interface HotkeyImplementation {
5+
ptr: number;
56
config(): Promise<HotkeyConfig> | HotkeyConfig;
67
setConfig(config: HotkeyConfig): void;
78
activate(): void;
@@ -10,7 +11,10 @@ export interface HotkeyImplementation {
1011
}
1112

1213
class GlobalHotkeys implements HotkeyImplementation {
13-
constructor(private hotkeySystem?: HotkeySystem) {}
14+
public ptr: number;
15+
constructor(private hotkeySystem?: HotkeySystem) {
16+
this.ptr = hotkeySystem?.ptr ?? 0;
17+
}
1418

1519
public async config(): Promise<HotkeyConfig> {
1620
return expect(

src/type-definitions/assets.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
declare module "*.scss";
2+
declare module "*.css";
3+
declare module "*.woff";
4+
declare module "*.svg";
5+
declare module "*.wasm";

src/type-definitions/images.d.ts

Lines changed: 0 additions & 1 deletion
This file was deleted.

src/type-definitions/scss.d.ts

Lines changed: 0 additions & 2 deletions
This file was deleted.

src/type-definitions/wasm.d.ts

Lines changed: 0 additions & 1 deletion
This file was deleted.

src/ui/LiveSplit.tsx

Lines changed: 120 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ import {
1313
TimingMethod,
1414
TimerPhase,
1515
Event,
16+
LayoutRefMut,
1617
} from "../livesplit-core";
18+
import { Layout as ShowLayout } from "./components/Layout";
1719
import {
1820
FILE_EXT_LAYOUTS,
1921
convertFileToArrayBuffer,
@@ -41,15 +43,24 @@ import { LayoutView } from "./views/LayoutView";
4143
import { ToastContainer, toast } from "react-toastify";
4244
import * as Storage from "../storage";
4345
import { UrlCache } from "../util/UrlCache";
44-
import { ServerProtocol, WebRenderer } from "../livesplit-core/livesplit_core";
46+
import {
47+
HotkeySystem_add_window,
48+
ServerProtocol,
49+
WebRenderer,
50+
} from "../livesplit-core/livesplit_core";
4551
import { LiveSplitServer } from "../api/LiveSplitServer";
4652
import { LSOCommandSink } from "../util/LSOCommandSink";
4753
import { DialogContainer } from "./components/Dialog";
4854
import { createHotkeys, HotkeyImplementation } from "../platform/Hotkeys";
4955
import { Menu } from "lucide-react";
56+
import { createRoot } from "react-dom/client";
5057

5158
import * as variables from "../css/variables.icss.scss";
5259

60+
import LiveSplitIcon from "../assets/icon.svg";
61+
import timerFont from "../css/timer.woff";
62+
import firaFont from "../css/FiraSans-Regular.woff";
63+
5364
import "react-toastify/dist/ReactToastify.css";
5465
import * as classes from "../css/LiveSplit.module.scss";
5566
import * as sidebarClasses from "../css/Sidebar.module.scss";
@@ -1068,4 +1079,112 @@ export class LiveSplit extends React.Component<Props, State> {
10681079
document.title = "LiveSplit One";
10691080
}
10701081
}
1082+
1083+
popOut(): void {
1084+
const { layoutWidth, layoutHeight, generalSettings } = this.state;
1085+
popOut(
1086+
this.state.commandSink,
1087+
() => this.state.layout,
1088+
generalSettings,
1089+
layoutWidth,
1090+
layoutHeight,
1091+
);
1092+
}
1093+
}
1094+
1095+
async function popOut(
1096+
commandSink: LSOCommandSink,
1097+
getLayout: () => LayoutRefMut,
1098+
generalSettings: GeneralSettings,
1099+
width: number,
1100+
height: number,
1101+
) {
1102+
const childWindow = window.open(
1103+
"",
1104+
"_blank",
1105+
`popup,width=${width},height=${height}`,
1106+
);
1107+
if (!childWindow) {
1108+
return;
1109+
}
1110+
1111+
const childDoc = childWindow.document;
1112+
1113+
childDoc.title = "LiveSplit One";
1114+
1115+
const link = childDoc.createElement("link");
1116+
link.rel = "icon";
1117+
link.type = "image/svg+xml";
1118+
link.href = LiveSplitIcon;
1119+
childDoc.head.appendChild(link);
1120+
1121+
childDoc.body.style.margin = "0";
1122+
childDoc.body.style.background = variables.mainBackgroundColor;
1123+
1124+
const timerFontFace = new FontFace("timer", `url(${timerFont})`);
1125+
const firaFontFace = new FontFace("fira", `url(${firaFont})`);
1126+
childDoc.fonts.add(timerFontFace);
1127+
childDoc.fonts.add(firaFontFace);
1128+
await Promise.all([timerFontFace.load(), firaFontFace.load()]);
1129+
1130+
const layoutState = LayoutState.new();
1131+
const urlCache = new UrlCache();
1132+
1133+
const renderer = new WebRenderer();
1134+
childWindow.addEventListener("unload", async () => {
1135+
async function sleep(ms: number) {
1136+
return new Promise((resolve) => setTimeout(resolve, ms));
1137+
}
1138+
while (!childWindow.closed) {
1139+
await sleep(1);
1140+
}
1141+
renderer.free();
1142+
layoutState[Symbol.dispose]();
1143+
urlCache.imageCache[Symbol.dispose]();
1144+
});
1145+
1146+
const element = renderer.element();
1147+
element.style.width = "100%";
1148+
element.style.height = "100%";
1149+
1150+
if (hotkeySystem) HotkeySystem_add_window(hotkeySystem.ptr, childWindow);
1151+
1152+
createRoot(childDoc.body).render(
1153+
<ShowLayout
1154+
getState={() => {
1155+
const layout = getLayout();
1156+
if (layout.ptr !== 0) {
1157+
commandSink.updateLayoutState(
1158+
layout,
1159+
layoutState,
1160+
urlCache.imageCache,
1161+
);
1162+
urlCache.collect();
1163+
}
1164+
return layoutState;
1165+
}}
1166+
layoutUrlCache={urlCache}
1167+
allowResize={false}
1168+
width="100%"
1169+
height="100%"
1170+
generalSettings={generalSettings}
1171+
renderer={renderer}
1172+
onResize={(width, height) =>
1173+
childWindow.resizeTo(
1174+
width + childWindow.outerWidth - childWindow.innerWidth,
1175+
height + childWindow.outerHeight - childWindow.innerHeight,
1176+
)
1177+
}
1178+
onScroll={(e) => {
1179+
const delta = Math.sign(-e.deltaY);
1180+
const layout = getLayout();
1181+
if (delta === 1) {
1182+
layout.scrollUp();
1183+
} else if (delta === -1) {
1184+
layout.scrollDown();
1185+
}
1186+
}}
1187+
window={childWindow}
1188+
/>,
1189+
);
10711190
}

src/ui/components/Layout.tsx

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { GeneralSettings } from "../views/MainSettings";
88

99
import * as classes from "../../css/Layout.module.scss";
1010

11-
export default function Layout({
11+
export function Layout({
1212
getState,
1313
layoutUrlCache,
1414
allowResize,
@@ -17,16 +17,24 @@ export default function Layout({
1717
generalSettings,
1818
renderer,
1919
onResize,
20+
onScroll,
21+
window,
2022
}: {
2123
getState: () => LayoutStateRef;
2224
layoutUrlCache: UrlCache;
23-
allowResize: boolean;
24-
width: number;
25-
height: number;
2625
generalSettings: GeneralSettings;
2726
renderer: WebRenderer;
2827
onResize: (width: number, height: number) => void;
29-
}) {
28+
onScroll?: (e: WheelEvent) => void;
29+
window: Window;
30+
} & (
31+
| {
32+
allowResize: false;
33+
width: string | number;
34+
height: string | number;
35+
}
36+
| { allowResize: true; width: number; height: number }
37+
)) {
3038
const update = () => {
3139
const layoutState = getState();
3240
const newDims = renderer.render(
@@ -39,12 +47,19 @@ export default function Layout({
3947
};
4048

4149
return (
42-
<AutoRefresh frameRate={generalSettings.frameRate} update={update}>
50+
<AutoRefresh
51+
frameRate={generalSettings.frameRate}
52+
update={update}
53+
window={window}
54+
>
4355
<div style={{ width, height }}>
4456
<div
4557
style={{ width, height }}
4658
ref={(element) => {
4759
element?.appendChild(renderer.element());
60+
if (onScroll) {
61+
element?.addEventListener("wheel", onScroll);
62+
}
4863
}}
4964
/>
5065
{allowResize && (

0 commit comments

Comments
 (0)