Skip to content

Commit 6c60ff0

Browse files
committed
Add fullscreen mode
1 parent 238040c commit 6c60ff0

File tree

4 files changed

+266
-0
lines changed

4 files changed

+266
-0
lines changed

cypress/e2e/settings/fullscreen.cy.ts

+146
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
/// <reference types="cypress" />
2+
3+
import { GameState, GamePersistentState } from "../../../src/game";
4+
import { Preferences } from "../../../src/preferences";
5+
6+
describe("fullscreen", () => {
7+
before(() => {
8+
cy.log(`
9+
NOTE: Fullscreen testing in Cypress is unreliable at the moment.
10+
These tests verify that the function calls are made.
11+
Verify the actual fullscreen functionality manually for now.
12+
`);
13+
});
14+
15+
beforeEach(() => {
16+
cy.visit("/", {
17+
onBeforeLoad: () => {
18+
const gameState: GameState = {
19+
board: [
20+
[0, 0, 0, 0],
21+
[0, 2, 0, 0],
22+
[0, 0, 4, 0],
23+
[0, 0, 0, 0],
24+
],
25+
ended: false,
26+
won: false,
27+
score: 0,
28+
didUndo: false,
29+
achievedHighscore: false,
30+
moveCount: 0,
31+
};
32+
const persistentState: GamePersistentState = {
33+
highscore: 0,
34+
unlockables: {},
35+
hasPlayedBefore: true,
36+
};
37+
const preferences: Preferences = {
38+
theme: "dark",
39+
};
40+
window.localStorage.setItem("game-state", JSON.stringify(gameState));
41+
window.localStorage.setItem("persistent-state", JSON.stringify(persistentState));
42+
window.localStorage.setItem("preferences", JSON.stringify(preferences));
43+
},
44+
});
45+
cy.document().then((doc) => {
46+
cy.stub(doc.documentElement, "requestFullscreen").as("requestFullscreen");
47+
cy.stub(doc, "exitFullscreen").as("exitFullscreen");
48+
});
49+
});
50+
51+
// TODO: Fix issue where using 'f' key toggles Cypress sidebar instead of toggling fullscreen mode
52+
it.skip("should toggle fullscreen mode on and off using 'f' key", () => {
53+
// NOTE: Triggering the keydown event programmatically will not satisfy user-initiated request for fullscreen
54+
// cy.get("body").trigger("keydown", {
55+
// eventConstructor: "KeyboardEvent",
56+
// key: "f",
57+
// });
58+
cy.get("@requestFullscreen").should("not.have.been.called");
59+
cy.get("body").focus().realType("f");
60+
cy.get("@requestFullscreen").should("have.been.called");
61+
cy.window().then((win) => {
62+
cy.stub(win.document, "fullscreenElement").value(win.document.documentElement);
63+
});
64+
cy.get("@exitFullscreen").should("not.have.been.called");
65+
cy.get("body").focus().realType("f");
66+
cy.get("@exitFullscreen").should("have.been.called");
67+
});
68+
69+
it("should toggle fullscreen mode on and off using settings option", () => {
70+
cy.get(".settings-link").click();
71+
cy.get("@requestFullscreen").should("not.have.been.called");
72+
cy.contains("Fullscreen").realClick();
73+
cy.get("@requestFullscreen").should("have.been.called");
74+
cy.window().then((win) => {
75+
cy.stub(win.document, "fullscreenElement").value(win.document.documentElement);
76+
});
77+
cy.get("@exitFullscreen").should("not.have.been.called");
78+
cy.contains("Fullscreen").realClick();
79+
cy.get("@exitFullscreen").should("have.been.called");
80+
});
81+
82+
it("should not show fullscreen prompt if fullscreen preference is disabled", () => {
83+
cy.contains("Fullscreen mode is enabled. Do you want to turn it on?").should("not.exist");
84+
});
85+
86+
describe("fullscreen prompt if fullscreen preference is enabled", () => {
87+
beforeEach(() => {
88+
cy.visit("/", {
89+
onBeforeLoad: (win) => {
90+
const preferences: Preferences = {
91+
theme: "dark",
92+
fullscreen: "enabled",
93+
};
94+
win.localStorage.setItem("preferences", JSON.stringify(preferences));
95+
},
96+
});
97+
98+
cy.document().then((doc) => {
99+
cy.stub(doc.documentElement, "requestFullscreen").as("requestFullscreen");
100+
cy.stub(doc, "exitFullscreen").as("exitFullscreen");
101+
});
102+
103+
cy.contains("Fullscreen mode is enabled. Do you want to turn it on?").should(
104+
"be.visible"
105+
);
106+
});
107+
108+
it("should enable fullscreen if user confirms the prompt", () => {
109+
cy.get("@exitFullscreen").should("not.have.been.called");
110+
cy.get("@requestFullscreen").should("not.have.been.called");
111+
cy.contains("Yes").realClick();
112+
cy.get("@exitFullscreen").should("not.have.been.called");
113+
cy.get("@requestFullscreen").should("have.been.called");
114+
});
115+
116+
it("should not enable fullscreen if user cancels the prompt", () => {
117+
cy.window().then((win) => {
118+
cy.stub(win.document, "fullscreenElement").value(win.document.documentElement);
119+
});
120+
cy.get("@requestFullscreen").should("not.have.been.called");
121+
cy.get("@exitFullscreen").should("not.have.been.called");
122+
cy.contains("Cancel").click();
123+
cy.get("@requestFullscreen").should("not.have.been.called");
124+
cy.get("@exitFullscreen").should("have.been.called");
125+
});
126+
});
127+
128+
it("should show fullscreen option on desktop", () => {
129+
cy.viewport(1024, 768);
130+
cy.get(".settings-link").click();
131+
cy.get(".setting.fullscreen").should("be.visible");
132+
});
133+
134+
it("should hide fullscreen option on phones", () => {
135+
cy.visit("/", {
136+
onBeforeLoad: (win) => {
137+
Object.defineProperty(win.navigator, "userAgent", {
138+
value: "Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.1",
139+
});
140+
},
141+
});
142+
cy.viewport("iphone-6");
143+
cy.get(".settings-link").click();
144+
cy.get(".setting.fullscreen").should("not.be.visible");
145+
});
146+
});

