Replace emoji UI icons with a shared SVG icon system.
Add an icons sprite and helpers so nav, actions, and protocol cards use consistent stroke icons. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+131
-10
@@ -303,6 +303,27 @@ a:hover {
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.lang-code {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 2.2rem;
|
||||||
|
height: 1.6rem;
|
||||||
|
padding: 0 6px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: rgba(139, 92, 246, 0.12);
|
||||||
|
color: var(--accent-light);
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast .ui-icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* ===== Buttons ===== */
|
/* ===== Buttons ===== */
|
||||||
.btn {
|
.btn {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
@@ -391,6 +412,110 @@ a:hover {
|
|||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-icon .ui-icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm .ui-icon {
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== UI Icons (SVG sprite) ===== */
|
||||||
|
.ui-icon {
|
||||||
|
width: 1.15em;
|
||||||
|
height: 1.15em;
|
||||||
|
display: inline-block;
|
||||||
|
flex-shrink: 0;
|
||||||
|
vertical-align: -0.15em;
|
||||||
|
stroke: currentColor;
|
||||||
|
fill: none;
|
||||||
|
color: inherit;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-icon-lg {
|
||||||
|
width: 1.5rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-icon-xl {
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-icon-2xl {
|
||||||
|
width: 2.75rem;
|
||||||
|
height: 2.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link .ui-icon {
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link.active .ui-icon {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle .ui-icon {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title .icon {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: rgba(139, 92, 246, 0.12);
|
||||||
|
color: var(--accent-light);
|
||||||
|
font-size: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title .icon .ui-icon {
|
||||||
|
width: 1.1rem;
|
||||||
|
height: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-icon .ui-icon {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-icon .ui-icon,
|
||||||
|
.protocol-icon .ui-icon,
|
||||||
|
.promo-icon .ui-icon {
|
||||||
|
width: 1.35em;
|
||||||
|
height: 1.35em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state .empty-icon {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 4.5rem;
|
||||||
|
height: 4.5rem;
|
||||||
|
margin: 0 auto var(--space-lg);
|
||||||
|
border-radius: var(--radius-xl);
|
||||||
|
background: rgba(139, 92, 246, 0.08);
|
||||||
|
border: 1px solid rgba(139, 92, 246, 0.12);
|
||||||
|
color: var(--accent-light);
|
||||||
|
font-size: 0;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state .empty-icon .ui-icon {
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
.btn:disabled {
|
.btn:disabled {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
@@ -450,10 +575,6 @@ a:hover {
|
|||||||
gap: var(--space-md);
|
gap: var(--space-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-title .icon {
|
|
||||||
font-size: 1.3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===== Server Card ===== */
|
/* ===== Server Card ===== */
|
||||||
.server-grid {
|
.server-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -1190,6 +1311,12 @@ a:hover {
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.client-avatar .ui-icon {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
.client-name {
|
.client-name {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 0.92rem;
|
font-size: 0.92rem;
|
||||||
@@ -1396,12 +1523,6 @@ a:hover {
|
|||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-state .empty-icon {
|
|
||||||
font-size: 3.5rem;
|
|
||||||
margin-bottom: var(--space-lg);
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state .empty-title {
|
.empty-state .empty-title {
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<!-- Lucide-style stroke icons, 24�24 -->
|
||||||
|
<symbol id="server" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<rect x="2" y="3" width="20" height="7" rx="2"/><rect x="2" y="14" width="20" height="7" rx="2"/><line x1="6" y1="6.5" x2="6.01" y2="6.5"/><line x1="6" y1="17.5" x2="6.01" y2="17.5"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="users" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="user" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="settings" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="plus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M5 12h14"/><path d="M12 5v14"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="pencil" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/><path d="m15 5 4 4"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="trash" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="copy" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<rect width="14" height="14" x="8" y="8" rx="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="file" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="play" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<polygon points="6 3 20 12 6 21 6 3"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="pause" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<rect x="6" y="4" width="4" height="16" rx="1"/><rect x="14" y="4" width="4" height="16" rx="1"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="globe" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<circle cx="12" cy="12" r="10"/><path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"/><path d="M2 12h20"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="shield" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M20 13c0 5-3.5 7.5-8 10-4.5-2.5-8-5-8-10V6l8-3 8 3Z"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="shield-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M20 13c0 5-3.5 7.5-8 10-4.5-2.5-8-5-8-10V6l8-3 8 3Z"/><path d="m9 12 2 2 4-4"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="zap" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="sparkles" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="radio" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M4.9 19.1C1 15.2 1 8.8 4.9 4.9"/><path d="M7.8 16.2c-2.3-2.3-2.3-6.1 0-8.5"/><circle cx="12" cy="12" r="2"/><path d="M16.2 7.8c2.3 2.3 2.3 6.1 0 8.5"/><path d="M19.1 4.9C23 8.8 23 15.1 19.1 19"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="lock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<rect width="18" height="11" x="3" y="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="plane" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M17.8 19.2 16 11l3.5-3.5C21 6 21.5 4 21 3c-1-.5-3 0-4.5 1.5L13 8 4.8 6.2c-.5-.1-.9.1-1.1.5l-.3.5c-.2.5-.1 1 .3 1.3L9 12l-2 3H4l-1 1 3 2 2 3 1-1v-3l3-2 3.5 5.3c.3.4.8.5 1.3.3l.5-.2c.4-.3.6-.7.5-1.2z"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="network" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<rect x="16" y="16" width="6" height="6" rx="1"/><rect x="2" y="16" width="6" height="6" rx="1"/><rect x="9" y="2" width="6" height="6" rx="1"/><path d="M5 16v-3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v3"/><path d="M12 12V8"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="wrench" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="package" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M11 21.73a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73z"/><path d="M12 22V12"/><path d="m3.3 7 8.7 5 8.7-5"/><path d="m7.5 4.2 9 5.2"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="cart" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<circle cx="8" cy="21" r="1"/><circle cx="19" cy="21" r="1"/><path d="M2.05 2.05h2l2.66 12.42a2 2 0 0 0 2 1.58h9.78a2 2 0 0 0 1.95-1.57l1.65-7.43H5.12"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="alert" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"/><path d="M12 9v4"/><path d="M12 17h.01"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M20 6 9 17l-5-5"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M18 6 6 18"/><path d="m6 6 12 12"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="info" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="share" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="key" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="m15.5 7.5 2.3 2.3a1 1 0 0 0 1.4 0l2.1-2.1a1 1 0 0 0 0-1.4L19 4"/><path d="m21 2-9.6 9.6"/><circle cx="7.5" cy="15.5" r="5.5"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="refresh" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/><path d="M8 16H3v5"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="brain" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M12 5a3 3 0 1 0-5.997.125 4 4 0 0 0-2.526 5.77 4 4 0 0 0 .556 6.588A4 4 0 1 0 12 18Z"/><path d="M12 5a3 3 0 1 1 5.997.125 4 4 0 0 1 2.526 5.77 4 4 0 0 1-.556 6.588A4 4 0 1 1 12 18Z"/><path d="M15 13a4.5 4.5 0 0 1-3-4 4.5 4.5 0 0 1-3 4"/><path d="M17.599 6.5a3 3 0 0 0 .399-1.375"/><path d="M6.003 5.125A3 3 0 0 0 6.401 6.5"/><path d="M3.077 10.877a4 4 0 0 0-.71.822"/><path d="M19.62 10.504a4 4 0 0 1 .71.822"/><path d="M9 20a1 1 0 0 0 6 0"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="layers" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="m12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83Z"/><path d="m22 17.65-9.17 4.16a2 2 0 0 1-1.66 0L2 17.65"/><path d="m22 12.65-9.17 4.16a2 2 0 0 1-1.66 0L2 12.65"/>
|
||||||
|
</symbol>
|
||||||
|
</svg>
|
||||||
+32
-28
@@ -28,12 +28,13 @@
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
{% from "macros/icons.html" import icon %}
|
||||||
<div class="toast-container" id="toastContainer"></div>
|
<div class="toast-container" id="toastContainer"></div>
|
||||||
|
|
||||||
<div class="app-container">
|
<div class="app-container">
|
||||||
<header class="app-header">
|
<header class="app-header">
|
||||||
<a href="/" class="app-logo" style="text-decoration:none">
|
<a href="/" class="app-logo" style="text-decoration:none">
|
||||||
<div class="logo-icon">{{ site_settings.logo }}</div>
|
<div class="logo-icon">{% if site_settings.logo and site_settings.logo|length <= 3 and not site_settings.logo.startswith('<') %}{{ site_settings.logo }}{% else %}{{ icon('shield') }}{% endif %}</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="logo-text">{{ site_settings.title }}</div>
|
<div class="logo-text">{{ site_settings.title }}</div>
|
||||||
<div class="logo-subtitle">{{ site_settings.subtitle }}</div>
|
<div class="logo-subtitle">{{ site_settings.subtitle }}</div>
|
||||||
@@ -43,24 +44,23 @@
|
|||||||
{% if current_user %}
|
{% if current_user %}
|
||||||
<nav class="header-nav">
|
<nav class="header-nav">
|
||||||
{% if current_user.role in ['admin', 'support'] %}
|
{% if current_user.role in ['admin', 'support'] %}
|
||||||
<a href="/" class="nav-link">{{ _('nav_servers') }}</a>
|
<a href="/" class="nav-link">{{ icon('server') }} {{ _('nav_servers') }}</a>
|
||||||
<a href="/users" class="nav-link">{{ _('nav_users') }}</a>
|
<a href="/users" class="nav-link">{{ icon('users') }} {{ _('nav_users') }}</a>
|
||||||
<a href="/invites" class="nav-link">{{ _('nav_invites') }}</a>
|
<a href="/invites" class="nav-link">{{ icon('link') }} {{ _('nav_invites') }}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if current_user.role == 'admin' %}
|
{% if current_user.role == 'admin' %}
|
||||||
<a href="/settings" class="nav-link">{{ _('nav_settings') }}</a>
|
<a href="/settings" class="nav-link">{{ icon('settings') }} {{ _('nav_settings') }}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="/my" class="nav-link">{{ _('nav_connections') }}</a>
|
<a href="/my" class="nav-link">{{ icon('layers') }} {{ _('nav_connections') }}</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="nav-user">
|
<div class="nav-user">
|
||||||
<button class="theme-toggle" onclick="openModal('langModal')" title="{{ _('select_lang') }}"
|
<button class="theme-toggle" onclick="openModal('langModal')" title="{{ _('select_lang') }}"
|
||||||
style="margin-right: var(--space-sm);">
|
style="margin-right: var(--space-sm);" type="button">
|
||||||
{% if lang == 'ru' %}🇷🇺{% elif lang == 'fr' %}🇫🇷{% elif lang == 'zh' %}🇨🇳{% elif lang == 'fa'
|
{{ icon('globe') }}
|
||||||
%}🇮🇷{% else %}🇺🇸{% endif %}
|
|
||||||
</button>
|
</button>
|
||||||
<button class="theme-toggle" onclick="toggleTheme()" title="{{ _('toggle_theme') }}"
|
<button class="theme-toggle" onclick="toggleTheme()" title="{{ _('toggle_theme') }}"
|
||||||
id="themeToggle">🌙</button>
|
id="themeToggle" type="button">{{ icon('moon') }}</button>
|
||||||
<span class="nav-username">{{ current_user.username }}</span>
|
<span class="nav-username">{{ current_user.username }}</span>
|
||||||
<span
|
<span
|
||||||
class="badge badge-{{ 'success' if current_user.role == 'admin' else ('info' if current_user.role == 'support' else 'warn') }}"
|
class="badge badge-{{ 'success' if current_user.role == 'admin' else ('info' if current_user.role == 'support' else 'warn') }}"
|
||||||
@@ -72,12 +72,11 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
<div class="nav-user">
|
<div class="nav-user">
|
||||||
<button class="theme-toggle" onclick="openModal('langModal')" title="{{ _('select_lang') }}"
|
<button class="theme-toggle" onclick="openModal('langModal')" title="{{ _('select_lang') }}"
|
||||||
style="margin-right: var(--space-sm);">
|
style="margin-right: var(--space-sm);" type="button">
|
||||||
{% if lang == 'ru' %}🇷🇺{% elif lang == 'fr' %}🇫🇷{% elif lang == 'zh' %}🇨🇳{% elif lang == 'fa'
|
{{ icon('globe') }}
|
||||||
%}🇮🇷{% else %}🇺🇸{% endif %}
|
|
||||||
</button>
|
</button>
|
||||||
<button class="theme-toggle" onclick="toggleTheme()" title="{{ _('toggle_theme') }}"
|
<button class="theme-toggle" onclick="toggleTheme()" title="{{ _('toggle_theme') }}"
|
||||||
id="themeToggle">🌙</button>
|
id="themeToggle" type="button">{{ icon('moon') }}</button>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</header>
|
</header>
|
||||||
@@ -96,19 +95,19 @@
|
|||||||
</div>
|
</div>
|
||||||
<div style="display: flex; flex-direction: column; gap: var(--space-sm);">
|
<div style="display: flex; flex-direction: column; gap: var(--space-sm);">
|
||||||
{% set langs = [
|
{% set langs = [
|
||||||
('en', '🇺🇸', 'lang_en'),
|
('en', 'EN', 'lang_en'),
|
||||||
('ru', '🇷🇺', 'lang_ru'),
|
('ru', 'RU', 'lang_ru'),
|
||||||
('fr', '🇫🇷', 'lang_fr'),
|
('fr', 'FR', 'lang_fr'),
|
||||||
('zh', '🇨🇳', 'lang_zh'),
|
('zh', 'ZH', 'lang_zh'),
|
||||||
('fa', '🇮🇷', 'lang_fa')
|
('fa', 'FA', 'lang_fa')
|
||||||
] %}
|
] %}
|
||||||
{% for l_code, l_flag, l_key in langs %}
|
{% for l_code, l_flag, l_key in langs %}
|
||||||
<a href="/set_lang/{{ l_code }}" class="btn btn-secondary"
|
<a href="/set_lang/{{ l_code }}" class="btn btn-secondary"
|
||||||
style="justify-content: flex-start; gap: var(--space-md); padding: var(--space-md);">
|
style="justify-content: flex-start; gap: var(--space-md); padding: var(--space-md);">
|
||||||
<span style="font-size: 1.4rem;">{{ l_flag }}</span>
|
<span class="lang-code">{{ l_flag }}</span>
|
||||||
<span style="font-weight: 500;">{{ _(l_key) }}</span>
|
<span style="font-weight: 500;">{{ _(l_key) }}</span>
|
||||||
{% if lang == l_code %}
|
{% if lang == l_code %}
|
||||||
<span class="lang-check">✓</span>
|
<span class="lang-check">{{ icon('check') }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</a>
|
</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -120,6 +119,12 @@
|
|||||||
window.I18N = {{ translations_json | safe }};
|
window.I18N = {{ translations_json | safe }};
|
||||||
window._ = function (key) { return window.I18N[key] || key; };
|
window._ = function (key) { return window.I18N[key] || key; };
|
||||||
|
|
||||||
|
/* ===== SVG icons (same sprite as templates) ===== */
|
||||||
|
window.uiIcon = function (name, className) {
|
||||||
|
const cls = className ? `ui-icon ${className}` : 'ui-icon';
|
||||||
|
return `<svg class="${cls}" aria-hidden="true" focusable="false"><use href="/static/icons.svg#${name}"></use></svg>`;
|
||||||
|
};
|
||||||
|
|
||||||
/* ===== Theme Toggle ===== */
|
/* ===== Theme Toggle ===== */
|
||||||
(function () {
|
(function () {
|
||||||
const saved = localStorage.getItem('theme') || 'dark';
|
const saved = localStorage.getItem('theme') || 'dark';
|
||||||
@@ -128,10 +133,9 @@
|
|||||||
const updateUI = () => {
|
const updateUI = () => {
|
||||||
const isLight = document.documentElement.getAttribute('data-theme') === 'light';
|
const isLight = document.documentElement.getAttribute('data-theme') === 'light';
|
||||||
document.querySelectorAll('.theme-toggle').forEach(btn => {
|
document.querySelectorAll('.theme-toggle').forEach(btn => {
|
||||||
const isLang = btn.getAttribute('onclick').includes('langModal');
|
const onclick = btn.getAttribute('onclick') || '';
|
||||||
if (!isLang) {
|
if (onclick.includes('langModal')) return;
|
||||||
btn.textContent = isLight ? '☀️' : '🌙';
|
btn.innerHTML = uiIcon(isLight ? 'sun' : 'moon');
|
||||||
}
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -166,8 +170,8 @@
|
|||||||
const toast = document.createElement('div');
|
const toast = document.createElement('div');
|
||||||
toast.className = `toast toast-${type}`;
|
toast.className = `toast toast-${type}`;
|
||||||
|
|
||||||
const icons = { success: '✓', error: '✕', info: 'ℹ' };
|
const icons = { success: 'check', error: 'x', info: 'info' };
|
||||||
toast.innerHTML = `<span>${icons[type] || 'ℹ'}</span> <span>${message}</span>`;
|
toast.innerHTML = `${uiIcon(icons[type] || 'info')} <span>${message}</span>`;
|
||||||
|
|
||||||
container.appendChild(toast);
|
container.appendChild(toast);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -273,4 +277,4 @@
|
|||||||
{% block scripts %}{% endblock %}
|
{% block scripts %}{% endblock %}
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
{% from "macros/icons.html" import icon %}
|
||||||
|
|
||||||
{% block title_extra %} — {{ _('guest_title') }}{% endblock %}
|
{% block title_extra %} — {{ _('guest_title') }}{% endblock %}
|
||||||
|
|
||||||
@@ -12,7 +13,7 @@
|
|||||||
|
|
||||||
{% if need_password %}
|
{% if need_password %}
|
||||||
<div style="padding: var(--space-lg); text-align: center;">
|
<div style="padding: var(--space-lg); text-align: center;">
|
||||||
<div class="logo-icon" style="font-size: 3rem; margin-bottom: var(--space-md);">🔐</div>
|
<div class="logo-icon" style="width:64px;height:64px;margin:0 auto var(--space-md);">{{ icon('lock') }}</div>
|
||||||
<p style="margin-bottom: var(--space-md);">{{ _('guest_protected_desc') }}</p>
|
<p style="margin-bottom: var(--space-md);">{{ _('guest_protected_desc') }}</p>
|
||||||
|
|
||||||
<form id="authForm" onsubmit="authGuest(event)"
|
<form id="authForm" onsubmit="authGuest(event)"
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
{% from "macros/icons.html" import icon %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<section>
|
<section>
|
||||||
<div class="flex items-center justify-between" style="margin-bottom: var(--space-lg);">
|
<div class="flex items-center justify-between" style="margin-bottom: var(--space-lg);">
|
||||||
<h1 class="section-title" style="margin-bottom:0;">
|
<h1 class="section-title" style="margin-bottom:0;">
|
||||||
<span class="icon">🖥</span>
|
<span class="icon">{{ icon('server') }}</span>
|
||||||
{{ _('nav_servers') }}
|
{{ _('nav_servers') }}
|
||||||
</h1>
|
</h1>
|
||||||
<button class="btn btn-primary" onclick="openModal('addServerModal')">
|
<button class="btn btn-primary" onclick="openModal('addServerModal')">
|
||||||
<span>+</span> {{ _('add_server') }}
|
{{ icon('plus') }} {{ _('add_server') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -20,7 +21,7 @@
|
|||||||
data-username="{{ server.username }}" data-auth="{{ 'key' if server.private_key else 'password' }}"
|
data-username="{{ server.username }}" data-auth="{{ 'key' if server.private_key else 'password' }}"
|
||||||
draggable="true">
|
draggable="true">
|
||||||
<div class="server-meta">
|
<div class="server-meta">
|
||||||
<div class="server-icon">🖥</div>
|
<div class="server-icon">{{ icon('server') }}</div>
|
||||||
<div style="min-width: 0; flex: 1;">
|
<div style="min-width: 0; flex: 1;">
|
||||||
<div class="server-name">
|
<div class="server-name">
|
||||||
<span class="ping-dot ping-pending" id="ping-{{ loop.index0 }}"
|
<span class="ping-dot ping-pending" id="ping-{{ loop.index0 }}"
|
||||||
@@ -57,11 +58,11 @@
|
|||||||
</a>
|
</a>
|
||||||
<button class="btn btn-secondary btn-sm btn-icon" onclick="openEditServer(this)"
|
<button class="btn btn-secondary btn-sm btn-icon" onclick="openEditServer(this)"
|
||||||
title="{{ _('edit') }}">
|
title="{{ _('edit') }}">
|
||||||
✏️
|
{{ icon('pencil') }}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-danger btn-sm btn-icon" onclick="deleteServer({{ loop.index0 }})"
|
<button class="btn btn-danger btn-sm btn-icon" onclick="deleteServer({{ loop.index0 }})"
|
||||||
title="{{ _('delete') }}">
|
title="{{ _('delete') }}">
|
||||||
🗑
|
{{ icon('trash') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -69,13 +70,13 @@
|
|||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<div class="empty-icon">🛡</div>
|
<div class="empty-icon">{{ icon('shield') }}</div>
|
||||||
<div class="empty-title">{{ _('no_servers') }}</div>
|
<div class="empty-title">{{ _('no_servers') }}</div>
|
||||||
<div class="empty-desc">
|
<div class="empty-desc">
|
||||||
{{ _('add_server_desc') }}
|
{{ _('add_server_desc') }}
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-primary" onclick="openModal('addServerModal')">
|
<button class="btn btn-primary" onclick="openModal('addServerModal')">
|
||||||
<span>+</span> {{ _('add_server') }}
|
{{ icon('plus') }} {{ _('add_server') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
{% from "macros/icons.html" import icon %}
|
||||||
|
|
||||||
{% block title_extra %} — {{ _('invites_title') }}{% endblock %}
|
{% block title_extra %} — {{ _('invites_title') }}{% endblock %}
|
||||||
|
|
||||||
@@ -7,18 +8,18 @@
|
|||||||
<div style="display:flex; align-items:flex-end; justify-content:space-between; gap:var(--space-md); flex-wrap:wrap; margin-bottom:var(--space-lg);">
|
<div style="display:flex; align-items:flex-end; justify-content:space-between; gap:var(--space-md); flex-wrap:wrap; margin-bottom:var(--space-lg);">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="section-title" style="margin-bottom:var(--space-xs);">
|
<h1 class="section-title" style="margin-bottom:var(--space-xs);">
|
||||||
<span class="icon">🔗</span>
|
<span class="icon">{{ icon('link') }}</span>
|
||||||
{{ _('invites_title') }}
|
{{ _('invites_title') }}
|
||||||
</h1>
|
</h1>
|
||||||
<p style="color:var(--text-muted); margin:0; max-width:42rem; line-height:1.45;">{{ _('invites_hint') }}</p>
|
<p style="color:var(--text-muted); margin:0; max-width:42rem; line-height:1.45;">{{ _('invites_hint') }}</p>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-primary" onclick="openCreateInvite()">
|
<button class="btn btn-primary" onclick="openCreateInvite()">
|
||||||
<span>+</span> {{ _('invite_create') }}
|
{{ icon('plus') }} {{ _('invite_create') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="invitesEmpty" class="empty-state {% if invites %}hidden{% endif %}">
|
<div id="invitesEmpty" class="empty-state {% if invites %}hidden{% endif %}">
|
||||||
<div class="empty-icon">🔗</div>
|
<div class="empty-icon">{{ icon('link') }}</div>
|
||||||
<div class="empty-title">{{ _('invites_empty') }}</div>
|
<div class="empty-title">{{ _('invites_empty') }}</div>
|
||||||
<div class="empty-desc">{{ _('invites_empty_desc') }}</div>
|
<div class="empty-desc">{{ _('invites_empty_desc') }}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -58,12 +59,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display:flex; gap:var(--space-xs); flex-wrap:wrap;">
|
<div style="display:flex; gap:var(--space-xs); flex-wrap:wrap;">
|
||||||
<button class="btn btn-primary btn-sm" onclick="copyInviteUrl('{{ inv.token }}')">📋 {{ _('copy') }}</button>
|
<button class="btn btn-primary btn-sm" onclick="copyInviteUrl('{{ inv.token }}')">{{ icon('copy') }} {{ _('copy') }}</button>
|
||||||
<button class="btn btn-secondary btn-sm" onclick="editInvite('{{ inv.id }}')">✏️</button>
|
<button class="btn btn-secondary btn-sm btn-icon" onclick="editInvite('{{ inv.id }}')" title="{{ _('edit') }}">{{ icon('pencil') }}</button>
|
||||||
<button class="btn btn-secondary btn-sm" onclick="toggleInvite('{{ inv.id }}', {{ 'false' if inv.enabled else 'true' }})">
|
<button class="btn btn-secondary btn-sm btn-icon" onclick="toggleInvite('{{ inv.id }}', {{ 'false' if inv.enabled else 'true' }})" title="{% if inv.enabled %}{{ _('disabled') }}{% else %}{{ _('invite_active') }}{% endif %}">
|
||||||
{% if inv.enabled %}⏸{% else %}▶️{% endif %}
|
{% if inv.enabled %}{{ icon('pause') }}{% else %}{{ icon('play') }}{% endif %}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-danger btn-sm" onclick="deleteInvite('{{ inv.id }}')">🗑</button>
|
<button class="btn btn-danger btn-sm btn-icon" onclick="deleteInvite('{{ inv.id }}')" title="{{ _('delete') }}">{{ icon('trash') }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
+19
-17
@@ -76,20 +76,20 @@
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
{% from "macros/icons.html" import icon %}
|
||||||
<button class="theme-toggle" onclick="openModal('langModal')" id="langToggle"
|
<button class="theme-toggle" onclick="openModal('langModal')" id="langToggle"
|
||||||
style="position:fixed; top:20px; {% if lang == 'fa' %}left:65px;{% else %}right:65px;{% endif %} z-index:10;"
|
style="position:fixed; top:20px; {% if lang == 'fa' %}left:65px;{% else %}right:65px;{% endif %} z-index:10;"
|
||||||
title="{{ _('select_lang') }}">
|
title="{{ _('select_lang') }}" type="button">
|
||||||
{% if lang == 'ru' %}🇷🇺{% elif lang == 'fr' %}🇫🇷{% elif lang == 'zh' %}🇨🇳{% elif lang == 'fa' %}🇮🇷{%
|
{{ icon('globe') }}
|
||||||
else %}🇺🇸{% endif %}
|
|
||||||
</button>
|
</button>
|
||||||
<button class="theme-toggle" onclick="toggleTheme()" id="themeToggle"
|
<button class="theme-toggle" onclick="toggleTheme()" id="themeToggle"
|
||||||
style="position:fixed; top:20px; {% if lang == 'fa' %}left:20px;{% else %}right:20px;{% endif %} z-index:10;"
|
style="position:fixed; top:20px; {% if lang == 'fa' %}left:20px;{% else %}right:20px;{% endif %} z-index:10;"
|
||||||
title="{{ _('toggle_theme') }}">🌙</button>
|
title="{{ _('toggle_theme') }}" type="button">{{ icon('moon') }}</button>
|
||||||
|
|
||||||
<div class="login-wrapper">
|
<div class="login-wrapper">
|
||||||
<div class="login-card">
|
<div class="login-card">
|
||||||
<div class="login-logo">
|
<div class="login-logo">
|
||||||
<div class="logo-icon">{{ site_settings.logo or '🛡' }}</div>
|
<div class="logo-icon">{{ icon('shield') }}</div>
|
||||||
<div class="logo-text">{{ site_settings.title or 'Amnezia Panel' }}</div>
|
<div class="logo-text">{{ site_settings.title or 'Amnezia Panel' }}</div>
|
||||||
<div class="logo-subtitle">{{ site_settings.subtitle or 'Web Panel' }}</div>
|
<div class="logo-subtitle">{{ site_settings.subtitle or 'Web Panel' }}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -153,19 +153,19 @@
|
|||||||
</div>
|
</div>
|
||||||
<div style="display: flex; flex-direction: column; gap: var(--space-sm);">
|
<div style="display: flex; flex-direction: column; gap: var(--space-sm);">
|
||||||
{% set langs = [
|
{% set langs = [
|
||||||
('en', '🇺🇸', 'lang_en'),
|
('en', 'EN', 'lang_en'),
|
||||||
('ru', '🇷🇺', 'lang_ru'),
|
('ru', 'RU', 'lang_ru'),
|
||||||
('fr', '🇫🇷', 'lang_fr'),
|
('fr', 'FR', 'lang_fr'),
|
||||||
('zh', '🇨🇳', 'lang_zh'),
|
('zh', 'ZH', 'lang_zh'),
|
||||||
('fa', '🇮🇷', 'lang_fa')
|
('fa', 'FA', 'lang_fa')
|
||||||
] %}
|
] %}
|
||||||
{% for l_code, l_flag, l_key in langs %}
|
{% for l_code, l_flag, l_key in langs %}
|
||||||
<a href="/set_lang/{{ l_code }}" class="btn btn-secondary"
|
<a href="/set_lang/{{ l_code }}" class="btn btn-secondary"
|
||||||
style="justify-content: flex-start; gap: var(--space-md); padding: var(--space-md);">
|
style="justify-content: flex-start; gap: var(--space-md); padding: var(--space-md);">
|
||||||
<span style="font-size: 1.4rem;">{{ l_flag }}</span>
|
<span class="lang-code">{{ l_flag }}</span>
|
||||||
<span style="font-weight: 500;">{{ _(l_key) }}</span>
|
<span style="font-weight: 500;">{{ _(l_key) }}</span>
|
||||||
{% if lang == l_code %}
|
{% if lang == l_code %}
|
||||||
<span class="lang-check">✓</span>
|
<span class="lang-check">{{ icon('check') }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</a>
|
</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -177,6 +177,11 @@
|
|||||||
window.I18N = {{ translations_json | safe }};
|
window.I18N = {{ translations_json | safe }};
|
||||||
window._ = function (key) { return window.I18N[key] || key; };
|
window._ = function (key) { return window.I18N[key] || key; };
|
||||||
|
|
||||||
|
window.uiIcon = function (name, className) {
|
||||||
|
const cls = className ? `ui-icon ${className}` : 'ui-icon';
|
||||||
|
return `<svg class="${cls}" aria-hidden="true" focusable="false"><use href="/static/icons.svg#${name}"></use></svg>`;
|
||||||
|
};
|
||||||
|
|
||||||
/* Theme toggle */
|
/* Theme toggle */
|
||||||
(function () {
|
(function () {
|
||||||
const saved = localStorage.getItem('theme') || 'dark';
|
const saved = localStorage.getItem('theme') || 'dark';
|
||||||
@@ -184,11 +189,8 @@
|
|||||||
|
|
||||||
const updateUI = () => {
|
const updateUI = () => {
|
||||||
const isLight = document.documentElement.getAttribute('data-theme') === 'light';
|
const isLight = document.documentElement.getAttribute('data-theme') === 'light';
|
||||||
document.querySelectorAll('.theme-toggle').forEach(btn => {
|
const btn = document.getElementById('themeToggle');
|
||||||
if (btn.id === 'themeToggle') {
|
if (btn) btn.innerHTML = uiIcon(isLight ? 'sun' : 'moon');
|
||||||
btn.textContent = isLight ? '☀️' : '🌙';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (document.readyState === 'loading') {
|
if (document.readyState === 'loading') {
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{# SVG icon via sprite — usage: {% from "macros/icons.html" import icon %} {{ icon('server') }} #}
|
||||||
|
{% macro icon(name, class='', size=None) -%}
|
||||||
|
<svg class="ui-icon{% if class %} {{ class }}{% endif %}"{% if size %} style="width:{{ size }};height:{{ size }};"{% endif %} aria-hidden="true" focusable="false"><use href="/static/icons.svg#{{ name }}"></use></svg>
|
||||||
|
{%- endmacro %}
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
{% from "macros/icons.html" import icon %}
|
||||||
|
|
||||||
{% block title_extra %} — {{ _('my_connections_title') }}{% endblock %}
|
{% block title_extra %} — {{ _('my_connections_title') }}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<section>
|
<section>
|
||||||
<h1 class="section-title">
|
<h1 class="section-title">
|
||||||
<span class="icon">🔗</span>
|
<span class="icon">{{ icon('layers') }}</span>
|
||||||
{{ _('my_connections_title') }}
|
{{ _('my_connections_title') }}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
@@ -14,11 +15,11 @@
|
|||||||
{% for c in connections %}
|
{% for c in connections %}
|
||||||
<div class="client-item">
|
<div class="client-item">
|
||||||
<div class="client-info">
|
<div class="client-info">
|
||||||
<div class="client-avatar">🔗</div>
|
<div class="client-avatar">{{ icon('link') }}</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="client-name">{{ c.name or 'VPN Connection' }}</div>
|
<div class="client-name">{{ c.name or 'VPN Connection' }}</div>
|
||||||
<div class="client-meta">
|
<div class="client-meta">
|
||||||
<span>🖥 {{ c.server_name }}</span>
|
<span style="display:inline-flex;align-items:center;gap:4px;">{{ icon('server') }} {{ c.server_name }}</span>
|
||||||
<span class="badge badge-info" style="font-size:0.65rem;">
|
<span class="badge badge-info" style="font-size:0.65rem;">
|
||||||
{{ 'AmneziaWG' if c.protocol == 'awg' else ('AmneziaWG 2.0' if c.protocol == 'awg2' else ('AWG Legacy' if c.protocol == 'awg_legacy' else
|
{{ 'AmneziaWG' if c.protocol == 'awg' else ('AmneziaWG 2.0' if c.protocol == 'awg2' else ('AWG Legacy' if c.protocol == 'awg_legacy' else
|
||||||
('Xray' if c.protocol == 'xray' else c.protocol | upper))) }}
|
('Xray' if c.protocol == 'xray' else c.protocol | upper))) }}
|
||||||
@@ -40,7 +41,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<div class="empty-icon">🔗</div>
|
<div class="empty-icon">{{ icon('link') }}</div>
|
||||||
<div class="empty-title">{{ _('no_connections') }}</div>
|
<div class="empty-title">{{ _('no_connections') }}</div>
|
||||||
<div class="empty-desc">{{ _('no_connections_user_desc') }}</div>
|
<div class="empty-desc">{{ _('no_connections_user_desc') }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+55
-54
@@ -1,4 +1,5 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
{% from "macros/icons.html" import icon %}
|
||||||
|
|
||||||
{% block title_extra %} — {{ server.name }}{% endblock %}
|
{% block title_extra %} — {{ server.name }}{% endblock %}
|
||||||
|
|
||||||
@@ -125,7 +126,7 @@
|
|||||||
<div class="card" style="margin-bottom: var(--space-xl);">
|
<div class="card" style="margin-bottom: var(--space-xl);">
|
||||||
<div class="flex items-center justify-between" style="flex-wrap: wrap; gap: var(--space-md);">
|
<div class="flex items-center justify-between" style="flex-wrap: wrap; gap: var(--space-md);">
|
||||||
<div class="flex items-center gap-md">
|
<div class="flex items-center gap-md">
|
||||||
<div class="server-icon" style="width:56px; height:56px; font-size:1.6rem;">🖥</div>
|
<div class="server-icon" style="width:56px; height:56px; font-size:1.6rem;">{{ icon('server') }}</div>
|
||||||
<div>
|
<div>
|
||||||
<h1 style="font-size:1.4rem; font-weight:700;">{{ server.name }}</h1>
|
<h1 style="font-size:1.4rem; font-weight:700;">{{ server.name }}</h1>
|
||||||
<div class="font-mono text-muted text-sm">{{ server.host }}:{{ server.ssh_port }} • {{ server.username
|
<div class="font-mono text-muted text-sm">{{ server.host }}:{{ server.ssh_port }} • {{ server.username
|
||||||
@@ -146,10 +147,10 @@
|
|||||||
<div class="flex gap-md" style="flex-wrap:wrap" id="statusBadges"></div>
|
<div class="flex gap-md" style="flex-wrap:wrap" id="statusBadges"></div>
|
||||||
<div class="server-status-actions">
|
<div class="server-status-actions">
|
||||||
<button class="btn btn-primary btn-sm" onclick="openMarketplaceModal()" id="marketplaceBtn">
|
<button class="btn btn-primary btn-sm" onclick="openMarketplaceModal()" id="marketplaceBtn">
|
||||||
<span>🛒</span> {{ _('marketplace') }}
|
<span>{{ icon('cart') }}</span> {{ _('marketplace') }}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-secondary btn-sm" onclick="openManagementModal()" id="managementBtn">
|
<button class="btn btn-secondary btn-sm" onclick="openManagementModal()" id="managementBtn">
|
||||||
<span>⚙️</span> {{ _('management') }}
|
<span>{{ icon('settings') }}</span> {{ _('management') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -163,7 +164,7 @@
|
|||||||
<span>{{ _('checking_server') }}</span>
|
<span>{{ _('checking_server') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="empty-state hidden" id="installedAppsEmpty" style="padding: var(--space-xl) 0; margin-bottom: var(--space-xl);">
|
<div class="empty-state hidden" id="installedAppsEmpty" style="padding: var(--space-xl) 0; margin-bottom: var(--space-xl);">
|
||||||
<div class="empty-icon">🛒</div>
|
<div class="empty-icon">{{ icon('cart') }}</div>
|
||||||
<div class="empty-title">{{ _('no_installed_apps') }}</div>
|
<div class="empty-title">{{ _('no_installed_apps') }}</div>
|
||||||
<div class="empty-desc">{{ _('no_installed_apps_desc') }}</div>
|
<div class="empty-desc">{{ _('no_installed_apps_desc') }}</div>
|
||||||
<button class="btn btn-primary btn-sm" onclick="openMarketplaceModal()" style="margin-top: var(--space-md);">
|
<button class="btn btn-primary btn-sm" onclick="openMarketplaceModal()" style="margin-top: var(--space-md);">
|
||||||
@@ -175,7 +176,7 @@
|
|||||||
<!-- AWG2 Card (first - new version) -->
|
<!-- AWG2 Card (first - new version) -->
|
||||||
<div class="card card-hover protocol-card protocol-awg" id="proto-awg2">
|
<div class="card card-hover protocol-card protocol-awg" id="proto-awg2">
|
||||||
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:var(--space-sm);">
|
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:var(--space-sm);">
|
||||||
<div class="protocol-icon">✨</div>
|
<div class="protocol-icon">{{ icon('sparkles') }}</div>
|
||||||
<div class="flex gap-sm" id="awg2-ctrl" style="display:none!important;"></div>
|
<div class="flex gap-sm" id="awg2-ctrl" style="display:none!important;"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="protocol-name">AmneziaWG 2.0 <span
|
<div class="protocol-name">AmneziaWG 2.0 <span
|
||||||
@@ -201,7 +202,7 @@
|
|||||||
<!-- AWG Card -->
|
<!-- AWG Card -->
|
||||||
<div class="card card-hover protocol-card protocol-awg" id="proto-awg">
|
<div class="card card-hover protocol-card protocol-awg" id="proto-awg">
|
||||||
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:var(--space-sm);">
|
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:var(--space-sm);">
|
||||||
<div class="protocol-icon">🔮</div>
|
<div class="protocol-icon">{{ icon('shield') }}</div>
|
||||||
<div class="flex gap-sm" id="awg-ctrl" style="display:none!important;"></div>
|
<div class="flex gap-sm" id="awg-ctrl" style="display:none!important;"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="protocol-name">AmneziaWG</div>
|
<div class="protocol-name">AmneziaWG</div>
|
||||||
@@ -225,7 +226,7 @@
|
|||||||
<!-- AWG Legacy Card -->
|
<!-- AWG Legacy Card -->
|
||||||
<div class="card card-hover protocol-card protocol-legacy" id="proto-awg-legacy">
|
<div class="card card-hover protocol-card protocol-legacy" id="proto-awg-legacy">
|
||||||
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:var(--space-sm);">
|
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:var(--space-sm);">
|
||||||
<div class="protocol-icon">📡</div>
|
<div class="protocol-icon">{{ icon('radio') }}</div>
|
||||||
<div class="flex gap-sm" id="awg-legacy-ctrl" style="display:none!important;"></div>
|
<div class="flex gap-sm" id="awg-legacy-ctrl" style="display:none!important;"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="protocol-name">AmneziaWG Legacy</div>
|
<div class="protocol-name">AmneziaWG Legacy</div>
|
||||||
@@ -249,7 +250,7 @@
|
|||||||
<!-- Xray Card -->
|
<!-- Xray Card -->
|
||||||
<div class="card card-hover protocol-card protocol-xray" id="proto-xray">
|
<div class="card card-hover protocol-card protocol-xray" id="proto-xray">
|
||||||
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:var(--space-sm);">
|
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:var(--space-sm);">
|
||||||
<div class="protocol-icon">⚡</div>
|
<div class="protocol-icon">{{ icon('zap') }}</div>
|
||||||
<div class="flex gap-sm" id="xray-ctrl" style="display:none!important;"></div>
|
<div class="flex gap-sm" id="xray-ctrl" style="display:none!important;"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="protocol-name">Xray (VLESS-Reality)</div>
|
<div class="protocol-name">Xray (VLESS-Reality)</div>
|
||||||
@@ -273,7 +274,7 @@
|
|||||||
<!-- Telemt Card -->
|
<!-- Telemt Card -->
|
||||||
<div class="card card-hover protocol-card protocol-telemt" id="proto-telemt">
|
<div class="card card-hover protocol-card protocol-telemt" id="proto-telemt">
|
||||||
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:var(--space-sm);">
|
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:var(--space-sm);">
|
||||||
<div class="protocol-icon">✈</div>
|
<div class="protocol-icon">{{ icon('plane') }}</div>
|
||||||
<div class="flex gap-sm" id="telemt-ctrl" style="display:none!important;"></div>
|
<div class="flex gap-sm" id="telemt-ctrl" style="display:none!important;"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="protocol-name">Telemt (Telegram Proxy)</div>
|
<div class="protocol-name">Telemt (Telegram Proxy)</div>
|
||||||
@@ -297,7 +298,7 @@
|
|||||||
<!-- WireGuard Card -->
|
<!-- WireGuard Card -->
|
||||||
<div class="card card-hover protocol-card protocol-wireguard" id="proto-wireguard">
|
<div class="card card-hover protocol-card protocol-wireguard" id="proto-wireguard">
|
||||||
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:var(--space-sm);">
|
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:var(--space-sm);">
|
||||||
<div class="protocol-icon">🔒</div>
|
<div class="protocol-icon">{{ icon('lock') }}</div>
|
||||||
<div class="flex gap-sm" id="wireguard-ctrl" style="display:none!important;"></div>
|
<div class="flex gap-sm" id="wireguard-ctrl" style="display:none!important;"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="protocol-name">WireGuard</div>
|
<div class="protocol-name">WireGuard</div>
|
||||||
@@ -325,9 +326,9 @@
|
|||||||
<span class="promo-orb"></span>
|
<span class="promo-orb"></span>
|
||||||
<span class="promo-orb"></span>
|
<span class="promo-orb"></span>
|
||||||
</div>
|
</div>
|
||||||
<span class="promo-lock-badge">🔒 {{ _('coming_soon') }}</span>
|
<span class="promo-lock-badge">{{ icon('lock') }} {{ _('coming_soon') }}</span>
|
||||||
<div class="promo-content">
|
<div class="promo-content">
|
||||||
<div class="promo-icon" aria-hidden="true">🧠</div>
|
<div class="promo-icon" aria-hidden="true">{{ icon('brain') }}</div>
|
||||||
<div class="promo-text">
|
<div class="promo-text">
|
||||||
<div class="promo-title">AIVPN</div>
|
<div class="promo-title">AIVPN</div>
|
||||||
<div class="promo-subtitle">{{ _('aivpn_subtitle') }}</div>
|
<div class="promo-subtitle">{{ _('aivpn_subtitle') }}</div>
|
||||||
@@ -341,7 +342,7 @@
|
|||||||
<!-- DNS Card -->
|
<!-- DNS Card -->
|
||||||
<div class="card card-hover protocol-card protocol-dns" id="proto-dns">
|
<div class="card card-hover protocol-card protocol-dns" id="proto-dns">
|
||||||
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:var(--space-sm);">
|
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:var(--space-sm);">
|
||||||
<div class="protocol-icon">🔍</div>
|
<div class="protocol-icon">{{ icon('search') }}</div>
|
||||||
<div class="flex gap-sm" id="dns-ctrl" style="display:none!important;"></div>
|
<div class="flex gap-sm" id="dns-ctrl" style="display:none!important;"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="protocol-name">AmneziaDNS</div>
|
<div class="protocol-name">AmneziaDNS</div>
|
||||||
@@ -365,7 +366,7 @@
|
|||||||
<!-- AdGuard Home Card -->
|
<!-- AdGuard Home Card -->
|
||||||
<div class="card card-hover protocol-card protocol-adguard" id="proto-adguard">
|
<div class="card card-hover protocol-card protocol-adguard" id="proto-adguard">
|
||||||
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:var(--space-sm);">
|
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:var(--space-sm);">
|
||||||
<div class="protocol-icon">🛡️</div>
|
<div class="protocol-icon">{{ icon('shield-check') }}</div>
|
||||||
<div class="flex gap-sm" id="adguard-ctrl" style="display:none!important;"></div>
|
<div class="flex gap-sm" id="adguard-ctrl" style="display:none!important;"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="protocol-name">AdGuard Home</div>
|
<div class="protocol-name">AdGuard Home</div>
|
||||||
@@ -389,7 +390,7 @@
|
|||||||
<!-- SOCKS5 Card -->
|
<!-- SOCKS5 Card -->
|
||||||
<div class="card card-hover protocol-card protocol-socks5" id="proto-socks5">
|
<div class="card card-hover protocol-card protocol-socks5" id="proto-socks5">
|
||||||
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:var(--space-sm);">
|
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:var(--space-sm);">
|
||||||
<div class="protocol-icon">🧦</div>
|
<div class="protocol-icon">{{ icon('network') }}</div>
|
||||||
<div class="flex gap-sm" id="socks5-ctrl" style="display:none!important;"></div>
|
<div class="flex gap-sm" id="socks5-ctrl" style="display:none!important;"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="protocol-name">SOCKS5 Proxy</div>
|
<div class="protocol-name">SOCKS5 Proxy</div>
|
||||||
@@ -414,7 +415,7 @@
|
|||||||
<!-- NGINX Card -->
|
<!-- NGINX Card -->
|
||||||
<div class="card card-hover protocol-card protocol-nginx" id="proto-nginx">
|
<div class="card card-hover protocol-card protocol-nginx" id="proto-nginx">
|
||||||
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:var(--space-sm);">
|
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:var(--space-sm);">
|
||||||
<div class="protocol-icon">🌐</div>
|
<div class="protocol-icon">{{ icon('globe') }}</div>
|
||||||
<div class="flex gap-sm" id="nginx-ctrl" style="display:none!important;"></div>
|
<div class="flex gap-sm" id="nginx-ctrl" style="display:none!important;"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="protocol-name">NGINX</div>
|
<div class="protocol-name">NGINX</div>
|
||||||
@@ -443,9 +444,9 @@
|
|||||||
<span class="promo-orb"></span>
|
<span class="promo-orb"></span>
|
||||||
<span class="promo-orb"></span>
|
<span class="promo-orb"></span>
|
||||||
</div>
|
</div>
|
||||||
<span class="promo-lock-badge">🔒 {{ _('coming_soon') }}</span>
|
<span class="promo-lock-badge">{{ icon('lock') }} {{ _('coming_soon') }}</span>
|
||||||
<div class="promo-content">
|
<div class="promo-content">
|
||||||
<div class="promo-icon" aria-hidden="true">🛡️</div>
|
<div class="promo-icon" aria-hidden="true">{{ icon('shield-check') }}</div>
|
||||||
<div class="promo-text">
|
<div class="promo-text">
|
||||||
<div class="promo-title">{{ _('revproxy_title') }}</div>
|
<div class="promo-title">{{ _('revproxy_title') }}</div>
|
||||||
<div class="promo-subtitle">{{ _('revproxy_subtitle') }}</div>
|
<div class="promo-subtitle">{{ _('revproxy_subtitle') }}</div>
|
||||||
@@ -463,9 +464,9 @@
|
|||||||
<span class="promo-orb"></span>
|
<span class="promo-orb"></span>
|
||||||
<span class="promo-orb"></span>
|
<span class="promo-orb"></span>
|
||||||
</div>
|
</div>
|
||||||
<span class="promo-lock-badge">🔒 Coming soon</span>
|
<span class="promo-lock-badge">{{ icon('lock') }} Coming soon</span>
|
||||||
<div class="promo-content">
|
<div class="promo-content">
|
||||||
<div class="promo-icon" aria-hidden="true">🧠</div>
|
<div class="promo-icon" aria-hidden="true">{{ icon('brain') }}</div>
|
||||||
<div class="promo-text">
|
<div class="promo-text">
|
||||||
<div class="promo-title">AIVPN</div>
|
<div class="promo-title">AIVPN</div>
|
||||||
<div class="promo-subtitle">AI-driven protocol selection that picks the right tunnel for the moment. Land it sooner — drop a star.</div>
|
<div class="promo-subtitle">AI-driven protocol selection that picks the right tunnel for the moment. Land it sooner — drop a star.</div>
|
||||||
@@ -480,7 +481,7 @@
|
|||||||
<div class="clients-section" id="connectionsSection" style="display:none;">
|
<div class="clients-section" id="connectionsSection" style="display:none;">
|
||||||
<div class="clients-header">
|
<div class="clients-header">
|
||||||
<h2 class="section-title" style="margin-bottom:0;">
|
<h2 class="section-title" style="margin-bottom:0;">
|
||||||
<span class="icon">🔗</span>
|
<span class="icon">{{ icon('link') }}</span>
|
||||||
{{ _('connections') }}
|
{{ _('connections') }}
|
||||||
</h2>
|
</h2>
|
||||||
<div class="flex gap-sm items-center">
|
<div class="flex gap-sm items-center">
|
||||||
@@ -494,7 +495,7 @@
|
|||||||
<option value="wireguard">WireGuard</option>
|
<option value="wireguard">WireGuard</option>
|
||||||
</select>
|
</select>
|
||||||
<button class="btn btn-primary btn-sm" onclick="openAddConnectionModal()">
|
<button class="btn btn-primary btn-sm" onclick="openAddConnectionModal()">
|
||||||
<span>+</span> {{ _('add') }}
|
<span>{{ icon('plus') }}</span> {{ _('add') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -505,7 +506,7 @@
|
|||||||
<span>{{ _('loading_connections') }}</span>
|
<span>{{ _('loading_connections') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div id="connectionsEmpty" class="empty-state hidden" style="padding: var(--space-xl) 0;">
|
<div id="connectionsEmpty" class="empty-state hidden" style="padding: var(--space-xl) 0;">
|
||||||
<div class="empty-icon">🔗</div>
|
<div class="empty-icon">{{ icon('link') }}</div>
|
||||||
<div class="empty-title">{{ _('no_connections') }}</div>
|
<div class="empty-title">{{ _('no_connections') }}</div>
|
||||||
<div class="empty-desc">{{ _('no_connections_desc') }}</div>
|
<div class="empty-desc">{{ _('no_connections_desc') }}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -518,7 +519,7 @@
|
|||||||
<div class="modal" style="max-width: 980px;">
|
<div class="modal" style="max-width: 980px;">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="modal-title">🛒 {{ _('marketplace') }}</h2>
|
<h2 class="modal-title">{{ icon('cart') }} {{ _('marketplace') }}</h2>
|
||||||
<div class="text-muted text-sm">{{ _('marketplace_desc') }}</div>
|
<div class="text-muted text-sm">{{ _('marketplace_desc') }}</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="modal-close" onclick="closeModal('marketplaceModal')">×</button>
|
<button class="modal-close" onclick="closeModal('marketplaceModal')">×</button>
|
||||||
@@ -532,7 +533,7 @@
|
|||||||
<div class="modal" style="max-width: 520px;">
|
<div class="modal" style="max-width: 520px;">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="modal-title">⚙️ {{ _('server_management') }}</h2>
|
<h2 class="modal-title">{{ icon('settings') }} {{ _('server_management') }}</h2>
|
||||||
<div class="text-muted text-sm">{{ _('server_management_desc') }}</div>
|
<div class="text-muted text-sm">{{ _('server_management_desc') }}</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="modal-close" onclick="closeModal('managementModal')">×</button>
|
<button class="modal-close" onclick="closeModal('managementModal')">×</button>
|
||||||
@@ -758,7 +759,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="backupLoading" class="form-hint hidden">{{ _('loading') }}</div>
|
<div id="backupLoading" class="form-hint hidden">{{ _('loading') }}</div>
|
||||||
<div id="backupEmpty" class="empty-state hidden" style="padding:var(--space-xl);">
|
<div id="backupEmpty" class="empty-state hidden" style="padding:var(--space-xl);">
|
||||||
<div class="empty-icon">📦</div>
|
<div class="empty-icon">{{ icon('package') }}</div>
|
||||||
<div class="empty-title">{{ _('no_backups') }}</div>
|
<div class="empty-title">{{ _('no_backups') }}</div>
|
||||||
<div class="empty-desc">{{ _('no_backups_desc') }}</div>
|
<div class="empty-desc">{{ _('no_backups_desc') }}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -952,21 +953,21 @@
|
|||||||
const SERVER_ID = {{ server_id }};
|
const SERVER_ID = {{ server_id }};
|
||||||
const SERVER_HOST = "{{ server.host }}";
|
const SERVER_HOST = "{{ server.host }}";
|
||||||
const MARKETPLACE_APPS = [
|
const MARKETPLACE_APPS = [
|
||||||
{ proto: 'awg2', category: 'protocols', icon: '✨', title: 'AmneziaWG 2.0', descKey: 'awg_desc', badge: 'NEW' },
|
{ proto: 'awg2', category: 'protocols', icon: 'sparkles', title: 'AmneziaWG 2.0', descKey: 'awg_desc', badge: 'NEW' },
|
||||||
{ proto: 'awg', category: 'protocols', icon: '🔮', title: 'AmneziaWG', descKey: 'awg_desc' },
|
{ proto: 'awg', category: 'protocols', icon: 'shield', title: 'AmneziaWG', descKey: 'awg_desc' },
|
||||||
{ proto: 'awg_legacy', category: 'protocols', icon: '📡', title: 'AmneziaWG Legacy', descKey: 'awg_legacy_desc' },
|
{ proto: 'awg_legacy', category: 'protocols', icon: 'radio', title: 'AmneziaWG Legacy', descKey: 'awg_legacy_desc' },
|
||||||
{ proto: 'xray', category: 'protocols', icon: '⚡', title: 'Xray (VLESS-Reality)', descKey: 'xray_desc' },
|
{ proto: 'xray', category: 'protocols', icon: 'zap', title: 'Xray (VLESS-Reality)', descKey: 'xray_desc' },
|
||||||
{ proto: 'telemt', category: 'protocols', icon: '✈', title: 'Telemt (Telegram Proxy)', descKey: 'telemt_desc' },
|
{ proto: 'telemt', category: 'protocols', icon: 'plane', title: 'Telemt (Telegram Proxy)', descKey: 'telemt_desc' },
|
||||||
{ proto: 'wireguard', category: 'protocols', icon: '🔒', title: 'WireGuard', descKey: 'wireguard_desc' },
|
{ proto: 'wireguard', category: 'protocols', icon: 'lock', title: 'WireGuard', descKey: 'wireguard_desc' },
|
||||||
{ proto: 'dns', category: 'services', icon: '🔍', title: 'AmneziaDNS', descKey: 'dns_desc' },
|
{ proto: 'dns', category: 'services', icon: 'search', title: 'AmneziaDNS', descKey: 'dns_desc' },
|
||||||
{ proto: 'adguard', category: 'services', icon: '🛡️', title: 'AdGuard Home', descKey: 'adguard_desc' },
|
{ proto: 'adguard', category: 'services', icon: 'shield-check', title: 'AdGuard Home', descKey: 'adguard_desc' },
|
||||||
{ proto: 'socks5', category: 'services', icon: '🧦', title: 'SOCKS5 Proxy', descKey: 'socks5_desc' },
|
{ proto: 'socks5', category: 'services', icon: 'network', title: 'SOCKS5 Proxy', descKey: 'socks5_desc' },
|
||||||
{ proto: 'nginx', category: 'web_servers', icon: '🌐', title: 'NGINX', descKey: 'nginx_desc' },
|
{ proto: 'nginx', category: 'web_servers', icon: 'globe', title: 'NGINX', descKey: 'nginx_desc' },
|
||||||
];
|
];
|
||||||
const MARKETPLACE_CATEGORIES = [
|
const MARKETPLACE_CATEGORIES = [
|
||||||
{ id: 'protocols', icon: '🔐', titleKey: 'protocols' },
|
{ id: 'protocols', icon: 'lock', titleKey: 'protocols' },
|
||||||
{ id: 'services', icon: '🛠', titleKey: 'services' },
|
{ id: 'services', icon: 'wrench', titleKey: 'services' },
|
||||||
{ id: 'web_servers', icon: '🌐', titleKey: 'web_servers' },
|
{ id: 'web_servers', icon: 'globe', titleKey: 'web_servers' },
|
||||||
];
|
];
|
||||||
let currentInstallProto = 'awg';
|
let currentInstallProto = 'awg';
|
||||||
let currentInstallAnother = false;
|
let currentInstallAnother = false;
|
||||||
@@ -1041,16 +1042,16 @@
|
|||||||
el.innerHTML = `
|
el.innerHTML = `
|
||||||
<div class="flex" style="flex-direction: column; gap: var(--space-sm);">
|
<div class="flex" style="flex-direction: column; gap: var(--space-sm);">
|
||||||
<button class="btn btn-secondary" onclick="closeModal('managementModal'); checkServer();" style="justify-content: flex-start; padding: 12px 20px;">
|
<button class="btn btn-secondary" onclick="closeModal('managementModal'); checkServer();" style="justify-content: flex-start; padding: 12px 20px;">
|
||||||
<span style="font-size: 1.2rem; margin-right: 8px;">🔄</span> ${_('check_server_services')}
|
<span style="font-size: 1.2rem; margin-right: 8px; display:inline-flex;">${uiIcon('refresh')}</span> ${_('check_server_services')}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-warning" onclick="closeModal('managementModal'); rebootServer();" style="justify-content: flex-start; padding: 12px 20px;">
|
<button class="btn btn-warning" onclick="closeModal('managementModal'); rebootServer();" style="justify-content: flex-start; padding: 12px 20px;">
|
||||||
<span style="font-size: 1.2rem; margin-right: 8px;">🔁</span> ${_('reboot_server')}
|
<span style="font-size: 1.2rem; margin-right: 8px; display:inline-flex;">${uiIcon('refresh')}</span> ${_('reboot_server')}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-danger" onclick="closeModal('managementModal'); clearServer();" style="justify-content: flex-start; padding: 12px 20px;">
|
<button class="btn btn-danger" onclick="closeModal('managementModal'); clearServer();" style="justify-content: flex-start; padding: 12px 20px;">
|
||||||
<span style="font-size: 1.2rem; margin-right: 8px;">🧹</span> ${_('clear_server')}
|
<span style="font-size: 1.2rem; margin-right: 8px; display:inline-flex;">${uiIcon('trash')}</span> ${_('clear_server')}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-danger" onclick="closeModal('managementModal'); deleteServer();" style="justify-content: flex-start; padding: 12px 20px;">
|
<button class="btn btn-danger" onclick="closeModal('managementModal'); deleteServer();" style="justify-content: flex-start; padding: 12px 20px;">
|
||||||
<span style="font-size: 1.2rem; margin-right: 8px;">🗑️</span> ${_('remove_server')}
|
<span style="font-size: 1.2rem; margin-right: 8px; display:inline-flex;">${uiIcon('trash')}</span> ${_('remove_server')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -1080,7 +1081,7 @@
|
|||||||
const apps = MARKETPLACE_APPS.filter(app => app.category === cat.id);
|
const apps = MARKETPLACE_APPS.filter(app => app.category === cat.id);
|
||||||
return `
|
return `
|
||||||
<div class="marketplace-category">
|
<div class="marketplace-category">
|
||||||
<div class="marketplace-category-title"><span>${cat.icon}</span> ${_(cat.titleKey)}</div>
|
<div class="marketplace-category-title"><span>${uiIcon(cat.icon)}</span> ${_(cat.titleKey)}</div>
|
||||||
<div class="marketplace-grid">
|
<div class="marketplace-grid">
|
||||||
${apps.map(app => {
|
${apps.map(app => {
|
||||||
const installed = baseInstalled(app.proto);
|
const installed = baseInstalled(app.proto);
|
||||||
@@ -1089,7 +1090,7 @@
|
|||||||
<div class="marketplace-app-card">
|
<div class="marketplace-app-card">
|
||||||
<div class="marketplace-app-top">
|
<div class="marketplace-app-top">
|
||||||
<div class="flex items-center gap-sm">
|
<div class="flex items-center gap-sm">
|
||||||
<div class="protocol-icon" style="width:38px;height:38px;font-size:1.2rem;">${app.icon}</div>
|
<div class="protocol-icon" style="width:38px;height:38px;font-size:1.2rem;">${uiIcon(app.icon)}</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="marketplace-app-title">${app.title} ${app.badge ? `<span class="badge badge-info" style="font-size:0.65rem;">${app.badge}</span>` : ''}</div>
|
<div class="marketplace-app-title">${app.title} ${app.badge ? `<span class="badge badge-info" style="font-size:0.65rem;">${app.badge}</span>` : ''}</div>
|
||||||
${marketplaceAppStatus(app.proto)}
|
${marketplaceAppStatus(app.proto)}
|
||||||
@@ -1214,10 +1215,10 @@
|
|||||||
const el = document.getElementById('serverStats');
|
const el = document.getElementById('serverStats');
|
||||||
el.style.display = '';
|
el.style.display = '';
|
||||||
el.innerHTML = `
|
el.innerHTML = `
|
||||||
<span class="stat-pill" title="CPU">⚡ ${stats.cpu || 0}%</span>
|
<span class="stat-pill" title="CPU">${uiIcon('zap')} ${stats.cpu || 0}%</span>
|
||||||
<span class="stat-pill" title="RAM: ${formatBytes(stats.ram_used)} / ${formatBytes(stats.ram_total)}">🧠 ${stats.ram_percent || 0}%</span>
|
<span class="stat-pill" title="RAM: ${formatBytes(stats.ram_used)} / ${formatBytes(stats.ram_total)}">${uiIcon('brain')} ${stats.ram_percent || 0}%</span>
|
||||||
<span class="stat-pill" title="Disk: ${formatBytes(stats.disk_used)} / ${formatBytes(stats.disk_total)}">💾 ${stats.disk_percent || 0}%</span>
|
<span class="stat-pill" title="Disk: ${formatBytes(stats.disk_used)} / ${formatBytes(stats.disk_total)}">${uiIcon('package')} ${stats.disk_percent || 0}%</span>
|
||||||
<span class="stat-pill" title="Network">🌐 ↓${formatBytes(stats.net_rx)} ↑${formatBytes(stats.net_tx)}</span>
|
<span class="stat-pill" title="Network">${uiIcon('globe')} ↓${formatBytes(stats.net_rx)} ↑${formatBytes(stats.net_tx)}</span>
|
||||||
`;
|
`;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('Stats load failed:', err);
|
console.warn('Stats load failed:', err);
|
||||||
@@ -2220,11 +2221,11 @@
|
|||||||
if (!enabled) metaHtml += `<span class="badge badge-danger" style="font-size:0.65rem">${_('stop')}</span>`;
|
if (!enabled) metaHtml += `<span class="badge badge-danger" style="font-size:0.65rem">${_('stop')}</span>`;
|
||||||
|
|
||||||
const disabledStyle = enabled ? '' : 'opacity:0.5;';
|
const disabledStyle = enabled ? '' : 'opacity:0.5;';
|
||||||
const toggleIcon = enabled ? '🔵' : '⚫';
|
const toggleIcon = enabled ? uiIcon('pause') : uiIcon('play');
|
||||||
const toggleTitle = enabled ? _('stop') : _('starting');
|
const toggleTitle = enabled ? _('stop') : _('starting');
|
||||||
// Show config button for all WireGuard-based clients.
|
// Show config button for all WireGuard-based clients.
|
||||||
// If clientPrivateKey is missing (created via native app), we show a warning instead.
|
// If clientPrivateKey is missing (created via native app), we show a warning instead.
|
||||||
const canShowConfig = proto === 'xray' || true; // always show 📄 button
|
const canShowConfig = proto === 'xray' || true; // always show config button
|
||||||
|
|
||||||
listEl.innerHTML += `
|
listEl.innerHTML += `
|
||||||
<div class="client-item" style="${disabledStyle}">
|
<div class="client-item" style="${disabledStyle}">
|
||||||
@@ -2236,10 +2237,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="client-actions">
|
<div class="client-actions">
|
||||||
<button class="btn btn-secondary btn-sm btn-icon" onclick="showConnectionConfig('${escapeJs(client.clientId)}', '${escapeJs(name)}', ${!!userData.clientPrivateKey || proto === 'xray' || proto === 'telemt'})" title="${_('config')}">📄</button>
|
<button class="btn btn-secondary btn-sm btn-icon" onclick="showConnectionConfig('${escapeJs(client.clientId)}', '${escapeJs(name)}', ${!!userData.clientPrivateKey || proto === 'xray' || proto === 'telemt'})" title="${_('config')}">${uiIcon('file')}</button>
|
||||||
${proto === 'telemt' ? `<button class="btn btn-secondary btn-sm btn-icon" onclick="editConnection('${escapeJs(client.clientId)}')" title="${_('edit')}">✏️</button>` : ''}
|
${proto === 'telemt' ? `<button class="btn btn-secondary btn-sm btn-icon" onclick="editConnection('${escapeJs(client.clientId)}')" title="${_('edit')}">${uiIcon('pencil')}</button>` : ''}
|
||||||
<button class="btn btn-secondary btn-sm btn-icon" onclick="toggleConnection('${escapeJs(client.clientId)}', ${!enabled})" title="${toggleTitle}">${toggleIcon}</button>
|
<button class="btn btn-secondary btn-sm btn-icon" onclick="toggleConnection('${escapeJs(client.clientId)}', ${!enabled})" title="${toggleTitle}">${toggleIcon}</button>
|
||||||
<button class="btn btn-danger btn-sm btn-icon" onclick="removeConnection('${escapeJs(client.clientId)}')" title="${_('delete')}">🗑</button>
|
<button class="btn btn-danger btn-sm btn-icon" onclick="removeConnection('${escapeJs(client.clientId)}')" title="${_('delete')}">${uiIcon('trash')}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
{% from "macros/icons.html" import icon %}
|
||||||
|
|
||||||
{% block title_extra %} — {{ _('nav_settings') }}{% endblock %}
|
{% block title_extra %} — {{ _('nav_settings') }}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="section-title">
|
<div class="section-title">
|
||||||
<span class="icon">⚙️</span>
|
<span class="icon">{{ icon('settings') }}</span>
|
||||||
{{ _('settings_title') }}
|
{{ _('settings_title') }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -65,7 +66,7 @@
|
|||||||
<input type="text" class="form-input" id="guest_link_display" readonly
|
<input type="text" class="form-input" id="guest_link_display" readonly
|
||||||
value="{% if settings.guest.token %}{{ request.url.scheme }}://{{ request.url.netloc }}/guest/{{ settings.guest.token }}{% endif %}"
|
value="{% if settings.guest.token %}{{ request.url.scheme }}://{{ request.url.netloc }}/guest/{{ settings.guest.token }}{% endif %}"
|
||||||
placeholder="{{ _('guest_link_placeholder') }}">
|
placeholder="{{ _('guest_link_placeholder') }}">
|
||||||
<button type="button" class="btn btn-secondary btn-sm" onclick="copyGuestLink()">📋</button>
|
<button type="button" class="btn btn-secondary btn-sm btn-icon" onclick="copyGuestLink()" title="{{ _('copy') }}">{{ icon('copy') }}</button>
|
||||||
<button type="button" class="btn btn-secondary btn-sm" onclick="regenGuestToken()" title="{{ _('guest_regen_token') }}">🔄</button>
|
<button type="button" class="btn btn-secondary btn-sm" onclick="regenGuestToken()" title="{{ _('guest_regen_token') }}">🔄</button>
|
||||||
</div>
|
</div>
|
||||||
<input type="hidden" id="guest_token" value="{{ settings.guest.token or '' }}">
|
<input type="hidden" id="guest_token" value="{{ settings.guest.token or '' }}">
|
||||||
@@ -376,7 +377,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-danger btn-sm" onclick="deleteSyncRemnawave()"
|
<button type="button" class="btn btn-danger btn-sm" onclick="deleteSyncRemnawave()"
|
||||||
id="syncDelBtn">
|
id="syncDelBtn">
|
||||||
<span id="syncDelBtnText">🗑 {{ _('delete_sync_btn') }}</span>
|
<span id="syncDelBtnText">{{ icon('trash') }} {{ _('delete_sync_btn') }}</span>
|
||||||
<div class="spinner hidden" id="syncDelSpinner" style="width:14px; height:14px;"></div>
|
<div class="spinner hidden" id="syncDelSpinner" style="width:14px; height:14px;"></div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -452,7 +453,7 @@
|
|||||||
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom: var(--space-md);">
|
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom: var(--space-md);">
|
||||||
<h3 class="card-title" style="margin:0;">🔑 {{ _('api_tokens_title') }}</h3>
|
<h3 class="card-title" style="margin:0;">🔑 {{ _('api_tokens_title') }}</h3>
|
||||||
<button type="button" class="btn btn-primary btn-sm" onclick="openCreateTokenModal()">
|
<button type="button" class="btn btn-primary btn-sm" onclick="openCreateTokenModal()">
|
||||||
<span>+</span> {{ _('api_tokens_create') }}
|
{{ icon('plus') }} {{ _('api_tokens_create') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p class="form-hint" style="margin-bottom: var(--space-md);">
|
<p class="form-hint" style="margin-bottom: var(--space-md);">
|
||||||
@@ -546,7 +547,7 @@
|
|||||||
curl -H "Authorization: Bearer <your_token>" {{ request.url.scheme }}://{{ request.url.netloc }}/api/users
|
curl -H "Authorization: Bearer <your_token>" {{ request.url.scheme }}://{{ request.url.netloc }}/api/users
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button class="btn btn-secondary" onclick="copyNewToken()">📋 {{ _('copy') }}</button>
|
<button class="btn btn-secondary" onclick="copyNewToken()">{{ icon('copy') }} {{ _('copy') }}</button>
|
||||||
<button class="btn btn-primary" onclick="closeModal('newTokenModal')">{{ _('done') }}</button>
|
<button class="btn btn-primary" onclick="closeModal('newTokenModal')">{{ _('done') }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -838,7 +839,7 @@
|
|||||||
showToast(`${_('error')}: ` + err.message, 'error');
|
showToast(`${_('error')}: ` + err.message, 'error');
|
||||||
} finally {
|
} finally {
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
text.textContent = `🗑 ${_('delete_sync_btn')}`;
|
text.innerHTML = `${uiIcon('trash')} ${_('delete_sync_btn')}`;
|
||||||
spinner.classList.add('hidden');
|
spinner.classList.add('hidden');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-14
@@ -1,4 +1,5 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
{% from "macros/icons.html" import icon %}
|
||||||
|
|
||||||
{% block title_extra %} — {{ _('users_title') }}{% endblock %}
|
{% block title_extra %} — {{ _('users_title') }}{% endblock %}
|
||||||
|
|
||||||
@@ -7,19 +8,19 @@
|
|||||||
<div class="flex items-center justify-between"
|
<div class="flex items-center justify-between"
|
||||||
style="margin-bottom: var(--space-lg); gap: var(--space-md); flex-wrap: wrap;">
|
style="margin-bottom: var(--space-lg); gap: var(--space-md); flex-wrap: wrap;">
|
||||||
<h1 class="section-title" style="margin-bottom:0; flex-shrink: 0;">
|
<h1 class="section-title" style="margin-bottom:0; flex-shrink: 0;">
|
||||||
<span class="icon">👥</span>
|
<span class="icon">{{ icon('users') }}</span>
|
||||||
{{ _('users_title') }}
|
{{ _('users_title') }}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<div style="flex: 1; min-width: 250px; max-width: 500px; position: relative;">
|
<div style="flex: 1; min-width: 250px; max-width: 500px; position: relative;">
|
||||||
<input type="text" class="form-input" id="userSearch" placeholder="{{ _('search_placeholder') }}"
|
<input type="text" class="form-input" id="userSearch" placeholder="{{ _('search_placeholder') }}"
|
||||||
oninput="handleSearch(this.value)" style="padding-left: 40px;">
|
oninput="handleSearch(this.value)" style="padding-left: 40px;">
|
||||||
<span style="position: absolute; left: 14px; top: 50%; transform: translateY(-50%); opacity: 0.5;">🔍</span>
|
<span style="position: absolute; left: 14px; top: 50%; transform: translateY(-50%); color: var(--text-muted); display:flex;">{{ icon('search') }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if current_user.role == 'admin' %}
|
{% if current_user.role == 'admin' %}
|
||||||
<button class="btn btn-primary" onclick="openModal('addUserModal')" style="flex-shrink: 0;">
|
<button class="btn btn-primary" onclick="openModal('addUserModal')" style="flex-shrink: 0;">
|
||||||
<span>+</span> {{ _('add_user') }}
|
{{ icon('plus') }} {{ _('add_user') }}
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
@@ -45,7 +46,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="usersEmpty" class="empty-state hidden">
|
<div id="usersEmpty" class="empty-state hidden">
|
||||||
<div class="empty-icon">👤</div>
|
<div class="empty-icon">{{ icon('user') }}</div>
|
||||||
<div class="empty-title">{{ _('nothing_found') }}</div>
|
<div class="empty-title">{{ _('nothing_found') }}</div>
|
||||||
<div class="empty-desc">{{ _('search_empty_desc') }}</div>
|
<div class="empty-desc">{{ _('search_empty_desc') }}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -496,7 +497,7 @@
|
|||||||
${u.enabled ? '' : `<span class="badge badge-danger" style="font-size:0.65rem;">${_('disabled')}</span>`}
|
${u.enabled ? '' : `<span class="badge badge-danger" style="font-size:0.65rem;">${_('disabled')}</span>`}
|
||||||
</div>
|
</div>
|
||||||
<div class="client-meta">
|
<div class="client-meta">
|
||||||
<span style="cursor:pointer; color:var(--primary); text-decoration: underline;" onclick="viewUserConnections('${u.id}', '${u.username.replace(/'/g, "\\'")}')">🔗 ${u.connections_count} ${_('connections')}</span>
|
<span style="cursor:pointer; color:var(--primary); text-decoration: underline; display:inline-flex; align-items:center; gap:4px;" onclick="viewUserConnections('${u.id}', '${u.username.replace(/'/g, "\\'")}')">${uiIcon('link')} ${u.connections_count} ${_('connections')}</span>
|
||||||
${u.created_at ? `<span>📅 ${u.created_at.substring(0, 10)}</span>` : ''}
|
${u.created_at ? `<span>📅 ${u.created_at.substring(0, 10)}</span>` : ''}
|
||||||
${u.telegramId ? `<span>💬 ${u.telegramId}</span>` : ''}
|
${u.telegramId ? `<span>💬 ${u.telegramId}</span>` : ''}
|
||||||
${u.email ? `<span>📧 ${u.email}</span>` : ''}
|
${u.email ? `<span>📧 ${u.email}</span>` : ''}
|
||||||
@@ -542,22 +543,22 @@
|
|||||||
{% if current_user.role == 'admin' %}
|
{% if current_user.role == 'admin' %}
|
||||||
<button class="btn btn-secondary btn-sm btn-icon"
|
<button class="btn btn-secondary btn-sm btn-icon"
|
||||||
onclick="openAddConnectionForUser('${u.id}', '${u.username}')"
|
onclick="openAddConnectionForUser('${u.id}', '${u.username}')"
|
||||||
title="${_('add_connection')}">🔗</button>
|
title="${_('add_connection')}">${uiIcon('link')}</button>
|
||||||
<button class="btn btn-secondary btn-sm btn-icon"
|
<button class="btn btn-secondary btn-sm btn-icon"
|
||||||
onclick='openEditUser({"id": "${u.id}", "username": "${u.username}", "tg": "${u.telegramId || ""}", "email": "${u.email || ""}", "desc": "${u.description || ""}", "limit": "${u.traffic_limit ? (u.traffic_limit / Math.pow(1024, 3)).toFixed(2) : 0}", "strategy": "${u.traffic_reset_strategy || 'never'}", "expiration": "${u.expiration_date || ''}"})'
|
onclick='openEditUser({"id": "${u.id}", "username": "${u.username}", "tg": "${u.telegramId || ""}", "email": "${u.email || ""}", "desc": "${u.description || ""}", "limit": "${u.traffic_limit ? (u.traffic_limit / Math.pow(1024, 3)).toFixed(2) : 0}", "strategy": "${u.traffic_reset_strategy || 'never'}", "expiration": "${u.expiration_date || ''}"})'
|
||||||
title="${_('edit')}">✏️</button>
|
title="${_('edit')}">${uiIcon('pencil')}</button>
|
||||||
<button class="btn btn-secondary btn-sm btn-icon" onclick="openShareModal(this.dataset)"
|
<button class="btn btn-secondary btn-sm btn-icon" onclick="openShareModal(this.dataset)"
|
||||||
data-id="${u.id}" data-username="${u.username}" data-token="${u.share_token || ''}"
|
data-id="${u.id}" data-username="${u.username}" data-token="${u.share_token || ''}"
|
||||||
data-enabled="${u.share_enabled ? 'true' : 'false'}"
|
data-enabled="${u.share_enabled ? 'true' : 'false'}"
|
||||||
data-haspassword="${u.has_share_password ? 'true' : 'false'}"
|
data-haspassword="${u.has_share_password ? 'true' : 'false'}"
|
||||||
title="${_('share_access')}">📤</button>
|
title="${_('share_access')}">${uiIcon('share')}</button>
|
||||||
${u.id !== "{{ current_user.id }}" ? `
|
${u.id !== "{{ current_user.id }}" ? `
|
||||||
<button class="btn btn-secondary btn-sm btn-icon" onclick="toggleUser(this.dataset)"
|
<button class="btn btn-secondary btn-sm btn-icon" onclick="toggleUser(this.dataset)"
|
||||||
data-id="${u.id}" data-enable="${u.enabled ? 'false' : 'true'}"
|
data-id="${u.id}" data-enable="${u.enabled ? 'false' : 'true'}"
|
||||||
title="${u.enabled ? _('stop_btn') : _('start_btn')}">
|
title="${u.enabled ? _('stop_btn') : _('start_btn')}">
|
||||||
${u.enabled ? '🔵' : '⚫'}
|
${u.enabled ? uiIcon('pause') : uiIcon('play')}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-danger btn-sm btn-icon" onclick="deleteUser('${u.id}')" title="${_('delete')}">🗑</button>
|
<button class="btn btn-danger btn-sm btn-icon" onclick="deleteUser('${u.id}')" title="${_('delete')}">${uiIcon('trash')}</button>
|
||||||
` : ''}
|
` : ''}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
@@ -860,17 +861,17 @@
|
|||||||
listEl.innerHTML += `
|
listEl.innerHTML += `
|
||||||
<div class="client-item">
|
<div class="client-item">
|
||||||
<div class="client-info">
|
<div class="client-info">
|
||||||
<div class="client-avatar">🔗</div>
|
<div class="client-avatar">${uiIcon('link')}</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="client-name">${c.name || 'Connection'}</div>
|
<div class="client-name">${c.name || 'Connection'}</div>
|
||||||
<div class="client-meta">
|
<div class="client-meta">
|
||||||
<span>🖥 ${c.server_name || ''}</span>
|
<span style="display:inline-flex;align-items:center;gap:4px;">${uiIcon('server')} ${c.server_name || ''}</span>
|
||||||
<span>${c.protocol === 'awg' ? 'AmneziaWG' : (c.protocol === 'awg2' ? 'AmneziaWG 2.0' : (c.protocol === 'awg_legacy' ? 'AWG Legacy' : (c.protocol === 'xray' ? 'Xray' : c.protocol.toUpperCase())))}</span>
|
<span>${c.protocol === 'awg' ? 'AmneziaWG' : (c.protocol === 'awg2' ? 'AmneziaWG 2.0' : (c.protocol === 'awg_legacy' ? 'AWG Legacy' : (c.protocol === 'xray' ? 'Xray' : c.protocol.toUpperCase())))}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="client-actions">
|
<div class="client-actions">
|
||||||
<button class="btn btn-secondary btn-sm btn-icon" onclick="showUserConnectionConfig(${c.server_id}, '${c.client_id}', '${c.protocol}', '${(c.name || 'VPN').replace(/'/g, "\\'")}')" title="${_('config')}">📄</button>
|
<button class="btn btn-secondary btn-sm btn-icon" onclick="showUserConnectionConfig(${c.server_id}, '${c.client_id}', '${c.protocol}', '${(c.name || 'VPN').replace(/'/g, "\\'")}')" title="${_('config')}">${uiIcon('file')}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -949,7 +950,7 @@
|
|||||||
<div class="flex gap-sm">
|
<div class="flex gap-sm">
|
||||||
<input class="form-input" type="text" id="shareLink" readonly
|
<input class="form-input" type="text" id="shareLink" readonly
|
||||||
style="background: var(--bg-primary);">
|
style="background: var(--bg-primary);">
|
||||||
<button class="btn btn-secondary" onclick="copyShareLink()">📋</button>
|
<button class="btn btn-secondary btn-icon" onclick="copyShareLink()" title="{{ _('copy') }}">{{ icon('copy') }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user