mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-12-24 10:39:15 +01:00
[UI/UX] Split login and register into separate menus (#6851)
* [UI/UX] Split login and register into separate menus * Resize the container for the "Login" and "Register" buttons * Make container width dynamic Co-authored-by: Fabi <192151969+fabske0@users.noreply.github.com> * Make logo position dynamic * Apply suggestions - Consolidate code in `LoginPhase` - Use `truncateString` utility function in `form-modal-ui-handler.ts` - Move login form to match location of register form - Swap `x` values of username and download buttons * Apply suggestions --------- Co-authored-by: Fabi <192151969+fabske0@users.noreply.github.com>
This commit is contained in:
parent
b381d196cf
commit
e6de0fb95d
@ -31,6 +31,7 @@ export enum UiMode {
|
||||
POKEDEX,
|
||||
POKEDEX_SCAN,
|
||||
POKEDEX_PAGE,
|
||||
LOGIN_OR_REGISTER,
|
||||
LOGIN_FORM,
|
||||
REGISTRATION_FORM,
|
||||
LOADING,
|
||||
|
||||
@ -10,7 +10,13 @@ import i18next, { t } from "i18next";
|
||||
|
||||
export class LoginPhase extends Phase {
|
||||
public readonly phaseName = "LoginPhase";
|
||||
private showText: boolean;
|
||||
|
||||
/**
|
||||
* Whether to load the "login or register" text.
|
||||
* Only `true` the first time the phase runs, the text stays on screen after that.
|
||||
* @defaultValue `true`
|
||||
*/
|
||||
private readonly showText: boolean;
|
||||
|
||||
constructor(showText = true) {
|
||||
super();
|
||||
@ -18,103 +24,114 @@ export class LoginPhase extends Phase {
|
||||
this.showText = showText;
|
||||
}
|
||||
|
||||
start(): void {
|
||||
public override async start(): Promise<void> {
|
||||
const { gameData, ui } = globalScene;
|
||||
|
||||
super.start();
|
||||
|
||||
const hasSession = !!getCookie(sessionIdKey);
|
||||
|
||||
globalScene.ui.setMode(UiMode.LOADING, { buttonActions: [] });
|
||||
executeIf(bypassLogin || hasSession, updateUserInfo).then(response => {
|
||||
const success = response ? response[0] : false;
|
||||
const statusCode = response ? response[1] : null;
|
||||
if (!success) {
|
||||
if (!statusCode || statusCode === 400) {
|
||||
if (this.showText) {
|
||||
globalScene.ui.showText(i18next.t("menu:logInOrCreateAccount"));
|
||||
}
|
||||
ui.setMode(UiMode.LOADING, { buttonActions: [] });
|
||||
|
||||
globalScene.playSound("ui/menu_open");
|
||||
const response = await executeIf(bypassLogin || hasSession, updateUserInfo);
|
||||
const success = response?.[0] ?? false;
|
||||
const statusCode = response ? response[1] : null;
|
||||
|
||||
const loadData = () => {
|
||||
updateUserInfo().then(success => {
|
||||
if (!success[0]) {
|
||||
removeCookie(sessionIdKey);
|
||||
globalScene.reset(true, true);
|
||||
return;
|
||||
}
|
||||
globalScene.gameData.loadSystem().then(() => this.end());
|
||||
});
|
||||
};
|
||||
if (!success) {
|
||||
this.checkStatus(statusCode);
|
||||
return;
|
||||
}
|
||||
|
||||
globalScene.ui.setMode(UiMode.LOGIN_FORM, {
|
||||
buttonActions: [
|
||||
() => {
|
||||
globalScene.ui.playSelect();
|
||||
loadData();
|
||||
},
|
||||
() => {
|
||||
globalScene.playSound("ui/menu_open");
|
||||
globalScene.ui.setMode(UiMode.REGISTRATION_FORM, {
|
||||
buttonActions: [
|
||||
() => {
|
||||
globalScene.ui.playSelect();
|
||||
updateUserInfo().then(success => {
|
||||
if (!success[0]) {
|
||||
removeCookie(sessionIdKey);
|
||||
globalScene.reset(true, true);
|
||||
return;
|
||||
}
|
||||
this.end();
|
||||
});
|
||||
},
|
||||
() => {
|
||||
globalScene.phaseManager.unshiftNew("LoginPhase", false);
|
||||
this.end();
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
() => {
|
||||
const redirectUri = encodeURIComponent(`${import.meta.env.VITE_SERVER_URL}/auth/discord/callback`);
|
||||
const discordId = import.meta.env.VITE_DISCORD_CLIENT_ID;
|
||||
const discordUrl = `https://discord.com/api/oauth2/authorize?client_id=${discordId}&redirect_uri=${redirectUri}&response_type=code&scope=identify&prompt=none`;
|
||||
window.open(discordUrl, "_self");
|
||||
},
|
||||
() => {
|
||||
const redirectUri = encodeURIComponent(`${import.meta.env.VITE_SERVER_URL}/auth/google/callback`);
|
||||
const googleId = import.meta.env.VITE_GOOGLE_CLIENT_ID;
|
||||
const googleUrl = `https://accounts.google.com/o/oauth2/auth?client_id=${googleId}&redirect_uri=${redirectUri}&response_type=code&scope=openid`;
|
||||
window.open(googleUrl, "_self");
|
||||
},
|
||||
],
|
||||
});
|
||||
} else if (statusCode === 401) {
|
||||
removeCookie(sessionIdKey);
|
||||
globalScene.reset(true, true);
|
||||
} else {
|
||||
globalScene.phaseManager.unshiftNew("UnavailablePhase");
|
||||
super.end();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
globalScene.gameData.loadSystem().then(success => {
|
||||
if (success || bypassLogin) {
|
||||
this.end();
|
||||
} else {
|
||||
globalScene.ui.setMode(UiMode.MESSAGE);
|
||||
globalScene.ui.showText(t("menu:failedToLoadSaveData"));
|
||||
}
|
||||
});
|
||||
});
|
||||
await gameData.loadSystem();
|
||||
if (success || bypassLogin) {
|
||||
await this.end();
|
||||
return;
|
||||
}
|
||||
ui.setMode(UiMode.MESSAGE);
|
||||
ui.showText(t("menu:failedToLoadSaveData"));
|
||||
}
|
||||
|
||||
end(): void {
|
||||
public override async end(): Promise<void> {
|
||||
globalScene.ui.setMode(UiMode.MESSAGE);
|
||||
|
||||
if (!globalScene.gameData.gender) {
|
||||
globalScene.phaseManager.unshiftNew("SelectGenderPhase");
|
||||
}
|
||||
|
||||
handleTutorial(Tutorial.Intro).then(() => super.end());
|
||||
await handleTutorial(Tutorial.Intro);
|
||||
super.end();
|
||||
}
|
||||
|
||||
private checkStatus(statusCode: number | null): void {
|
||||
if (!statusCode || statusCode === 400) {
|
||||
this.showLoginRegister();
|
||||
return;
|
||||
}
|
||||
|
||||
if (statusCode === 401) {
|
||||
removeCookie(sessionIdKey);
|
||||
globalScene.reset(true, true);
|
||||
return;
|
||||
}
|
||||
|
||||
globalScene.phaseManager.unshiftNew("UnavailablePhase");
|
||||
super.end();
|
||||
}
|
||||
|
||||
private showLoginRegister(): void {
|
||||
const { gameData, phaseManager, ui } = globalScene;
|
||||
|
||||
const backButton = () => {
|
||||
phaseManager.unshiftNew("LoginPhase", false);
|
||||
this.end();
|
||||
};
|
||||
|
||||
const checkUserInfo = async (): Promise<boolean> => {
|
||||
ui.playSelect();
|
||||
const success = await updateUserInfo();
|
||||
if (!success[0]) {
|
||||
removeCookie(sessionIdKey);
|
||||
globalScene.reset(true, true);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const loginButton = async () => {
|
||||
const success = await checkUserInfo();
|
||||
if (!success) {
|
||||
return;
|
||||
}
|
||||
await gameData.loadSystem();
|
||||
this.end();
|
||||
};
|
||||
|
||||
const registerButton = async () => {
|
||||
const success = await checkUserInfo();
|
||||
if (!success) {
|
||||
return;
|
||||
}
|
||||
this.end();
|
||||
};
|
||||
|
||||
const goToLoginButton = () => {
|
||||
globalScene.playSound("ui/menu_open");
|
||||
|
||||
ui.setMode(UiMode.LOGIN_FORM, { buttonActions: [loginButton, backButton] });
|
||||
};
|
||||
|
||||
const goToRegistrationButton = () => {
|
||||
globalScene.playSound("ui/menu_open");
|
||||
|
||||
ui.setMode(UiMode.REGISTRATION_FORM, { buttonActions: [registerButton, backButton] });
|
||||
};
|
||||
|
||||
if (this.showText) {
|
||||
ui.showText(i18next.t("menu:logInOrCreateAccount"));
|
||||
}
|
||||
|
||||
globalScene.playSound("ui/menu_open");
|
||||
|
||||
ui.setMode(UiMode.LOGIN_OR_REGISTER, { buttonActions: [goToLoginButton, goToRegistrationButton] });
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,7 +5,8 @@ import type { ModalConfig } from "#ui/modal-ui-handler";
|
||||
import { ModalUiHandler } from "#ui/modal-ui-handler";
|
||||
import { addTextInputObject, addTextObject, getTextColor } from "#ui/text";
|
||||
import { addWindow, WindowVariant } from "#ui/ui-theme";
|
||||
import { fixedInt } from "#utils/common";
|
||||
import { fixedInt, truncateString } from "#utils/common";
|
||||
import type Phaser from "phaser";
|
||||
import type InputText from "phaser3-rex-plugins/plugins/inputtext";
|
||||
|
||||
export interface FormModalConfig extends ModalConfig {
|
||||
@ -24,30 +25,35 @@ export abstract class FormModalUiHandler extends ModalUiHandler {
|
||||
|
||||
/**
|
||||
* Get configuration for all fields that should be part of the modal
|
||||
* @remarks
|
||||
* Gets used by {@linkcode updateFields} to add the proper text inputs and labels to the view
|
||||
* @returns array of {@linkcode InputFieldConfig}
|
||||
*/
|
||||
abstract getInputFieldConfigs(): InputFieldConfig[];
|
||||
|
||||
getHeight(config?: ModalConfig): number {
|
||||
public override getHeight(config?: FormModalConfig): number {
|
||||
return (
|
||||
20 * this.getInputFieldConfigs().length
|
||||
+ (this.getModalTitle() ? 26 : 0)
|
||||
+ ((config as FormModalConfig)?.errorMessage ? 12 : 0)
|
||||
+ (config?.errorMessage ? 12 : 0)
|
||||
+ this.getButtonTopMargin()
|
||||
+ 28
|
||||
);
|
||||
}
|
||||
|
||||
getReadableErrorMessage(error: string): string {
|
||||
if (error?.indexOf("connection refused") > -1) {
|
||||
public getReadableErrorMessage(error: string): string {
|
||||
if (!error) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (error.includes("connection refused")) {
|
||||
return "Could not connect to the server";
|
||||
}
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
setup(): void {
|
||||
public override setup(): void {
|
||||
super.setup();
|
||||
|
||||
const config = this.getInputFieldConfigs();
|
||||
@ -58,16 +64,9 @@ export abstract class FormModalUiHandler extends ModalUiHandler {
|
||||
this.updateFields(config, hasTitle);
|
||||
}
|
||||
|
||||
this.errorMessage = addTextObject(
|
||||
10,
|
||||
(hasTitle ? 31 : 5) + 20 * (config.length - 1) + 16 + this.getButtonTopMargin(),
|
||||
"",
|
||||
TextStyle.TOOLTIP_CONTENT,
|
||||
{
|
||||
fontSize: "42px",
|
||||
wordWrap: { width: 850 },
|
||||
},
|
||||
)
|
||||
const errorMessageY = (hasTitle ? 31 : 5) + 20 * (config.length - 1) + 16 + this.getButtonTopMargin();
|
||||
const errorMessageOptions: Phaser.Types.GameObjects.Text.TextStyle = { fontSize: "42px", wordWrap: { width: 850 } };
|
||||
this.errorMessage = addTextObject(10, errorMessageY, "", TextStyle.TOOLTIP_CONTENT, errorMessageOptions)
|
||||
.setColor(getTextColor(TextStyle.SUMMARY_PINK))
|
||||
.setShadowColor(getTextColor(TextStyle.SUMMARY_PINK, true))
|
||||
.setVisible(false);
|
||||
@ -75,21 +74,18 @@ export abstract class FormModalUiHandler extends ModalUiHandler {
|
||||
}
|
||||
|
||||
protected updateFields(fieldsConfig: InputFieldConfig[], hasTitle: boolean) {
|
||||
const inputContainers = (this.inputContainers = new Array(fieldsConfig.length));
|
||||
const inputs = (this.inputs = new Array(fieldsConfig.length));
|
||||
const formLabels = (this.formLabels = new Array(fieldsConfig.length));
|
||||
this.inputContainers = new Array(fieldsConfig.length);
|
||||
this.inputs = new Array(fieldsConfig.length);
|
||||
this.formLabels = new Array(fieldsConfig.length);
|
||||
for (const [f, config] of fieldsConfig.entries()) {
|
||||
const labelY = (hasTitle ? 31 : 5) + 20 * f;
|
||||
// The Pokédex Scan Window uses width `300` instead of `160` like the other forms
|
||||
// Therefore, the label does not need to be shortened
|
||||
const label = addTextObject(
|
||||
10,
|
||||
(hasTitle ? 31 : 5) + 20 * f,
|
||||
config.label.length > 25 && this.getWidth() < 200 ? config.label.slice(0, 20) + "..." : config.label,
|
||||
TextStyle.TOOLTIP_CONTENT,
|
||||
);
|
||||
const labelContent = this.getWidth() < 200 ? truncateString(config.label, 25) : config.label;
|
||||
const label = addTextObject(10, labelY, labelContent, TextStyle.TOOLTIP_CONTENT);
|
||||
label.name = "formLabel" + f;
|
||||
|
||||
formLabels[f] = label;
|
||||
this.formLabels[f] = label;
|
||||
this.modalContainer.add(label);
|
||||
|
||||
const inputWidth = label.width < 320 ? 80 : 80 - (label.width - 320) / 5.5;
|
||||
@ -109,14 +105,14 @@ export abstract class FormModalUiHandler extends ModalUiHandler {
|
||||
|
||||
inputContainer.add([inputBg, input]);
|
||||
|
||||
inputContainers[f] = inputContainer;
|
||||
this.inputContainers[f] = inputContainer;
|
||||
this.modalContainer.add(inputContainer);
|
||||
|
||||
inputs[f] = input;
|
||||
this.inputs[f] = input;
|
||||
}
|
||||
}
|
||||
|
||||
override show(args: any[]): boolean {
|
||||
public override show(args: any[]): boolean {
|
||||
if (super.show(args)) {
|
||||
for (const ic of this.inputContainers) {
|
||||
ic.setActive(true).setVisible(true);
|
||||
@ -129,7 +125,7 @@ export abstract class FormModalUiHandler extends ModalUiHandler {
|
||||
|
||||
// Auto focus the first input field after a short delay, to prevent accidental inputs
|
||||
setTimeout(() => {
|
||||
this.inputs[0].setFocus();
|
||||
this.inputs[0]?.setFocus();
|
||||
}, 50);
|
||||
|
||||
// #region: Override button pointerDown
|
||||
@ -170,7 +166,7 @@ export abstract class FormModalUiHandler extends ModalUiHandler {
|
||||
return false;
|
||||
}
|
||||
|
||||
processInput(button: Button): boolean {
|
||||
public override processInput(button: Button): boolean {
|
||||
if (button === Button.SUBMIT && this.submitAction) {
|
||||
this.submitAction();
|
||||
return true;
|
||||
@ -179,13 +175,13 @@ export abstract class FormModalUiHandler extends ModalUiHandler {
|
||||
return false;
|
||||
}
|
||||
|
||||
sanitizeInputs(): void {
|
||||
public sanitizeInputs(): void {
|
||||
for (const input of this.inputs) {
|
||||
input.text = input.text.trim();
|
||||
}
|
||||
}
|
||||
|
||||
updateContainer(config?: ModalConfig): void {
|
||||
public override updateContainer(config?: ModalConfig): void {
|
||||
super.updateContainer(config);
|
||||
|
||||
this.errorMessage
|
||||
@ -193,21 +189,21 @@ export abstract class FormModalUiHandler extends ModalUiHandler {
|
||||
.setVisible(!!this.errorMessage.text);
|
||||
}
|
||||
|
||||
hide(): void {
|
||||
public hide(): void {
|
||||
this.modalContainer.setVisible(false).setActive(false);
|
||||
for (const ic of this.inputContainers) {
|
||||
ic.setVisible(false).setActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
unhide(): void {
|
||||
public unhide(): void {
|
||||
this.modalContainer.setActive(true).setVisible(true);
|
||||
for (const ic of this.inputContainers) {
|
||||
ic.setActive(true).setVisible(true);
|
||||
}
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
public override clear(): void {
|
||||
super.clear();
|
||||
this.modalContainer.setVisible(false);
|
||||
|
||||
|
||||
@ -1,152 +1,60 @@
|
||||
import { pokerogueApi } from "#api/pokerogue-api";
|
||||
import { globalScene } from "#app/global-scene";
|
||||
import { TextStyle } from "#enums/text-style";
|
||||
import { UiMode } from "#enums/ui-mode";
|
||||
import { languageOptions } from "#system/settings-language";
|
||||
import type { OptionSelectItem } from "#ui/abstract-option-select-ui-handler";
|
||||
import type { InputFieldConfig } from "#ui/form-modal-ui-handler";
|
||||
import { FormModalUiHandler } from "#ui/form-modal-ui-handler";
|
||||
import type { ModalConfig } from "#ui/modal-ui-handler";
|
||||
import { addTextObject } from "#ui/text";
|
||||
import { addWindow } from "#ui/ui-theme";
|
||||
import { fixedInt } from "#utils/common";
|
||||
import { OAuthProvidersUiHandler } from "#ui/oauth-providers-ui-handler";
|
||||
import i18next from "i18next";
|
||||
import JSZip from "jszip";
|
||||
|
||||
interface BuildInteractableImageOpts {
|
||||
scale?: number;
|
||||
x?: number;
|
||||
y?: number;
|
||||
origin?: { x: number; y: number };
|
||||
}
|
||||
const ERR_USERNAME: string = "invalid username";
|
||||
const ERR_PASSWORD: string = "invalid password";
|
||||
const ERR_ACCOUNT_EXIST: string = "account doesn't exist";
|
||||
const ERR_PASSWORD_MATCH: string = "password doesn't match";
|
||||
|
||||
/**
|
||||
* The maximum number of saves that are allowed to show up in the username panel pefore
|
||||
* the `P02: Too many saves` popup is displayed.
|
||||
*
|
||||
* @privateRemarks
|
||||
* This limitation is in place to allow for the password reset helpers to get
|
||||
* enough information in one screenshot. If the user has too many saves, this
|
||||
* complicates the interaction as it would require scrolling, which will
|
||||
* make tickets take longer to resolve.
|
||||
*/
|
||||
const MAX_SAVES_FOR_USERNAME_PANEL = 7;
|
||||
|
||||
export class LoginFormUiHandler extends FormModalUiHandler {
|
||||
private readonly ERR_USERNAME: string = "invalid username";
|
||||
private readonly ERR_PASSWORD: string = "invalid password";
|
||||
private readonly ERR_ACCOUNT_EXIST: string = "account doesn't exist";
|
||||
private readonly ERR_PASSWORD_MATCH: string = "password doesn't match";
|
||||
private readonly ERR_NO_SAVES: string = "No save files found";
|
||||
private readonly ERR_TOO_MANY_SAVES: string = "Too many save files found";
|
||||
|
||||
private googleImage: Phaser.GameObjects.Image;
|
||||
private discordImage: Phaser.GameObjects.Image;
|
||||
private usernameInfoImage: Phaser.GameObjects.Image;
|
||||
private saveDownloadImage: Phaser.GameObjects.Image;
|
||||
private changeLanguageImage: Phaser.GameObjects.Image;
|
||||
private externalPartyContainer: Phaser.GameObjects.Container;
|
||||
private infoContainer: Phaser.GameObjects.Container;
|
||||
private externalPartyBg: Phaser.GameObjects.NineSlice;
|
||||
private externalPartyTitle: Phaser.GameObjects.Text;
|
||||
export class LoginFormUiHandler extends OAuthProvidersUiHandler {
|
||||
constructor(mode: UiMode | null = null) {
|
||||
super(mode);
|
||||
}
|
||||
|
||||
setup(): void {
|
||||
super.setup();
|
||||
|
||||
this.buildExternalPartyContainer();
|
||||
this.buildInfoContainer();
|
||||
}
|
||||
|
||||
private buildExternalPartyContainer() {
|
||||
this.externalPartyContainer = globalScene.add.container(0, 0);
|
||||
this.externalPartyContainer.setInteractive(
|
||||
new Phaser.Geom.Rectangle(0, 0, globalScene.scaledCanvas.width / 2, globalScene.scaledCanvas.height / 2),
|
||||
Phaser.Geom.Rectangle.Contains,
|
||||
);
|
||||
this.externalPartyTitle = addTextObject(0, 4, "", TextStyle.SETTINGS_LABEL).setOrigin(0.5, 0);
|
||||
this.externalPartyBg = addWindow(0, 0, 0, 0);
|
||||
|
||||
this.googleImage = this.buildInteractableImage("google", "google-icon");
|
||||
this.discordImage = this.buildInteractableImage("discord", "discord-icon");
|
||||
|
||||
this.externalPartyContainer
|
||||
.add([this.externalPartyBg, this.externalPartyTitle, this.googleImage, this.discordImage])
|
||||
.setVisible(false);
|
||||
this.getUi().add(this.externalPartyContainer);
|
||||
}
|
||||
|
||||
private buildInfoContainer() {
|
||||
this.infoContainer = globalScene.add.container(0, 0);
|
||||
|
||||
this.usernameInfoImage = this.buildInteractableImage("settings_icon", "username-info-icon", {
|
||||
x: 20,
|
||||
scale: 0.5,
|
||||
});
|
||||
|
||||
this.saveDownloadImage = this.buildInteractableImage("saving_icon", "save-download-icon", {
|
||||
x: 0,
|
||||
scale: 0.75,
|
||||
});
|
||||
|
||||
this.changeLanguageImage = this.buildInteractableImage("language_icon", "change-language-icon", {
|
||||
x: 40,
|
||||
scale: 0.5,
|
||||
});
|
||||
|
||||
this.infoContainer
|
||||
.add([this.usernameInfoImage, this.saveDownloadImage, this.changeLanguageImage])
|
||||
.setVisible(false)
|
||||
.disableInteractive();
|
||||
this.getUi().add(this.infoContainer);
|
||||
}
|
||||
|
||||
override getModalTitle(_config?: ModalConfig): string {
|
||||
let key = "menu:login";
|
||||
public override getModalTitle(): string {
|
||||
if (import.meta.env.VITE_SERVER_URL === "https://apibeta.pokerogue.net") {
|
||||
key = "menu:loginBeta";
|
||||
return i18next.t("menu:loginBeta");
|
||||
}
|
||||
return i18next.t(key);
|
||||
return i18next.t("menu:login");
|
||||
}
|
||||
|
||||
override getWidth(_config?: ModalConfig): number {
|
||||
public override getWidth(): number {
|
||||
return 160;
|
||||
}
|
||||
|
||||
override getMargin(_config?: ModalConfig): [number, number, number, number] {
|
||||
return [0, 0, 48, 0];
|
||||
public override getMargin(): [number, number, number, number] {
|
||||
return [0, 20, 48, 0];
|
||||
}
|
||||
|
||||
override getButtonLabels(_config?: ModalConfig): string[] {
|
||||
return [i18next.t("menu:login"), i18next.t("menu:register")];
|
||||
public override getButtonLabels(): string[] {
|
||||
return [i18next.t("menu:login"), i18next.t("menu:goBack")];
|
||||
}
|
||||
|
||||
override getReadableErrorMessage(error: string): string {
|
||||
const colonIndex = error?.indexOf(":");
|
||||
if (colonIndex > 0) {
|
||||
error = error.slice(0, colonIndex);
|
||||
public override getReadableErrorMessage(error: string): string {
|
||||
if (!error) {
|
||||
return "";
|
||||
}
|
||||
|
||||
switch (error) {
|
||||
case this.ERR_USERNAME:
|
||||
case ERR_USERNAME:
|
||||
return i18next.t("menu:invalidLoginUsername");
|
||||
case this.ERR_PASSWORD:
|
||||
case ERR_PASSWORD:
|
||||
return i18next.t("menu:invalidLoginPassword");
|
||||
case this.ERR_ACCOUNT_EXIST:
|
||||
case ERR_ACCOUNT_EXIST:
|
||||
return i18next.t("menu:accountNonExistent");
|
||||
case this.ERR_PASSWORD_MATCH:
|
||||
case ERR_PASSWORD_MATCH:
|
||||
return i18next.t("menu:unmatchingPassword");
|
||||
case this.ERR_NO_SAVES:
|
||||
return "P01: " + i18next.t("menu:noSaves");
|
||||
case this.ERR_TOO_MANY_SAVES:
|
||||
return "P02: " + i18next.t("menu:tooManySaves");
|
||||
}
|
||||
|
||||
return super.getReadableErrorMessage(error);
|
||||
}
|
||||
|
||||
override getInputFieldConfigs(): InputFieldConfig[] {
|
||||
public override getInputFieldConfigs(): InputFieldConfig[] {
|
||||
const inputFieldConfigs: InputFieldConfig[] = [];
|
||||
inputFieldConfigs.push(
|
||||
{ label: i18next.t("menu:username") },
|
||||
@ -158,12 +66,13 @@ export class LoginFormUiHandler extends FormModalUiHandler {
|
||||
return inputFieldConfigs;
|
||||
}
|
||||
|
||||
override show(args: any[]): boolean {
|
||||
public override show(args: any[]): boolean {
|
||||
if (!super.show(args)) {
|
||||
return false;
|
||||
}
|
||||
const config = args[0] as ModalConfig;
|
||||
this.processExternalProvider(config);
|
||||
this.processExternalProvider();
|
||||
this.showInfoContainer(config);
|
||||
const originalLoginAction = this.submitAction;
|
||||
this.submitAction = () => {
|
||||
if (globalScene.tweens.getTweensOf(this.modalContainer).length > 0) {
|
||||
@ -173,7 +82,7 @@ export class LoginFormUiHandler extends FormModalUiHandler {
|
||||
this.submitAction = originalLoginAction;
|
||||
this.sanitizeInputs();
|
||||
globalScene.ui.setMode(UiMode.LOADING, { buttonActions: [] });
|
||||
const onFail = error => {
|
||||
const onFail = (error: string | null) => {
|
||||
globalScene.ui.setMode(UiMode.LOGIN_FORM, Object.assign(config, { errorMessage: error?.trim() }));
|
||||
globalScene.ui.playError();
|
||||
};
|
||||
@ -199,199 +108,4 @@ export class LoginFormUiHandler extends FormModalUiHandler {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
override clear() {
|
||||
super.clear();
|
||||
this.externalPartyContainer.setVisible(false).setActive(false);
|
||||
this.infoContainer.setVisible(false).setActive(false);
|
||||
this.setMouseCursorStyle("default"); //reset cursor
|
||||
|
||||
[
|
||||
this.discordImage,
|
||||
this.googleImage,
|
||||
this.usernameInfoImage,
|
||||
this.saveDownloadImage,
|
||||
this.changeLanguageImage,
|
||||
].forEach(img => {
|
||||
img.off("pointerdown");
|
||||
});
|
||||
}
|
||||
|
||||
override destroy() {
|
||||
super.destroy();
|
||||
this.externalPartyContainer.destroy();
|
||||
this.infoContainer.destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a panel with all usernames found in localStorage
|
||||
*
|
||||
* @remarks
|
||||
* Up to {@linkcode MAX_SAVES_FOR_USERNAME_PANEL} usernames are shown, otherwise P02 is triggered
|
||||
* @param onFail - Callback function for failure
|
||||
*/
|
||||
private showUsernames(config: ModalConfig) {
|
||||
if (globalScene.tweens.getTweensOf(this.infoContainer).length === 0) {
|
||||
const localStorageKeys = Object.keys(localStorage); // this gets the keys for localStorage
|
||||
const keyToFind = "data_";
|
||||
const dataKeys = localStorageKeys.filter(ls => ls.indexOf(keyToFind) >= 0);
|
||||
if (dataKeys.length === 0) {
|
||||
this.onFail(this.ERR_NO_SAVES, config);
|
||||
return;
|
||||
}
|
||||
if (dataKeys.length > MAX_SAVES_FOR_USERNAME_PANEL) {
|
||||
this.onFail(this.ERR_TOO_MANY_SAVES, config);
|
||||
return;
|
||||
}
|
||||
const options: OptionSelectItem[] = [];
|
||||
const handler = () => {
|
||||
globalScene.ui.revertMode();
|
||||
this.infoContainer.disableInteractive();
|
||||
return true;
|
||||
};
|
||||
for (const key of dataKeys) {
|
||||
options.push({
|
||||
label: key.replace(keyToFind, ""),
|
||||
handler,
|
||||
});
|
||||
}
|
||||
globalScene.ui.setOverlayMode(UiMode.OPTION_SELECT, {
|
||||
options,
|
||||
delay: 1000,
|
||||
});
|
||||
this.infoContainer.setInteractive(
|
||||
new Phaser.Geom.Rectangle(0, 0, globalScene.game.canvas.width, globalScene.game.canvas.height),
|
||||
Phaser.Geom.Rectangle.Contains,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private onFail(error: string, config: ModalConfig) {
|
||||
const ui = globalScene.ui;
|
||||
ui.setMode(UiMode.LOADING, { buttonActions: [] });
|
||||
ui.setModeForceTransition(UiMode.LOGIN_FORM, Object.assign(config, { errorMessage: error?.trim() }));
|
||||
ui.playError();
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect the user's save files from localStorage and download them as a zip file
|
||||
*
|
||||
* @remarks
|
||||
* Used as the `pointerDown` callback for the save download image
|
||||
* @param config - The modal configuration
|
||||
*/
|
||||
private async downloadSaves(config: ModalConfig): Promise<void> {
|
||||
// find all data_ and sessionData keys, put them in a .txt file and download everything in a single zip
|
||||
const localStorageKeys = Object.keys(localStorage); // this gets the keys for localStorage
|
||||
const keyToFind = "data_";
|
||||
const sessionKeyToFind = "sessionData";
|
||||
const dataKeys = localStorageKeys.filter(ls => ls.indexOf(keyToFind) >= 0);
|
||||
const sessionKeys = localStorageKeys.filter(ls => ls.indexOf(sessionKeyToFind) >= 0);
|
||||
if (dataKeys.length <= 0 && sessionKeys.length <= 0) {
|
||||
this.onFail(this.ERR_NO_SAVES, config);
|
||||
return;
|
||||
}
|
||||
const zip = new JSZip();
|
||||
// Bang is safe here because of the filter above
|
||||
for (const dataKey of dataKeys) {
|
||||
zip.file(dataKey + ".prsv", localStorage.getItem(dataKey)!);
|
||||
}
|
||||
for (const sessionKey of sessionKeys) {
|
||||
zip.file(sessionKey + ".prsv", localStorage.getItem(sessionKey)!);
|
||||
}
|
||||
const content = await zip.generateAsync({ type: "blob" });
|
||||
const url = URL.createObjectURL(content);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = "pokerogue_saves.zip";
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
private processExternalProvider(config: ModalConfig): void {
|
||||
this.externalPartyTitle
|
||||
.setText(i18next.t("menu:orUse") ?? "")
|
||||
.setX(20 + this.externalPartyTitle.text.length)
|
||||
.setVisible(true);
|
||||
|
||||
const externalPartyContainer = this.externalPartyContainer
|
||||
.setPositionRelative(this.modalContainer, 175, 0)
|
||||
.setVisible(true);
|
||||
|
||||
const externalPartyBg = this.externalPartyBg.setSize(this.externalPartyTitle.text.length + 50, this.modalBg.height);
|
||||
this.getUi().moveTo(externalPartyContainer, this.getUi().length - 1);
|
||||
|
||||
const externalPartyIconWidth = externalPartyBg.width / 3.1;
|
||||
this.discordImage;
|
||||
const infoContainer = this.infoContainer.setPosition(5, -76).setVisible(true);
|
||||
this.getUi().moveTo(infoContainer, this.getUi().length - 1);
|
||||
|
||||
this.discordImage // formatting
|
||||
.setPosition(externalPartyIconWidth, externalPartyBg.height - 40)
|
||||
.on("pointerdown", () => {
|
||||
const redirectUri = encodeURIComponent(`${import.meta.env.VITE_SERVER_URL}/auth/discord/callback`);
|
||||
const discordId = import.meta.env.VITE_DISCORD_CLIENT_ID;
|
||||
const discordUrl = `https://discord.com/api/oauth2/authorize?client_id=${discordId}&redirect_uri=${redirectUri}&response_type=code&scope=identify&prompt=none`;
|
||||
window.open(discordUrl, "_self");
|
||||
});
|
||||
|
||||
this.googleImage // formatting
|
||||
.setPosition(externalPartyIconWidth, externalPartyBg.height - 60)
|
||||
.on("pointerdown", () => {
|
||||
const redirectUri = encodeURIComponent(`${import.meta.env.VITE_SERVER_URL}/auth/google/callback`);
|
||||
const googleId = import.meta.env.VITE_GOOGLE_CLIENT_ID;
|
||||
const googleUrl = `https://accounts.google.com/o/oauth2/auth?client_id=${googleId}&redirect_uri=${redirectUri}&response_type=code&scope=openid`;
|
||||
window.open(googleUrl, "_self");
|
||||
});
|
||||
|
||||
this.usernameInfoImage // formatting
|
||||
.setPositionRelative(infoContainer, 0, 0)
|
||||
.on("pointerdown", () => this.showUsernames(config));
|
||||
|
||||
this.saveDownloadImage // formatting
|
||||
.setPositionRelative(infoContainer, 20, 0)
|
||||
.on("pointerdown", () => this.downloadSaves(config));
|
||||
|
||||
this.changeLanguageImage.setPositionRelative(infoContainer, 40, 0).on("pointerdown", () => {
|
||||
globalScene.ui.setOverlayMode(UiMode.OPTION_SELECT, {
|
||||
options: languageOptions,
|
||||
maxOptions: 7,
|
||||
delay: 1000,
|
||||
});
|
||||
});
|
||||
|
||||
this.externalPartyContainer.setAlpha(0);
|
||||
globalScene.tweens.add({
|
||||
targets: this.externalPartyContainer,
|
||||
duration: fixedInt(1000),
|
||||
ease: "Sine.easeInOut",
|
||||
y: "-=24",
|
||||
alpha: 1,
|
||||
});
|
||||
|
||||
this.infoContainer.setAlpha(0);
|
||||
globalScene.tweens.add({
|
||||
targets: this.infoContainer,
|
||||
duration: fixedInt(1000),
|
||||
ease: "Sine.easeInOut",
|
||||
y: "-=24",
|
||||
alpha: 1,
|
||||
});
|
||||
}
|
||||
|
||||
private buildInteractableImage(texture: string, name: string, opts: BuildInteractableImageOpts = {}) {
|
||||
const { scale = 0.07, x = 0, y = 0, origin = { x: 0, y: 0 } } = opts;
|
||||
const img = globalScene.add
|
||||
.image(x, y, texture)
|
||||
.setName(name)
|
||||
.setOrigin(origin.x, origin.y)
|
||||
.setScale(scale)
|
||||
.setInteractive();
|
||||
this.addInteractionHoverEffect(img);
|
||||
|
||||
return img;
|
||||
}
|
||||
}
|
||||
|
||||
72
src/ui/handlers/login-or-register-ui-handler.ts
Normal file
72
src/ui/handlers/login-or-register-ui-handler.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import { globalScene } from "#app/global-scene";
|
||||
import type { InputFieldConfig } from "#ui/form-modal-ui-handler";
|
||||
import { LoginRegisterInfoContainerUiHandler } from "#ui/login-register-info-container-ui-handler";
|
||||
import type { ModalConfig } from "#ui/modal-ui-handler";
|
||||
import i18next from "i18next";
|
||||
import type Phaser from "phaser";
|
||||
|
||||
export class LoginOrRegisterUiHandler extends LoginRegisterInfoContainerUiHandler {
|
||||
private logo: Phaser.GameObjects.Image;
|
||||
|
||||
public override getModalTitle(): string {
|
||||
return "";
|
||||
}
|
||||
|
||||
public override getWidth(): number {
|
||||
const buttonWidth = this.buttonLabels.reduce((sum, label) => sum + label.width, 0) / 6;
|
||||
return buttonWidth + 50;
|
||||
}
|
||||
|
||||
public override getHeight(): number {
|
||||
return 32;
|
||||
}
|
||||
|
||||
public override getMargin(): [number, number, number, number] {
|
||||
return [0, 0, 30, 0];
|
||||
}
|
||||
|
||||
public override getButtonLabels(): string[] {
|
||||
return [i18next.t("menu:login"), i18next.t("menu:register")];
|
||||
}
|
||||
|
||||
// TODO: use mixins so it's not necessary to inherit from `FormModalUiHandler`
|
||||
public override getInputFieldConfigs(): InputFieldConfig[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
public override setup(): void {
|
||||
super.setup();
|
||||
|
||||
// logo width is 150
|
||||
this.logo = globalScene.add //
|
||||
.image(-((150 - this.getWidth()) / 2), -52, "logo")
|
||||
.setOrigin(0);
|
||||
|
||||
this.modalContainer.add(this.logo);
|
||||
}
|
||||
|
||||
public override show(args: [ModalConfig, ...any[]]): boolean {
|
||||
this.logo //
|
||||
.setVisible(true)
|
||||
.setActive(true);
|
||||
|
||||
const config = args[0];
|
||||
this.showInfoContainer(config);
|
||||
|
||||
return super.show(args);
|
||||
}
|
||||
|
||||
public override clear(): void {
|
||||
super.clear();
|
||||
|
||||
this.logo //
|
||||
.setVisible(false)
|
||||
.setActive(false);
|
||||
}
|
||||
|
||||
public override destroy(): void {
|
||||
super.destroy();
|
||||
|
||||
this.logo.destroy();
|
||||
}
|
||||
}
|
||||
233
src/ui/handlers/login-register-info-container-ui-handler.ts
Normal file
233
src/ui/handlers/login-register-info-container-ui-handler.ts
Normal file
@ -0,0 +1,233 @@
|
||||
import { globalScene } from "#app/global-scene";
|
||||
import { UiMode } from "#enums/ui-mode";
|
||||
import { languageOptions } from "#system/settings-language";
|
||||
import type { OptionSelectItem } from "#ui/abstract-option-select-ui-handler";
|
||||
import { FormModalUiHandler } from "#ui/form-modal-ui-handler";
|
||||
import type { ModalConfig } from "#ui/modal-ui-handler";
|
||||
import { fixedInt } from "#utils/common";
|
||||
import i18next from "i18next";
|
||||
import JSZip from "jszip";
|
||||
|
||||
interface BuildInteractableImageOpts {
|
||||
scale?: number;
|
||||
x?: number;
|
||||
y?: number;
|
||||
origin?: { x: number; y: number };
|
||||
}
|
||||
|
||||
/**
|
||||
* The maximum number of saves that are allowed to show up in the username panel pefore
|
||||
* the `P02: Too many saves` popup is displayed.
|
||||
*
|
||||
* @privateRemarks
|
||||
* This limitation is in place to allow for the password reset helpers to get
|
||||
* enough information in one screenshot. If the user has too many saves, this
|
||||
* complicates the interaction as it would require scrolling, which will
|
||||
* make tickets take longer to resolve.
|
||||
*/
|
||||
const MAX_SAVES_FOR_USERNAME_PANEL = 7;
|
||||
|
||||
const ERR_NO_SAVES: string = "No save files found";
|
||||
const ERR_TOO_MANY_SAVES: string = "Too many save files found";
|
||||
|
||||
// TODO: use mixins
|
||||
export abstract class LoginRegisterInfoContainerUiHandler extends FormModalUiHandler {
|
||||
private usernameInfoImage: Phaser.GameObjects.Image;
|
||||
private saveDownloadImage: Phaser.GameObjects.Image;
|
||||
private changeLanguageImage: Phaser.GameObjects.Image;
|
||||
private infoContainer: Phaser.GameObjects.Container;
|
||||
|
||||
public override getReadableErrorMessage(error: string): string {
|
||||
if (!error) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const colonIndex = error.indexOf(":");
|
||||
if (colonIndex > 0) {
|
||||
error = error.slice(0, colonIndex);
|
||||
}
|
||||
|
||||
switch (error) {
|
||||
case ERR_NO_SAVES:
|
||||
return "P01: " + i18next.t("menu:noSaves");
|
||||
case ERR_TOO_MANY_SAVES:
|
||||
return "P02: " + i18next.t("menu:tooManySaves");
|
||||
}
|
||||
|
||||
return super.getReadableErrorMessage(error);
|
||||
}
|
||||
|
||||
public override setup(): void {
|
||||
super.setup();
|
||||
this.buildInfoContainer();
|
||||
}
|
||||
|
||||
public override clear(): void {
|
||||
super.clear();
|
||||
this.infoContainer //
|
||||
.setVisible(false)
|
||||
.setActive(false);
|
||||
this.setMouseCursorStyle("default"); // reset cursor
|
||||
}
|
||||
|
||||
public override destroy(): void {
|
||||
super.destroy();
|
||||
this.infoContainer.destroy();
|
||||
}
|
||||
|
||||
private buildInfoContainer() {
|
||||
this.usernameInfoImage = this.buildInteractableImage("settings_icon", "username-info-icon", { x: 0, scale: 0.5 });
|
||||
this.saveDownloadImage = this.buildInteractableImage("saving_icon", "save-download-icon", { x: 20, scale: 0.75 });
|
||||
this.changeLanguageImage = this.buildInteractableImage("language_icon", "change-language-icon", {
|
||||
x: 40,
|
||||
scale: 0.5,
|
||||
});
|
||||
|
||||
this.infoContainer = globalScene.add
|
||||
.container(0, 0)
|
||||
.add([this.usernameInfoImage, this.saveDownloadImage, this.changeLanguageImage])
|
||||
.setVisible(false)
|
||||
.disableInteractive();
|
||||
|
||||
this.getUi().add(this.infoContainer);
|
||||
}
|
||||
|
||||
protected showInfoContainer(config: ModalConfig) {
|
||||
this.infoContainer //
|
||||
.setPosition(5, -76)
|
||||
.setVisible(true);
|
||||
this.getUi().moveTo(this.infoContainer, this.getUi().length - 1);
|
||||
|
||||
this.usernameInfoImage //
|
||||
.setPositionRelative(this.infoContainer, 0, 0)
|
||||
.on("pointerdown", () => this.showUsernames(config));
|
||||
|
||||
this.saveDownloadImage //
|
||||
.setPositionRelative(this.infoContainer, 20, 0)
|
||||
.on("pointerdown", () => this.downloadSaves(config));
|
||||
|
||||
this.changeLanguageImage //
|
||||
.setPositionRelative(this.infoContainer, 40, 0)
|
||||
.on("pointerdown", () => {
|
||||
globalScene.ui.setOverlayMode(UiMode.OPTION_SELECT, {
|
||||
options: languageOptions,
|
||||
maxOptions: 7,
|
||||
delay: 1000,
|
||||
});
|
||||
});
|
||||
|
||||
this.infoContainer.setAlpha(0);
|
||||
globalScene.tweens.add({
|
||||
targets: this.infoContainer,
|
||||
duration: fixedInt(1000),
|
||||
ease: "Sine.easeInOut",
|
||||
y: "-=24",
|
||||
alpha: 1,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a panel with all usernames found in localStorage
|
||||
* @remarks
|
||||
* Up to {@linkcode MAX_SAVES_FOR_USERNAME_PANEL} usernames are shown, otherwise P02 is triggered
|
||||
* @param config - The modal configuration
|
||||
*/
|
||||
private showUsernames(config: ModalConfig): void {
|
||||
if (globalScene.tweens.getTweensOf(this.infoContainer).length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const localStorageKeys = Object.keys(localStorage);
|
||||
const keyToFind = "data_";
|
||||
const dataKeys = localStorageKeys.filter(ls => ls.includes(keyToFind));
|
||||
|
||||
if (dataKeys.length === 0) {
|
||||
this.onFail(ERR_NO_SAVES, config);
|
||||
return;
|
||||
}
|
||||
if (dataKeys.length > MAX_SAVES_FOR_USERNAME_PANEL) {
|
||||
this.onFail(ERR_TOO_MANY_SAVES, config);
|
||||
return;
|
||||
}
|
||||
|
||||
const options: OptionSelectItem[] = [];
|
||||
const handler = () => {
|
||||
globalScene.ui.revertMode();
|
||||
this.infoContainer.disableInteractive();
|
||||
return true;
|
||||
};
|
||||
|
||||
for (const key of dataKeys) {
|
||||
options.push({ label: key.replace(keyToFind, ""), handler });
|
||||
}
|
||||
|
||||
globalScene.ui.setOverlayMode(UiMode.OPTION_SELECT, { options, delay: 1000 });
|
||||
this.infoContainer.setInteractive(
|
||||
new Phaser.Geom.Rectangle(0, 0, globalScene.game.canvas.width, globalScene.game.canvas.height),
|
||||
Phaser.Geom.Rectangle.Contains,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect the user's save files from localStorage and download them as a zip file
|
||||
* @remarks
|
||||
* Used as the `pointerDown` callback for the save download image
|
||||
* @param config - The modal configuration
|
||||
*/
|
||||
private async downloadSaves(config: ModalConfig): Promise<void> {
|
||||
// find all data_ and sessionData keys, put them in a .txt file and download everything in a single zip
|
||||
const localStorageKeys = Object.keys(localStorage);
|
||||
const keyToFind = "data_";
|
||||
const sessionKeyToFind = "sessionData";
|
||||
const dataKeys = localStorageKeys.filter(ls => ls.includes(keyToFind));
|
||||
const sessionKeys = localStorageKeys.filter(ls => ls.includes(sessionKeyToFind));
|
||||
|
||||
if (dataKeys.length <= 0 && sessionKeys.length <= 0) {
|
||||
this.onFail(ERR_NO_SAVES, config);
|
||||
return;
|
||||
}
|
||||
|
||||
const zip = new JSZip();
|
||||
// Bang is safe here because of the filter above
|
||||
for (const dataKey of dataKeys) {
|
||||
zip.file(dataKey + ".prsv", localStorage.getItem(dataKey)!);
|
||||
}
|
||||
for (const sessionKey of sessionKeys) {
|
||||
zip.file(sessionKey + ".prsv", localStorage.getItem(sessionKey)!);
|
||||
}
|
||||
|
||||
const content = await zip.generateAsync({ type: "blob" });
|
||||
const url = URL.createObjectURL(content);
|
||||
const a = document.createElement("a");
|
||||
|
||||
a.href = url;
|
||||
a.download = "pokerogue_saves.zip";
|
||||
a.click();
|
||||
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
private onFail(error: string, config: ModalConfig): void {
|
||||
const ui = globalScene.ui;
|
||||
ui.setMode(UiMode.LOADING, { buttonActions: [] });
|
||||
ui.setModeForceTransition(UiMode.LOGIN_OR_REGISTER, Object.assign(config, { errorMessage: error?.trim() }));
|
||||
ui.playError();
|
||||
}
|
||||
|
||||
protected buildInteractableImage(
|
||||
texture: string,
|
||||
name: string,
|
||||
opts: BuildInteractableImageOpts = {},
|
||||
): Phaser.GameObjects.Image {
|
||||
const { scale = 0.07, x = 0, y = 0, origin = { x: 0, y: 0 } } = opts;
|
||||
const img = globalScene.add
|
||||
.image(x, y, texture)
|
||||
.setName(name)
|
||||
.setOrigin(origin.x, origin.y)
|
||||
.setScale(scale)
|
||||
.setInteractive();
|
||||
this.addInteractionHoverEffect(img);
|
||||
|
||||
return img;
|
||||
}
|
||||
}
|
||||
107
src/ui/handlers/oauth-providers-ui-handler.ts
Normal file
107
src/ui/handlers/oauth-providers-ui-handler.ts
Normal file
@ -0,0 +1,107 @@
|
||||
import { globalScene } from "#app/global-scene";
|
||||
import { TextStyle } from "#enums/text-style";
|
||||
import { LoginRegisterInfoContainerUiHandler } from "#ui/login-register-info-container-ui-handler";
|
||||
import { addTextObject } from "#ui/text";
|
||||
import { addWindow } from "#ui/ui-theme";
|
||||
import { fixedInt } from "#utils/common";
|
||||
import i18next from "i18next";
|
||||
|
||||
// TODO: use mixins
|
||||
export abstract class OAuthProvidersUiHandler extends LoginRegisterInfoContainerUiHandler {
|
||||
private discordImage: Phaser.GameObjects.Image;
|
||||
private googleImage: Phaser.GameObjects.Image;
|
||||
|
||||
private externalPartyContainer: Phaser.GameObjects.Container;
|
||||
private externalPartyBg: Phaser.GameObjects.NineSlice;
|
||||
private externalPartyTitle: Phaser.GameObjects.Text;
|
||||
|
||||
public override setup(): void {
|
||||
super.setup();
|
||||
this.buildExternalPartyContainer();
|
||||
}
|
||||
|
||||
public override clear(): void {
|
||||
super.clear();
|
||||
|
||||
this.externalPartyContainer //
|
||||
.setVisible(false)
|
||||
.setActive(false);
|
||||
|
||||
[this.discordImage, this.googleImage].forEach(img => {
|
||||
img.off("pointerdown");
|
||||
});
|
||||
}
|
||||
|
||||
public override destroy(): void {
|
||||
super.destroy();
|
||||
this.externalPartyContainer.destroy();
|
||||
}
|
||||
|
||||
private buildExternalPartyContainer(): void {
|
||||
const { height, width } = globalScene.scaledCanvas;
|
||||
|
||||
this.externalPartyContainer = globalScene.add
|
||||
.container(0, 0)
|
||||
.setInteractive(new Phaser.Geom.Rectangle(0, 0, width / 2, height / 2), Phaser.Geom.Rectangle.Contains);
|
||||
this.externalPartyTitle = addTextObject(0, 4, "", TextStyle.SETTINGS_LABEL) //
|
||||
.setOrigin(0.5, 0);
|
||||
this.externalPartyBg = addWindow(0, 0, 0, 0);
|
||||
|
||||
this.discordImage = this.buildInteractableImage("discord", "discord-icon");
|
||||
this.googleImage = this.buildInteractableImage("google", "google-icon");
|
||||
|
||||
this.externalPartyContainer
|
||||
.add([this.externalPartyBg, this.externalPartyTitle, this.discordImage, this.googleImage])
|
||||
.setVisible(false);
|
||||
this.getUi().add(this.externalPartyContainer);
|
||||
}
|
||||
|
||||
protected processExternalProvider(): void {
|
||||
const titleX = 22;
|
||||
this.externalPartyTitle
|
||||
.setText(i18next.t("menu:orUse"))
|
||||
.setX(titleX + this.externalPartyTitle.text.length)
|
||||
.setVisible(true);
|
||||
|
||||
this.externalPartyContainer //
|
||||
.setPositionRelative(this.modalContainer, 175, 0)
|
||||
.setVisible(true);
|
||||
|
||||
const bgWidth = this.externalPartyTitle.text.length + 50;
|
||||
this.externalPartyBg.setSize(bgWidth, this.modalBg.height);
|
||||
this.getUi().moveTo(this.externalPartyContainer, this.getUi().length - 1);
|
||||
|
||||
const externalPartyIconWidth = this.externalPartyBg.width / 3.1;
|
||||
|
||||
const getRedirectUri = (service: string): string => {
|
||||
return encodeURIComponent(`${import.meta.env.VITE_SERVER_URL}/auth/${service}/callback`);
|
||||
};
|
||||
|
||||
this.discordImage //
|
||||
.setPosition(externalPartyIconWidth, this.externalPartyBg.height - 40)
|
||||
.on("pointerdown", () => {
|
||||
const redirectUri = getRedirectUri("discord");
|
||||
const discordId = import.meta.env.VITE_DISCORD_CLIENT_ID;
|
||||
const discordUrl = `https://discord.com/api/oauth2/authorize?client_id=${discordId}&redirect_uri=${redirectUri}&response_type=code&scope=identify&prompt=none`;
|
||||
window.open(discordUrl, "_self");
|
||||
});
|
||||
|
||||
this.googleImage //
|
||||
.setPosition(externalPartyIconWidth, this.externalPartyBg.height - 60)
|
||||
.on("pointerdown", () => {
|
||||
const redirectUri = getRedirectUri("google");
|
||||
const googleId = import.meta.env.VITE_GOOGLE_CLIENT_ID;
|
||||
const googleUrl = `https://accounts.google.com/o/oauth2/auth?client_id=${googleId}&redirect_uri=${redirectUri}&response_type=code&scope=openid`;
|
||||
window.open(googleUrl, "_self");
|
||||
});
|
||||
|
||||
this.externalPartyContainer.setAlpha(0);
|
||||
globalScene.tweens.add({
|
||||
targets: this.externalPartyContainer,
|
||||
duration: fixedInt(1000),
|
||||
ease: "Sine.easeInOut",
|
||||
y: "-=24",
|
||||
alpha: 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -3,33 +3,33 @@ import { globalScene } from "#app/global-scene";
|
||||
import { TextStyle } from "#enums/text-style";
|
||||
import { UiMode } from "#enums/ui-mode";
|
||||
import type { InputFieldConfig } from "#ui/form-modal-ui-handler";
|
||||
import { FormModalUiHandler } from "#ui/form-modal-ui-handler";
|
||||
import type { ModalConfig } from "#ui/modal-ui-handler";
|
||||
import { addTextObject } from "#ui/text";
|
||||
import i18next from "i18next";
|
||||
import { LoginRegisterInfoContainerUiHandler } from "./login-register-info-container-ui-handler";
|
||||
|
||||
export class RegistrationFormUiHandler extends FormModalUiHandler {
|
||||
getModalTitle(_config?: ModalConfig): string {
|
||||
export class RegistrationFormUiHandler extends LoginRegisterInfoContainerUiHandler {
|
||||
public override getModalTitle(): string {
|
||||
return i18next.t("menu:register");
|
||||
}
|
||||
|
||||
getWidth(_config?: ModalConfig): number {
|
||||
public override getWidth(): number {
|
||||
return 160;
|
||||
}
|
||||
|
||||
getMargin(_config?: ModalConfig): [number, number, number, number] {
|
||||
return [0, 0, 48, 0];
|
||||
public override getMargin(): [number, number, number, number] {
|
||||
return [0, 20, 48, 0];
|
||||
}
|
||||
|
||||
getButtonTopMargin(): number {
|
||||
public override getButtonTopMargin(): number {
|
||||
return 12;
|
||||
}
|
||||
|
||||
getButtonLabels(_config?: ModalConfig): string[] {
|
||||
return [i18next.t("menu:register"), i18next.t("menu:backToLogin")];
|
||||
public override getButtonLabels(): string[] {
|
||||
return [i18next.t("menu:register"), i18next.t("menu:goBack")];
|
||||
}
|
||||
|
||||
getReadableErrorMessage(error: string): string {
|
||||
public override getReadableErrorMessage(error: string): string {
|
||||
const colonIndex = error?.indexOf(":");
|
||||
if (colonIndex > 0) {
|
||||
error = error.slice(0, colonIndex);
|
||||
@ -46,7 +46,7 @@ export class RegistrationFormUiHandler extends FormModalUiHandler {
|
||||
return super.getReadableErrorMessage(error);
|
||||
}
|
||||
|
||||
override getInputFieldConfigs(): InputFieldConfig[] {
|
||||
public override getInputFieldConfigs(): InputFieldConfig[] {
|
||||
const inputFieldConfigs: InputFieldConfig[] = [];
|
||||
inputFieldConfigs.push({ label: i18next.t("menu:username") });
|
||||
inputFieldConfigs.push({
|
||||
@ -60,7 +60,7 @@ export class RegistrationFormUiHandler extends FormModalUiHandler {
|
||||
return inputFieldConfigs;
|
||||
}
|
||||
|
||||
setup(): void {
|
||||
public override setup(): void {
|
||||
super.setup();
|
||||
|
||||
const label = addTextObject(10, 87, i18next.t("menu:registrationAgeWarning"), TextStyle.TOOLTIP_CONTENT, {
|
||||
@ -71,60 +71,61 @@ export class RegistrationFormUiHandler extends FormModalUiHandler {
|
||||
this.modalContainer.add(label);
|
||||
}
|
||||
|
||||
show(args: any[]): boolean {
|
||||
if (super.show(args)) {
|
||||
const config = args[0] as ModalConfig;
|
||||
|
||||
const originalRegistrationAction = this.submitAction;
|
||||
this.submitAction = () => {
|
||||
if (globalScene.tweens.getTweensOf(this.modalContainer).length === 0) {
|
||||
// Prevent overlapping overrides on action modification
|
||||
this.submitAction = originalRegistrationAction;
|
||||
this.sanitizeInputs();
|
||||
globalScene.ui.setMode(UiMode.LOADING, { buttonActions: [] });
|
||||
const onFail = error => {
|
||||
globalScene.ui.setMode(UiMode.REGISTRATION_FORM, Object.assign(config, { errorMessage: error?.trim() }));
|
||||
globalScene.ui.playError();
|
||||
};
|
||||
if (!this.inputs[0].text) {
|
||||
return onFail(i18next.t("menu:emptyUsername"));
|
||||
}
|
||||
if (!this.inputs[1].text) {
|
||||
return onFail(this.getReadableErrorMessage("invalid password"));
|
||||
}
|
||||
if (this.inputs[1].text !== this.inputs[2].text) {
|
||||
return onFail(i18next.t("menu:passwordNotMatchingConfirmPassword"));
|
||||
}
|
||||
const [usernameInput, passwordInput] = this.inputs;
|
||||
pokerogueApi.account
|
||||
.register({
|
||||
username: usernameInput.text,
|
||||
password: passwordInput.text,
|
||||
})
|
||||
.then(registerError => {
|
||||
if (!registerError) {
|
||||
pokerogueApi.account
|
||||
.login({
|
||||
username: usernameInput.text,
|
||||
password: passwordInput.text,
|
||||
})
|
||||
.then(loginError => {
|
||||
if (!loginError) {
|
||||
originalRegistrationAction?.();
|
||||
} else {
|
||||
onFail(loginError);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
onFail(registerError);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return true;
|
||||
public override show(args: [ModalConfig, ...any[]]): boolean {
|
||||
if (!super.show(args)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
const config = args[0];
|
||||
this.showInfoContainer(config);
|
||||
|
||||
const originalRegistrationAction = this.submitAction;
|
||||
this.submitAction = () => {
|
||||
if (globalScene.tweens.getTweensOf(this.modalContainer).length === 0) {
|
||||
// Prevent overlapping overrides on action modification
|
||||
this.submitAction = originalRegistrationAction;
|
||||
this.sanitizeInputs();
|
||||
globalScene.ui.setMode(UiMode.LOADING, { buttonActions: [] });
|
||||
const onFail = error => {
|
||||
globalScene.ui.setMode(UiMode.REGISTRATION_FORM, Object.assign(config, { errorMessage: error?.trim() }));
|
||||
globalScene.ui.playError();
|
||||
};
|
||||
if (!this.inputs[0].text) {
|
||||
return onFail(i18next.t("menu:emptyUsername"));
|
||||
}
|
||||
if (!this.inputs[1].text) {
|
||||
return onFail(this.getReadableErrorMessage("invalid password"));
|
||||
}
|
||||
if (this.inputs[1].text !== this.inputs[2].text) {
|
||||
return onFail(i18next.t("menu:passwordNotMatchingConfirmPassword"));
|
||||
}
|
||||
const [usernameInput, passwordInput] = this.inputs;
|
||||
pokerogueApi.account
|
||||
.register({
|
||||
username: usernameInput.text,
|
||||
password: passwordInput.text,
|
||||
})
|
||||
.then(registerError => {
|
||||
if (registerError) {
|
||||
onFail(registerError);
|
||||
} else {
|
||||
pokerogueApi.account
|
||||
.login({
|
||||
username: usernameInput.text,
|
||||
password: passwordInput.text,
|
||||
})
|
||||
.then(loginError => {
|
||||
if (loginError) {
|
||||
onFail(loginError);
|
||||
} else {
|
||||
originalRegistrationAction?.();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,6 +26,7 @@ import { GamepadBindingUiHandler } from "#ui/gamepad-binding-ui-handler";
|
||||
import { KeyboardBindingUiHandler } from "#ui/keyboard-binding-ui-handler";
|
||||
import { LoadingModalUiHandler } from "#ui/loading-modal-ui-handler";
|
||||
import { LoginFormUiHandler } from "#ui/login-form-ui-handler";
|
||||
import { LoginOrRegisterUiHandler } from "#ui/login-or-register-ui-handler";
|
||||
import { MenuUiHandler } from "#ui/menu-ui-handler";
|
||||
import { MessageUiHandler } from "#ui/message-ui-handler";
|
||||
import { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler";
|
||||
@ -163,6 +164,7 @@ export class UI extends Phaser.GameObjects.Container {
|
||||
new PokedexUiHandler(),
|
||||
new PokedexScanUiHandler(UiMode.TEST_DIALOGUE),
|
||||
new PokedexPageUiHandler(),
|
||||
new LoginOrRegisterUiHandler(),
|
||||
new LoginFormUiHandler(),
|
||||
new RegistrationFormUiHandler(),
|
||||
new LoadingModalUiHandler(),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user