diff --git a/.gitignore b/.gitignore
index 4d11d0efa..43027469e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,6 +10,7 @@ resources/.DS_Store
.DS_Store
.clinic/
CLAUDE.md
+NOTES.md
.claude/
.idea/
# this is autogenerated by script
diff --git a/resources/images/ExitFullscreenIconWhite.svg b/resources/images/ExitFullscreenIconWhite.svg
new file mode 100644
index 000000000..b01820a2c
--- /dev/null
+++ b/resources/images/ExitFullscreenIconWhite.svg
@@ -0,0 +1,6 @@
+
diff --git a/resources/images/FullscreenIconWhite.svg b/resources/images/FullscreenIconWhite.svg
new file mode 100644
index 000000000..7327db0f3
--- /dev/null
+++ b/resources/images/FullscreenIconWhite.svg
@@ -0,0 +1,6 @@
+
diff --git a/resources/lang/en.json b/resources/lang/en.json
index e22b467c8..ae23fb7b2 100644
--- a/resources/lang/en.json
+++ b/resources/lang/en.json
@@ -1087,5 +1087,22 @@
"dismiss": "Dismiss",
"go_to_item": "Go to item {num}",
"firefox_warning": "OpenFront.io doesn't perform well with [Firefox-based browsers](https://simple.wikipedia.org/wiki/Web_browsers_based_on_Firefox). We recommend you to use a [Chromium-based browser](https://en.wikipedia.org/wiki/Chromium_(web_browser)#Browsers_based_on_Chromium) for best performance."
+ },
+ "ios_banner": {
+ "text": "For fullscreen, add OpenFront to your Home Screen",
+ "how": "How",
+ "later": "Later",
+ "never": "Never",
+ "modal_title": "Fullscreen on iOS",
+ "modal_desc": "Your browser doesn't support fullscreen, but you can add OpenFront to your Home Screen for an immersive experience:",
+ "step_share": "Tap the Share button in your browser",
+ "step_scroll_and_tap": "Scroll down and tap",
+ "step_add_to_home_label": "Add to Home Screen",
+ "step_open": "Open from your Home Screen — launches fullscreen without the address bar",
+ "got_it": "Got it"
+ },
+ "fullscreen": {
+ "enter": "Enter fullscreen",
+ "exit": "Exit fullscreen"
}
}
diff --git a/src/client/GameModeSelector.ts b/src/client/GameModeSelector.ts
index 3f75576b3..3ed313913 100644
--- a/src/client/GameModeSelector.ts
+++ b/src/client/GameModeSelector.ts
@@ -10,6 +10,7 @@ import {
Trios,
} from "../core/game/Game";
import { PublicGameInfo, PublicGames } from "../core/Schemas";
+import "./components/IOSAddToHomeScreenBanner";
import { crazyGamesSDK } from "./CrazyGamesSDK";
import { HostLobbyModal } from "./HostLobbyModal";
import { JoinLobbyModal } from "./JoinLobbyModal";
@@ -142,6 +143,9 @@ export class GameModeSelector extends LitElement {
"bg-slate-600 hover:bg-slate-500 active:bg-slate-700",
)}
+
+
+
{
+ if (e.target === e.currentTarget) this.closeGuide();
+ }}
+ >
+
+
+
+
+ ${translateText("ios_banner.modal_title")}
+
+
+
+
+
+ ${translateText("ios_banner.modal_desc")}
+
+
+
+ -
+ 1
+ ${translateText("ios_banner.step_share")}
+
+ -
+ 2
+ ${translateText("ios_banner.step_scroll_and_tap")}
+ ${translateText(
+ "ios_banner.step_add_to_home_label",
+ )}
+
+ -
+ 3
+ ${translateText("ios_banner.step_open")}
+
+
+
+
+
+
+
+ `;
+ }
+
+ render() {
+ if (!Platform.isIOS) return nothing;
+ if (this.dismissed || this.later) return nothing;
+ if (
+ (navigator as any).standalone === true ||
+ window.matchMedia("(display-mode: standalone)").matches
+ ) {
+ return nothing;
+ }
+
+ return html`
+ ${this.renderGuideModal()}
+
+
+
+ ${translateText("ios_banner.text")}
+
+
+
+
+
+
+
+
+ `;
+ }
+}
diff --git a/src/client/graphics/layers/GameRightSidebar.ts b/src/client/graphics/layers/GameRightSidebar.ts
index 0daf3f323..1ec12be84 100644
--- a/src/client/graphics/layers/GameRightSidebar.ts
+++ b/src/client/graphics/layers/GameRightSidebar.ts
@@ -18,6 +18,8 @@ const FastForwardIconSolid = assetUrl("images/FastForwardIconSolidWhite.svg");
const pauseIcon = assetUrl("images/PauseIconWhite.svg");
const playIcon = assetUrl("images/PlayIconWhite.svg");
const settingsIcon = assetUrl("images/SettingIconWhite.svg");
+const fullscreenIcon = assetUrl("images/FullscreenIconWhite.svg");
+const exitFullscreenIcon = assetUrl("images/ExitFullscreenIconWhite.svg");
@customElement("game-right-sidebar")
export class GameRightSidebar extends LitElement implements Layer {
@@ -36,6 +38,9 @@ export class GameRightSidebar extends LitElement implements Layer {
@state()
private isPaused: boolean = false;
+ @state()
+ private isFullscreen: boolean = false;
+
@state()
private timer: number = 0;
@@ -80,6 +85,21 @@ export class GameRightSidebar extends LitElement implements Layer {
this.requestUpdate();
}
+ private onFullscreenChange = () => {
+ this.isFullscreen = !!document.fullscreenElement;
+ };
+
+ connectedCallback() {
+ super.connectedCallback();
+ document.addEventListener("fullscreenchange", this.onFullscreenChange);
+ this.onFullscreenChange();
+ }
+
+ disconnectedCallback() {
+ super.disconnectedCallback();
+ document.removeEventListener("fullscreenchange", this.onFullscreenChange);
+ }
+
getTickIntervalMs() {
return 250;
}
@@ -177,6 +197,18 @@ export class GameRightSidebar extends LitElement implements Layer {
);
}
+ private onFullscreenButtonClick() {
+ if (!document.fullscreenElement) {
+ document.documentElement.requestFullscreen().catch((err) => {
+ console.warn("Failed to enter fullscreen:", err);
+ });
+ } else {
+ document.exitFullscreen().catch((err) => {
+ console.warn("Failed to exit fullscreen:", err);
+ });
+ }
+ }
+
render() {
if (this.game === undefined) return html``;
@@ -204,6 +236,22 @@ export class GameRightSidebar extends LitElement implements Layer {
+ ${document.fullscreenEnabled
+ ? html`
+

+
`
+ : ""}
+