diff --git a/resources/flags/custom/beta_tester_hole.svg b/resources/flags/custom/beta_tester_circle.svg similarity index 100% rename from resources/flags/custom/beta_tester_hole.svg rename to resources/flags/custom/beta_tester_circle.svg diff --git a/resources/flags/custom/ofm_2025.svg b/resources/flags/custom/ofm_2025.svg new file mode 100644 index 000000000..fe9bd04c4 --- /dev/null +++ b/resources/flags/custom/ofm_2025.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/lang/en.json b/resources/lang/en.json index aa090c7b0..da22417ee 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -184,5 +184,86 @@ }, "select_lang": { "title": "Select Language" + }, + "flag_input": { + "title": "Flag Selector", + "real": "Real Flags", + "custom": "Custom Flags", + "select_layer": "Select a Layer", + "max_layer_reached_1": "You've reached the maximum number of layers.", + "max_layer_reached_2": "Please remove some to add new ones.", + "preview": "Preview", + "apply": "Apply Custom Flag", + "layer": "Layer", + "fixed": "fixed", + "remove": "Remove", + "color": "Color", + "layers": { + "frame": "Frame", + "full": "Full Background", + "center_circle": "Center Circle", + "center_star": "Center Star", + "center_flower": "Center Flower", + "center_hline": "Horizontal Stripe", + "center_vline": "Vertical Stripe", + "flower_tc": "Top Center Flower", + "flower_tl": "Top Left Flower", + "flower_tr": "Top Right Flower", + "diag_bl": "Bottom Left Diagonal", + "diag_br": "Bottom Right Diagonal", + "half_l": "Left Half", + "half_r": "Right Half", + "half_t": "Top Half", + "half_b": "Bottom Half", + "triangle_t": "Top Triangle", + "triangle_b": "Bottom Triangle", + "triangle_l": "Left Triangle", + "triangle_r": "Right Triangle", + "triangle_tl": "Top Left Triangle", + "triangle_tr": "Top Right Triangle", + "triangle_bl": "Bottom Left Triangle", + "triangle_br": "Bottom Right Triangle", + "tricolor_l": "Left Tricolor", + "tricolor_c": "Center Tricolor", + "tricolor_r": "Right Tricolor", + "tricolor_t": "Top Tricolor", + "tricolor_m": "Middle Tricolor", + "tricolor_b": "Bottom Tricolor", + "mini_tr_tl": "Mini Top Left Triangle", + "mini_tr_tr": "Mini Top Right Triangle", + "mini_tr_bl": "Mini Bottom Left Triangle", + "mini_tr_br": "Mini Bottom Right Triangle", + "nato_emblem": "NATO Emblem", + "eu_star": "EU Stars", + "laurel_wreath": "Laurel Wreath", + "octagram": "Octagram", + "octagram_2": "Diagonal Octagram", + "rocket": "Rocket", + "rocket_mini": "Mini Rocket", + "ofm_2025": "OFM 2025", + "translator": "Translator", + "beta_tester": "Beta Tester", + "beta_tester_circle": "Beta Circle", + "admin_contributors": "Contributors Emblem", + "admin_shield": "Admin Shield", + "admin_shield_r": "Right Admin Shield", + "admin_evan": "Evan's Crown", + "og": "OG Emblem", + "og_plus": "OG+ Emblem" + }, + "reason": { + "admin_evan": "Only Evan can use this layer", + "admin": "Admin only", + "contributors": "Contributors only", + "beta": "Beta testers only", + "supporters": "Supporters only", + "og": "OG players only", + "og100": "OG100 players only", + "translator": "Translator only", + "well_known": "Well-known players only", + "known": "Known players only", + "seen": "Seen players only", + "login": "Login required" + } } } diff --git a/resources/lang/ja.json b/resources/lang/ja.json index a86528b71..597e16116 100644 --- a/resources/lang/ja.json +++ b/resources/lang/ja.json @@ -176,5 +176,86 @@ }, "select_lang": { "title": "言語を選択" + }, + "flag_input": { + "title": "旗選択", + "real": "実在の旗", + "custom": "カスタム旗", + "select_layer": "レイヤーを選択", + "max_layer_reached_1": "レイヤー数の上限に達しています。", + "max_layer_reached_2": "新しく追加するには、いくつか削除してください。", + "preview": "プレビュー", + "apply": "カスタム旗を適用", + "layer": "レイヤー", + "fixed": "固定", + "remove": "削除", + "color": "色", + "layers": { + "frame": "フレーム", + "full": "背景", + "center_circle": "中央の円", + "center_star": "中央の星", + "center_flower": "中央の花", + "center_hline": "横ストライプ", + "center_vline": "縦ストライプ", + "flower_tc": "上中央の花", + "flower_tl": "左上の花", + "flower_tr": "右上の花", + "diag_bl": "左下の斜線", + "diag_br": "右下の斜線", + "half_l": "左半分", + "half_r": "右半分", + "half_t": "上半分", + "half_b": "下半分", + "triangle_t": "上三角形", + "triangle_b": "下三角形", + "triangle_l": "左三角形", + "triangle_r": "右三角形", + "triangle_tl": "左上三角形", + "triangle_tr": "右上三角形", + "triangle_bl": "左下三角形", + "triangle_br": "右下三角形", + "tricolor_l": "三色旗(左)", + "tricolor_c": "三色旗(中央)", + "tricolor_r": "三色旗(右)", + "tricolor_t": "三色旗(上)", + "tricolor_m": "三色旗(中央)", + "tricolor_b": "三色旗(下)", + "mini_tr_tl": "ミニ左上三角形", + "mini_tr_tr": "ミニ右上三角形", + "mini_tr_bl": "ミニ左下三角形", + "mini_tr_br": "ミニ右下三角形", + "nato_emblem": "NATOエンブレム", + "eu_star": "EUの星", + "laurel_wreath": "月桂冠", + "octagram": "八芒星", + "octagram_2": "斜めの八芒星", + "rocket": "ロケット", + "rocket_mini": "ミニロケット", + "ofm_2025": "OFM 2025", + "translator": "翻訳者マーク", + "beta_tester": "ベータテスター", + "beta_tester_circle": "ベータサークル", + "admin_contributors": "貢献者エンブレム", + "admin_shield": "管理者の盾", + "admin_shield_r": "管理者の盾(右)", + "admin_evan": "Evan様の冠", + "og": "OGエンブレム", + "og_plus": "OG+エンブレム" + }, + "reason": { + "admin_evan": "このレイヤーはEvanのみ使用できます", + "admin": "管理者専用", + "contributors": "貢献者専用", + "beta": "ベータテスター専用", + "supporters": "支援者専用", + "og": "OGプレイヤー専用", + "og100": "OG100プレイヤー専用", + "translator": "翻訳者専用", + "well_known": "有名プレイヤー専用", + "known": "知られているプレイヤー専用", + "seen": "確認済みプレイヤー専用", + "login": "ログインが必要です" + } } } diff --git a/src/client/FlagInput.ts b/src/client/FlagInput.ts index 49622d812..91ecd2532 100644 --- a/src/client/FlagInput.ts +++ b/src/client/FlagInput.ts @@ -1,11 +1,8 @@ import { LitElement, css, html } from "lit"; import { customElement, state } from "lit/decorators.js"; -import Countries from "./data/countries.json"; +import { translateText } from "../client/Utils"; const flagKey: string = "flag"; -import disabled from "../../resources/images/DisabledIcon.svg"; -import locked from "../../resources/images/Locked.svg"; - import frame from "../../resources/flags/custom/frame.svg"; import center_circle from "../../resources/flags/custom/center_circle.svg"; @@ -32,6 +29,7 @@ import mini_tr_tr from "../../resources/flags/custom/mini_tr_tr.svg"; import nato_emblem from "../../resources/flags/custom/nato_emblem.svg"; import octagram from "../../resources/flags/custom/octagram.svg"; import octagram_2 from "../../resources/flags/custom/octagram_2.svg"; +import ofm_2025 from "../../resources/flags/custom/ofm_2025.svg"; import triangle_b from "../../resources/flags/custom/triangle_b.svg"; import triangle_bl from "../../resources/flags/custom/triangle_bl.svg"; import triangle_br from "../../resources/flags/custom/triangle_br.svg"; @@ -56,7 +54,7 @@ import og_plus from "../../resources/flags/custom/og_plus.svg"; import translator from "../../resources/flags/custom/translator.svg"; import beta_tester from "../../resources/flags/custom/beta_tester.svg"; -import beta_tester_hole from "../../resources/flags/custom/beta_tester_hole.svg"; +import beta_tester_circle from "../../resources/flags/custom/beta_tester_circle.svg"; import admin_contributors from "../../resources/flags/custom/admin_contributors.svg"; import admin_shield from "../../resources/flags/custom/admin_shield.svg"; @@ -103,8 +101,9 @@ export const FlagMap: Record = { laurel_wreath, octagram, octagram_2, + ofm_2025, beta_tester, - beta_tester_hole, + beta_tester_circle, rocket, rocket_mini, admin_contributors, @@ -155,12 +154,13 @@ export const LayerShortNames: Record = { nato_emblem: "ne", eu_star: "es", laurel_wreath: "lw", + ofm_2025: "ofm25", octagram: "oc", octagram_2: "oc2", og: "og", og_plus: "ogp", beta_tester: "bt", - beta_tester_hole: "bth", + beta_tester_circle: "btc", rocket: "rc", rocket_mini: "rcm", translator: "tlr", @@ -213,39 +213,41 @@ export const ColorShortNames: Record = { water: "wt", // soft blue breathing animation }; -let isDebug_: boolean = false; -const isEvan: boolean = false; -const isAdmin: boolean = false; -const isOg: boolean = false; -const isOg100: boolean = false; -const isSupporters: boolean = false; -const isBetaTester: boolean = false; -const isContributors: boolean = false; -const isTranslator: boolean = false; -const isWellKnownPlayer: boolean = false; -const isKnownPlayer: boolean = false; -const isSeenplayer: boolean = false; -const isLoginPlayer: boolean = false; +export const isDebug_: boolean = true; +export const isEvan: boolean = false; +export const isAdmin: boolean = false; +export const isOg: boolean = false; +export const isOg100: boolean = false; +export const isSupporters: boolean = false; +export const isBetaTester: boolean = false; +export const isContributors: boolean = false; +export const isTranslator: boolean = false; +export const isWellKnownPlayer: boolean = false; +export const isKnownPlayer: boolean = false; +export const isSeenplayer: boolean = false; +export const isLoginPlayer: boolean = false; -let MAX_LAYER = 50; +export let MAX_LAYER = 50; type LockReasonMap = Record; -function checkPermission(): [string[], string[], LockReasonMap] { +export function checkPermission(): [string[], string[], LockReasonMap, number] { const lockedLayers_: string[] = []; const lockedColors_: string[] = []; const lockedReasons_: LockReasonMap = {}; MAX_LAYER = 50; - const lock = (list: string[], reason: string) => { + const lock = (list: string[], reasonKey: string) => { + const reason = translateText(reasonKey); for (const item of list) { lockedLayers_.push(item); lockedReasons_[item] = reason; } }; - const lockColor = (list: string[], reason: string) => { + const lockColor = (list: string[], reasonKey: string) => { + const reason = translateText(reasonKey); for (const color of list) { lockedColors_.push(color); lockedReasons_[color] = reason; @@ -254,13 +256,13 @@ function checkPermission(): [string[], string[], LockReasonMap] { if (isEvan || isDebug_) { MAX_LAYER = 50; - return [lockedLayers_, lockedColors_, lockedReasons_]; + return [lockedLayers_, lockedColors_, lockedReasons_, MAX_LAYER]; } - lock(["admin_evan"], "Only Evan can use this layer"); + lock(["admin_evan"], "flag_input.reason.admin_evan"); if (!isAdmin) { - lock(["admin_shield", "admin_shield_r"], "Admin only"); + lock(["admin_shield", "admin_shield_r"], "flag_input.reason.admin"); } if (isAdmin) { @@ -282,15 +284,15 @@ function checkPermission(): [string[], string[], LockReasonMap] { } if (!isContributors) { - lock(["admin_contributors"], "Contributors only"); + lock(["admin_contributors"], "flag_input.reason.contributors"); } if (!isBetaTester) { - lock(["beta_tester", "beta_tester_hole"], "Beta testers only"); + lock(["beta_tester", "beta_tester_circle"], "flag_input.reason.beta"); } if (!isSupporters) { - lock(["rocket_mini", "rocket"], "Supporters only"); + lock(["rocket_mini", "rocket"], "flag_input.reason.supporters"); lockColor( [ "rainbow", @@ -302,22 +304,22 @@ function checkPermission(): [string[], string[], LockReasonMap] { "glitch", "water", ], - "Supporters only", + "flag_input.reason.supporters", ); } else { - return [lockedLayers_, lockedColors_, lockedReasons_]; + return [lockedLayers_, lockedColors_, lockedReasons_, MAX_LAYER]; } if (!isOg) { - lock(["og_plus"], "OG players only"); + lock(["og_plus"], "flag_input.reason.og"); } if (!isOg100) { - lock(["og"], "OG100 players only"); + lock(["og"], "flag_input.reason.og100"); } if (!isTranslator) { - lock(["beta_tester", "beta_tester_hole"], "Beta testers only"); + lock(["translator"], "flag_input.reason.translator"); } if (!isWellKnownPlayer) { @@ -335,7 +337,7 @@ function checkPermission(): [string[], string[], LockReasonMap] { "octagram", "octagram_2", ], - "Well-known players only", + "flag_input.reason.well_known", ); lockColor( [ @@ -354,7 +356,7 @@ function checkPermission(): [string[], string[], LockReasonMap] { "#36454f", "#fffff0", ], - "Well-known players only", + "flag_input.reason.well_known", ); if (!isKnownPlayer) { @@ -375,7 +377,7 @@ function checkPermission(): [string[], string[], LockReasonMap] { "mini_tr_br", "mini_tr_bl", ], - "Known players only", + "flag_input.reason.known", ); lockColor( [ @@ -390,91 +392,33 @@ function checkPermission(): [string[], string[], LockReasonMap] { "#8b0000", "#191970", ], - "Known players only", + "flag_input.reason.known", ); if (!isSeenplayer) { - lock(["half_l", "half_r", "half_b", "half_t"], "Seen players only"); - lockColor(["#ffa500", "#00ffff"], "Seen players only"); + lock( + ["half_l", "half_r", "half_b", "half_t"], + "flag_input.reason.seen", + ); + lockColor(["#ffa500", "#00ffff"], "flag_input.reason.seen"); if (!isLoginPlayer) { lock( ["triangle_br", "triangle_bl", "triangle_tr", "triangle_tl"], - "Login required", + "flag_input.reason.login", ); - lockColor(["#ffff00", "#008000"], "Login required"); + lockColor(["#ffff00", "#008000"], "flag_input.reason.login"); } } } } - return [lockedLayers_, lockedColors_, lockedReasons_]; + + return [lockedLayers_, lockedColors_, lockedReasons_, MAX_LAYER]; } @customElement("flag-input") export class FlagInput extends LitElement { - @state() private flag: string = ""; - @state() private search: string = ""; - @state() private showModal: boolean = false; - @state() private activeTab: "real" | "custom" = "real"; - @state() private selectedColor: string = "#ff0000"; - @state() private openColorIndex: number | null = null; - - private readonly colorOptions: string[] = Object.keys(ColorShortNames); - - @state() private customLayers: { name: string; color: string }[] = []; - - @state() private hoveredColor: string | null = null; - @state() private hoverPosition = { x: 0, y: 0 }; - @state() private hoverReason: string | null = null; - - private addLayer(name: string) { - const totalLayers = this.customLayers.length; - - if (totalLayers >= MAX_LAYER) { - alert(`You can only add up to ${MAX_LAYER} layers.`); - return; - } - - const newLayer = { name, color: this.selectedColor }; - this.customLayers = [ - this.customLayers[0], - newLayer, - ...this.customLayers.slice(1), - ]; - } - private removeLayer(index: number) { - this.customLayers = this.customLayers.filter((_, i) => i !== index); - } - - private moveLayerUp(index: number) { - if (index === 0) return; - const newLayers = [...this.customLayers]; - [newLayers[index - 1], newLayers[index]] = [ - newLayers[index], - newLayers[index - 1], - ]; - this.customLayers = newLayers; - } - - private moveLayerDown(index: number) { - if (index === this.customLayers.length - 1) return; - const newLayers = [...this.customLayers]; - [newLayers[index], newLayers[index + 1]] = [ - newLayers[index + 1], - newLayers[index], - ]; - this.customLayers = newLayers; - } - - private updateLayerColor(index: number, color: string) { - const newLayers = [...this.customLayers]; - newLayers[index].color = color; - this.customLayers = newLayers; - } - - private toggleColorPicker(index: number) { - this.openColorIndex = this.openColorIndex === index ? null : index; - } + @state() public flag: string = ""; static styles = css` @media (max-width: 768px) { @@ -488,19 +432,6 @@ export class FlagInput extends LitElement { } `; - private handleSearch(e: Event) { - this.search = String((e.target as HTMLInputElement).value); - } - - private setFlag(flag: string) { - if (flag == "xx") { - flag = ""; - } - this.flag = flag; - this.showModal = false; - this.storeFlag(flag); - } - public getCurrentFlag(): string { return this.flag; } @@ -513,14 +444,6 @@ export class FlagInput extends LitElement { return ""; } - private storeFlag(flag: string) { - if (flag) { - localStorage.setItem(flagKey, flag); - } else if (flag === "") { - localStorage.removeItem(flagKey); - } - } - private dispatchFlagEvent() { this.dispatchEvent( new CustomEvent("flag-change", { @@ -535,543 +458,26 @@ export class FlagInput extends LitElement { super.connectedCallback(); this.flag = this.getStoredFlag(); this.dispatchFlagEvent(); - - if (this.isCustomFlag(this.flag)) { - this.customLayers = this.decodeCustomFlag(this.flag); - } else { - if (this.customLayers.length === 0) { - this.customLayers = [ - { name: "full", color: "#ffffff" }, - { name: "frame", color: "#000000" }, - ]; - } - } } createRenderRoot() { return this; } - private lockedLayers: string[] = []; - - private lockedColors: string[] = []; - - private lockedReasons: Record = {}; - render() { - isDebug_ = true; - const result = checkPermission(); - this.lockedLayers = Array.isArray(result[0]) ? result[0] : [result[0]]; - this.lockedColors = Array.isArray(result[1]) ? result[1] : [result[1]]; - this.lockedReasons = result[2] || {}; return html`
- - ${this.showModal - ? html` -
- -
- - -
- - ${this.activeTab === "real" - ? html` - -
- ${Countries.filter( - (country) => - country.name - .toLowerCase() - .includes(this.search.toLowerCase()) || - country.code - .toLowerCase() - .includes(this.search.toLowerCase()), - ).map( - (country) => html` - - `, - )} -
- ` - : html` -
- -
-
- ${this.colorOptions.map((color) => { - const isLocked = - this.lockedColors.includes(color); - const isSpecial = !color.startsWith("#"); - const colorClass = isSpecial - ? `flag-color-${color}` - : ""; - const inlineStyle = isSpecial - ? "" - : `background-color: ${color};`; - const isSelected = this.selectedColor === color; - return html` - - `; - })} -
- -

- Select a Layer -

- - ${this.customLayers.length >= MAX_LAYER - ? html` -

- You've reached the maximum number of - layers.
- Please remove some to add new ones. -

- ` - : null} - -
- ${Object.entries(FlagMap) - .filter( - ([name]) => name !== "frame" && name !== "full", - ) - .map(([name, src]) => { - const isLocked = - this.lockedLayers.includes(name); - const isDisabled = - !isLocked && - this.customLayers.length >= MAX_LAYER; - - const isSpecial = - !this.selectedColor.startsWith("#"); - const colorClass = isSpecial - ? `flag-color-${this.selectedColor}` - : ""; - const colorStyle = isSpecial - ? "" - : `background-color: ${this.selectedColor};`; - - const reason = isLocked - ? this.lockedReasons[name] || "Locked" - : isDisabled - ? `You can only add up to ${MAX_LAYER} layers.` - : ""; - - return html` - - `; - })} -
-
- - -
-

- Preview -

-
- ${this.customLayers.map(({ name, color }) => { - const src = FlagMap[name]; - if (!src) return null; - console.log("color", color); - const isSpecial = !color.startsWith("#"); - const colorClass = isSpecial - ? `flag-color-${color}` - : ""; - const bgStyle = isSpecial - ? "" - : `background-color: ${color};`; - - return html` -
- `; - })} -
- - - -
-

- Layers (${this.customLayers.length}) -

-
    - ${this.customLayers.map((_, i, arr) => { - const index = arr.length - 1 - i; - const { name, color } = arr[index]; - const isFixed = - name === "full" || name === "frame"; - - const isSpecial = !color.startsWith("#"); - const colorClass = isSpecial - ? `flag-color-${color}` - : ""; - const inlineStyle = isSpecial - ? "" - : `background-color: ${color};`; - - return html` -
  • -
    - - - ${name} - ${isFixed - ? html`(fixed)` - : ""} - -
    - ${!isFixed - ? html` - ${index > 1 && - this.customLayers[index - 1] - .name !== "full" - ? html` - - ` - : ""} - ${index < - this.customLayers.length - 2 && - this.customLayers[index + 1] - .name !== "frame" - ? html` - - ` - : ""} - - ` - : null} - -
    -
    - - ${this.openColorIndex === index - ? html` -
    - ${this.colorOptions.map((color) => { - const isLocked = - this.lockedColors.includes( - color, - ); - const isSpecial = - !color.startsWith("#"); - const colorClass = isSpecial - ? `flag-color-${color}` - : ""; - const inlineStyle = isSpecial - ? "" - : `background-color: ${color};`; - - return html` - - `; - })} -
    - ` - : ""} -
  • - `; - })} -
-
-
-
- `} -
- ` - : ""}
- ${this.hoveredColor && this.lockedReasons[this.hoveredColor] - ? html` -
- ${this.lockedReasons[this.hoveredColor]} -
- ` - : null} `; } - private encodeCustomFlag(): string { - return ( - "ctmfg" + - this.customLayers - .map(({ name, color }) => { - const shortName = LayerShortNames[name] || name; - const shortColor = ColorShortNames[color] || color.replace("#", ""); - return `${shortName}-${shortColor}`; - }) - .join("_") - ); - } - private isCustomFlag(flag: string): boolean { return flag.startsWith("ctmfg"); } @@ -1129,6 +535,17 @@ export class FlagInput extends LitElement { } } +const animationDurations: Record = { + rainbow: 4000, + "bright-rainbow": 4000, + "copper-glow": 3000, + "silver-glow": 3000, + "gold-glow": 3000, + neon: 3000, + glitch: 600, + water: 6200, +}; + export function renderPlayerFlag(flagCode: string, target: HTMLElement) { const reverseNameMap = Object.fromEntries( Object.entries(LayerShortNames).map(([k, v]) => [v, k]), @@ -1167,7 +584,12 @@ export function renderPlayerFlag(flagCode: string, target: HTMLElement) { const isSpecial = !color.startsWith("#"); if (isSpecial) { + const duration = animationDurations[color] ?? 5000; + const now = performance.now(); + const offset = now % duration; + if (!duration) console.warn(`No animation duration for: ${color}`); layer.classList.add(`flag-color-${color}`); + layer.style.animationDelay = `-${offset}ms`; } else { layer.style.backgroundColor = color; } @@ -1185,3 +607,41 @@ export function renderPlayerFlag(flagCode: string, target: HTMLElement) { target.appendChild(layer); } } + +export function analyzePlayerFlag(flagCode: string): { + colors: string[]; + layers: string[]; + count: number; +} { + if (!flagCode.startsWith("ctmfg")) + return { colors: [], layers: [], count: 0 }; + + const reverseNameMap = Object.fromEntries( + Object.entries(LayerShortNames).map(([k, v]) => [v, k]), + ); + + const reverseColorMap = Object.fromEntries( + Object.entries(ColorShortNames).map(([k, v]) => [v, k]), + ); + + const code = flagCode.replace("ctmfg", ""); + const segments = code.split("_"); + + const layers: string[] = []; + const colors: string[] = []; + + for (const segment of segments) { + const [shortName, shortColor] = segment.split("-"); + const name = reverseNameMap[shortName] || shortName; + const color = reverseColorMap[shortColor] || shortColor; + + layers.push(name); + colors.push(color); + } + + return { + colors, + layers, + count: layers.length, + }; +} diff --git a/src/client/FlagInputModal.ts b/src/client/FlagInputModal.ts new file mode 100644 index 000000000..122917b4b --- /dev/null +++ b/src/client/FlagInputModal.ts @@ -0,0 +1,680 @@ +import { LitElement, css, html } from "lit"; +import { customElement, query, state } from "lit/decorators.js"; +import { translateText } from "../client/Utils"; +import Countries from "./data/countries.json"; +import { + ColorShortNames, + FlagMap, + LayerShortNames, + MAX_LAYER, + checkPermission, +} from "./FlagInput"; + +import { FlagInput } from "./FlagInput"; + +import disabled from "../../resources/images/DisabledIcon.svg"; +import locked from "../../resources/images/Locked.svg"; + +const flagKey: string = "flag"; + +@customElement("flag-input-modal") +export class FlagInputModal extends LitElement { + @query("o-modal") private modalEl!: HTMLElement & { + open: () => void; + close: () => void; + }; + + createRenderRoot() { + return this; + } + + @state() private flag: string = ""; + @state() private search: string = ""; + @state() private showModal: boolean = false; + @state() private activeTab: "real" | "custom" = "real"; + @state() private selectedColor: string = "#ff0000"; + @state() private openColorIndex: number | null = null; + + private readonly colorOptions: string[] = Object.keys(ColorShortNames); + + @state() private customLayers: { name: string; color: string }[] = []; + + @state() private hoveredColor: string | null = null; + @state() private hoverPosition = { x: 0, y: 0 }; + @state() private hoverReason: string | null = null; + + private addLayer(name: string) { + const totalLayers = this.customLayers.length; + + if (totalLayers >= MAX_LAYER) { + alert(`You can only add up to ${MAX_LAYER} layers.`); + return; + } + + const newLayer = { name, color: this.selectedColor }; + this.customLayers = [ + this.customLayers[0], + newLayer, + ...this.customLayers.slice(1), + ]; + } + + private removeLayer(index: number) { + this.customLayers = this.customLayers.filter((_, i) => i !== index); + } + + private moveLayerUp(index: number) { + if (index === 0) return; + const newLayers = [...this.customLayers]; + [newLayers[index - 1], newLayers[index]] = [ + newLayers[index], + newLayers[index - 1], + ]; + this.customLayers = newLayers; + } + + private moveLayerDown(index: number) { + if (index === this.customLayers.length - 1) return; + const newLayers = [...this.customLayers]; + [newLayers[index], newLayers[index + 1]] = [ + newLayers[index + 1], + newLayers[index], + ]; + this.customLayers = newLayers; + } + + private updateLayerColor(index: number, color: string) { + const newLayers = [...this.customLayers]; + newLayers[index].color = color; + this.customLayers = newLayers; + } + + private toggleColorPicker(index: number) { + this.openColorIndex = this.openColorIndex === index ? null : index; + } + + static styles = css` + @media (max-width: 768px) { + .flag-modal { + width: 80vw; + } + + .dropdown-item { + width: calc(100% / 3 - 15px); + } + } + `; + + private handleSearch(e: Event) { + this.search = String((e.target as HTMLInputElement).value); + } + + private setFlag(flag: string) { + if (flag == "xx") { + flag = ""; + } + this.flag = flag; + this.showModal = false; + this.storeFlag(flag); + this.close(); + const el = document.querySelector("flag-input") as FlagInput; + el.flag = this.flag; + el.requestUpdate(); + } + + public getCurrentFlag(): string { + return this.flag; + } + + private getStoredFlag(): string { + const storedFlag = localStorage.getItem(flagKey); + if (storedFlag) { + return storedFlag; + } + return ""; + } + + private storeFlag(flag: string) { + if (flag) { + localStorage.setItem(flagKey, flag); + } else if (flag === "") { + localStorage.removeItem(flagKey); + } + } + + private dispatchFlagEvent() { + this.dispatchEvent( + new CustomEvent("flag-change", { + detail: { flag: this.flag }, + bubbles: true, + composed: true, + }), + ); + } + + connectedCallback() { + super.connectedCallback(); + this.flag = this.getStoredFlag(); + this.dispatchFlagEvent(); + + if (this.isCustomFlag(this.flag)) { + this.customLayers = this.decodeCustomFlag(this.flag); + } else { + if (this.customLayers.length === 0) { + this.customLayers = [ + { name: "full", color: "#ffffff" }, + { name: "frame", color: "#000000" }, + ]; + } + } + } + + private lockedLayers: string[] = []; + + private lockedColors: string[] = []; + + private lockedReasons: Record = {}; + + private encodeCustomFlag(): string { + return ( + "ctmfg" + + this.customLayers + .map(({ name, color }) => { + const shortName = LayerShortNames[name] || name; + const shortColor = ColorShortNames[color] || color.replace("#", ""); + return `${shortName}-${shortColor}`; + }) + .join("_") + ); + } + + private isCustomFlag(flag: string): boolean { + return flag.startsWith("ctmfg"); + } + + private decodeCustomFlag(code: string): { name: string; color: string }[] { + if (!this.isCustomFlag(code)) return []; + + const short = code.replace("ctmfg", ""); + const reverseNameMap = Object.fromEntries( + Object.entries(LayerShortNames).map(([k, v]) => [v, k]), + ); + const reverseColorMap = Object.fromEntries( + Object.entries(ColorShortNames).map(([k, v]) => [v, k]), + ); + + return short.split("_").map((segment) => { + const [shortName, shortColor] = segment.split("-"); + const name = reverseNameMap[shortName] || shortName; + const color = reverseColorMap[shortColor] || `#${shortColor}`; + return { name, color }; + }); + } + + render() { + const result = checkPermission(); + this.lockedLayers = Array.isArray(result[0]) ? result[0] : [result[0]]; + this.lockedColors = Array.isArray(result[1]) ? result[1] : [result[1]]; + this.lockedReasons = result[2] || {}; + return html` + ${this.hoveredColor && this.lockedReasons[this.hoveredColor] + ? html` +
+ ${this.lockedReasons[this.hoveredColor]} +
+ ` + : null} + + +
+ + +
+ + ${this.activeTab === "real" + ? html` + +
+ ${Countries.filter( + (country) => + country.name + .toLowerCase() + .includes(this.search.toLowerCase()) || + country.code + .toLowerCase() + .includes(this.search.toLowerCase()), + ).map( + (country) => html` + + `, + )} +
+ ` + : html` +
+ +
+
+ ${this.colorOptions.map((color) => { + const isLocked = this.lockedColors.includes(color); + const isSpecial = !color.startsWith("#"); + const colorClass = isSpecial ? `flag-color-${color}` : ""; + const inlineStyle = isSpecial + ? "" + : `background-color: ${color};`; + const isSelected = this.selectedColor === color; + return html` + + `; + })} +
+ +

+ ${translateText(`flag_input.select_layer`)} +

+ + ${this.customLayers.length >= MAX_LAYER + ? html` +

+ ${translateText("flag_input.max_layer_reached_1")}
+ ${translateText("flag_input.max_layer_reached_2")} +

+ ` + : null} + +
+ ${Object.entries(FlagMap) + .filter(([name]) => name !== "frame" && name !== "full") + .map(([name, src]) => { + const isLocked = this.lockedLayers.includes(name); + const isDisabled = + !isLocked && this.customLayers.length >= MAX_LAYER; + + const isSpecial = !this.selectedColor.startsWith("#"); + const colorClass = isSpecial + ? `flag-color-${this.selectedColor}` + : ""; + const colorStyle = isSpecial + ? "" + : `background-color: ${this.selectedColor};`; + + const reason = isLocked + ? this.lockedReasons[name] || "Locked" + : isDisabled + ? `You can only add up to ${MAX_LAYER} layers.` + : ""; + + return html` + + `; + })} +
+
+ + +
+

+ ${translateText(`flag_input.preview`)} +

+
+ ${this.customLayers.map(({ name, color }) => { + const src = FlagMap[name]; + if (!src) return null; + console.log("color", color); + const isSpecial = !color.startsWith("#"); + const colorClass = isSpecial ? `flag-color-${color}` : ""; + const bgStyle = isSpecial + ? "" + : `background-color: ${color};`; + + return html` +
+ `; + })} +
+ + + +
+

+ ${translateText("flag_input.layer")} + (${this.customLayers.length}) +

+
    + ${this.customLayers.map((_, i, arr) => { + const index = arr.length - 1 - i; + const { name, color } = arr[index]; + const isFixed = name === "full" || name === "frame"; + + const isSpecial = !color.startsWith("#"); + const colorClass = isSpecial + ? `flag-color-${color}` + : ""; + const inlineStyle = isSpecial + ? "" + : `background-color: ${color};`; + + return html` +
  • +
    + + + ${translateText(`flag_input.layers.${name}`)} + ${isFixed + ? html`(${translateText( + "flag_input.fixed", + )})` + : ""} + +
    + ${!isFixed + ? html` + ${index > 1 && + this.customLayers[index - 1].name !== + "full" + ? html` + + ` + : ""} + ${index < this.customLayers.length - 2 && + this.customLayers[index + 1].name !== + "frame" + ? html` + + ` + : ""} + + ` + : null} + +
    +
    + + ${this.openColorIndex === index + ? html` +
    + ${this.colorOptions.map((color) => { + const isLocked = + this.lockedColors.includes(color); + const isSpecial = !color.startsWith("#"); + const colorClass = isSpecial + ? `flag-color-${color}` + : ""; + const inlineStyle = isSpecial + ? "" + : `background-color: ${color};`; + + return html` + + `; + })} +
    + ` + : ""} +
  • + `; + })} +
+
+
+
+ `} +
+ `; + } + + public open() { + this.modalEl?.open(); + } + + public close() { + this.modalEl?.close(); + } +} diff --git a/src/client/LangSelector.ts b/src/client/LangSelector.ts index 8698e8483..ce4d92f80 100644 --- a/src/client/LangSelector.ts +++ b/src/client/LangSelector.ts @@ -173,6 +173,7 @@ export class LangSelector extends LitElement { "help-modal", "username-input", "public-lobby", + "flag-input-modal", "o-modal", "o-button", ]; diff --git a/src/client/Main.ts b/src/client/Main.ts index 567a6673b..6867c283b 100644 --- a/src/client/Main.ts +++ b/src/client/Main.ts @@ -9,7 +9,8 @@ import { joinLobby } from "./ClientGameRunner"; import "./DarkModeButton"; import { DarkModeButton } from "./DarkModeButton"; import "./FlagInput"; -import { FlagInput } from "./FlagInput"; +import { FlagInput, analyzePlayerFlag, checkPermission } from "./FlagInput"; +import { FlagInputModal } from "./FlagInputModal"; import { GameStartingModal } from "./GameStartingModal"; import "./GoogleAdElement"; import GoogleAdElement from "./GoogleAdElement"; @@ -119,6 +120,14 @@ class Client { hlpModal.open(); }); + const flagInputModal = document.querySelector( + "flag-input-modal", + ) as FlagInputModal; + flagInputModal instanceof FlagInputModal; + document.getElementById("flag-input_").addEventListener("click", () => { + flagInputModal.open(); + }); + const settingsModal = document.querySelector( "user-setting", ) as UserSettingModal; @@ -194,14 +203,32 @@ class Client { } const config = await getServerConfigFromClient(); + let rawFlag = this.flagInput.getCurrentFlag(); + if (rawFlag.startsWith("ctmfg")) { + const result = checkPermission(); + const lockedLayers = Array.isArray(result[0]) ? result[0] : [result[0]]; + const lockedColors = Array.isArray(result[1]) ? result[1] : [result[1]]; + const MAX_LAYER = result[3]; + const flagInfo = analyzePlayerFlag(rawFlag); + const hasLockedLayer = flagInfo.layers.some((layer) => + lockedLayers.includes(layer), + ); + const hasLockedColor = flagInfo.colors.some((color) => + lockedColors.includes(color), + ); + const isLayerCountExceeded = flagInfo.count > MAX_LAYER; + if (hasLockedLayer || hasLockedColor || isLayerCountExceeded) { + rawFlag = "xx"; + console.warn("Blocked custom flag code."); + } + } + const flag = rawFlag === "xx" ? "" : rawFlag; + this.gameStop = joinLobby( { gameID: lobby.gameID, serverConfig: config, - flag: - this.flagInput.getCurrentFlag() == "xx" - ? "" - : this.flagInput.getCurrentFlag(), + flag: flag, playerName: this.usernameInput.getCurrentUsername(), persistentID: getPersistentIDFromCookie(), clientID: lobby.clientID, diff --git a/src/client/index.html b/src/client/index.html index d0ddc600c..3f10e54be 100644 --- a/src/client/index.html +++ b/src/client/index.html @@ -349,6 +349,7 @@ +