From 4fd162415a9acd4fdcf3d26808e33b2525881e00 Mon Sep 17 00:00:00 2001 From: Ivan Batsulin Date: Thu, 23 Apr 2026 21:38:07 +0300 Subject: [PATCH] Add fullscreen support: HUD button (desktop/Android) + iOS Add to Home Screen banner (#3688) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves #3685 ## Description: Adds fullscreen support for both desktop and mobile: **Desktop / Android** — a fullscreen toggle button in the in-game HUD (right sidebar), next to the settings button. Icon switches between expand/compress depending on current state, synced with `fullscreenchange` event (works with F11 too). Hidden on browsers that don't support `document.fullscreenEnabled`. **iOS** — since Safari doesn't support the Fullscreen API, a dismissible banner is shown on the main screen (above the lobby cards) explaining how to add the game to the Home Screen for a fullscreen experience. The banner includes: - **How** button — opens a step-by-step guide modal with iOS version detection (iOS 26+ shows updated steps for the new ··· menu location, including the extra Share step inside the menu) - **Later** — hides until next visit - **Never** — permanently dismisses via localStorage - **Click here** button styled as a speech bubble with a tail pointing toward the Share button location (center for iOS ≤18, right for iOS 26+) All user-facing strings are wired through `translateText()` with keys added to `en.json`. ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## UI changes: ### For [Fullscreen API supported browsers](https://caniuse.com/?search=fullscreen+api): https://github.com/user-attachments/assets/026e6a67-d070-4a7e-897b-52396a43191e ### For safari on ios: (add to homescreen modal) IMG_2242 CleanShot 2026-04-22 at 11 29 27@2x ## Please put your Discord username so you can be contacted if a bug or regression is found: fghjk_60845 --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .gitignore | 1 + resources/images/ExitFullscreenIconWhite.svg | 6 + resources/images/FullscreenIconWhite.svg | 6 + resources/lang/en.json | 17 ++ src/client/GameModeSelector.ts | 4 + .../components/IOSAddToHomeScreenBanner.ts | 195 ++++++++++++++++++ .../graphics/layers/GameRightSidebar.ts | 48 +++++ 7 files changed, 277 insertions(+) create mode 100644 resources/images/ExitFullscreenIconWhite.svg create mode 100644 resources/images/FullscreenIconWhite.svg create mode 100644 src/client/components/IOSAddToHomeScreenBanner.ts 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(); + }} + > +
+ +
+
+ `; + } + + 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 { settings + ${document.fullscreenEnabled + ? html`
+ ${this.isFullscreen +
` + : ""} +
exit