diff --git a/resources/lang/en.json b/resources/lang/en.json
index e39b0a6b5..2ae02389d 100644
--- a/resources/lang/en.json
+++ b/resources/lang/en.json
@@ -198,7 +198,8 @@
"not_found": "Not Found",
"clear_session": "Clear Session",
"failed_to_send_recovery_email": "Failed to send recovery email",
- "enter_email_address": "Please enter an email address"
+ "enter_email_address": "Please enter an email address",
+ "personal_player_id": "Personal Player ID:"
},
"stats_modal": {
"title": "Stats",
diff --git a/src/client/AccountModal.ts b/src/client/AccountModal.ts
index 505537be6..8169e9566 100644
--- a/src/client/AccountModal.ts
+++ b/src/client/AccountModal.ts
@@ -13,16 +13,16 @@ import "./components/baseComponents/stats/GameList";
import "./components/baseComponents/stats/PlayerStatsTable";
import "./components/baseComponents/stats/PlayerStatsTree";
import { BaseModal } from "./components/BaseModal";
+import "./components/CopyButton";
import "./components/Difficulties";
import "./components/PatternButton";
import { modalHeader } from "./components/ui/ModalHeader";
-import { copyToClipboard, translateText } from "./Utils";
+import { translateText } from "./Utils";
@customElement("account-modal")
export class AccountModal extends BaseModal {
@state() private email: string = "";
@state() private isLoadingUser: boolean = false;
- @state() private showCopied: boolean = false;
private userMeResponse: UserMeResponse | null = null;
private statsTree: PlayerStatsTree | null = null;
@@ -47,17 +47,6 @@ export class AccountModal extends BaseModal {
});
}
- private async copyIdToClipboard() {
- const id = this.userMeResponse?.player?.publicId;
- if (!id) return;
-
- await copyToClipboard(
- id,
- () => (this.showCopied = true),
- () => (this.showCopied = false),
- );
- }
-
private hasAnyStats(): boolean {
if (!this.statsTree) return false;
// Check if statsTree has any data
@@ -106,6 +95,8 @@ export class AccountModal extends BaseModal {
private renderInner() {
const isLoggedIn = !!this.userMeResponse?.user;
const title = translateText("account_modal.title");
+ const publicId = this.userMeResponse?.player?.publicId ?? "";
+ const displayId = publicId || translateText("account_modal.not_found");
return html`
ID:${translateText("account_modal.personal_player_id")}
-
+
`
: undefined,
diff --git a/src/client/HostLobbyModal.ts b/src/client/HostLobbyModal.ts
index e7d709c12..2ca75b207 100644
--- a/src/client/HostLobbyModal.ts
+++ b/src/client/HostLobbyModal.ts
@@ -1,6 +1,6 @@
import { TemplateResult, html } from "lit";
import { customElement, state } from "lit/decorators.js";
-import { copyToClipboard, translateText } from "../client/Utils";
+import { translateText } from "../client/Utils";
import { getServerConfigFromClient } from "../core/configuration/ConfigLoader";
import {
Difficulty,
@@ -15,7 +15,6 @@ import {
mapCategories,
} from "../core/game/Game";
import { getCompactMapNationCount } from "../core/game/NationCreation";
-import { UserSettings } from "../core/game/UserSettings";
import {
ClientInfo,
GameConfig,
@@ -26,6 +25,7 @@ import {
import { generateID } from "../core/Util";
import "./components/baseComponents/Modal";
import { BaseModal } from "./components/BaseModal";
+import "./components/CopyButton";
import "./components/Difficulties";
import "./components/FluentSlider";
import "./components/LobbyTeamView";
@@ -65,19 +65,16 @@ export class HostLobbyModal extends BaseModal {
@state() private startingGold: boolean = false;
@state() private startingGoldValue: number | undefined = undefined;
@state() private lobbyId = "";
- @state() private copySuccess = false;
@state() private lobbyUrlSuffix = "";
@state() private clients: ClientInfo[] = [];
@state() private useRandomMap: boolean = false;
@state() private disabledUnits: UnitType[] = [];
@state() private lobbyCreatorClientID: string = "";
- @state() private lobbyIdVisible: boolean = true;
@state() private nationCount: number = 0;
private playersInterval: NodeJS.Timeout | null = null;
// Add a new timer for debouncing bot changes
private botsUpdateTimer: number | null = null;
- private userSettings: UserSettings = new UserSettings();
private mapLoader = terrainMapFileLoader;
private leaveLobbyOnClose = true;
@@ -144,91 +141,11 @@ export class HostLobbyModal extends BaseModal {
},
ariaLabel: translateText("common.back"),
rightContent: html`
-
-
-
-
-
-
+
`,
})}
@@ -997,10 +914,6 @@ export class HostLobbyModal extends BaseModal {
protected onOpen(): void {
this.lobbyCreatorClientID = generateID();
- this.lobbyIdVisible = this.userSettings.get(
- "settings.lobbyIdVisibility",
- true,
- );
createLobby(this.lobbyCreatorClientID)
.then(async (lobby) => {
@@ -1119,10 +1032,8 @@ export class HostLobbyModal extends BaseModal {
this.useRandomMap = false;
this.disabledUnits = [];
this.lobbyId = "";
- this.copySuccess = false;
this.clients = [];
this.lobbyCreatorClientID = "";
- this.lobbyIdVisible = true;
this.nationCount = 0;
this.goldMultiplier = false;
this.goldMultiplierValue = undefined;
@@ -1403,15 +1314,6 @@ export class HostLobbyModal extends BaseModal {
return response;
}
- private async copyToClipboard() {
- const url = await this.buildLobbyUrl();
- await copyToClipboard(
- url,
- () => (this.copySuccess = true),
- () => (this.copySuccess = false),
- );
- }
-
private async pollPlayers() {
const config = await getServerConfigFromClient();
fetch(`/${config.workerPath(this.lobbyId)}/api/game/${this.lobbyId}`, {
diff --git a/src/client/JoinPrivateLobbyModal.ts b/src/client/JoinPrivateLobbyModal.ts
index cb65b428f..117e6d753 100644
--- a/src/client/JoinPrivateLobbyModal.ts
+++ b/src/client/JoinPrivateLobbyModal.ts
@@ -1,6 +1,6 @@
import { html, TemplateResult } from "lit";
import { customElement, query, state } from "lit/decorators.js";
-import { copyToClipboard, translateText } from "../client/Utils";
+import { translateText } from "../client/Utils";
import {
ClientInfo,
GAME_ID_REGEX,
@@ -11,10 +11,10 @@ import {
import { generateID } from "../core/Util";
import { getServerConfigFromClient } from "../core/configuration/ConfigLoader";
import { GameMode } from "../core/game/Game";
-import { UserSettings } from "../core/game/UserSettings";
import { getApiBase } from "./Api";
import { JoinLobbyEvent } from "./Main";
import { BaseModal } from "./components/BaseModal";
+import "./components/CopyButton";
import "./components/Difficulties";
import "./components/LobbyTeamView";
import { modalHeader } from "./components/ui/ModalHeader";
@@ -26,12 +26,9 @@ export class JoinPrivateLobbyModal extends BaseModal {
@state() private players: ClientInfo[] = [];
@state() private gameConfig: GameConfig | null = null;
@state() private lobbyCreatorClientID: string | null = null;
- @state() private lobbyIdVisible: boolean = true;
- @state() private copySuccess: boolean = false;
@state() private currentLobbyId: string = "";
private playersInterval: NodeJS.Timeout | null = null;
- private userSettings: UserSettings = new UserSettings();
private leaveLobbyOnClose = true;
@@ -50,91 +47,7 @@ export class JoinPrivateLobbyModal extends BaseModal {
ariaLabel: translateText("common.close"),
rightContent: this.hasJoined
? html`
-
-
-
-
{
- (e.currentTarget as HTMLElement).classList.add(
- "select-all",
- );
- }}
- @mouseleave=${(e: Event) => {
- (e.currentTarget as HTMLElement).classList.remove(
- "select-all",
- );
- }}
- class="font-mono text-xs font-bold text-white px-2 cursor-pointer select-none min-w-[80px] text-center truncate tracking-wider"
- title="${translateText("common.click_to_copy")}"
- >
- ${this.copySuccess
- ? translateText("common.copied")
- : this.lobbyIdVisible
- ? this.currentLobbyId
- : "••••••••"}
-
-
-
+
`
: undefined,
})}
@@ -347,10 +260,6 @@ export class JoinPrivateLobbyModal extends BaseModal {
public open(id: string = "") {
super.open();
- this.lobbyIdVisible = this.userSettings.get(
- "settings.lobbyIdVisibility",
- true,
- );
if (id) {
this.setLobbyId(id);
this.joinLobby();
@@ -396,15 +305,6 @@ export class JoinPrivateLobbyModal extends BaseModal {
this.close();
}
- private async copyToClipboard() {
- const config = await getServerConfigFromClient();
- await copyToClipboard(
- `${location.origin}/${config.workerPath(this.currentLobbyId)}/game/${this.currentLobbyId}`,
- () => (this.copySuccess = true),
- () => (this.copySuccess = false),
- );
- }
-
private isValidLobbyId(value: string): boolean {
return GAME_ID_REGEX.test(value);
}
diff --git a/src/client/components/CopyButton.ts b/src/client/components/CopyButton.ts
new file mode 100644
index 000000000..13742cc58
--- /dev/null
+++ b/src/client/components/CopyButton.ts
@@ -0,0 +1,206 @@
+import { LitElement, html } from "lit";
+import { customElement, property, state } from "lit/decorators.js";
+import { getServerConfigFromClient } from "../../core/configuration/ConfigLoader";
+import { UserSettings } from "../../core/game/UserSettings";
+import { copyToClipboard, translateText } from "../Utils";
+
+@customElement("copy-button")
+export class CopyButton extends LitElement {
+ @property({ type: String, attribute: "lobby-id" }) lobbyId = "";
+ @property({ type: String, attribute: "lobby-suffix" }) lobbySuffix = "";
+ @property({ type: Boolean, attribute: "include-lobby-query" })
+ includeLobbyQuery = false;
+ @property({ type: String, attribute: "copy-text" }) copyText = "";
+ @property({ type: String, attribute: "display-text" }) displayText = "";
+ @property({ type: Boolean, attribute: "show-visibility-toggle" })
+ showVisibilityToggle = true;
+ @property({ type: Boolean, attribute: "show-copy-icon" })
+ showCopyIcon = true;
+ @property({ type: Boolean }) compact = false;
+
+ @state() private copySuccess = false;
+ @state() private lobbyIdVisible = true;
+
+ private userSettings: UserSettings = new UserSettings();
+ private maskLabel = html`••••••••`;
+
+ createRenderRoot() {
+ return this;
+ }
+
+ protected willUpdate(
+ changedProperties: Map,
+ ) {
+ if (changedProperties.has("lobbyId")) {
+ this.lobbyIdVisible = this.userSettings.get(
+ "settings.lobbyIdVisibility",
+ true,
+ );
+ this.copySuccess = false;
+ }
+ if (changedProperties.has("copyText")) {
+ this.copySuccess = false;
+ }
+ if (
+ changedProperties.has("showVisibilityToggle") ||
+ changedProperties.has("compact")
+ ) {
+ if (!this.showVisibilityToggle || this.compact) {
+ this.lobbyIdVisible = true;
+ }
+ }
+ }
+
+ private toggleVisibility() {
+ if (!this.showVisibilityToggle || this.compact) return;
+ this.lobbyIdVisible = !this.lobbyIdVisible;
+ }
+
+ private enableSelectAll(e: Event) {
+ (e.currentTarget as HTMLElement).classList.add("select-all");
+ }
+
+ private clearSelectAll(e: Event) {
+ (e.currentTarget as HTMLElement).classList.remove("select-all");
+ }
+
+ private async buildCopyUrl(): Promise {
+ const config = await getServerConfigFromClient();
+ let url = `${window.location.origin}/${config.workerPath(this.lobbyId)}/game/${this.lobbyId}`;
+ if (this.includeLobbyQuery) {
+ url += `?lobby&s=${encodeURIComponent(this.lobbySuffix)}`;
+ }
+ return url;
+ }
+
+ private async resolveCopyText(): Promise {
+ if (this.copyText) return this.copyText;
+ if (!this.lobbyId) return "";
+ return await this.buildCopyUrl();
+ }
+
+ private async handleCopy() {
+ const text = await this.resolveCopyText();
+ if (!text) return;
+ await copyToClipboard(
+ text,
+ () => (this.copySuccess = true),
+ () => (this.copySuccess = false),
+ );
+ }
+
+ private canCopy() {
+ return Boolean(this.copyText || this.lobbyId);
+ }
+
+ render() {
+ const canCopy = this.canCopy();
+ const allowMask = this.showVisibilityToggle && !this.compact;
+ const rawLabel = this.displayText || this.lobbyId || this.copyText;
+ const label = this.copySuccess
+ ? translateText("common.copied")
+ : allowMask && !this.lobbyIdVisible
+ ? this.maskLabel
+ : rawLabel;
+ const disabledClass = canCopy ? "" : "opacity-60 cursor-not-allowed";
+ const toggleDisabled = !this.lobbyId;
+ const toggleClass = toggleDisabled ? "opacity-60 cursor-not-allowed" : "";
+
+ if (this.compact) {
+ return html`
+
+ `;
+ }
+
+ return html`
+
+ ${this.showVisibilityToggle
+ ? html`
`
+ : ""}
+
+ ${this.showCopyIcon
+ ? html`
`
+ : ""}
+
+ `;
+ }
+}