index.html

+11
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,17 @@ <h1>Settings</h1>
146146
</div>
147147
</div>
148148
</div>
149+
<div class="settings-item setting fullscreen">
150+
<div class="settings-item-column">
151+
<span>Fullscreen</span>
152+
</div>
153+
<div class="settings-item-column">
154+
<div class="knob">
155+
<div class="knob-inside">
156+
</div>
157+
</div>
158+
</div>
159+
</div>
149160
<div class="settings-item setting block">
150161
<div class="settings-item-column">
151162
<span>Block Style</span>

src/index.ts

+67
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { UndoManager } from "./manager/undo";
3333
import { AssetManager } from "./manager/asset";
3434
import { formatTilesetName } from "./util/format";
3535
import * as marked from "marked";
36+
import { FullscreenManager } from "./manager/fullscreen";
3637

3738
import "./styles/global.css";
3839

@@ -61,11 +62,13 @@ const ANIMATIONS_PREFERENCE_NAME = "animations";
6162
const BLOCK_STYLE_PREFERENCE_NAME = "block";
6263
const DEBUG_HUD_ENABLED_PREFERENCE_NAME = "debugHudEnabled";
6364
const DEBUG_HUD_VISIBLE_PREFERENCE_NAME = "debugHudVisible";
65+
const FULLSCREEN_PREFERENCE_NAME = "fullscreen";
6466

6567
const THEME_SETTING_NAME = "theme-switch";
6668
const TILESET_SETTING_NAME = "tileset-switch";
6769
const ANIMATIONS_SETTING_NAME = "animations";
6870
const BLOCK_STYLE_SETTING_NAME = "block";
71+
const FULLSCREEN_SETTING_NAME = "fullscreen";
6972
const CLEAR_DATA_SETTING_NAME = "clear-all-data";
7073

