feat: complete SrTool overhaul with lineup builder, character build modal, equipment, and FireflyGo/RobinSR modes
This commit is contained in:
1 parent
5cce9bdd04
commit
e1667fa109
3 files changed
+1067
-254
No files matched your search
+264
-95
@@ -1,10 +1,10 @@
|
||||
:root {
|
||||
--bg-primary: #0a0d14;
|
||||
--bg-secondary: #121826;
|
||||
--bg-card: rgba(22, 30, 48, 0.7);
|
||||
--bg-card-hover: rgba(30, 41, 66, 0.85);
|
||||
--bg-primary: #080a10;
|
||||
--bg-secondary: #0f1422;
|
||||
--bg-card: rgba(18, 24, 38, 0.75);
|
||||
--bg-card-hover: rgba(26, 35, 56, 0.9);
|
||||
--border-color: rgba(99, 120, 160, 0.25);
|
||||
--border-focus: #4ef2bb;
|
||||
--border-focus: #38bdf8;
|
||||
|
||||
--text-main: #f0f4fc;
|
||||
--text-muted: #8e9bb5;
|
||||
@@ -13,18 +13,18 @@
|
||||
--accent-cyan: #38bdf8;
|
||||
--accent-green: #34d399;
|
||||
--accent-gold: #fbbf24;
|
||||
--accent-purple: #a855f7;
|
||||
--accent-purple: #c084fc;
|
||||
--accent-red: #f87171;
|
||||
|
||||
--glow-cyan: 0 0 15px rgba(56, 189, 248, 0.35);
|
||||
--glow-green: 0 0 15px rgba(52, 211, 153, 0.35);
|
||||
--glow-gold: 0 0 15px rgba(251, 191, 36, 0.35);
|
||||
--glow-cyan: 0 0 20px rgba(56, 189, 248, 0.35);
|
||||
--glow-gold: 0 0 20px rgba(251, 191, 36, 0.35);
|
||||
--glow-purple: 0 0 20px rgba(192, 132, 252, 0.35);
|
||||
|
||||
--font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
--radius-sm: 6px;
|
||||
--radius-md: 10px;
|
||||
--radius-lg: 16px;
|
||||
--transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
* {
|
||||
@@ -37,8 +37,9 @@ body {
|
||||
font-family: var(--font-family);
|
||||
background-color: var(--bg-primary);
|
||||
background-image:
|
||||
radial-gradient(circle at 15% 15%, rgba(56, 189, 248, 0.08) 0%, transparent 40%),
|
||||
radial-gradient(circle at 85% 85%, rgba(168, 85, 247, 0.08) 0%, transparent 45%);
|
||||
radial-gradient(circle at 10% 10%, rgba(56, 189, 248, 0.08) 0%, transparent 40%),
|
||||
radial-gradient(circle at 90% 90%, rgba(168, 85, 247, 0.08) 0%, transparent 45%),
|
||||
linear-gradient(180deg, rgba(8, 10, 16, 0.95) 0%, rgba(12, 16, 26, 0.98) 100%);
|
||||
color: var(--text-main);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
@@ -52,8 +53,8 @@ body {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 50;
|
||||
background: rgba(10, 13, 20, 0.85);
|
||||
backdrop-filter: blur(14px);
|
||||
background: rgba(8, 10, 16, 0.88);
|
||||
backdrop-filter: blur(16px);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding: 0.75rem 1.5rem;
|
||||
display: flex;
|
||||
@@ -65,28 +66,29 @@ body {
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
gap: 0.85rem;
|
||||
}
|
||||
|
||||
.brand-icon {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: var(--radius-md);
|
||||
background: linear-gradient(135deg, #38bdf8, #818cf8);
|
||||
background: linear-gradient(135deg, #0284c7, #6366f1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: var(--glow-cyan);
|
||||
font-size: 1.25rem;
|
||||
font-size: 1.35rem;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
border: 1px solid rgba(56, 189, 248, 0.4);
|
||||
}
|
||||
|
||||
.brand-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
font-size: 1.35rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.025em;
|
||||
background: linear-gradient(90deg, #ffffff, #cbd5e1);
|
||||
background: linear-gradient(90deg, #ffffff, #cbd5e1, #38bdf8);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
@@ -95,25 +97,55 @@ body {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.header-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.85rem;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* Server Mode Selector */
|
||||
.server-mode-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
padding: 0.4rem 0.85rem;
|
||||
background: rgba(2, 132, 199, 0.15);
|
||||
border: 1px solid rgba(56, 189, 248, 0.4);
|
||||
border-radius: 9999px;
|
||||
font-size: 0.825rem;
|
||||
font-weight: 700;
|
||||
color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
.server-mode-select {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: inherit;
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.server-mode-select option {
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
.connection-pill {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.35rem 0.75rem;
|
||||
padding: 0.4rem 0.85rem;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 9999px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: var(--transition);
|
||||
}
|
||||
@@ -123,8 +155,8 @@ body {
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--accent-red);
|
||||
box-shadow: 0 0 8px var(--accent-red);
|
||||
@@ -133,13 +165,13 @@ body {
|
||||
|
||||
.status-dot.connected {
|
||||
background-color: var(--accent-green);
|
||||
box-shadow: var(--glow-green);
|
||||
box-shadow: 0 0 10px var(--accent-green);
|
||||
}
|
||||
|
||||
/* Navigation Tabs */
|
||||
.nav-tabs {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
gap: 0.35rem;
|
||||
background: var(--bg-secondary);
|
||||
padding: 0.5rem 1.5rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
@@ -150,7 +182,7 @@ body {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
padding: 0.5rem 1rem;
|
||||
padding: 0.6rem 1.1rem;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
@@ -164,21 +196,21 @@ body {
|
||||
|
||||
.tab-btn:hover {
|
||||
color: var(--text-main);
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
color: #fff;
|
||||
background: rgba(56, 189, 248, 0.15);
|
||||
border: 1px solid rgba(56, 189, 248, 0.3);
|
||||
box-shadow: 0 0 10px rgba(56, 189, 248, 0.15);
|
||||
border: 1px solid rgba(56, 189, 248, 0.35);
|
||||
box-shadow: 0 0 12px rgba(56, 189, 248, 0.2);
|
||||
}
|
||||
|
||||
/* Container */
|
||||
/* Main Container */
|
||||
.main-container {
|
||||
flex: 1;
|
||||
padding: 1.5rem;
|
||||
max-width: 1300px;
|
||||
max-width: 1400px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
@@ -197,7 +229,7 @@ body {
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* Cards & Grid */
|
||||
/* Grids */
|
||||
.grid-2 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
||||
@@ -212,13 +244,14 @@ body {
|
||||
|
||||
.grid-4 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||||
gap: 1.1rem;
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
/* Cards */
|
||||
.card {
|
||||
background: var(--bg-card);
|
||||
backdrop-filter: blur(12px);
|
||||
backdrop-filter: blur(14px);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 1.25rem;
|
||||
@@ -229,8 +262,8 @@ body {
|
||||
|
||||
.card:hover {
|
||||
background: var(--bg-card-hover);
|
||||
border-color: rgba(99, 120, 160, 0.45);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
|
||||
border-color: rgba(99, 120, 160, 0.5);
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
@@ -253,11 +286,11 @@ body {
|
||||
.stat-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 2rem;
|
||||
font-size: 2.25rem;
|
||||
font-weight: 800;
|
||||
color: var(--accent-cyan);
|
||||
line-height: 1;
|
||||
@@ -268,6 +301,7 @@ body {
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
@@ -276,7 +310,7 @@ body {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.6rem 1.1rem;
|
||||
padding: 0.6rem 1.15rem;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
@@ -304,14 +338,14 @@ body {
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
box-shadow: var(--glow-green);
|
||||
box-shadow: 0 0 15px rgba(52, 211, 153, 0.4);
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
|
||||
.btn-gold {
|
||||
background: linear-gradient(135deg, #d97706, #f59e0b);
|
||||
color: #fff;
|
||||
border-color: rgba(251, 191, 36, 0.4);
|
||||
border-color: rgba(251, 191, 36, 0.5);
|
||||
}
|
||||
|
||||
.btn-gold:hover {
|
||||
@@ -335,34 +369,62 @@ body {
|
||||
font-size: 0.775rem;
|
||||
}
|
||||
|
||||
/* Avatar Card */
|
||||
/* ==========================================================================
|
||||
Honkai: Star Rail Themed Avatar Cards & Portraits
|
||||
========================================================================== */
|
||||
|
||||
.avatar-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
min-height: 180px;
|
||||
min-height: 220px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
border-width: 1.5px;
|
||||
}
|
||||
|
||||
/* 5-Star Card Theme */
|
||||
.avatar-card.card-rarity-5 {
|
||||
border-color: rgba(251, 191, 36, 0.4);
|
||||
background: linear-gradient(145deg, rgba(251, 191, 36, 0.05) 0%, rgba(18, 24, 38, 0.8) 100%);
|
||||
}
|
||||
|
||||
.avatar-card.card-rarity-5:hover {
|
||||
border-color: rgba(251, 191, 36, 0.8);
|
||||
box-shadow: var(--glow-gold);
|
||||
}
|
||||
|
||||
/* 4-Star Card Theme */
|
||||
.avatar-card.card-rarity-4 {
|
||||
border-color: rgba(192, 132, 252, 0.35);
|
||||
background: linear-gradient(145deg, rgba(192, 132, 252, 0.05) 0%, rgba(18, 24, 38, 0.8) 100%);
|
||||
}
|
||||
|
||||
.avatar-card.card-rarity-4:hover {
|
||||
border-color: rgba(192, 132, 252, 0.8);
|
||||
box-shadow: var(--glow-purple);
|
||||
}
|
||||
|
||||
.avatar-badge-rarity {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
top: 0.85rem;
|
||||
right: 0.85rem;
|
||||
font-size: 0.75rem;
|
||||
padding: 0.15rem 0.5rem;
|
||||
border-radius: var(--radius-sm);
|
||||
font-weight: 700;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.rarity-5 {
|
||||
background: rgba(251, 191, 36, 0.2);
|
||||
border: 1px solid rgba(251, 191, 36, 0.5);
|
||||
border: 1px solid rgba(251, 191, 36, 0.6);
|
||||
color: var(--accent-gold);
|
||||
}
|
||||
|
||||
.rarity-4 {
|
||||
background: rgba(168, 85, 247, 0.2);
|
||||
border: 1px solid rgba(168, 85, 247, 0.5);
|
||||
background: rgba(192, 132, 252, 0.2);
|
||||
border: 1px solid rgba(192, 132, 252, 0.6);
|
||||
color: var(--accent-purple);
|
||||
}
|
||||
|
||||
@@ -372,29 +434,39 @@ body {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Circular / Rounded Avatar Portrait */
|
||||
.avatar-image-container {
|
||||
position: relative;
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
border-radius: var(--radius-md);
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border: 2px solid var(--border-color);
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.card-rarity-5 .avatar-image-container {
|
||||
border-color: var(--accent-gold);
|
||||
}
|
||||
|
||||
.card-rarity-4 .avatar-image-container {
|
||||
border-color: var(--accent-purple);
|
||||
}
|
||||
|
||||
.avatar-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.25s ease;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.card:hover .avatar-img {
|
||||
transform: scale(1.08);
|
||||
.avatar-card:hover .avatar-img {
|
||||
transform: scale(1.12);
|
||||
}
|
||||
|
||||
.avatar-icon-placeholder {
|
||||
@@ -405,28 +477,59 @@ body {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
font-size: 1.1rem;
|
||||
font-size: 1.25rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.avatar-name {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
font-size: 1.05rem;
|
||||
font-weight: 800;
|
||||
color: var(--text-highlight);
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.avatar-subinfo {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.775rem;
|
||||
margin-top: 0.35rem;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
gap: 0.4rem;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Star Rail Element Badges */
|
||||
.element-badge {
|
||||
padding: 0.15rem 0.45rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.725rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.elem-fire { background: rgba(239, 68, 68, 0.2); color: #f87171; border: 1px solid rgba(239, 68, 68, 0.4); }
|
||||
.elem-ice { background: rgba(56, 189, 248, 0.2); color: #38bdf8; border: 1px solid rgba(56, 189, 248, 0.4); }
|
||||
.elem-lightning { background: rgba(168, 85, 247, 0.2); color: #c084fc; border: 1px solid rgba(168, 85, 247, 0.4); }
|
||||
.elem-wind { background: rgba(16, 185, 129, 0.2); color: #34d399; border: 1px solid rgba(16, 185, 129, 0.4); }
|
||||
.elem-physical { background: rgba(156, 163, 175, 0.2); color: #e5e7eb; border: 1px solid rgba(156, 163, 175, 0.4); }
|
||||
.elem-quantum { background: rgba(99, 102, 241, 0.2); color: #818cf8; border: 1px solid rgba(99, 102, 241, 0.4); }
|
||||
.elem-imaginary { background: rgba(245, 158, 11, 0.2); color: #fbbf24; border: 1px solid rgba(245, 158, 11, 0.4); }
|
||||
|
||||
.path-badge {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--text-main);
|
||||
padding: 0.15rem 0.45rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.725rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Eidolon Selector */
|
||||
.eidolon-selector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 1rem;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--border-color);
|
||||
@@ -434,18 +537,18 @@ body {
|
||||
|
||||
.eidolon-buttons {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
.eidolon-pip {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--border-color);
|
||||
background: transparent;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.75rem;
|
||||
font-weight: bold;
|
||||
font-weight: 800;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -459,10 +562,69 @@ body {
|
||||
}
|
||||
|
||||
.eidolon-pip.active {
|
||||
background: var(--accent-cyan);
|
||||
border-color: var(--accent-cyan);
|
||||
background: linear-gradient(135deg, #0284c7, #38bdf8);
|
||||
border-color: #38bdf8;
|
||||
color: #0b0d14;
|
||||
box-shadow: 0 0 8px rgba(56, 189, 248, 0.5);
|
||||
box-shadow: 0 0 10px rgba(56, 189, 248, 0.6);
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Party / Lineup Builder
|
||||
========================================================================== */
|
||||
.lineup-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.lineup-slot-card {
|
||||
background: var(--bg-card);
|
||||
border: 2px dashed var(--border-color);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 1.25rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
min-height: 200px;
|
||||
cursor: pointer;
|
||||
transition: var(--transition);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.lineup-slot-card.filled {
|
||||
border-style: solid;
|
||||
border-color: rgba(56, 189, 248, 0.4);
|
||||
background: linear-gradient(180deg, rgba(56, 189, 248, 0.08) 0%, rgba(18, 24, 38, 0.85) 100%);
|
||||
}
|
||||
|
||||
.lineup-slot-card:hover {
|
||||
border-color: var(--accent-cyan);
|
||||
box-shadow: var(--glow-cyan);
|
||||
}
|
||||
|
||||
.lineup-slot-img {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid var(--accent-cyan);
|
||||
object-fit: cover;
|
||||
margin-bottom: 0.75rem;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.lineup-slot-num {
|
||||
position: absolute;
|
||||
top: 0.75rem;
|
||||
left: 0.75rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 800;
|
||||
color: var(--accent-cyan);
|
||||
background: rgba(2, 132, 199, 0.2);
|
||||
padding: 0.2rem 0.5rem;
|
||||
border-radius: 9999px;
|
||||
border: 1px solid rgba(56, 189, 248, 0.3);
|
||||
}
|
||||
|
||||
/* Ex Rank Toggle Switch */
|
||||
@@ -470,7 +632,7 @@ body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1rem;
|
||||
padding: 1rem 1.25rem;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-md);
|
||||
@@ -480,6 +642,7 @@ body {
|
||||
|
||||
.ex-switch-card:hover {
|
||||
background: var(--bg-card-hover);
|
||||
border-color: rgba(99, 120, 160, 0.45);
|
||||
}
|
||||
|
||||
.switch {
|
||||
@@ -518,14 +681,14 @@ body {
|
||||
|
||||
input:checked + .slider {
|
||||
background-color: var(--accent-green);
|
||||
box-shadow: var(--glow-green);
|
||||
box-shadow: 0 0 10px var(--accent-green);
|
||||
}
|
||||
|
||||
input:checked + .slider:before {
|
||||
transform: translateX(22px);
|
||||
}
|
||||
|
||||
/* Inputs & Form */
|
||||
/* Forms */
|
||||
.form-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
@@ -542,7 +705,7 @@ input:checked + .slider:before {
|
||||
|
||||
.form-input, .form-select {
|
||||
width: 100%;
|
||||
padding: 0.6rem 0.85rem;
|
||||
padding: 0.65rem 0.85rem;
|
||||
background: rgba(10, 13, 20, 0.7);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-sm);
|
||||
@@ -554,10 +717,10 @@ input:checked + .slider:before {
|
||||
|
||||
.form-input:focus, .form-select:focus {
|
||||
border-color: var(--accent-cyan);
|
||||
box-shadow: 0 0 10px rgba(56, 189, 248, 0.2);
|
||||
box-shadow: 0 0 10px rgba(56, 189, 248, 0.25);
|
||||
}
|
||||
|
||||
/* JSON Code Editor Area */
|
||||
/* JSON Editor */
|
||||
.json-editor-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -583,7 +746,7 @@ input:checked + .slider:before {
|
||||
border-color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
/* Toast notifications */
|
||||
/* Toast */
|
||||
.toast-container {
|
||||
position: fixed;
|
||||
bottom: 1.5rem;
|
||||
@@ -622,12 +785,12 @@ input:checked + .slider:before {
|
||||
to { transform: translateX(0); opacity: 1; }
|
||||
}
|
||||
|
||||
/* Modal */
|
||||
/* Modals */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
backdrop-filter: blur(6px);
|
||||
background: rgba(0, 0, 0, 0.75);
|
||||
backdrop-filter: blur(8px);
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -643,10 +806,11 @@ input:checked + .slider:before {
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-lg);
|
||||
max-width: 560px;
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
padding: 1.5rem;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.6);
|
||||
padding: 1.75rem;
|
||||
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.7);
|
||||
animation: fadeIn 0.2s ease-out;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
@@ -654,10 +818,18 @@ input:checked + .slider:before {
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1.25rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
@media (max-width: 900px) {
|
||||
.lineup-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.app-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
@@ -666,10 +838,7 @@ input:checked + .slider:before {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.nav-tabs {
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
.main-container {
|
||||
padding: 1rem;
|
||||
.lineup-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
+303
-103
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>GaC-Srtool — FreeSR & Game Management Dashboard</title>
|
||||
<title>GaC-Srtool — Ultimate Star Rail Server & Build Manager</title>
|
||||
<link rel="stylesheet" href="css/styles.css">
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🌌</text></svg>">
|
||||
</head>
|
||||
@@ -14,54 +14,78 @@
|
||||
<div class="brand">
|
||||
<div class="brand-icon">✦</div>
|
||||
<div>
|
||||
<div class="brand-title" data-i18n="appName">GaC-Srtool</div>
|
||||
<div class="brand-subtitle" data-i18n="appSubtitle">Star Rail FreeSR & Server Controller</div>
|
||||
<div class="brand-title">GaC-Srtool</div>
|
||||
<div class="brand-subtitle">Star Rail FreeSR & Server Controller</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="header-controls">
|
||||
<!-- Language Selector -->
|
||||
<select id="lang-select" class="form-select btn-sm" style="width: auto; padding: 0.35rem 0.65rem; background: var(--bg-secondary); cursor: pointer;" title="Language / 语言 / 言語 / ภาษา">
|
||||
<!-- Injected by i18n.js -->
|
||||
<!-- Server Mode Selector -->
|
||||
<div class="server-mode-badge" title="โหมดการเชื่อมต่อเซิร์ฟเวอร์">
|
||||
<span>🌐</span>
|
||||
<select id="server-mode-select" class="server-mode-select">
|
||||
<option value="gac" selected>⚡ GaC-hkrpg-go (เซิร์ฟเวอร์หลัก / Full Control)</option>
|
||||
<option value="firefly">🔥 FireflyGo (FreeSR Compatible)</option>
|
||||
<option value="robin">🕊️ RobinSR (FreeSR Compatible)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Language Selector (13 Languages) -->
|
||||
<select id="lang-select" class="form-select btn-sm" style="width: auto; padding: 0.4rem 0.75rem; background: var(--bg-secondary); cursor: pointer;" title="Language / 语言 / 言語 / ภาษา">
|
||||
</select>
|
||||
|
||||
<div class="connection-pill" id="btn-open-settings" title="Click to configure API connection">
|
||||
<!-- Connection Pill -->
|
||||
<div class="connection-pill" id="btn-open-settings" title="คลิกเพื่อตั้งค่าการเชื่อมต่อ API">
|
||||
<span class="status-dot" id="status-dot"></span>
|
||||
<span id="connection-status-text" data-i18n="checking">กำลังตรวจสอบ...</span>
|
||||
<span id="connection-status-text">กำลังตรวจสอบ...</span>
|
||||
</div>
|
||||
<button class="btn btn-secondary btn-sm" id="btn-quick-ping" title="Refresh connection">
|
||||
<span data-i18n="refresh">🔄 รีเฟรช</span>
|
||||
|
||||
<button class="btn btn-secondary btn-sm" id="btn-quick-ping" title="รีเฟรชการเชื่อมต่อ">
|
||||
🔄 รีเฟรช
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Navigation Bar -->
|
||||
<nav class="nav-tabs">
|
||||
<button class="tab-btn active" data-tab="tab-dashboard" data-i18n="tabDashboard">📊 แดชบอร์ด (Overview)</button>
|
||||
<button class="tab-btn" data-tab="tab-avatars" data-i18n="tabAvatars">👥 ตัวละคร (Avatars)</button>
|
||||
<button class="tab-btn" data-tab="tab-exrank" data-i18n="tabExrank">⚡ Ex-Rank (สกิลพิเศษ)</button>
|
||||
<button class="tab-btn" data-tab="tab-teleport" data-i18n="tabTeleport">🚀 วาร์ปฉาก (Teleport)</button>
|
||||
<button class="tab-btn" data-tab="tab-json" data-i18n="tabJson">📝 จัดการไฟล์ JSON</button>
|
||||
<button class="tab-btn" data-tab="tab-settings" data-i18n="tabSettings">⚙️ ตั้งค่า & คู่มือ Host</button>
|
||||
<button class="tab-btn active" data-tab="tab-dashboard">📊 แดชบอร์ด & ทีม (Lineup)</button>
|
||||
<button class="tab-btn" data-tab="tab-avatars">👥 ตัวละคร (Avatars & Build)</button>
|
||||
<button class="tab-btn" data-tab="tab-equipment">🗡️ กรวยแสง & รีลิกส์ (Equipment)</button>
|
||||
<button class="tab-btn" data-tab="tab-persistent" id="tab-btn-persistent">💾 จัดการเซฟ GaC (Persistent)</button>
|
||||
<button class="tab-btn" data-tab="tab-exrank">⚡ Ex-Rank (สกิลพิเศษ)</button>
|
||||
<button class="tab-btn" data-tab="tab-teleport">🚀 วาร์ปฉาก (Teleport)</button>
|
||||
<button class="tab-btn" data-tab="tab-json">📝 จัดการไฟล์ JSON</button>
|
||||
<button class="tab-btn" data-tab="tab-settings">⚙️ ตั้งค่า & ข้อมูลโหมด</button>
|
||||
</nav>
|
||||
|
||||
<!-- Main Container -->
|
||||
<main class="main-container">
|
||||
|
||||
<!-- 1. Dashboard Tab -->
|
||||
<!-- 1. Dashboard Tab & Party Lineup -->
|
||||
<section id="tab-dashboard" class="tab-content active">
|
||||
<div class="grid-3" style="margin-bottom: 1.5rem;">
|
||||
<div class="card stat-box">
|
||||
<div class="stat-number" id="stat-avatars">0</div>
|
||||
<div class="stat-label" data-i18n="statAvatars">ตัวละครที่ครอบครอง (Avatars)</div>
|
||||
<div class="stat-label">ตัวละครที่ครอบครอง (Avatars)</div>
|
||||
</div>
|
||||
<div class="card stat-box">
|
||||
<div class="stat-number" id="stat-lightcones">0</div>
|
||||
<div class="stat-label" data-i18n="statLightcones">กรวยแสง (Lightcones)</div>
|
||||
<div class="stat-label">กรวยแสง (Lightcones)</div>
|
||||
</div>
|
||||
<div class="card stat-box">
|
||||
<div class="stat-number" id="stat-relics">0</div>
|
||||
<div class="stat-label" data-i18n="statRelics">รีลิกส์ (Relics)</div>
|
||||
<div class="stat-label">รีลิกส์ (Relics)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Party Lineup Visualizer -->
|
||||
<div class="card" style="margin-bottom: 1.5rem;">
|
||||
<div class="card-title">
|
||||
<span>🎮 สมาชิกในทีมปัจจุบัน (Active Party Lineup)</span>
|
||||
<span style="font-size: 0.8rem; color: var(--accent-cyan); font-weight: normal;">* คลิกที่ช่องเพื่อเปลี่ยนตัวละครในทีม</span>
|
||||
</div>
|
||||
<div class="lineup-grid" id="lineup-container">
|
||||
<!-- Rendered by JS -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -69,12 +93,16 @@
|
||||
<!-- Server & Map Card -->
|
||||
<div class="card">
|
||||
<div class="card-title">
|
||||
<span data-i18n="serverStatusTitle">📍 สถานะเซิร์ฟเวอร์ & ตำแหน่งปัจจุบัน</span>
|
||||
<span>📍 สถานะเซิร์ฟเวอร์ & พิกัดปัจจุบัน</span>
|
||||
<span class="btn-sm btn-secondary" id="badge-version">API Live</span>
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: column; gap: 0.75rem;">
|
||||
<div>
|
||||
<span class="form-label" data-i18n="lblApiGatewayUrl">API Gateway Endpoint</span>
|
||||
<span class="form-label">โหมดเซิร์ฟเวอร์ที่ใช้งาน</span>
|
||||
<strong id="lbl-active-mode" style="color: var(--accent-cyan);">GaC-hkrpg-go</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span class="form-label">API Gateway Endpoint</span>
|
||||
<code id="lbl-api-url" style="color: var(--accent-cyan); font-size: 0.9rem;">-</code>
|
||||
</div>
|
||||
<div style="display: flex; gap: 1rem;">
|
||||
@@ -88,7 +116,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span class="form-label" data-i18n="coords">พิกัดตัวละคร (X, Y, Z)</span>
|
||||
<span class="form-label">พิกัดตัวละคร (X, Y, Z)</span>
|
||||
<span id="lbl-coordinates" style="color: var(--text-muted); font-size: 0.85rem;">-</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -96,84 +124,192 @@
|
||||
|
||||
<!-- Quick Actions Card -->
|
||||
<div class="card">
|
||||
<div class="card-title" data-i18n="quickActionsTitle">⚡ ดำเนินการด่วน (Quick Actions)</div>
|
||||
<p class="card-subtitle" data-i18n="quickActionsSubtitle">สั่งการส่งไปยัง REST API Gateway ทันที</p>
|
||||
<div class="card-title">⚡ ดำเนินการด่วน (Quick Actions)</div>
|
||||
<p class="card-subtitle">ส่งคำสั่งไปยังเซิร์ฟเวอร์ทันที</p>
|
||||
<div style="display: flex; flex-direction: column; gap: 0.75rem;">
|
||||
<button class="btn btn-gold" id="btn-quick-max-all">
|
||||
<span data-i18n="btnMaxAll">⭐ ปรับเลเวล 80 & ปลด E6 ตัวละครทั้งหมด (Max All)</span>
|
||||
⭐ ปรับเลเวล 80 & ปลด E6 ตัวละครทั้งหมด (Max All)
|
||||
</button>
|
||||
<button class="btn btn-primary" id="btn-quick-warp-express">
|
||||
<span data-i18n="btnWarpExpress">🚂 วาร์ปกลับรถไฟ Astral Express ทันที</span>
|
||||
🚂 วาร์ปกลับห้องรับรองรถไฟ Astral Express ทันที
|
||||
</button>
|
||||
<button class="btn btn-secondary" id="btn-quick-sync-persistent">
|
||||
<span data-i18n="btnSyncPersistent">💾 ซิงค์ข้อมูล Persistent ล่าสุด</span>
|
||||
💾 ซิงค์ข้อมูล Persistent / เซฟล่าสุด
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Current Lineup -->
|
||||
<div class="card" style="margin-top: 1.5rem;">
|
||||
<div class="card-title" data-i18n="currentLineupTitle">🎮 ทีมปัจจุบัน (Current Lineup)</div>
|
||||
<div id="lineup-container" class="grid-4" style="margin-top: 0.5rem;">
|
||||
<div class="card" style="text-align: center; color: var(--text-muted);">กำลังโหลดข้อมูลทีม...</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 2. Avatars Tab -->
|
||||
<!-- 2. Avatars Tab (Build & Eidolon Editor) -->
|
||||
<section id="tab-avatars" class="tab-content">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.25rem; flex-wrap: wrap; gap: 1rem;">
|
||||
<div style="display: flex; gap: 0.75rem; flex: 1; max-width: 480px;">
|
||||
<input type="text" id="avatar-search" class="form-input" data-i18n-placeholder="searchPlaceholder" placeholder="🔍 ค้นหาชื่อหรือ ID ตัวละคร...">
|
||||
<div style="display: flex; gap: 0.75rem; flex: 1; max-width: 520px;">
|
||||
<input type="text" id="avatar-search" class="form-input" placeholder="🔍 ค้นหาชื่อหรือ ID ตัวละคร...">
|
||||
<select id="avatar-filter-rarity" class="form-select" style="max-width: 140px;">
|
||||
<option value="all" data-i18n="filterAllRarity">ทุกระดับดาว</option>
|
||||
<option value="5" data-i18n="filter5Star">5 ดาว (SSR)</option>
|
||||
<option value="4" data-i18n="filter4Star">4 ดาว (SR)</option>
|
||||
<option value="all">ทุกระดับดาว</option>
|
||||
<option value="5">5 ดาว (SSR)</option>
|
||||
<option value="4">4 ดาว (SR)</option>
|
||||
</select>
|
||||
<select id="avatar-filter-element" class="form-select" style="max-width: 140px;">
|
||||
<option value="all">ทุกธาตุ</option>
|
||||
<option value="Fire">ไฟ (Fire)</option>
|
||||
<option value="Ice">น้ำแข็ง (Ice)</option>
|
||||
<option value="Lightning">สายฟ้า (Lightning)</option>
|
||||
<option value="Wind">ลม (Wind)</option>
|
||||
<option value="Physical">กายภาพ (Physical)</option>
|
||||
<option value="Quantum">ควอนตัม (Quantum)</option>
|
||||
<option value="Imaginary">จินตภาพ (Imaginary)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div style="display: flex; gap: 0.5rem;">
|
||||
<button class="btn btn-gold" id="btn-max-all-avatars" data-i18n="btnMaxAll">⭐ ปรับ Max E6 ทุกตัว</button>
|
||||
<button class="btn btn-secondary" id="btn-reload-avatars" data-i18n="btnReload">🔄 รีโหลด</button>
|
||||
<button class="btn btn-gold" id="btn-max-all-avatars">⭐ ปรับ Max E6 ทุกตัว</button>
|
||||
<button class="btn btn-secondary" id="btn-reload-avatars">🔄 รีโหลด</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="avatars-grid" class="grid-4">
|
||||
<div style="color: var(--text-muted); grid-column: 1/-1; text-align: center; padding: 2rem;">
|
||||
กำลังดึงข้อมูลตัวละครจากเซิร์ฟเวอร์...
|
||||
กำลังดึงข้อมูลตัวละคร...
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 3. Ex-Rank Tab -->
|
||||
<!-- 3. Equipment Tab (Lightcones & Relics) -->
|
||||
<section id="tab-equipment" class="tab-content">
|
||||
<div class="grid-2">
|
||||
<!-- Lightcones Card -->
|
||||
<div class="card">
|
||||
<div class="card-title">
|
||||
<span>🗡️ กรวยแสงในคลัง (Lightcones)</span>
|
||||
<span class="btn-sm btn-secondary" id="badge-total-lc">0 ชิ้น</span>
|
||||
</div>
|
||||
<p class="card-subtitle">รายการกรวยแสงทั้งหมดจาก freesr-data.json</p>
|
||||
<div id="lightcones-list" style="max-height: 520px; overflow-y: auto; display: flex; flex-direction: column; gap: 0.6rem;">
|
||||
<div style="color: var(--text-muted); text-align: center; padding: 1.5rem;">กำลังโหลดข้อมูลกรวยแสง...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Relics Card -->
|
||||
<div class="card">
|
||||
<div class="card-title">
|
||||
<span>🛡️ รีลิกส์ในคลัง (Relics)</span>
|
||||
<span class="btn-sm btn-secondary" id="badge-total-relics">0 ชิ้น</span>
|
||||
</div>
|
||||
<p class="card-subtitle">รายการรีลิกส์ทั้งหมดจาก freesr-data.json</p>
|
||||
<div id="relics-list" style="max-height: 520px; overflow-y: auto; display: flex; flex-direction: column; gap: 0.6rem;">
|
||||
<div style="color: var(--text-muted); text-align: center; padding: 1.5rem;">กำลังโหลดข้อมูลรีลิกส์...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 4. Persistent Save Settings (Exclusive to GaC-hkrpg-go) -->
|
||||
<section id="tab-persistent" class="tab-content">
|
||||
<div class="card" style="margin-bottom: 1.5rem;">
|
||||
<div class="card-title">
|
||||
<span>💾 จัดการไฟล์เซฟ Persistent (GaC-hkrpg-go Dedicated)</span>
|
||||
<span class="btn-sm btn-success">เซิร์ฟเวอร์ของเราเอง</span>
|
||||
</div>
|
||||
<p class="card-subtitle">
|
||||
ไฟล์ <code>persistent</code> เก็บการตั้งค่าสถานะผู้เล่นเฉพาะของเซิร์ฟเวอร์ GaC เช่น เพศ/ธาตุตัวเอก, สเตตัส March 7th, สวิตช์ฟังก์ชัน Global ต่างๆ
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid-2">
|
||||
<!-- Main Character & March Path -->
|
||||
<div class="card">
|
||||
<div class="card-title">👤 ผู้บุกเบิก & March 7th</div>
|
||||
<form id="form-persistent-chars">
|
||||
<div class="form-group">
|
||||
<label class="form-label">เพศ & ธาตุของตัวเอก (Main Character)</label>
|
||||
<select id="persistent-mc-select" class="form-select">
|
||||
<option value="MalePhysical">ผู้บุกเบิกชาย - กายภาพ (Male Physical)</option>
|
||||
<option value="FemalePhysical">ผู้บุกเบิกหญิง - กายภาพ (Female Physical)</option>
|
||||
<option value="MalePreservation">ผู้บุกเบิกชาย - ไฟ (Male Preservation)</option>
|
||||
<option value="FemalePreservation">ผู้บุกเบิกหญิง - ไฟ (Female Preservation)</option>
|
||||
<option value="MaleHarmony">ผู้บุกเบิกชาย - ประสาน (Male Harmony)</option>
|
||||
<option value="FemaleHarmony">ผู้บุกเบิกหญิง - ประสาน (Female Harmony)</option>
|
||||
<option value="MaleRemembrance">ผู้บุกเบิกชาย - ความทรงจำ (Male Remembrance)</option>
|
||||
<option value="FemaleRemembrance">ผู้บุกเบิกหญิง - ความทรงจำ (Female Remembrance)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">สายของ March 7th</label>
|
||||
<select id="persistent-march-select" class="form-select">
|
||||
<option value="MarchHunt">March 7th - ล่าสังหาร (The Hunt / จินตภาพ)</option>
|
||||
<option value="MarchPreservation">March 7th - อนุรักษ์ (Preservation / น้ำแข็ง)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary" style="width: 100%;">
|
||||
💾 บันทึกการตั้งค่าตัวละคร
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Global Features & Toggles -->
|
||||
<div class="card">
|
||||
<div class="card-title">⚙️ สวิตช์ระบบพิเศษของ GaC</div>
|
||||
<div style="display: flex; flex-direction: column; gap: 1rem; margin-top: 1rem;">
|
||||
<div class="ex-switch-card">
|
||||
<div>
|
||||
<div style="font-weight: 700;">Silver Wolf Global Feature</div>
|
||||
<div style="font-size: 0.8rem; color: var(--text-muted);">เปิดใช้งานฟังก์ชันพิเศษ SW ทั่วทั้งเซิร์ฟเวอร์</div>
|
||||
</div>
|
||||
<label class="switch">
|
||||
<input type="checkbox" id="toggle-sw-global">
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="ex-switch-card">
|
||||
<div>
|
||||
<div style="font-weight: 700;">Castorice Global Feature</div>
|
||||
<div style="font-size: 0.8rem; color: var(--text-muted);">เปิดใช้งานฟังก์ชันพิเศษ Castorice ทั่วทั้งเซิร์ฟเวอร์</div>
|
||||
</div>
|
||||
<label class="switch">
|
||||
<input type="checkbox" id="toggle-castorice-global">
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-success" id="btn-save-persistent-toggles">
|
||||
💾 บันทึกสวิตช์ Global
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 5. Ex-Rank Tab -->
|
||||
<section id="tab-exrank" class="tab-content">
|
||||
<div class="card" style="margin-bottom: 1.5rem;">
|
||||
<div class="card-title">
|
||||
<span data-i18n="exrankTitle">⚡ ระบบปลดล็อค Ex-Rank Skill</span>
|
||||
<span>⚡ ระบบปลดล็อค Ex-Rank Skill</span>
|
||||
<div style="display: flex; gap: 0.5rem;">
|
||||
<button class="btn btn-success btn-sm" id="btn-exrank-enable-all" data-i18n="btnEnableAll">เปิด Ex-Rank ทั้งหมด</button>
|
||||
<button class="btn btn-secondary btn-sm" id="btn-exrank-disable-all" data-i18n="btnDisableAll">ปิด Ex-Rank ทั้งหมด</button>
|
||||
<button class="btn btn-success btn-sm" id="btn-exrank-enable-all">เปิด Ex-Rank ทั้งหมด</button>
|
||||
<button class="btn btn-secondary btn-sm" id="btn-exrank-disable-all">ปิด Ex-Rank ทั้งหมด</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="card-subtitle" data-i18n="exrankSubtitle">
|
||||
<p class="card-subtitle">
|
||||
เปิดใช้งานพรสวรรค์และกลไกพิเศษ Ex-Rank สำหรับตัวละครเฉพาะใน FreeSR (เช่น Jing Yuan โดน CC ไม่หยุดชะงัก, Dan Heng IL, Acheron)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div id="exrank-list" style="display: flex; flex-direction: column; gap: 0.75rem;">
|
||||
<div style="color: var(--text-muted); text-align: center; padding: 2rem;">
|
||||
กำลังโหลดข้อมูล Ex-Rank...
|
||||
</div>
|
||||
<!-- Rendered by JS -->
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 4. Teleport Tab -->
|
||||
<!-- 6. Teleport Tab -->
|
||||
<section id="tab-teleport" class="tab-content">
|
||||
<div class="grid-2">
|
||||
<!-- Presets -->
|
||||
<div class="card">
|
||||
<div class="card-title" data-i18n="presetsTitle">📍 จุดวาร์ปปลอดภัยยอดนิยม (Safe Presets)</div>
|
||||
<p class="card-subtitle" data-i18n="presetsSubtitle">คลิกจุดวาร์ปเพื่อย้ายฉากของผู้เล่นในไฟล์เซฟได้ทันที</p>
|
||||
<div class="card-title">📍 จุดวาร์ปปลอดภัยยอดนิยม (Safe Presets)</div>
|
||||
<p class="card-subtitle">คลิกจุดวาร์ปเพื่อย้ายฉากของผู้เล่นในไฟล์เซฟได้ทันที</p>
|
||||
<div id="teleport-presets-list" style="display: flex; flex-direction: column; gap: 0.6rem;">
|
||||
<!-- Rendered by JS -->
|
||||
</div>
|
||||
@@ -181,8 +317,8 @@
|
||||
|
||||
<!-- Custom Coordinate Warp -->
|
||||
<div class="card">
|
||||
<div class="card-title" data-i18n="customTeleportTitle">🎯 กำหนดพิกัดวาร์ปเอง (Custom Teleport)</div>
|
||||
<p class="card-subtitle" data-i18n="customTeleportSubtitle">ใส่ PlaneID, FloorID และ EntryID ตามต้องการ</p>
|
||||
<div class="card-title">🎯 กำหนดพิกัดวาร์ปเอง (Custom Teleport)</div>
|
||||
<p class="card-subtitle">ใส่ PlaneID, FloorID และ EntryID ตามต้องการ</p>
|
||||
|
||||
<form id="form-custom-teleport">
|
||||
<div class="grid-2" style="gap: 0.75rem;">
|
||||
@@ -215,7 +351,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary" style="width: 100%; margin-top: 0.5rem;" data-i18n="btnExecuteWarp">
|
||||
<button type="submit" class="btn btn-primary" style="width: 100%; margin-top: 0.5rem;">
|
||||
🚀 ยืนยันการวาร์ป (Execute Teleport)
|
||||
</button>
|
||||
</form>
|
||||
@@ -223,106 +359,170 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 5. JSON Management Tab -->
|
||||
<!-- 7. JSON Management Tab -->
|
||||
<section id="tab-json" class="tab-content">
|
||||
<div class="card">
|
||||
<div class="card-title" style="flex-wrap: wrap; gap: 0.75rem;">
|
||||
<div style="display: flex; gap: 0.5rem;">
|
||||
<button class="btn btn-sm btn-primary active" id="btn-select-freesr" data-i18n="btnFreesr">📄 freesr-data.json</button>
|
||||
<button class="btn btn-sm btn-secondary" id="btn-select-persistent" data-i18n="btnPersistent">💾 persistent (Save)</button>
|
||||
<button class="btn btn-sm btn-primary active" id="btn-select-freesr">📄 freesr-data.json</button>
|
||||
<button class="btn btn-sm btn-secondary" id="btn-select-persistent">💾 persistent (Save)</button>
|
||||
</div>
|
||||
<div style="display: flex; gap: 0.5rem;">
|
||||
<button class="btn btn-sm btn-secondary" id="btn-json-beautify" data-i18n="btnBeautify">✨ จัดรูปแบบ JSON</button>
|
||||
<button class="btn btn-sm btn-secondary" id="btn-json-download" data-i18n="btnDownload">📥 ดาวน์โหลด Backup</button>
|
||||
<button class="btn btn-sm btn-secondary" id="btn-json-beautify">✨ จัดรูปแบบ JSON</button>
|
||||
<button class="btn btn-sm btn-secondary" id="btn-json-download">📥 ดาวน์โหลด Backup</button>
|
||||
<label class="btn btn-sm btn-secondary" style="cursor: pointer; margin: 0;">
|
||||
<span data-i18n="btnUpload">📤 อัปโหลดไฟล์</span> <input type="file" id="json-file-input" accept=".json,text/plain" style="display: none;">
|
||||
<span>📤 อัปโหลดไฟล์</span> <input type="file" id="json-file-input" accept=".json,text/plain" style="display: none;">
|
||||
</label>
|
||||
<button class="btn btn-sm btn-success" id="btn-json-save" data-i18n="btnSaveServer">💾 บันทึกไปยังเซิร์ฟเวอร์</button>
|
||||
<button class="btn btn-sm btn-success" id="btn-json-save">💾 บันทึกไปยังเซิร์ฟเวอร์</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="json-editor-wrapper">
|
||||
<textarea id="json-editor" class="json-textarea" spellcheck="false" placeholder="..."></textarea>
|
||||
<div style="display: flex; justify-content: space-between; font-size: 0.8rem; color: var(--text-muted);">
|
||||
<span id="json-status-info" data-i18n="jsonStatusReady">พร้อมใช้งาน</span>
|
||||
<span data-i18n="jsonTip">Tip: สามารถแก้ไขค่าดิบแล้วกด 'บันทึกไปยังเซิร์ฟเวอร์' ได้ทันที</span>
|
||||
<span id="json-status-info">พร้อมใช้งาน</span>
|
||||
<span>Tip: สามารถแก้ไขค่าดิบแล้วกด 'บันทึกไปยังเซิร์ฟเวอร์' ได้ทันที</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 6. Settings & GitHub Hosting Guide -->
|
||||
<!-- 8. Settings & Compatibility Guide -->
|
||||
<section id="tab-settings" class="tab-content">
|
||||
<div class="grid-2">
|
||||
<!-- Endpoint config -->
|
||||
<div class="card">
|
||||
<div class="card-title" data-i18n="apiSettingsTitle">⚙️ ตั้งค่าการเชื่อมต่อ API Gateway</div>
|
||||
<p class="card-subtitle" data-i18n="apiSettingsSubtitle">ปรับเปลี่ยนพอร์ตหรือ IP ได้ตามไฟล์ server.json ของคุณ</p>
|
||||
<div class="card-title">⚙️ ตั้งค่าการเชื่อมต่อ API Gateway</div>
|
||||
<p class="card-subtitle">ปรับเปลี่ยนพอร์ตหรือ IP ได้ตามเซิร์ฟเวอร์ที่คุณใช้</p>
|
||||
|
||||
<form id="form-settings">
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="lblApiGatewayUrl">REST API Gateway URL</label>
|
||||
<label class="form-label">โหมดเซิร์ฟเวอร์ (Target Server)</label>
|
||||
<select id="setting-server-mode" class="form-select">
|
||||
<option value="gac">⚡ GaC-hkrpg-go (เซิร์ฟเวอร์หลัก - แก้ได้ทั้ง FreeSR + Persistent)</option>
|
||||
<option value="firefly">🔥 FireflyGo (FreeSR Standard)</option>
|
||||
<option value="robin">🕊️ RobinSR (FreeSR Standard)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">REST API Gateway URL</label>
|
||||
<input type="text" id="setting-api-url" class="form-input" placeholder="http://localhost:21000">
|
||||
<small style="color: var(--text-muted); display: block; margin-top: 0.35rem;">
|
||||
* Port 21000 by default (GaC-hkrpg-go SDK Server)
|
||||
* ค่าเริ่มต้นพอร์ต 21000 (SDK server)
|
||||
</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="lblPingInterval">ความถี่ในการตรวจสอบสถานะ (วินาที)</label>
|
||||
<label class="form-label">ความถี่ในการตรวจสอบสถานะ (วินาที)</label>
|
||||
<input type="number" id="setting-ping-interval" class="form-input" min="3" max="60" value="10">
|
||||
</div>
|
||||
<div style="display: flex; gap: 0.75rem;">
|
||||
<button type="submit" class="btn btn-success" style="flex: 1;" data-i18n="btnSaveSettings">💾 บันทึกการตั้งค่า</button>
|
||||
<button type="button" class="btn btn-secondary" id="btn-reset-settings" data-i18n="btnResetDefault">คืนค่าเริ่มต้น</button>
|
||||
<button type="submit" class="btn btn-success" style="flex: 1;">💾 บันทึกการตั้งค่า</button>
|
||||
<button type="button" class="btn btn-secondary" id="btn-reset-settings">คืนค่าเริ่มต้น</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- GitHub Pages Hosting Guide -->
|
||||
<!-- Mode Explanation Card -->
|
||||
<div class="card">
|
||||
<div class="card-title" data-i18n="hostGuideTitle">🌐 วิธีนำขึ้นโฮสต์ GitHub Pages (ฟรี 100%)</div>
|
||||
<p class="card-subtitle" data-i18n="hostGuideSubtitle">สามารถเปิดใช้งานเว็บนี้บนอินเทอร์เน็ตได้โดยไม่ต้องเสียค่าโฮสต์ใดๆ</p>
|
||||
|
||||
<ol style="margin-left: 1.25rem; font-size: 0.875rem; color: var(--text-muted); display: flex; flex-direction: column; gap: 0.65rem;">
|
||||
<li>
|
||||
<strong style="color: var(--text-main);">1. Create GitHub Repository:</strong><br>
|
||||
Create new repo e.g. <code>GaC-srtool</code>
|
||||
</li>
|
||||
<li>
|
||||
<strong style="color: var(--text-main);">2. Push to GitHub:</strong><br>
|
||||
<code>git remote add origin https://github.com/<user>/GaC-srtool.git</code><br>
|
||||
<code>git add . && git commit -m "Init" && git push -u origin main</code>
|
||||
</li>
|
||||
<li>
|
||||
<strong style="color: var(--text-main);">3. Enable GitHub Pages:</strong><br>
|
||||
Go to Repo > <strong>Settings</strong> > <strong>Pages</strong> > Source: <strong>Deploy from a branch</strong> > Branch: <strong>main</strong> / (root) > Click <strong>Save</strong>
|
||||
</li>
|
||||
<li>
|
||||
<strong style="color: var(--text-main);">4. Done:</strong><br>
|
||||
Your dashboard will be online at <code>https://<user>.github.io/GaC-srtool/</code>!
|
||||
</li>
|
||||
</ol>
|
||||
<div class="card-title">💡 ข้อมูลโหมดเซิร์ฟเวอร์ & สำหรับผู้อื่น</div>
|
||||
<div style="display: flex; flex-direction: column; gap: 0.85rem; font-size: 0.875rem; color: var(--text-muted);">
|
||||
<div>
|
||||
<strong style="color: var(--accent-cyan);">⚡ GaC-hkrpg-go (Full Access):</strong><br>
|
||||
โหมดเฉพาะของเซิร์ฟเวอร์เรา สามารถอ่านและเขียนทั้ง <code>freesr-data.json</code> และ <code>persistent</code> ได้แบบ 100% เรียลไทม์ผ่าน REST API Gateway พร้อมระบบ Ex-Rank และ Teleport
|
||||
</div>
|
||||
<div>
|
||||
<strong style="color: #f87171;">🔥 FireflyGo:</strong><br>
|
||||
สำหรับผู้ใช้งานที่รันเซิร์ฟเวอร์ FireflyGo สามารถใช้เครื่องมือนี้จัดทีม, แม็กซ์ตัวละคร, หรือปรับแต่งอาวุธ/รีลิกส์ แล้วสั่งซิงค์ผ่าน <code>/srtools</code> หรือกดดาวน์โหลดไฟล์ <code>freesr-data.json</code> ไปวางในเซิร์ฟเวอร์ได้ทันที
|
||||
</div>
|
||||
<div>
|
||||
<strong style="color: #c084fc;">🕊️ RobinSR:</strong><br>
|
||||
สำหรับผู้ใช้งาน RobinSR สามารถใช้เครื่องมือนี้ในการสร้างเซฟและตัวละครตามมาตรฐาน FreeSR ได้อย่างสมบูรณ์แบบ
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
|
||||
<!-- Character Detail / Build Modal -->
|
||||
<div class="modal-overlay" id="modal-avatar-detail">
|
||||
<div class="modal-content" style="max-width: 640px;">
|
||||
<div class="modal-header">
|
||||
<h3 id="modal-char-title" style="font-weight: 800; font-size: 1.15rem; color: var(--text-highlight);">
|
||||
ปรับแต่งตัวละคร
|
||||
</h3>
|
||||
<button class="btn btn-sm btn-secondary" id="btn-close-avatar-modal">✕</button>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 1.25rem; align-items: center; margin-bottom: 1.25rem;">
|
||||
<div class="avatar-image-container" style="width: 76px; height: 76px;">
|
||||
<img id="modal-char-img" class="avatar-img" src="" alt="">
|
||||
</div>
|
||||
<div>
|
||||
<div id="modal-char-name" style="font-size: 1.25rem; font-weight: 800;"></div>
|
||||
<div id="modal-char-meta" style="font-size: 0.85rem; color: var(--text-muted); margin-top: 0.25rem;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-2" style="gap: 1rem; margin-bottom: 1rem;">
|
||||
<div class="form-group">
|
||||
<label class="form-label">ระดับเลเวล (Level 1-80)</label>
|
||||
<input type="number" id="modal-char-level" class="form-input" min="1" max="80" value="80">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">การเลื่อนขั้น (Ascension 0-6)</label>
|
||||
<input type="number" id="modal-char-promotion" class="form-input" min="0" max="6" value="6">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Eidolon (E0 - E6)</label>
|
||||
<div class="eidolon-buttons" id="modal-char-eidolon-btns" style="margin-top: 0.35rem;">
|
||||
<!-- Injected pips -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 0.75rem; margin-top: 1.5rem; justify-content: flex-end;">
|
||||
<button class="btn btn-gold" id="btn-modal-max-skills">
|
||||
⭐ อัพสกิล & Traces ทั้งหมด (10/10)
|
||||
</button>
|
||||
<button class="btn btn-success" id="btn-modal-save-avatar">
|
||||
💾 บันทึกตัวละคร
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Party Member Picker Modal -->
|
||||
<div class="modal-overlay" id="modal-lineup-picker">
|
||||
<div class="modal-content" style="max-width: 720px; max-height: 85vh; display: flex; flex-direction: column;">
|
||||
<div class="modal-header">
|
||||
<h3 id="modal-picker-title" style="font-weight: 800;">เลือกตัวละครเข้าทีม</h3>
|
||||
<button class="btn btn-sm btn-secondary" id="btn-close-picker-modal">✕</button>
|
||||
</div>
|
||||
<input type="text" id="picker-search" class="form-input" placeholder="🔍 ค้นหาตัวละครเพื่อลงทีม..." style="margin-bottom: 1rem;">
|
||||
<div id="picker-avatars-grid" class="grid-4" style="overflow-y: auto; flex: 1; padding-right: 0.5rem;">
|
||||
<!-- Injected by JS -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Settings Modal -->
|
||||
<div class="modal-overlay" id="modal-settings">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3 style="font-weight: 700;" data-i18n="apiSettingsTitle">⚙️ ตั้งค่าการเชื่อมต่อ API Server</h3>
|
||||
<h3 style="font-weight: 700;">⚙️ ตั้งค่าการเชื่อมต่อ API Server</h3>
|
||||
<button class="btn btn-sm btn-secondary" id="btn-close-modal">✕</button>
|
||||
</div>
|
||||
<div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="lblApiGatewayUrl">Server REST API URL</label>
|
||||
<label class="form-label">Server REST API URL</label>
|
||||
<input type="text" id="modal-api-url" class="form-input">
|
||||
</div>
|
||||
<div style="display: flex; justify-content: flex-end; gap: 0.5rem; margin-top: 1rem;">
|
||||
<button class="btn btn-secondary" id="btn-modal-cancel">Cancel</button>
|
||||
<button class="btn btn-primary" id="btn-modal-save" data-i18n="btnSaveSettings">บันทึก & ทดสอบ</button>
|
||||
<button class="btn btn-secondary" id="btn-modal-cancel">ยกเลิก</button>
|
||||
<button class="btn btn-primary" id="btn-modal-save">บันทึก & ทดสอบ</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,20 +1,28 @@
|
||||
import { AVATAR_METADATA, getAvatarInfo } from './avatars-data.js';
|
||||
import { PRESET_TELEPORTS, getTeleportName } from './teleports-data.js';
|
||||
import { SUPPORTED_LANGUAGES, getCurrentLanguage, setLanguage, t } from './i18n.js';
|
||||
import { SUPPORTED_LANGUAGES, getCurrentLanguage, setLanguage, t, translateElement, translatePath } from './i18n.js';
|
||||
|
||||
// Application State
|
||||
const state = {
|
||||
serverMode: localStorage.getItem('gac_srtool_mode') || 'gac', // 'gac' | 'firefly' | 'robin'
|
||||
apiUrl: localStorage.getItem('gac_srtool_api_url') || 'http://localhost:21000',
|
||||
pingIntervalSec: parseInt(localStorage.getItem('gac_srtool_ping_interval') || '10', 10),
|
||||
isConnected: false,
|
||||
statusData: null,
|
||||
avatarsList: [],
|
||||
currentJsonMode: 'freesr', // 'freesr' or 'persistent'
|
||||
activeTab: 'tab-dashboard'
|
||||
fullFreesrData: null,
|
||||
persistentData: null,
|
||||
currentJsonMode: 'freesr',
|
||||
activeTab: 'tab-dashboard',
|
||||
selectedLineupSlot: null,
|
||||
selectedAvatarDetail: null
|
||||
};
|
||||
|
||||
// UI Elements
|
||||
const el = {
|
||||
serverModeSelect: document.getElementById('server-mode-select'),
|
||||
settingServerMode: document.getElementById('setting-server-mode'),
|
||||
lblActiveMode: document.getElementById('lbl-active-mode'),
|
||||
langSelect: document.getElementById('lang-select'),
|
||||
statusDot: document.getElementById('status-dot'),
|
||||
connectionStatusText: document.getElementById('connection-status-text'),
|
||||
@@ -43,9 +51,25 @@ const el = {
|
||||
avatarsGrid: document.getElementById('avatars-grid'),
|
||||
avatarSearch: document.getElementById('avatar-search'),
|
||||
avatarFilterRarity: document.getElementById('avatar-filter-rarity'),
|
||||
avatarFilterElement: document.getElementById('avatar-filter-element'),
|
||||
btnMaxAllAvatars: document.getElementById('btn-max-all-avatars'),
|
||||
btnReloadAvatars: document.getElementById('btn-reload-avatars'),
|
||||
|
||||
// Equipment
|
||||
lightconesList: document.getElementById('lightcones-list'),
|
||||
relicsList: document.getElementById('relics-list'),
|
||||
badgeTotalLc: document.getElementById('badge-total-lc'),
|
||||
badgeTotalRelics: document.getElementById('badge-total-relics'),
|
||||
|
||||
// Persistent Tab
|
||||
tabBtnPersistent: document.getElementById('tab-btn-persistent'),
|
||||
formPersistentChars: document.getElementById('form-persistent-chars'),
|
||||
persistentMcSelect: document.getElementById('persistent-mc-select'),
|
||||
persistentMarchSelect: document.getElementById('persistent-march-select'),
|
||||
toggleSwGlobal: document.getElementById('toggle-sw-global'),
|
||||
toggleCastoriceGlobal: document.getElementById('toggle-castorice-global'),
|
||||
btnSavePersistentToggles: document.getElementById('btn-save-persistent-toggles'),
|
||||
|
||||
// Ex-Rank
|
||||
exrankList: document.getElementById('exrank-list'),
|
||||
btnExrankEnableAll: document.getElementById('btn-exrank-enable-all'),
|
||||
@@ -77,6 +101,26 @@ const el = {
|
||||
settingPingInterval: document.getElementById('setting-ping-interval'),
|
||||
btnResetSettings: document.getElementById('btn-reset-settings'),
|
||||
|
||||
// Character Detail Modal
|
||||
modalAvatarDetail: document.getElementById('modal-avatar-detail'),
|
||||
btnCloseAvatarModal: document.getElementById('btn-close-avatar-modal'),
|
||||
modalCharTitle: document.getElementById('modal-char-title'),
|
||||
modalCharImg: document.getElementById('modal-char-img'),
|
||||
modalCharName: document.getElementById('modal-char-name'),
|
||||
modalCharMeta: document.getElementById('modal-char-meta'),
|
||||
modalCharLevel: document.getElementById('modal-char-level'),
|
||||
modalCharPromotion: document.getElementById('modal-char-promotion'),
|
||||
modalCharEidolonBtns: document.getElementById('modal-char-eidolon-btns'),
|
||||
btnModalMaxSkills: document.getElementById('btn-modal-max-skills'),
|
||||
btnModalSaveAvatar: document.getElementById('btn-modal-save-avatar'),
|
||||
|
||||
// Lineup Picker Modal
|
||||
modalLineupPicker: document.getElementById('modal-lineup-picker'),
|
||||
btnClosePickerModal: document.getElementById('btn-close-picker-modal'),
|
||||
modalPickerTitle: document.getElementById('modal-picker-title'),
|
||||
pickerSearch: document.getElementById('picker-search'),
|
||||
pickerAvatarsGrid: document.getElementById('picker-avatars-grid'),
|
||||
|
||||
toastContainer: document.getElementById('toast-container')
|
||||
};
|
||||
|
||||
@@ -150,7 +194,21 @@ function setConnectionStatus(connected, extraText = '') {
|
||||
}
|
||||
}
|
||||
|
||||
// Apply multi-language translations across whole UI
|
||||
// Helper to get element CSS class
|
||||
function getElementClass(elem) {
|
||||
const map = {
|
||||
Fire: 'elem-fire',
|
||||
Ice: 'elem-ice',
|
||||
Lightning: 'elem-lightning',
|
||||
Wind: 'elem-wind',
|
||||
Physical: 'elem-physical',
|
||||
Quantum: 'elem-quantum',
|
||||
Imaginary: 'elem-imaginary'
|
||||
};
|
||||
return map[elem] || 'elem-physical';
|
||||
}
|
||||
|
||||
// Apply multi-language translations across UI
|
||||
function applyTranslations() {
|
||||
document.querySelectorAll('[data-i18n]').forEach(elem => {
|
||||
const key = elem.getAttribute('data-i18n');
|
||||
@@ -162,7 +220,6 @@ function applyTranslations() {
|
||||
elem.placeholder = t(key);
|
||||
});
|
||||
|
||||
// Re-render localized UI parts
|
||||
if (state.statusData) {
|
||||
renderLineup(state.statusData.lineups);
|
||||
renderExRanks(state.statusData.ex_ranks || []);
|
||||
@@ -173,7 +230,7 @@ function applyTranslations() {
|
||||
}
|
||||
}
|
||||
|
||||
// Language selector initialization
|
||||
// Setup Language Selector
|
||||
function setupLanguageSelector() {
|
||||
el.langSelect.innerHTML = '';
|
||||
SUPPORTED_LANGUAGES.forEach(lang => {
|
||||
@@ -190,14 +247,73 @@ function setupLanguageSelector() {
|
||||
const newLang = e.target.value;
|
||||
setLanguage(newLang);
|
||||
applyTranslations();
|
||||
showToast(`Language switched to ${SUPPORTED_LANGUAGES.find(l => l.code === newLang)?.name || newLang}`, 'info');
|
||||
showToast(`Language: ${SUPPORTED_LANGUAGES.find(l => l.code === newLang)?.name || newLang}`, 'info');
|
||||
});
|
||||
}
|
||||
|
||||
// Refresh status from /api/status
|
||||
// Setup Server Mode Selector
|
||||
function setupServerModes() {
|
||||
const updateModeDisplay = (mode) => {
|
||||
state.serverMode = mode;
|
||||
localStorage.setItem('gac_srtool_mode', mode);
|
||||
el.serverModeSelect.value = mode;
|
||||
el.settingServerMode.value = mode;
|
||||
|
||||
if (mode === 'gac') {
|
||||
el.lblActiveMode.textContent = 'GaC-hkrpg-go (เซิร์ฟเวอร์หลัก / Full Control)';
|
||||
el.lblActiveMode.style.color = 'var(--accent-cyan)';
|
||||
el.tabBtnPersistent.style.display = 'inline-flex';
|
||||
} else if (mode === 'firefly') {
|
||||
el.lblActiveMode.textContent = 'FireflyGo (FreeSR Compatible)';
|
||||
el.lblActiveMode.style.color = '#f87171';
|
||||
el.tabBtnPersistent.style.display = 'none';
|
||||
} else if (mode === 'robin') {
|
||||
el.lblActiveMode.textContent = 'RobinSR (FreeSR Compatible)';
|
||||
el.lblActiveMode.style.color = '#c084fc';
|
||||
el.tabBtnPersistent.style.display = 'none';
|
||||
}
|
||||
};
|
||||
|
||||
el.serverModeSelect.addEventListener('change', (e) => {
|
||||
updateModeDisplay(e.target.value);
|
||||
showToast(`สลับโหมดเซิร์ฟเวอร์เป็น: ${el.lblActiveMode.textContent}`, 'info');
|
||||
refreshStatus();
|
||||
});
|
||||
|
||||
el.settingServerMode.addEventListener('change', (e) => {
|
||||
updateModeDisplay(e.target.value);
|
||||
showToast(`สลับโหมดเซิร์ฟเวอร์เป็น: ${el.lblActiveMode.textContent}`, 'info');
|
||||
refreshStatus();
|
||||
});
|
||||
|
||||
updateModeDisplay(state.serverMode);
|
||||
}
|
||||
|
||||
// Refresh status from server
|
||||
async function refreshStatus() {
|
||||
try {
|
||||
const data = await apiCall('/api/status');
|
||||
let data;
|
||||
if (state.serverMode === 'gac') {
|
||||
data = await apiCall('/api/status');
|
||||
} else {
|
||||
// FireflyGo / RobinSR fallback check
|
||||
const freesr = await apiCall('/api/freesr').catch(() => null);
|
||||
if (freesr && freesr.data) {
|
||||
data = {
|
||||
status: 200,
|
||||
avatar_count: Object.keys(freesr.data.avatars || {}).length,
|
||||
lightcone_count: (freesr.data.lightcones || []).length,
|
||||
relic_count: (freesr.data.relics || []).length,
|
||||
lineups: freesr.data.lineups || { 0: 8001 },
|
||||
scene: freesr.data.scene,
|
||||
position: freesr.data.position,
|
||||
ex_ranks: []
|
||||
};
|
||||
} else {
|
||||
throw new Error('Server not reachable');
|
||||
}
|
||||
}
|
||||
|
||||
state.statusData = data;
|
||||
setConnectionStatus(true);
|
||||
|
||||
@@ -223,45 +339,135 @@ async function refreshStatus() {
|
||||
}
|
||||
}
|
||||
|
||||
// Render current lineup cards
|
||||
// Render party lineup cards
|
||||
function renderLineup(lineups) {
|
||||
if (!lineups || Object.keys(lineups).length === 0) {
|
||||
el.lineupContainer.innerHTML = `<div style="color: var(--text-muted); grid-column: 1/-1;">${t('slotEmpty')}</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
el.lineupContainer.innerHTML = '';
|
||||
for (let slot = 0; slot < 4; slot++) {
|
||||
const avatarId = lineups[slot] || 0;
|
||||
const card = document.createElement('div');
|
||||
card.className = 'card';
|
||||
card.style.padding = '0.85rem';
|
||||
const avatarId = lineups ? (lineups[slot] || 0) : 0;
|
||||
const slotCard = document.createElement('div');
|
||||
slotCard.className = `lineup-slot-card ${avatarId ? 'filled' : ''}`;
|
||||
slotCard.title = 'คลิกเพื่อเลือกตัวละครลงตำแหน่งนี้';
|
||||
|
||||
if (avatarId === 0) {
|
||||
card.innerHTML = `
|
||||
<div style="font-size: 0.75rem; color: var(--text-muted);">${t('slot')} ${slot + 1}</div>
|
||||
<div style="font-weight: 600; color: var(--text-muted); margin-top: 0.25rem;">${t('slotEmpty')}</div>
|
||||
slotCard.innerHTML = `
|
||||
<div class="lineup-slot-num">Slot ${slot + 1}</div>
|
||||
<div style="font-size: 2.2rem; color: var(--text-muted); opacity: 0.5;">➕</div>
|
||||
<div style="font-weight: 700; color: var(--text-muted); margin-top: 0.5rem;">ว่าง (คลิกเพื่อเพิ่ม)</div>
|
||||
`;
|
||||
} else {
|
||||
const info = getAvatarInfo(avatarId);
|
||||
card.innerHTML = `
|
||||
<div style="display: flex; gap: 0.75rem; align-items: center;">
|
||||
<div class="avatar-image-container" style="width: 44px; height: 44px;">
|
||||
<img class="avatar-img"
|
||||
src="./assets/avatars/${avatarId}.png"
|
||||
onerror="if(this.src.indexOf('jsdelivr')===-1){this.src='https://cdn.jsdelivr.net/gh/Mar-7th/StarRailRes@master/icon/avatar/${avatarId}.png';}else{this.style.display='none';this.nextElementSibling.style.display='flex';}"
|
||||
alt="${info.name}">
|
||||
<div class="avatar-icon-placeholder" style="display: none;">${info.name.charAt(0)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size: 0.75rem; color: var(--accent-cyan);">${t('slot')} ${slot + 1}</div>
|
||||
<div style="font-weight: 700; margin-top: 0.15rem; font-size: 0.95rem;">${info.name}</div>
|
||||
<div style="font-size: 0.75rem; color: var(--text-muted);">${info.element} • ${info.path}</div>
|
||||
</div>
|
||||
const elemClass = getElementClass(info.element);
|
||||
slotCard.innerHTML = `
|
||||
<div class="lineup-slot-num">Slot ${slot + 1}</div>
|
||||
<img class="lineup-slot-img"
|
||||
src="./assets/avatars/${avatarId}.png"
|
||||
onerror="if(this.src.indexOf('jsdelivr')===-1){this.src='https://cdn.jsdelivr.net/gh/Mar-7th/StarRailRes@master/icon/avatar/${avatarId}.png';}else{this.style.display='none';}"
|
||||
alt="${info.name}">
|
||||
<div style="font-weight: 800; font-size: 1rem; color: var(--text-highlight);">${info.name}</div>
|
||||
<div style="display: flex; gap: 0.35rem; margin-top: 0.35rem;">
|
||||
<span class="element-badge ${elemClass}">${info.element}</span>
|
||||
<span class="path-badge">${info.path}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
el.lineupContainer.appendChild(card);
|
||||
|
||||
slotCard.addEventListener('click', () => {
|
||||
openLineupPicker(slot);
|
||||
});
|
||||
|
||||
el.lineupContainer.appendChild(slotCard);
|
||||
}
|
||||
}
|
||||
|
||||
// Lineup Picker Modal
|
||||
function openLineupPicker(slotIndex) {
|
||||
state.selectedLineupSlot = slotIndex;
|
||||
el.modalPickerTitle.textContent = `เลือกตัวละครเข้าสู่ Slot ${slotIndex + 1}`;
|
||||
el.pickerSearch.value = '';
|
||||
renderPickerGrid();
|
||||
el.modalLineupPicker.classList.add('active');
|
||||
}
|
||||
|
||||
function renderPickerGrid() {
|
||||
const search = el.pickerSearch.value.trim().toLowerCase();
|
||||
el.pickerAvatarsGrid.innerHTML = '';
|
||||
|
||||
// Add Remove button
|
||||
const removeCard = document.createElement('div');
|
||||
removeCard.className = 'card';
|
||||
removeCard.style.padding = '0.75rem';
|
||||
removeCard.style.textAlign = 'center';
|
||||
removeCard.style.cursor = 'pointer';
|
||||
removeCard.innerHTML = `
|
||||
<div style="font-size: 1.5rem; color: var(--accent-red);">❌</div>
|
||||
<div style="font-size: 0.85rem; font-weight: bold; margin-top: 0.35rem; color: var(--accent-red);">นำออกจากทีม</div>
|
||||
`;
|
||||
removeCard.addEventListener('click', async () => {
|
||||
await assignAvatarToLineup(state.selectedLineupSlot, 0);
|
||||
el.modalLineupPicker.classList.remove('active');
|
||||
});
|
||||
el.pickerAvatarsGrid.appendChild(removeCard);
|
||||
|
||||
// Available avatars
|
||||
state.avatarsList.forEach(item => {
|
||||
const meta = getAvatarInfo(item.avatar_id);
|
||||
if (search && !meta.name.toLowerCase().includes(search) && !String(item.avatar_id).includes(search)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const elemClass = getElementClass(meta.element);
|
||||
const card = document.createElement('div');
|
||||
card.className = `card avatar-card card-rarity-${meta.rarity}`;
|
||||
card.style.minHeight = 'auto';
|
||||
card.style.padding = '0.85rem';
|
||||
card.style.cursor = 'pointer';
|
||||
|
||||
card.innerHTML = `
|
||||
<div style="display: flex; align-items: center; gap: 0.65rem;">
|
||||
<div class="avatar-image-container" style="width: 44px; height: 44px;">
|
||||
<img class="avatar-img"
|
||||
src="./assets/avatars/${item.avatar_id}.png"
|
||||
onerror="if(this.src.indexOf('jsdelivr')===-1){this.src='https://cdn.jsdelivr.net/gh/Mar-7th/StarRailRes@master/icon/avatar/${item.avatar_id}.png';}else{this.style.display='none';}"
|
||||
alt="${meta.name}">
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-weight: 700; font-size: 0.9rem;">${meta.name}</div>
|
||||
<div style="font-size: 0.725rem; color: var(--text-muted);">Lv. ${item.level} • E${item.rank}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
card.addEventListener('click', async () => {
|
||||
await assignAvatarToLineup(state.selectedLineupSlot, item.avatar_id);
|
||||
el.modalLineupPicker.classList.remove('active');
|
||||
});
|
||||
|
||||
el.pickerAvatarsGrid.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
// Assign avatar to lineup slot
|
||||
async function assignAvatarToLineup(slot, avatarId) {
|
||||
try {
|
||||
if (!state.statusData || !state.statusData.lineups) return;
|
||||
const lineups = { ...state.statusData.lineups };
|
||||
lineups[slot] = avatarId;
|
||||
state.statusData.lineups = lineups;
|
||||
|
||||
if (state.serverMode === 'gac') {
|
||||
await apiCall('/api/persistent', 'POST', { lineups });
|
||||
} else {
|
||||
// In FreeSR standard mode
|
||||
const freesr = await apiCall('/api/freesr');
|
||||
if (freesr && freesr.data) {
|
||||
freesr.data.lineups = lineups;
|
||||
await apiCall('/api/freesr', 'POST', freesr.data);
|
||||
}
|
||||
}
|
||||
showToast(`อัปเดตทีม Slot ${slot + 1} เรียบร้อย`, 'success');
|
||||
renderLineup(lineups);
|
||||
} catch (err) {
|
||||
showToast(`เกิดข้อผิดพลาด: ${err.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,24 +486,27 @@ async function loadAvatars() {
|
||||
function renderAvatarsGrid() {
|
||||
const searchTerm = el.avatarSearch.value.trim().toLowerCase();
|
||||
const rarityFilter = el.avatarFilterRarity.value;
|
||||
const elementFilter = el.avatarFilterElement.value;
|
||||
|
||||
const filtered = state.avatarsList.filter(item => {
|
||||
const meta = getAvatarInfo(item.avatar_id);
|
||||
const matchesSearch = meta.name.toLowerCase().includes(searchTerm) || String(item.avatar_id).includes(searchTerm);
|
||||
const matchesRarity = rarityFilter === 'all' || String(meta.rarity) === rarityFilter;
|
||||
return matchesSearch && matchesRarity;
|
||||
const matchesElement = elementFilter === 'all' || meta.element === elementFilter;
|
||||
return matchesSearch && matchesRarity && matchesElement;
|
||||
});
|
||||
|
||||
if (filtered.length === 0) {
|
||||
el.avatarsGrid.innerHTML = '<div style="grid-column: 1/-1; text-align: center; color: var(--text-muted); padding: 2rem;">No matching avatars</div>';
|
||||
el.avatarsGrid.innerHTML = '<div style="grid-column: 1/-1; text-align: center; color: var(--text-muted); padding: 2rem;">ไม่พบตัวละครที่ตรงกับเงื่อนไข</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
el.avatarsGrid.innerHTML = '';
|
||||
filtered.forEach(item => {
|
||||
const meta = getAvatarInfo(item.avatar_id);
|
||||
const elemClass = getElementClass(meta.element);
|
||||
const card = document.createElement('div');
|
||||
card.className = 'card avatar-card';
|
||||
card.className = `card avatar-card card-rarity-${meta.rarity}`;
|
||||
|
||||
const pipsHtml = [0, 1, 2, 3, 4, 5, 6].map(rank => `
|
||||
<button class="eidolon-pip ${item.rank >= rank ? 'active' : ''}" data-avatar="${item.avatar_id}" data-rank="${rank}" title="Eidolon ${rank}">
|
||||
@@ -319,18 +528,16 @@ function renderAvatarsGrid() {
|
||||
<div>
|
||||
<div class="avatar-name">${meta.name}</div>
|
||||
<div class="avatar-subinfo">
|
||||
<span>Lv. ${item.level}</span>
|
||||
<span>•</span>
|
||||
<span>${meta.element}</span>
|
||||
<span>•</span>
|
||||
<span>${meta.path}</span>
|
||||
<span class="element-badge ${elemClass}">${meta.element}</span>
|
||||
<span class="path-badge">${meta.path}</span>
|
||||
<span style="font-weight: 700; color: var(--accent-cyan);">Lv. ${item.level}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="eidolon-selector">
|
||||
<span style="font-size: 0.75rem; color: var(--text-muted); font-weight: 600;">${t('eidolonLabel')}</span>
|
||||
<span style="font-size: 0.75rem; color: var(--text-muted); font-weight: 700;">${t('eidolonLabel')}</span>
|
||||
<div class="eidolon-buttons">
|
||||
${pipsHtml}
|
||||
</div>
|
||||
@@ -347,17 +554,121 @@ function renderAvatarsGrid() {
|
||||
});
|
||||
});
|
||||
|
||||
// Clicking card opens full character build modal!
|
||||
card.addEventListener('click', () => {
|
||||
openAvatarDetailModal(item);
|
||||
});
|
||||
|
||||
el.avatarsGrid.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
// Character Detail / Build Modal
|
||||
function openAvatarDetailModal(avatarItem) {
|
||||
state.selectedAvatarDetail = avatarItem;
|
||||
const meta = getAvatarInfo(avatarItem.avatar_id);
|
||||
const elemClass = getElementClass(meta.element);
|
||||
|
||||
el.modalCharTitle.textContent = `ปรับแต่งตัวละคร: ${meta.name} (ID: ${avatarItem.avatar_id})`;
|
||||
el.modalCharImg.src = `./assets/avatars/${avatarItem.avatar_id}.png`;
|
||||
el.modalCharImg.onerror = function() {
|
||||
this.src = `https://cdn.jsdelivr.net/gh/Mar-7th/StarRailRes@master/icon/avatar/${avatarItem.avatar_id}.png`;
|
||||
};
|
||||
el.modalCharName.textContent = meta.name;
|
||||
el.modalCharMeta.innerHTML = `
|
||||
<span class="element-badge ${elemClass}">${meta.element}</span>
|
||||
<span class="path-badge">${meta.path}</span>
|
||||
<span class="avatar-badge-rarity rarity-${meta.rarity}">★ ${meta.rarity}</span>
|
||||
`;
|
||||
|
||||
el.modalCharLevel.value = avatarItem.level || 80;
|
||||
el.modalCharPromotion.value = avatarItem.promotion || 6;
|
||||
|
||||
// Render modal eidolon pips
|
||||
el.modalCharEidolonBtns.innerHTML = '';
|
||||
for (let r = 0; r <= 6; r++) {
|
||||
const btn = document.createElement('button');
|
||||
btn.className = `eidolon-pip ${avatarItem.rank >= r ? 'active' : ''}`;
|
||||
btn.textContent = r;
|
||||
btn.style.width = '32px';
|
||||
btn.style.height = '32px';
|
||||
btn.style.fontSize = '0.9rem';
|
||||
btn.addEventListener('click', () => {
|
||||
avatarItem.rank = r;
|
||||
el.modalCharEidolonBtns.querySelectorAll('.eidolon-pip').forEach((b, idx) => {
|
||||
if (idx <= r) b.classList.add('active');
|
||||
else b.classList.remove('active');
|
||||
});
|
||||
});
|
||||
el.modalCharEidolonBtns.appendChild(btn);
|
||||
}
|
||||
|
||||
el.modalAvatarDetail.classList.add('active');
|
||||
}
|
||||
|
||||
// Save avatar details from modal
|
||||
async function saveAvatarDetails() {
|
||||
if (!state.selectedAvatarDetail) return;
|
||||
const aid = state.selectedAvatarDetail.avatar_id;
|
||||
const level = parseInt(el.modalCharLevel.value, 10) || 80;
|
||||
const promotion = parseInt(el.modalCharPromotion.value, 10) || 6;
|
||||
const rank = state.selectedAvatarDetail.rank ?? 6;
|
||||
|
||||
try {
|
||||
await apiCall('/api/freesr/avatars', 'POST', {
|
||||
avatar_id: aid,
|
||||
level,
|
||||
promotion,
|
||||
rank
|
||||
});
|
||||
showToast(`บันทึกตัวละคร ${aid} สำเร็จ (Lv. ${level}, Prom. ${promotion}, E${rank})`, 'success');
|
||||
el.modalAvatarDetail.classList.remove('active');
|
||||
|
||||
// Update local state and re-render
|
||||
const target = state.avatarsList.find(a => a.avatar_id === aid);
|
||||
if (target) {
|
||||
target.level = level;
|
||||
target.promotion = promotion;
|
||||
target.rank = rank;
|
||||
renderAvatarsGrid();
|
||||
}
|
||||
} catch (err) {
|
||||
showToast(`เกิดข้อผิดพลาด: ${err.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Max Skills & Traces for character
|
||||
async function maxSkillsForAvatar() {
|
||||
if (!state.selectedAvatarDetail) return;
|
||||
const aid = state.selectedAvatarDetail.avatar_id;
|
||||
try {
|
||||
// Read freesr data, max skill levels for this avatar, and push back
|
||||
const freesr = await apiCall('/api/freesr');
|
||||
if (freesr && freesr.data && freesr.data.avatars && freesr.data.avatars[aid]) {
|
||||
const av = freesr.data.avatars[aid];
|
||||
if (av.data && av.data.skills) {
|
||||
for (const k in av.data.skills) {
|
||||
av.data.skills[k] = 10;
|
||||
}
|
||||
}
|
||||
av.level = 80;
|
||||
av.promotion = 6;
|
||||
av.data.rank = 6;
|
||||
await apiCall('/api/freesr', 'POST', freesr.data);
|
||||
showToast(`อัพสกิลและ Traces ทั้งหมดของตัวละคร ${aid} เป็น 10/10 สำเร็จ!`, 'success');
|
||||
loadAvatars();
|
||||
}
|
||||
} catch (err) {
|
||||
showToast(`เกิดข้อผิดพลาด: ${err.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Set avatar rank API
|
||||
async function setAvatarRank(avatarId, rank) {
|
||||
try {
|
||||
await apiCall('/api/avatar/rank', 'POST', { avatar_id: avatarId, rank });
|
||||
showToast(t('avatarRankSuccess', { id: avatarId, rank }), 'success');
|
||||
|
||||
// Update local state and re-render
|
||||
const target = state.avatarsList.find(a => a.avatar_id === avatarId);
|
||||
if (target) {
|
||||
target.rank = rank;
|
||||
@@ -381,10 +692,94 @@ async function maxAllAvatars() {
|
||||
}
|
||||
}
|
||||
|
||||
// Load Equipment (Lightcones & Relics)
|
||||
async function loadEquipment() {
|
||||
try {
|
||||
const res = await apiCall('/api/freesr');
|
||||
if (!res || !res.data) return;
|
||||
state.fullFreesrData = res.data;
|
||||
|
||||
const lcs = res.data.lightcones || [];
|
||||
const relics = res.data.relics || [];
|
||||
|
||||
el.badgeTotalLc.textContent = `${lcs.length} ชิ้น`;
|
||||
el.badgeTotalRelics.textContent = `${relics.length} ชิ้น`;
|
||||
|
||||
// Render Lightcones
|
||||
el.lightconesList.innerHTML = '';
|
||||
lcs.slice(0, 100).forEach(lc => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'card';
|
||||
item.style.padding = '0.75rem';
|
||||
item.style.display = 'flex';
|
||||
item.style.justifyContent = 'space-between';
|
||||
item.style.alignItems = 'center';
|
||||
|
||||
const equipped = lc.equip_avatar ? getAvatarInfo(lc.equip_avatar).name : 'ไม่ได้สวมใส่';
|
||||
item.innerHTML = `
|
||||
<div>
|
||||
<div style="font-weight: 700; font-size: 0.95rem;">Lightcone ID: ${lc.item_id} (UID: ${lc.internal_uid})</div>
|
||||
<div style="font-size: 0.775rem; color: var(--text-muted); margin-top: 0.2rem;">
|
||||
Lv. ${lc.level} • ขัดเกลา S${lc.rank} • สวมใส่โดย: <strong style="color: var(--accent-cyan);">${equipped}</strong>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
el.lightconesList.appendChild(item);
|
||||
});
|
||||
|
||||
// Render Relics
|
||||
el.relicsList.innerHTML = '';
|
||||
relics.slice(0, 100).forEach(r => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'card';
|
||||
item.style.padding = '0.75rem';
|
||||
item.style.display = 'flex';
|
||||
item.style.justifyContent = 'space-between';
|
||||
item.style.alignItems = 'center';
|
||||
|
||||
const equipped = r.equip_avatar ? getAvatarInfo(r.equip_avatar).name : 'ไม่ได้สวมใส่';
|
||||
const slotNum = r.relic_id % 10;
|
||||
item.innerHTML = `
|
||||
<div>
|
||||
<div style="font-weight: 700; font-size: 0.95rem;">Relic ID: ${r.relic_id} (Slot: ${slotNum})</div>
|
||||
<div style="font-size: 0.775rem; color: var(--text-muted); margin-top: 0.2rem;">
|
||||
Lv. ${r.level} • สเตตัสหลัก: ${r.main_affix_id} • สวมใส่โดย: <strong style="color: var(--accent-gold);">${equipped}</strong>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
el.relicsList.appendChild(item);
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
showToast(`โหลดคลังอุปกรณ์ไม่สำเร็จ: ${err.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Load Persistent Save Settings
|
||||
async function loadPersistentSettings() {
|
||||
try {
|
||||
const res = await apiCall('/api/persistent');
|
||||
if (!res || !res.persistent) return;
|
||||
const p = res.persistent;
|
||||
state.persistentData = p;
|
||||
|
||||
if (p.main_character) {
|
||||
el.persistentMcSelect.value = p.main_character;
|
||||
}
|
||||
if (p.march_type) {
|
||||
el.persistentMarchSelect.value = p.march_type;
|
||||
}
|
||||
el.toggleSwGlobal.checked = !!p.enable_sw_global;
|
||||
el.toggleCastoriceGlobal.checked = !!p.enable_castorice_global;
|
||||
} catch (err) {
|
||||
console.warn('Persistent settings not available in current mode:', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Render Ex-Ranks
|
||||
function renderExRanks(exList) {
|
||||
if (!exList || exList.length === 0) {
|
||||
el.exrankList.innerHTML = '<div style="color: var(--text-muted); text-align: center; padding: 1rem;">No Ex-Rank Avatars</div>';
|
||||
el.exrankList.innerHTML = '<div style="color: var(--text-muted); text-align: center; padding: 1rem;">ไม่มีข้อมูลตัวละคร Ex-Rank</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -396,7 +791,7 @@ function renderExRanks(exList) {
|
||||
|
||||
card.innerHTML = `
|
||||
<div style="display: flex; gap: 0.85rem; align-items: center;">
|
||||
<div class="avatar-image-container" style="width: 44px; height: 44px;">
|
||||
<div class="avatar-image-container" style="width: 52px; height: 52px;">
|
||||
<img class="avatar-img"
|
||||
src="./assets/avatars/${item.avatar_id}.png"
|
||||
onerror="if(this.src.indexOf('jsdelivr')===-1){this.src='https://cdn.jsdelivr.net/gh/Mar-7th/StarRailRes@master/icon/avatar/${item.avatar_id}.png';}else{this.style.display='none';this.nextElementSibling.style.display='flex';}"
|
||||
@@ -404,7 +799,7 @@ function renderExRanks(exList) {
|
||||
<div class="avatar-icon-placeholder" style="display: none;">${localizedMeta.name.charAt(0)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-weight: 700; font-size: 1rem;">${localizedMeta.name}</div>
|
||||
<div style="font-weight: 800; font-size: 1.05rem;">${localizedMeta.name}</div>
|
||||
<div style="font-size: 0.8rem; color: var(--text-muted);">
|
||||
Avatar ID: ${item.avatar_id} • Ex-Rank Skill ID: ${item.ex_rank_id}
|
||||
</div>
|
||||
@@ -514,7 +909,7 @@ async function loadJsonData(mode) {
|
||||
async function saveJsonData() {
|
||||
const text = el.jsonEditor.value.trim();
|
||||
if (!text) {
|
||||
showToast('No JSON data to save', 'error');
|
||||
showToast('ไม่มีข้อมูล JSON ให้บันทึก', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -554,6 +949,10 @@ function setupTabs() {
|
||||
// On-demand tab refresh
|
||||
if (targetId === 'tab-avatars' && state.avatarsList.length === 0) {
|
||||
loadAvatars();
|
||||
} else if (targetId === 'tab-equipment') {
|
||||
loadEquipment();
|
||||
} else if (targetId === 'tab-persistent') {
|
||||
loadPersistentSettings();
|
||||
} else if (targetId === 'tab-json') {
|
||||
loadJsonData(state.currentJsonMode);
|
||||
}
|
||||
@@ -590,6 +989,15 @@ function setupEvents() {
|
||||
}
|
||||
});
|
||||
|
||||
// Character Detail Modal Controls
|
||||
el.btnCloseAvatarModal.addEventListener('click', () => el.modalAvatarDetail.classList.remove('active'));
|
||||
el.btnModalSaveAvatar.addEventListener('click', saveAvatarDetails);
|
||||
el.btnModalMaxSkills.addEventListener('click', maxSkillsForAvatar);
|
||||
|
||||
// Lineup Picker Modal Controls
|
||||
el.btnClosePickerModal.addEventListener('click', () => el.modalLineupPicker.classList.remove('active'));
|
||||
el.pickerSearch.addEventListener('input', renderPickerGrid);
|
||||
|
||||
// Quick Action Buttons
|
||||
el.btnQuickMaxAll.addEventListener('click', maxAllAvatars);
|
||||
el.btnMaxAllAvatars.addEventListener('click', maxAllAvatars);
|
||||
@@ -602,16 +1010,50 @@ function setupEvents() {
|
||||
el.btnQuickSyncPersistent.addEventListener('click', async () => {
|
||||
try {
|
||||
await apiCall('/api/persistent');
|
||||
showToast('Persistent data synced', 'success');
|
||||
showToast('ซิงค์ข้อมูล Persistent เรียบร้อย', 'success');
|
||||
refreshStatus();
|
||||
} catch (err) {
|
||||
showToast(`Sync failed: ${err.message}`, 'error');
|
||||
}
|
||||
});
|
||||
|
||||
// Avatar Search & Filter
|
||||
// Persistent Form Submit
|
||||
el.formPersistentChars.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const mc = el.persistentMcSelect.value;
|
||||
const march = el.persistentMarchSelect.value;
|
||||
try {
|
||||
await apiCall('/api/persistent', 'POST', {
|
||||
main_character: mc,
|
||||
march_type: march
|
||||
});
|
||||
showToast('บันทึกการตั้งค่าตัวละครสำเร็จ!', 'success');
|
||||
refreshStatus();
|
||||
} catch (err) {
|
||||
showToast(`เกิดข้อผิดพลาด: ${err.message}`, 'error');
|
||||
}
|
||||
});
|
||||
|
||||
// Persistent Toggles
|
||||
el.btnSavePersistentToggles.addEventListener('click', async () => {
|
||||
const sw = el.toggleSwGlobal.checked;
|
||||
const castorice = el.toggleCastoriceGlobal.checked;
|
||||
try {
|
||||
await apiCall('/api/persistent', 'POST', {
|
||||
enable_sw_global: sw,
|
||||
enable_castorice_global: castorice
|
||||
});
|
||||
showToast('บันทึกสวิตช์ Global เรียบร้อย!', 'success');
|
||||
refreshStatus();
|
||||
} catch (err) {
|
||||
showToast(`เกิดข้อผิดพลาด: ${err.message}`, 'error');
|
||||
}
|
||||
});
|
||||
|
||||
// Avatar Search & Filters
|
||||
el.avatarSearch.addEventListener('input', renderAvatarsGrid);
|
||||
el.avatarFilterRarity.addEventListener('change', renderAvatarsGrid);
|
||||
el.avatarFilterElement.addEventListener('change', renderAvatarsGrid);
|
||||
el.btnReloadAvatars.addEventListener('click', loadAvatars);
|
||||
|
||||
// Ex-Rank All Controls
|
||||
@@ -657,7 +1099,7 @@ function setupEvents() {
|
||||
try {
|
||||
const parsed = JSON.parse(el.jsonEditor.value);
|
||||
el.jsonEditor.value = JSON.stringify(parsed, null, 2);
|
||||
showToast('JSON beautified', 'info');
|
||||
showToast('จัดรูปแบบ JSON สวยงามแล้ว', 'info');
|
||||
} catch (err) {
|
||||
showToast(t('jsonInvalid'), 'error');
|
||||
}
|
||||
@@ -672,7 +1114,7 @@ function setupEvents() {
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.download = `${state.currentJsonMode}-${Date.now()}.json`;
|
||||
a.click();
|
||||
showToast(`Downloaded ${a.download}`, 'success');
|
||||
showToast(`ดาวน์โหลด ${a.download} เรียบร้อย`, 'success');
|
||||
});
|
||||
|
||||
el.jsonFileInput.addEventListener('change', (e) => {
|
||||
@@ -683,7 +1125,7 @@ function setupEvents() {
|
||||
try {
|
||||
const parsed = JSON.parse(event.target.result);
|
||||
el.jsonEditor.value = JSON.stringify(parsed, null, 2);
|
||||
showToast(`Loaded ${file.name}`, 'success');
|
||||
showToast(`โหลดเนื้อหาไฟล์ ${file.name} เรียบร้อย`, 'success');
|
||||
} catch (err) {
|
||||
showToast(t('jsonInvalid'), 'error');
|
||||
}
|
||||
@@ -728,11 +1170,13 @@ function startPingLoop() {
|
||||
|
||||
// Initialize Application
|
||||
function init() {
|
||||
setupServerModes();
|
||||
setupLanguageSelector();
|
||||
setupTabs();
|
||||
setupEvents();
|
||||
applyTranslations();
|
||||
refreshStatus();
|
||||
loadAvatars();
|
||||
startPingLoop();
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user