mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 09:10:42 +00:00
Added User Settings Modal (#379)
## Description: This PR adds a **User Settings** modal accessible from the main UI. Currently available settings include: - Toggle for Dark Mode - Writing Speed Multiplier (slider) - Bug Count (number input) - Troop and Worker Ratio (slider) - Left Click to Open Menu - Emoji toggle Settings are saved via `localStorage` and persist across sessions. There's also a hidden Easter Egg... https://discord.com/channels/1284581928254701718/1286741605310533653/1355900228712009908 <img width="787" alt="スクリーンショット 2025-03-31 8 40 08" src="https://github.com/user-attachments/assets/a9943834-cf40-4fa6-b828-06a8476172da" /> Fixes #482 ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced - [x] I understand that submitting code with bugs that could have been caught through manual testing blocks releases and new features for all contributors ## Please put your Discord username so you can be contacted if a bug or regression is found: <DISCORD USERNAME> aotumuri
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="svg4" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" version="1.1" viewBox="0 0 800 800">
|
||||
<!-- Generator: Adobe Illustrator 29.3.1, SVG Export Plug-In . SVG Version: 2.1.0 Build 151) -->
|
||||
<defs>
|
||||
<style>
|
||||
.st0 {
|
||||
stroke-width: 125px;
|
||||
}
|
||||
|
||||
.st0, .st1 {
|
||||
fill: none;
|
||||
stroke: #fff;
|
||||
stroke-miterlimit: 10;
|
||||
}
|
||||
|
||||
.st1 {
|
||||
stroke-dasharray: 98 98;
|
||||
stroke-width: 100px;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<sodipodi:namedview id="namedview6" bordercolor="#000000" borderopacity="0.25" inkscape:current-layer="svg4" inkscape:cx="401.69492" inkscape:cy="400" inkscape:deskcolor="#d1d1d1" inkscape:pagecheckerboard="0" inkscape:pageopacity="0.0" inkscape:showpageshadow="2" inkscape:window-height="987" inkscape:window-maximized="1" inkscape:window-width="1536" inkscape:window-x="0" inkscape:window-y="0" inkscape:zoom="0.295" pagecolor="#ffffff" showgrid="false"/>
|
||||
<circle class="st1" cx="400" cy="400" r="249.7"/>
|
||||
<ellipse class="st0" cx="400" cy="400" rx="159.6" ry="164.9"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -215,6 +215,7 @@ export class ClientGameRunner {
|
||||
|
||||
public start() {
|
||||
consolex.log("starting client game");
|
||||
|
||||
this.isActive = true;
|
||||
this.lastMessageTime = Date.now();
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -22,6 +22,7 @@ import { LanguageModal } from "./LanguageModal";
|
||||
import "./PublicLobby";
|
||||
import { PublicLobby } from "./PublicLobby";
|
||||
import { SinglePlayerModal } from "./SinglePlayerModal";
|
||||
import { UserSettingModal } from "./UserSettingModal";
|
||||
import "./UsernameInput";
|
||||
import { UsernameInput } from "./UsernameInput";
|
||||
import { generateCryptoRandomUUID } from "./Utils";
|
||||
@@ -118,6 +119,14 @@ class Client {
|
||||
hlpModal.open();
|
||||
});
|
||||
|
||||
const settingsModal = document.querySelector(
|
||||
"user-setting",
|
||||
) as UserSettingModal;
|
||||
settingsModal instanceof UserSettingModal;
|
||||
document.getElementById("settings-button").addEventListener("click", () => {
|
||||
settingsModal.open();
|
||||
});
|
||||
|
||||
const hostModal = document.querySelector(
|
||||
"host-lobby-modal",
|
||||
) as HostPrivateLobbyModal;
|
||||
@@ -200,6 +209,33 @@ class Client {
|
||||
gameRecord: lobby.gameRecord,
|
||||
},
|
||||
() => {
|
||||
console.log("Closing modals");
|
||||
document.getElementById("settings-button").classList.add("hidden");
|
||||
[
|
||||
"single-player-modal",
|
||||
"host-lobby-modal",
|
||||
"join-private-lobby-modal",
|
||||
"game-starting-modal",
|
||||
"top-bar",
|
||||
"help-modal",
|
||||
"user-setting",
|
||||
].forEach((tag) => {
|
||||
const modal = document.querySelector(tag) as HTMLElement & {
|
||||
close?: () => void;
|
||||
isModalOpen?: boolean;
|
||||
};
|
||||
if (modal?.close) {
|
||||
modal.close();
|
||||
} else if ("isModalOpen" in modal) {
|
||||
modal.isModalOpen = false;
|
||||
}
|
||||
});
|
||||
this.publicLobby.stop();
|
||||
document.querySelectorAll(".ad").forEach((ad) => {
|
||||
(ad as HTMLElement).style.display = "none";
|
||||
});
|
||||
|
||||
// show when the game loads
|
||||
const startingModal = document.querySelector(
|
||||
"game-starting-modal",
|
||||
) as GameStartingModal;
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, query, state } from "lit/decorators.js";
|
||||
import { UserSettings } from "../core/game/UserSettings";
|
||||
import "./components/baseComponents/setting/SettingNumber";
|
||||
import "./components/baseComponents/setting/SettingSlider";
|
||||
import "./components/baseComponents/setting/SettingToggle";
|
||||
|
||||
@customElement("user-setting")
|
||||
export class UserSettingModal extends LitElement {
|
||||
private userSettings: UserSettings = new UserSettings();
|
||||
|
||||
@state() private darkMode: boolean = this.userSettings.darkMode();
|
||||
|
||||
@state() private keySequence: string[] = [];
|
||||
@state() private showEasterEggSettings = false;
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
window.addEventListener("keydown", this.handleKeyDown);
|
||||
}
|
||||
|
||||
@query("o-modal") private modalEl!: HTMLElement & {
|
||||
open: () => void;
|
||||
close: () => void;
|
||||
isModalOpen: boolean;
|
||||
};
|
||||
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
window.removeEventListener("keydown", this.handleKeyDown);
|
||||
super.disconnectedCallback();
|
||||
document.body.style.overflow = "auto";
|
||||
}
|
||||
|
||||
private handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (!this.modalEl?.isModalOpen || this.showEasterEggSettings) return;
|
||||
|
||||
const key = e.key.toLowerCase();
|
||||
const nextSequence = [...this.keySequence, key].slice(-4);
|
||||
this.keySequence = nextSequence;
|
||||
|
||||
if (nextSequence.join("") === "evan") {
|
||||
this.triggerEasterEgg();
|
||||
this.keySequence = [];
|
||||
}
|
||||
};
|
||||
|
||||
private triggerEasterEgg() {
|
||||
console.log("🪺 Setting~ unlocked by EVAN combo!");
|
||||
this.showEasterEggSettings = true;
|
||||
const popup = document.createElement("div");
|
||||
popup.className = "easter-egg-popup";
|
||||
popup.textContent = "🎉 You found a secret setting!";
|
||||
document.body.appendChild(popup);
|
||||
|
||||
setTimeout(() => {
|
||||
popup.remove();
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
toggleDarkMode(e: CustomEvent<{ checked: boolean }>) {
|
||||
const enabled = e.detail?.checked;
|
||||
|
||||
if (typeof enabled !== "boolean") {
|
||||
console.warn("Unexpected toggle event payload", e);
|
||||
return;
|
||||
}
|
||||
|
||||
this.userSettings.set("settings.darkMode", enabled);
|
||||
|
||||
if (enabled) {
|
||||
document.documentElement.classList.add("dark");
|
||||
} else {
|
||||
document.documentElement.classList.remove("dark");
|
||||
}
|
||||
|
||||
console.log("🌙 Dark Mode:", enabled ? "ON" : "OFF");
|
||||
}
|
||||
|
||||
private toggleEmojis(e: CustomEvent<{ checked: boolean }>) {
|
||||
const enabled = e.detail?.checked;
|
||||
if (typeof enabled !== "boolean") return;
|
||||
|
||||
this.userSettings.set("settings.emojis", enabled);
|
||||
|
||||
console.log("🤡 Emojis:", enabled ? "ON" : "OFF");
|
||||
}
|
||||
|
||||
private toggleLeftClickOpensMenu(e: CustomEvent<{ checked: boolean }>) {
|
||||
const enabled = e.detail?.checked;
|
||||
if (typeof enabled !== "boolean") return;
|
||||
|
||||
this.userSettings.set("settings.leftClickOpensMenu", enabled);
|
||||
console.log("🖱️ Left Click Opens Menu:", enabled ? "ON" : "OFF");
|
||||
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private sliderAttackRatio(e: CustomEvent<{ value: number }>) {
|
||||
const value = e.detail?.value;
|
||||
if (typeof value === "number") {
|
||||
const ratio = value / 100;
|
||||
localStorage.setItem("settings.attackRatio", ratio.toString());
|
||||
} else {
|
||||
console.warn("Slider event missing detail.value", e);
|
||||
}
|
||||
}
|
||||
|
||||
private sliderTroopRatio(e: CustomEvent<{ value: number }>) {
|
||||
const value = e.detail?.value;
|
||||
if (typeof value === "number") {
|
||||
const ratio = value / 100;
|
||||
localStorage.setItem("settings.troopRatio", ratio.toString());
|
||||
} else {
|
||||
console.warn("Slider event missing detail.value", e);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<o-modal title="User Settings">
|
||||
<div class="modal-overlay">
|
||||
<div class="modal-content user-setting-modal">
|
||||
<div class="settings-list">
|
||||
<setting-toggle
|
||||
label="🌙 Dark Mode"
|
||||
description="Toggle the site’s appearance between light and dark themes"
|
||||
id="dark-mode-toggle"
|
||||
.checked=${this.userSettings.darkMode()}
|
||||
@change=${(e: CustomEvent<{ checked: boolean }>) =>
|
||||
this.toggleDarkMode(e)}
|
||||
></setting-toggle>
|
||||
|
||||
<setting-toggle
|
||||
label="😊 Emojis"
|
||||
description="Toggle whether emojis are shown in game"
|
||||
id="emoji-toggle"
|
||||
.checked=${this.userSettings.emojis()}
|
||||
@change=${this.toggleEmojis}
|
||||
></setting-toggle>
|
||||
|
||||
<setting-toggle
|
||||
label="🖱️ Left Click to Open Menu"
|
||||
description="When ON, left-click opens menu and sword button attacks. When OFF, right-click attacks directly."
|
||||
id="left-click-toggle"
|
||||
.checked=${this.userSettings.leftClickOpensMenu()}
|
||||
@change=${this.toggleLeftClickOpensMenu}
|
||||
></setting-toggle>
|
||||
|
||||
<setting-slider
|
||||
label="⚔️ Attack Ratio"
|
||||
description="What percentage of your troops to send in an attack (1–100%)"
|
||||
min="1"
|
||||
max="100"
|
||||
.value=${Number(
|
||||
localStorage.getItem("settings.attackRatio") ?? "0.2",
|
||||
) * 100}
|
||||
@change=${this.sliderAttackRatio}
|
||||
></setting-slider>
|
||||
|
||||
<setting-slider
|
||||
label="🪖🛠️ Troops and Workers Ratio"
|
||||
description="Adjust the balance between troops (for combat) and workers (for gold production) (1–100%)"
|
||||
min="1"
|
||||
max="100"
|
||||
.value=${Number(
|
||||
localStorage.getItem("settings.troopRatio") ?? "0.95",
|
||||
) * 100}
|
||||
@change=${this.sliderTroopRatio}
|
||||
></setting-slider>
|
||||
|
||||
${this.showEasterEggSettings
|
||||
? html`
|
||||
<setting-slider
|
||||
label="Writing Speed Multiplier"
|
||||
description="Adjust how fast you pretend to code (x1–x100)"
|
||||
min="0"
|
||||
max="100"
|
||||
value="40"
|
||||
easter="true"
|
||||
@change=${(e: CustomEvent) => {
|
||||
const value = e.detail?.value;
|
||||
if (typeof value !== "undefined") {
|
||||
console.log("Changed:", value);
|
||||
} else {
|
||||
console.warn("Slider event missing detail.value", e);
|
||||
}
|
||||
}}
|
||||
></setting-slider>
|
||||
|
||||
<setting-number
|
||||
label="Bug Count"
|
||||
description="How many bugs you're okay with (0–1000, emotionally)"
|
||||
value="100"
|
||||
min="0"
|
||||
max="1000"
|
||||
easter="true"
|
||||
@change=${(e: CustomEvent) => {
|
||||
const value = e.detail?.value;
|
||||
if (typeof value !== "undefined") {
|
||||
console.log("Changed:", value);
|
||||
} else {
|
||||
console.warn("Slider event missing detail.value", e);
|
||||
}
|
||||
}}
|
||||
></setting-number>
|
||||
`
|
||||
: null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</o-modal>
|
||||
`;
|
||||
}
|
||||
|
||||
public open() {
|
||||
this.modalEl?.open();
|
||||
}
|
||||
|
||||
public close() {
|
||||
this.modalEl?.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
@customElement("setting-number")
|
||||
export class SettingNumber extends LitElement {
|
||||
@property() label = "Setting";
|
||||
@property() description = "";
|
||||
@property({ type: Number }) value = 0;
|
||||
@property({ type: Number }) min = 0;
|
||||
@property({ type: Number }) max = 100;
|
||||
@property({ type: Boolean }) easter = false;
|
||||
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
private handleInput(e: Event) {
|
||||
const input = e.target as HTMLInputElement;
|
||||
const newValue = Number(input.value);
|
||||
this.value = newValue;
|
||||
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("change", {
|
||||
detail: { value: newValue },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="setting-item${this.easter ? " easter-egg" : ""}">
|
||||
<div class="setting-label-group">
|
||||
<label class="setting-label" for="setting-number-input"
|
||||
>${this.label}</label
|
||||
>
|
||||
<div class="setting-description">${this.description}</div>
|
||||
</div>
|
||||
<input
|
||||
type="number"
|
||||
id="setting-number-input"
|
||||
class="setting-input number"
|
||||
.value=${String(this.value ?? 0)}
|
||||
min=${this.min}
|
||||
max=${this.max}
|
||||
@input=${this.handleInput}
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
@customElement("setting-slider")
|
||||
export class SettingSlider extends LitElement {
|
||||
@property() label = "Setting";
|
||||
@property() description = "";
|
||||
@property({ type: Number }) value = 0;
|
||||
@property({ type: Number }) min = 0;
|
||||
@property({ type: Number }) max = 100;
|
||||
@property({ type: Boolean }) easter = false;
|
||||
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
private handleInput(e: Event) {
|
||||
const input = e.target as HTMLInputElement;
|
||||
this.value = Number(input.value);
|
||||
this.updateSliderStyle(input);
|
||||
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("change", {
|
||||
detail: { value: this.value },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private handleSliderChange(e: Event) {
|
||||
const detail = (e as CustomEvent)?.detail;
|
||||
if (!detail || typeof detail.value === "undefined") {
|
||||
console.warn("Invalid slider change event", e);
|
||||
return;
|
||||
}
|
||||
|
||||
const value = detail.value;
|
||||
console.log("Slider changed to", value);
|
||||
}
|
||||
|
||||
private updateSliderStyle(slider: HTMLInputElement) {
|
||||
const percent = ((this.value - this.min) / (this.max - this.min)) * 100;
|
||||
slider.style.background = `linear-gradient(to right, #2196f3 ${percent}%, #444 ${percent}%)`;
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
const slider = this.renderRoot.querySelector(
|
||||
"input[type=range]",
|
||||
) as HTMLInputElement;
|
||||
if (slider) this.updateSliderStyle(slider);
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="setting-item vertical${this.easter ? " easter-egg" : ""}">
|
||||
<div class="setting-label-group">
|
||||
<label class="setting-label" for="setting-slider-input"
|
||||
>${this.label}</label
|
||||
>
|
||||
<div class="setting-description">${this.description}</div>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
id="setting-slider-input"
|
||||
class="setting-input slider full-width"
|
||||
min=${this.min}
|
||||
max=${this.max}
|
||||
.value=${String(this.value)}
|
||||
@input=${this.handleInput}
|
||||
/>
|
||||
<div class="slider-value">${this.value}%</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
@customElement("setting-toggle")
|
||||
export class SettingToggle extends LitElement {
|
||||
@property() label = "Setting";
|
||||
@property() description = "";
|
||||
@property() id = "";
|
||||
@property({ type: Boolean, reflect: true }) checked = false;
|
||||
@property({ type: Boolean }) easter = false;
|
||||
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
private handleChange(e: Event) {
|
||||
const input = e.target as HTMLInputElement;
|
||||
this.checked = input.checked;
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("change", {
|
||||
detail: { checked: this.checked },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="setting-item vertical${this.easter ? " easter-egg" : ""}">
|
||||
<div class="toggle-row">
|
||||
<label class="setting-label" for=${this.id}>${this.label}</label>
|
||||
<label class="switch">
|
||||
<input
|
||||
type="checkbox"
|
||||
id=${this.id}
|
||||
?checked=${this.checked}
|
||||
@change=${this.handleChange}
|
||||
/>
|
||||
<span class="slider-round"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="setting-description">${this.description}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -56,8 +56,16 @@ export class ControlPanel extends LitElement implements Layer {
|
||||
|
||||
private _popRateIsIncreasing: boolean = true;
|
||||
|
||||
private init_: boolean = false;
|
||||
|
||||
init() {
|
||||
this.attackRatio = 0.2;
|
||||
this.attackRatio = Number(
|
||||
localStorage.getItem("settings.attackRatio") ?? "0.2",
|
||||
);
|
||||
this.targetTroopRatio = Number(
|
||||
localStorage.getItem("settings.troopRatio") ?? "0.95",
|
||||
);
|
||||
this.init_ = true;
|
||||
this.uiState.attackRatio = this.attackRatio;
|
||||
this.currentTroopRatio = this.targetTroopRatio;
|
||||
this.eventBus.on(AttackRatioEvent, (event) => {
|
||||
@@ -87,6 +95,13 @@ export class ControlPanel extends LitElement implements Layer {
|
||||
}
|
||||
|
||||
tick() {
|
||||
if (this.init_) {
|
||||
this.eventBus.emit(
|
||||
new SendSetTargetTroopRatioEvent(this.targetTroopRatio),
|
||||
);
|
||||
this.init_ = false;
|
||||
}
|
||||
|
||||
if (!this._isVisible && !this.game.inSpawnPhase()) {
|
||||
this.setVisibile(true);
|
||||
}
|
||||
|
||||
@@ -279,6 +279,20 @@
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- User Setting -->
|
||||
<button
|
||||
id="settings-button"
|
||||
title="Settings"
|
||||
class="fixed bottom-4 right-4 z-50 rounded-full p-2 shadow-lg transition-colors duration-300 flex items-center justify-center"
|
||||
style="width: 80px; height: 80px; background-color: #0075ff"
|
||||
>
|
||||
<img
|
||||
src="../../resources/images/SettingIconWhite.svg"
|
||||
alt="Settings"
|
||||
style="width: 72px; height: 72px"
|
||||
/>
|
||||
</button>
|
||||
|
||||
<!-- Game components -->
|
||||
<div id="customMenu" class="mt-4 sm:mt-6 lg:mt-8">
|
||||
<ul></ul>
|
||||
@@ -353,6 +367,7 @@
|
||||
<player-panel></player-panel>
|
||||
<help-modal></help-modal>
|
||||
<dark-mode-button></dark-mode-button>
|
||||
<user-setting></user-setting>
|
||||
<div
|
||||
id="language-modal"
|
||||
class="fixed inset-0 bg-black bg-opacity-50 z-50 hidden flex justify-center items-center"
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
@import url("./styles/layout/container.css");
|
||||
@import url("./styles/components/button.css");
|
||||
@import url("./styles/components/modal.css");
|
||||
@import url("./styles/components/setting.css");
|
||||
@import url("./styles/components/controls.css");
|
||||
* {
|
||||
-webkit-box-sizing: border-box;
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
.settings-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
background: #1e1e1e;
|
||||
border: 1px solid #333;
|
||||
border-radius: 10px;
|
||||
padding: 12px 20px;
|
||||
width: 360px !important;
|
||||
max-width: 360px !important;
|
||||
min-width: 360px !important;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.4);
|
||||
transition: background 0.3s ease;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
@keyframes rainbow-background {
|
||||
0% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.setting-item.easter-egg {
|
||||
background: linear-gradient(
|
||||
270deg,
|
||||
#990033,
|
||||
#996600,
|
||||
#336600,
|
||||
#008080,
|
||||
#1c3f99,
|
||||
#5e0099,
|
||||
#990033
|
||||
);
|
||||
background-size: 1400% 1400%;
|
||||
animation: rainbow-background 10s ease infinite;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.easter-egg-popup {
|
||||
position: fixed;
|
||||
top: 40px;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%) scale(0.9);
|
||||
padding: 16px 24px;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
color: #fff;
|
||||
font-size: 20px;
|
||||
border-radius: 12px;
|
||||
animation: fadePop 5s ease-out forwards;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
@keyframes fadePop {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, -50%) scale(0.6);
|
||||
}
|
||||
30% {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, -50%) scale(1.05);
|
||||
}
|
||||
70% {
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, -50%) scale(0.9);
|
||||
}
|
||||
}
|
||||
|
||||
.setting-item:hover {
|
||||
background: #2a2a2a;
|
||||
}
|
||||
|
||||
.setting-item.easter-egg:hover {
|
||||
background: linear-gradient(
|
||||
270deg,
|
||||
#990033,
|
||||
#996600,
|
||||
#336600,
|
||||
#008080,
|
||||
#1c3f99,
|
||||
#5e0099,
|
||||
#990033
|
||||
);
|
||||
background-size: 1400% 1400%;
|
||||
animation: rainbow-background 10s ease infinite;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.setting-label {
|
||||
color: #f0f0f0;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.setting-input {
|
||||
margin-left: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.setting-item.vertical {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.toggle-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.slider-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.setting-input.slider.full-width {
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.setting-input.slider {
|
||||
-webkit-appearance: none;
|
||||
width: 180px;
|
||||
height: 10px;
|
||||
background: linear-gradient(to right, #2196f3 50%, #444 50%);
|
||||
border-radius: 5px;
|
||||
outline: none;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.setting-input.slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
border: 2px solid #2196f3;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.setting-input.slider::-moz-range-thumb {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
border: 2px solid #2196f3;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.setting-input.slider::-moz-range-track {
|
||||
background: linear-gradient(to right, #2196f3 50%, #444 50%);
|
||||
height: 10px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.setting-input.slider:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.slider-value {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.setting-input.number {
|
||||
width: 80px;
|
||||
padding: 6px 8px;
|
||||
border: 1px solid #aaa;
|
||||
border-radius: 6px;
|
||||
background-color: #ffffff;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 50px;
|
||||
height: 26px;
|
||||
}
|
||||
|
||||
.switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.switch.switch-right {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.slider-round {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #d9534f;
|
||||
transition: 0.4s;
|
||||
border-radius: 34px;
|
||||
}
|
||||
|
||||
.slider-round::before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
left: 3px;
|
||||
bottom: 3px;
|
||||
background-color: white;
|
||||
transition: 0.4s;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.switch input:checked + .slider-round {
|
||||
background-color: #4caf50;
|
||||
}
|
||||
|
||||
.switch input:checked + .slider-round::before {
|
||||
transform: translateX(24px);
|
||||
}
|
||||
|
||||
.setting-label-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.setting-description {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
margin-top: 2px;
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
}
|
||||
Reference in New Issue
Block a user