7174
const SETTING_ENABLED = "enabled";
@@ -101,6 +104,7 @@ document.addEventListener("DOMContentLoaded", async () => {
101104
let animationManager = new AnimationManager();
102105
let undoManager = new UndoManager();
103106
let gameStorage = new BrowserGameStorage();
107+
let fullscreenManager = new FullscreenManager(gameStorage);
104108
let assetManager = new AssetManager(document.querySelector(".loader-wrapper") as HTMLElement);
105109
// Store unlockable statuses so that their unlock messages don't display again if player achieved the same conditions again
106110
let unlockedClassic = false;
@@ -111,6 +115,9 @@ document.addEventListener("DOMContentLoaded", async () => {
111115

112116
const swipeSensitivity = 50;
113117

118+
const md = new MobileDetect(window.navigator.userAgent);
119+
const isMobile = md.mobile() !== null;
120+
114121
const eventHandler = (event: string, data: any) => {
115122
switch (event) {
116123
case "init":
@@ -292,6 +299,16 @@ document.addEventListener("DOMContentLoaded", async () => {
292299
});
293300
return;
294301
}
302+
if (key === "f" && !isMobile) {
303+
fullscreenManager.toggleFullscreen();
304+
const knob = document.querySelector(".setting.fullscreen .knob") as HTMLElement;
305+
if (fullscreenManager.isFullscreenEnabled()) {
306+
knob.classList.add("enabled");
307+
} else {
308+
knob.classList.remove("enabled");
309+
}
310+
return;
311+
}
295312
if (gameState.ended) {
296313
return;
297314
}
@@ -325,6 +342,14 @@ document.addEventListener("DOMContentLoaded", async () => {
325342
handleKeyInput(e.key.toLowerCase());
326343
});
327344

345+
document.addEventListener("fullscreenchange", () => {
346+
if (!document.fullscreenElement) {
347+
fullscreenManager.setFullscreenPreference(false);
348+
const knob = document.querySelector(".setting.fullscreen .knob") as HTMLElement;
349+
knob.classList.remove("enabled");
350+
}
351+
});
352+
328353
const promptNewGame = (onNewGameStarted?: () => void) => {
329354
// If game ended, no need to prompt
330355
if (gameState.ended) {
@@ -348,6 +373,31 @@ document.addEventListener("DOMContentLoaded", async () => {
348373
});
349374
};
350375

376+
const promptFullscreen = () => {
377+
const dialogElem = createDialogContentFromTemplate("#prompt-dialog-content");
378+
(dialogElem.querySelector(".prompt-text") as HTMLSpanElement).innerText =
379+
"Fullscreen mode is enabled. Do you want to turn it on?";
380+
renderPromptDialog(dialogElem, {
381+
fadeIn: true,
382+
onConfirm: () => {
383+
fullscreenManager.toggleFullscreen(true);
384+
const setting = document.querySelector(
385+
`.setting.${FULLSCREEN_SETTING_NAME}`
386+
) as HTMLElement;
387+
const knob = setting.querySelector(".knob") as HTMLElement;
388+
knob.classList.add("enabled");
389+
},
390+
onCancel: () => {
391+
fullscreenManager.toggleFullscreen(false);
392+
const setting = document.querySelector(
393+
`.setting.${FULLSCREEN_SETTING_NAME}`
394+
) as HTMLElement;
395+
const knob = setting.querySelector(".knob") as HTMLElement;
396+
knob.classList.remove("enabled");
397+
},
398+
});
399+
};
400+
351401
const helpLink = document.querySelector(".help-link") as HTMLElement;
352402
helpLink.addEventListener("click", (e) => {
353403
e.preventDefault();
@@ -594,6 +644,14 @@ document.addEventListener("DOMContentLoaded", async () => {
594644
switchBlockStyle(nextBlockStyle);
595645
savePreferenceValue(BLOCK_STYLE_PREFERENCE_NAME, nextBlockStyle);
596646
toggle.innerText = nextBlockStyle;
647+
} else if (elem.classList.contains(FULLSCREEN_SETTING_NAME)) {
648+
fullscreenManager.toggleFullscreen();
649+
const knob = setting.querySelector(".knob") as HTMLElement;
650+
if (fullscreenManager.isFullscreenEnabled()) {
651+
knob.classList.add("enabled");
652+
} else {
653+
knob.classList.remove("enabled");
654+
}
597655
} else if (elem.classList.contains(CLEAR_DATA_SETTING_NAME)) {
598656
const dialogElem = createDialogContentFromTemplate("#prompt-dialog-content");
599657
(dialogElem.querySelector(".prompt-text") as HTMLSpanElement).innerHTML =
@@ -616,6 +674,12 @@ document.addEventListener("DOMContentLoaded", async () => {
616674
});
617675
});
618676

677+
// Hide fullscreen setting on mobile devices
678+
const fullscreenOption = document.querySelector(".setting.fullscreen") as HTMLElement;
679+
if (isMobile) {
680+
fullscreenOption.style.display = "none";
681+
}
682+
619683
initPreferences(gameStorage, {
620684
[ANIMATIONS_PREFERENCE_NAME]: SETTING_ENABLED,
621685
});
@@ -650,6 +714,9 @@ document.addEventListener("DOMContentLoaded", async () => {
650714
`.setting.${BLOCK_STYLE_SETTING_NAME}`
651715
) as HTMLElement;
652716
(blockStyleSetting.querySelector(".toggle") as HTMLElement).innerText = selectedBlockStyle;
717+
if (getPreferenceValue(FULLSCREEN_PREFERENCE_NAME) === SETTING_ENABLED) {
718+
promptFullscreen();
719+
}
653720

654721
const generateShareText = (gameState: GameState) => {
655722
return `I got a score of ${gameState.score} in 2048-clone${

src/manager/fullscreen.ts

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { IGameStorage } from "../storage";
2+
3+
export class FullscreenManager {
4+
private isFullscreen: boolean = false;
5+
private gameStorage: IGameStorage;
6+
7+
constructor(gameStorage: IGameStorage) {
8+
this.gameStorage = gameStorage;
9+
}
10+
11+
public isFullscreenEnabled() {
12+
return this.isFullscreen;
13+
}
14+
15+
public toggleFullscreen(fullscreen?: boolean) {
16+
if (typeof fullscreen === "undefined") {
17+
fullscreen = !this.isFullscreen;
18+
}
19+
if (fullscreen) {
20+
if (!document.fullscreenElement) {
21+
document.documentElement.requestFullscreen();
22+
}
23+
} else {
24+
if (document.fullscreenElement) {
25+
document.exitFullscreen();
26+
}
27+
}
28+
this.isFullscreen = fullscreen;
29+
this.saveFullscreenPreference(this.isFullscreen);
30+
}
31+
32+
public setFullscreenPreference(fullscreen: boolean) {
33+
this.isFullscreen = fullscreen;
34+
this.saveFullscreenPreference(fullscreen);
35+
}
36+
37+
private saveFullscreenPreference(fullscreen: boolean) {
38+
const preferences = this.gameStorage.loadPreferences();
39+
preferences.fullscreen = fullscreen ? "enabled" : "disabled";
40+
this.gameStorage.savePreferences(preferences);
41+
}
42+
}

0 commit comments

Comments
 (0)