Files
deploy-test/animex/settings.html
2026-03-29 21:19:53 -05:00

1407 lines
58 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, viewport-fit=cover" />
<title>Settings - Media App</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
<link href="https://fonts.googleapis.com/css2?family=Bitcount+Grid+Single:wght@100..900&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" />
<style>
:root {
--font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
--background-primary: #121212;
--background-secondary: #1e1e1e;
--background-tertiary-hover: #2a2a2a;
--background-modal: #1c1c1e;
--foreground-primary: #e6e6e6;
--foreground-secondary: #a0a0a0;
--foreground-tertiary: #505050;
--border-color: #2c2c2c;
--input-border-color: #3a3a3c;
--accent-primary: #ff9500;
--accent-primary-rgb: 255, 149, 0;
--accent-primary-hover: #e68600;
--destructive: #ff453a;
--radius-sm: 10px;
--radius-md: 18px;
/* Profile-only accent — only avatar ring + glow */
--profile-accent: #ff9500;
--profile-accent-rgb: 255, 149, 0;
}
body[data-theme="light"] {
--background-primary: #f2f2f7;
--background-secondary: #ffffff;
--background-tertiary-hover: #f1f1f1;
--background-modal: #ffffff;
--foreground-primary: #1c1c1e;
--foreground-secondary: #636366;
--foreground-tertiary: #aeaeb2;
--border-color: #e5e5e5;
--input-border-color: #c7c7cc;
}
*, *::before, *::after { box-sizing: border-box; }
html, body {
font-family: var(--font-sans);
background-color: var(--background-primary);
color: var(--foreground-primary);
margin: 0;
padding: 0;
-webkit-font-smoothing: antialiased;
min-height: 100vh;
}
.app-container {
max-width: 840px;
margin: 0 auto;
padding: 0rem 0rem 5rem;
}
/* =====================
PROFILE HERO
===================== */
.profile-hero {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
padding: 4rem 1rem 2.5rem;
overflow: visible;
}
.profile-hero-bg {
position: fixed;
top: 0;
left: 50%;
transform: translateX(-50%);
width: 180vw;
max-width: 100%;
min-width: 650px;
height: 500px;
pointer-events: none;
transition: background 0.6s ease;
z-index: 0;
background: radial-gradient(ellipse 50% 60% at 50% 0%, rgba(var(--profile-accent-rgb), 0.28) 0%, transparent 70%);
}
.avatar-wrapper {
position: relative;
width: 104px;
height: 104px;
margin-bottom: 1.25rem;
z-index: 1;
}
/* Soft glow halo behind avatar */
.avatar-wrapper::before {
content: '';
position: absolute;
inset: -8px;
border-radius: 50%;
background: var(--profile-accent);
opacity: 0.45;
filter: blur(16px);
transition: background 0.6s ease;
z-index: 0;
}
#userAvatar {
width: 104px;
height: 104px;
border-radius: 50%;
object-fit: cover;
border: 3px solid var(--profile-accent);
position: relative;
z-index: 1;
transition: border-color 0.5s ease;
cursor: pointer;
display: block;
}
.avatar-edit-badge {
position: absolute;
bottom: 2px;
right: 2px;
width: 30px;
height: 30px;
border-radius: 50%;
background: #ffffff;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.45);
z-index: 2;
transition: transform 0.15s ease;
-webkit-tap-highlight-color: transparent;
}
.avatar-edit-badge:active { transform: scale(0.88); }
.avatar-edit-badge i { font-size: 0.65rem; color: #1c1c1e; }
.profile-hero .username {
font-size: 1.75rem;
font-weight: 700;
margin: 0 0 0.5rem;
position: relative;
z-index: 1;
}
.edit-profile-link {
color: var(--foreground-secondary);
text-decoration: none;
font-size: 0.8125rem;
font-weight: 500;
position: relative;
z-index: 1;
letter-spacing: 0.01em;
}
.edit-profile-link i { margin-right: 4px; font-size: 0.7rem; }
/* =====================
SETTINGS LAYOUT
===================== */
.settings-sections {
display: flex;
flex-direction: column;
padding: 0 1rem;
}
.section-label {
font-size: 0.6875rem;
font-weight: 600;
letter-spacing: 0.07em;
color: var(--foreground-tertiary);
text-transform: uppercase;
padding: 1.5rem 0.25rem 0.5rem;
}
.settings-group {
background-color: var(--background-secondary);
border-radius: var(--radius-md);
overflow: visible;
}
.settings-item {
display: flex;
align-items: center;
gap: 0.875rem;
padding: 0.8125rem 1rem;
cursor: pointer;
border-bottom: 1px solid var(--border-color);
transition: background-color 0.12s ease, transform 0.1s ease;
-webkit-tap-highlight-color: transparent;
user-select: none;
}
.settings-group .settings-item:last-child { border-bottom: none; }
.settings-item:active:not(.no-hover) {
background-color: var(--background-tertiary-hover);
transform: scale(0.992);
}
.settings-item.no-hover { cursor: default; }
.item-text {
flex-grow: 1;
font-weight: 500;
font-size: 0.9375rem;
}
.item-action {
color: var(--foreground-secondary);
font-size: 0.8125rem;
}
/* =====================
ICON SQUARES
===================== */
.icon-sq {
width: 30px;
height: 30px;
min-width: 30px;
border-radius: 7px;
background-color: transparent;
border: 1px solid var(--border-color);
display: flex;
align-items: center;
justify-content: center;
}
.icon-sq i { color: var(--foreground-secondary); font-size: 0.78rem; }
/* =====================
ACCENT COLOR SWATCHES
===================== */
.accent-swatches {
display: flex;
gap: 7px;
align-items: center;
}
.swatch {
width: 24px;
height: 24px;
border-radius: 50%;
border: 2.5px solid transparent;
cursor: pointer;
padding: 0;
outline: none;
transition: transform 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;
-webkit-tap-highlight-color: transparent;
}
.swatch:hover { transform: scale(1.18); }
.swatch:active { transform: scale(0.88); }
.swatch.active {
border-color: var(--foreground-primary);
box-shadow: 0 0 0 1px var(--foreground-primary);
}
/* =====================
TOGGLE SWITCH
===================== */
.toggle-switch {
position: relative;
display: inline-block;
width: 51px;
height: 31px;
flex-shrink: 0;
}
.toggle-switch input { opacity: 0; width: 0; height: 0; }
.slider {
position: absolute;
cursor: pointer;
inset: 0;
background-color: var(--foreground-tertiary);
transition: background-color 0.28s, box-shadow 0.28s;
border-radius: 34px;
}
.slider:before {
position: absolute;
content: "";
height: 27px;
width: 27px;
left: 2px;
bottom: 2px;
background-color: white;
transition: transform 0.28s cubic-bezier(0.34, 1.56, 0.64, 1);
border-radius: 50%;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3);
}
input:checked + .slider {
background-color: var(--accent-primary);
box-shadow: 0 0 10px rgba(var(--accent-primary-rgb), 0.35);
}
input:checked + .slider:before { transform: translateX(20px); }
/* =====================
CUSTOM DROPDOWN
===================== */
.custom-dropdown-wrapper { position: relative; min-width: 130px; }
.custom-dropdown-btn {
width: 100%;
background: none;
border: none;
color: var(--foreground-secondary);
font-size: 0.9rem;
padding: 0.25rem 0.25rem 0.25rem 0.5rem;
cursor: pointer;
display: flex;
align-items: center;
gap: 5px;
justify-content: flex-end;
font-family: var(--font-sans);
}
.custom-dropdown-list {
display: none;
position: absolute;
top: 110%;
right: 0;
min-width: 165px;
background: var(--background-secondary);
border-radius: var(--radius-sm);
z-index: 10010;
border: 1px solid var(--border-color);
list-style: none;
padding: 0.25rem 0;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.35);
margin: 0;
}
.custom-dropdown-wrapper.is-open .custom-dropdown-list { display: block; }
.custom-dropdown-list li { padding: 0.75rem 1rem; cursor: pointer; font-size: 0.9rem; }
.custom-dropdown-list li:hover,
.custom-dropdown-list li.selected { background: var(--accent-primary); color: #fff; }
/* =====================
SKELETON LOADING
===================== */
#loadingScreen { max-width: 640px; margin: 0 auto; }
.skeleton-hero {
height: 230px;
background: linear-gradient(180deg, rgba(30,30,30,0.9) 0%, var(--background-primary) 100%);
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-end;
padding-bottom: 2.25rem;
gap: 0.75rem;
}
.skeleton-circle {
width: 104px;
height: 104px;
border-radius: 50%;
background: var(--border-color);
animation: shimmer 1.6s ease-in-out infinite;
}
.skeleton-line {
height: 11px;
border-radius: 6px;
background: var(--border-color);
animation: shimmer 1.6s ease-in-out infinite;
}
.skeleton-group {
background: var(--background-secondary);
border-radius: var(--radius-md);
overflow: hidden;
margin: 0 1rem;
}
.skeleton-row {
display: flex;
align-items: center;
gap: 0.875rem;
padding: 0.8125rem 1rem;
border-bottom: 1px solid var(--border-color);
}
.skeleton-row:last-child { border-bottom: none; }
.skeleton-sq {
width: 30px;
height: 30px;
min-width: 30px;
border-radius: 7px;
background: var(--border-color);
animation: shimmer 1.6s ease-in-out infinite;
}
.skeleton-label {
height: 8px;
width: 80px;
border-radius: 4px;
background: var(--border-color);
margin: 1.25rem 1.25rem 0.5rem;
animation: shimmer 1.6s ease-in-out infinite;
}
@keyframes shimmer {
0%, 100% { opacity: 0.5; }
50% { opacity: 1; }
}
/* =====================
BOTTOM SHEET MODALS
===================== */
.modal-backdrop {
position: fixed;
inset: 0;
background-color: rgba(0, 0, 0, 0.55);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
display: flex;
align-items: flex-end;
justify-content: center;
z-index: 1000;
opacity: 0;
pointer-events: none;
transition: opacity 0.28s ease;
}
.modal-backdrop.active {
opacity: 1;
pointer-events: all;
}
.modal-content {
background-color: var(--background-modal);
border-radius: 22px 22px 0 0;
width: 100%;
max-width: 640px;
max-height: 86vh;
overflow-y: auto;
position: relative;
transform: translateY(100%);
transition: transform 0.38s cubic-bezier(0.32, 0.72, 0, 1);
padding-bottom: env(safe-area-inset-bottom, 1.5rem);
}
.modal-backdrop.active .modal-content { transform: translateY(0); }
@media (min-width: 768px) {
.app-container {
max-width: 900px;
padding: 0rem 1.5rem 5rem;
}
.profile-hero {
padding: 5rem 2rem 3rem;
}
.settings-sections {
padding: 0 2rem;
}
.modal-backdrop {
align-items: center;
padding: 2rem;
}
.modal-content {
max-width: 600px;
border-radius: 24px;
transform: translateY(0);
}
.modal-backdrop .modal-content {
box-shadow: 0 40px 120px rgba(0, 0, 0, 0.18);
}
}
@media (max-width: 767px) {
.modal-backdrop {
padding-bottom: 3.5rem;
}
.modal-content {
margin-bottom: calc(env(safe-area-inset-bottom, 0) + 2rem);
}
}
.modal-handle {
width: 38px;
height: 4px;
border-radius: 2px;
background: var(--border-color);
margin: 12px auto 0;
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1.25rem 1.25rem 0;
}
.modal-title {
font-size: 1.2rem;
font-weight: 700;
margin: 0;
}
.modal-close-x {
width: 28px;
height: 28px;
border-radius: 50%;
background: var(--background-tertiary-hover);
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: var(--foreground-secondary);
font-size: 0.8rem;
flex-shrink: 0;
transition: background 0.15s ease;
-webkit-tap-highlight-color: transparent;
}
.modal-close-x:hover { background: var(--border-color); }
.modal-close-x:active { transform: scale(0.9); }
.modal-body { padding: 0.875rem 1.25rem 0; }
.modal-subtitle {
color: var(--foreground-secondary);
font-size: 0.875rem;
margin: 0.375rem 0 1rem;
line-height: 1.45;
}
.modal-footer {
display: flex;
gap: 0.75rem;
padding: 1.25rem 1.25rem 0;
}
.modal-footer.col { flex-direction: column; gap: 0.5rem; }
/* Backward compat for old modal-close-btn (About modal) */
.modal-close-btn {
position: absolute;
top: 0.75rem;
right: 0.75rem;
background: rgba(0,0,0,0.45);
border: none;
color: white;
cursor: pointer;
width: 30px;
height: 30px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.125rem;
z-index: 10;
}
/* Module/Addon modals need more height */
#moduleManagementModal .modal-content,
#addOnManagementModal .modal-content { max-height: 91vh; }
/* About modal — iframe fills */
#aboutModal .modal-content {
padding: 0;
overflow: hidden;
background-color: transparent;
border-radius: 22px 22px 0 0;
height: 80vh;
max-height: 80vh;
}
#aboutIframe {
width: 100%;
height: 100%;
border: none;
border-radius: 22px 22px 0 0;
}
/* Module management inner lists */
.module-list { list-style: none; padding: 0; margin: 0; }
.module-list .settings-item { padding: 0.75rem 1rem; }
.module-reorder-controls { display: flex; flex-direction: column; gap: 2px; margin-left: 0.5rem; }
.module-reorder-controls i { cursor: pointer; color: var(--foreground-secondary); padding: 4px; border-radius: 4px; }
.module-reorder-controls i:hover { background-color: var(--background-tertiary-hover); }
#moduleCategoriesContainer, #addOnCategoriesContainer { display: flex; flex-direction: column; gap: 1rem; }
/* =====================
BUTTONS
===================== */
.primary-button {
flex: 1;
padding: 0.875rem;
font-size: 0.9375rem;
font-weight: 600;
border-radius: var(--radius-sm);
cursor: pointer;
border: none;
background-color: var(--accent-primary);
color: white;
font-family: var(--font-sans);
transition: background 0.15s ease, transform 0.1s ease;
-webkit-tap-highlight-color: transparent;
}
.primary-button:active { transform: scale(0.97); }
.primary-button:disabled { opacity: 0.45; cursor: not-allowed; transform: none; }
.secondary-button {
flex: 1;
padding: 0.875rem;
font-size: 0.9375rem;
font-weight: 600;
border-radius: var(--radius-sm);
cursor: pointer;
border: 1px solid var(--border-color);
background-color: transparent;
color: var(--foreground-primary);
font-family: var(--font-sans);
transition: background 0.15s ease, transform 0.1s ease;
-webkit-tap-highlight-color: transparent;
}
.secondary-button:active { transform: scale(0.97); }
/* =====================
FORM INPUTS
===================== */
.ip-input {
width: 100%;
background: var(--background-primary);
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
padding: 0.875rem 1rem;
font-size: 1rem;
color: var(--foreground-primary);
outline: none;
transition: box-shadow 0.2s, border-color 0.2s;
font-family: var(--font-sans);
}
.ip-input:focus {
border-color: var(--accent-primary);
box-shadow: 0 0 0 3px rgba(var(--accent-primary-rgb), 0.18);
}
.status-indicator { margin-top: 0.75rem; font-size: 0.875rem; min-height: 1.25rem; color: var(--foreground-secondary); }
.status-indicator.connected { color: #34c759; }
.status-indicator.error { color: #ff3b30; }
</style>
</head>
<body data-theme="dark">
<!-- ===== SKELETON LOADING ===== -->
<div id="loadingScreen">
<div class="skeleton-hero">
<div class="skeleton-circle"></div>
<div class="skeleton-line" style="width:110px"></div>
<div class="skeleton-line" style="width:70px;height:8px;opacity:0.6"></div>
</div>
<div style="display:flex;flex-direction:column;gap:0.25rem;margin-top:0.5rem">
<div class="skeleton-label"></div>
<div class="skeleton-group">
<div class="skeleton-row"><div class="skeleton-sq"></div><div class="skeleton-line" style="flex:1;max-width:60%"></div></div>
</div>
<div class="skeleton-label" style="margin-top:1.25rem"></div>
<div class="skeleton-group">
<div class="skeleton-row"><div class="skeleton-sq"></div><div class="skeleton-line" style="flex:1;max-width:55%"></div></div>
<div class="skeleton-row"><div class="skeleton-sq"></div><div class="skeleton-line" style="flex:1;max-width:65%"></div></div>
</div>
<div class="skeleton-label" style="margin-top:1.25rem"></div>
<div class="skeleton-group">
<div class="skeleton-row"><div class="skeleton-sq"></div><div class="skeleton-line" style="flex:1;max-width:50%"></div></div>
<div class="skeleton-row"><div class="skeleton-sq"></div><div class="skeleton-line" style="flex:1;max-width:70%"></div></div>
<div class="skeleton-row"><div class="skeleton-sq"></div><div class="skeleton-line" style="flex:1;max-width:60%"></div></div>
</div>
</div>
</div>
<!-- ===== MAIN SETTINGS ===== -->
<div class="app-container" id="settingsContainer" style="display:none">
<!-- PROFILE HERO -->
<header class="profile-hero">
<div class="profile-hero-bg" id="profileHeroBg"></div>
<div class="avatar-wrapper">
<img id="userAvatar" src="https://placehold.co/104/1e1e1e/a0a0a0?text=?" alt="User Avatar" />
<button class="avatar-edit-badge" id="avatarEditBadge" aria-label="Change avatar">
<i class="fas fa-pencil-alt"></i>
</button>
<input type="file" id="avatarUpload" accept="image/*" style="display:none" />
</div>
<h1 class="username" id="usernameDisplay">Profile</h1>
<a href="#" id="editUsernameBtn" class="edit-profile-link">
<i class="fas fa-pencil-alt"></i>Edit Profile
</a>
</header>
<div class="settings-sections">
<!-- PERSONAL FLAIR -->
<div class="section-label">Personal Flair</div>
<div class="settings-group">
<div class="settings-item no-hover">
<div class="icon-sq" style="--sq-color:#af52de"><i class="fas fa-palette"></i></div>
<span class="item-text">Accent Color</span>
<div class="accent-swatches" id="accentSwatches">
<button class="swatch" data-color="#ff9500" data-rgb="255,149,0" style="background:#ff9500" title="Amber" aria-label="Amber"></button>
<button class="swatch" data-color="#007aff" data-rgb="0,122,255" style="background:#007aff" title="Blue" aria-label="Blue"></button>
<button class="swatch" data-color="#af52de" data-rgb="175,82,222" style="background:#af52de" title="Purple" aria-label="Purple"></button>
<button class="swatch" data-color="#34c759" data-rgb="52,199,89" style="background:#34c759" title="Green" aria-label="Green"></button>
<button class="swatch" data-color="#ff453a" data-rgb="255,69,58" style="background:#ff453a" title="Red" aria-label="Red"></button>
<button class="swatch" data-color="#ff375f" data-rgb="255,55,95" style="background:#ff375f" title="Pink" aria-label="Pink"></button>
</div>
</div>
</div>
<!-- PROFILE -->
<div class="section-label">Profile</div>
<div class="settings-group">
<div class="settings-item" id="switchProfileItem">
<div class="icon-sq" style="--sq-color:#007aff"><i class="fas fa-users"></i></div>
<span class="item-text">Change Profile</span>
<i class="item-action fas fa-chevron-right"></i>
</div>
</div>
<!-- APPEARANCE & NOTIFICATIONS -->
<div class="section-label">Appearance & Notifications</div>
<div class="settings-group">
<div class="settings-item">
<div class="icon-sq" style="--sq-color:#6e6e73"><i class="fas fa-moon"></i></div>
<span class="item-text">Light Mode</span>
<div class="item-action">
<label class="toggle-switch"><input type="checkbox" id="themeToggle" /><span class="slider"></span></label>
</div>
</div>
<div class="settings-item">
<div class="icon-sq" style="--sq-color:#ff3b30"><i class="fas fa-bell"></i></div>
<span class="item-text">Push Notifications</span>
<div class="item-action">
<label class="toggle-switch"><input type="checkbox" id="pushNotificationsToggle" /><span class="slider"></span></label>
</div>
</div>
</div>
<!-- PLAYBACK -->
<div class="section-label">Playback</div>
<div class="settings-group">
<div class="settings-item">
<div class="icon-sq" style="--sq-color:#32ade6"><i class="fas fa-download"></i></div>
<span class="item-text">Download Quality</span>
<div class="item-action custom-dropdown-wrapper" id="customDropdownWrapper">
<button type="button" class="custom-dropdown-btn" id="customDropdownBtn">
<span id="customDropdownSelected">1080p</span>
<i class="fas fa-chevron-down" style="font-size:0.65rem"></i>
</button>
<ul class="custom-dropdown-list" id="customDropdownList">
<li data-value="1080p">1080p (Best)</li>
<li data-value="720p">720p (Good)</li>
<li data-value="360p">360p (Data Saver)</li>
</ul>
</div>
</div>
</div>
<!-- EXTENSIONS & POWER -->
<div class="section-label">Extensions & Power</div>
<div class="settings-group">
<div class="settings-item" id="extensionServerItem">
<div class="icon-sq" style="--sq-color:#34c759"><i class="fas fa-server"></i></div>
<span class="item-text">Extension Server</span>
<i class="item-action fas fa-chevron-right"></i>
</div>
<div class="settings-item" id="moduleManagementItem">
<div class="icon-sq" style="--sq-color:#ff9500"><i class="fas fa-puzzle-piece"></i></div>
<span class="item-text">Module Management</span>
<i class="item-action fas fa-chevron-right"></i>
</div>
<div class="settings-item" id="addOnManagementItem">
<div class="icon-sq" style="--sq-color:#c8870a"><i class="fas fa-puzzle-piece"></i></div>
<span class="item-text">Add-on Management</span>
<i class="item-action fas fa-chevron-right"></i>
</div>
</div>
<!-- SYSTEM -->
<div class="section-label">System</div>
<div class="settings-group">
<div class="settings-item" id="clearCacheItem">
<div class="icon-sq" style="--sq-color:#ff453a"><i class="fas fa-broom"></i></div>
<span class="item-text">Clear Cache</span>
<i class="item-action fas fa-chevron-right"></i>
</div>
<div class="settings-item" id="aboutItem">
<div class="icon-sq" style="--sq-color:#6e6e73"><i class="fas fa-info-circle"></i></div>
<span class="item-text">About</span>
<i class="item-action fas fa-chevron-right"></i>
</div>
</div>
</div><!-- /settings-sections -->
</div><!-- /app-container -->
<!-- ===========================
MODALS (Bottom Sheets)
=========================== -->
<!-- About Modal (iframe) -->
<div id="aboutModal" class="modal-backdrop">
<div class="modal-content">
<iframe id="aboutIframe" src="about.html" title="About the developer"></iframe>
</div>
<button id="closeAboutModalBtn" class="modal-close-btn">×</button>
</div>
<!-- Extension Server Modal -->
<div id="extensionServerModal" class="modal-backdrop">
<div class="modal-content">
<div class="modal-handle"></div>
<div class="modal-header">
<h2 class="modal-title">Extension Server</h2>
<button class="modal-close-x" id="closeModalBtn"><i class="fas fa-times"></i></button>
</div>
<div class="modal-body">
<p class="modal-subtitle">Enter the IP address of your extension server.</p>
<input type="text" id="ip-input" class="ip-input" placeholder="e.g., 192.168.1.100" />
<div id="status-indicator" class="status-indicator"></div>
</div>
<div class="modal-footer">
<button id="checkConnectionBtn" class="secondary-button">Check Connection</button>
<button id="saveConnectionBtn" class="primary-button" disabled>Save</button>
</div>
<!-- spacer -->
<div style="height:1rem"></div>
</div>
</div>
<!-- Edit Profile Modal -->
<div id="editProfileModal" class="modal-backdrop">
<div class="modal-content">
<div class="modal-handle"></div>
<div class="modal-header">
<h2 class="modal-title">Edit Profile</h2>
<button class="modal-close-x" id="closeEditProfileModalBtn"><i class="fas fa-times"></i></button>
</div>
<div class="modal-body">
<p class="modal-subtitle">Enter your new username.</p>
<input type="text" id="username-input" class="ip-input" placeholder="New username" />
</div>
<div class="modal-footer">
<button id="saveUsernameBtn" class="primary-button">Save</button>
</div>
<div style="height:1rem"></div>
</div>
</div>
<!-- Module Management Modal -->
<div id="moduleManagementModal" class="modal-backdrop">
<div class="modal-content">
<div class="modal-handle"></div>
<div class="modal-header">
<h2 class="modal-title">Module Management</h2>
<button class="modal-close-x" id="closeModuleModalBtn"><i class="fas fa-times"></i></button>
</div>
<div class="modal-body">
<p class="modal-subtitle">Enable/disable modules and set their preferred order.</p>
<div id="moduleCategoriesContainer"></div>
</div>
<div class="modal-footer">
<button id="saveModulePreferencesBtn" class="primary-button">Save Preferences</button>
</div>
<div style="height:1rem"></div>
</div>
</div>
<!-- Add-on Management Modal -->
<div id="addOnManagementModal" class="modal-backdrop">
<div class="modal-content">
<div class="modal-handle"></div>
<div class="modal-header">
<h2 class="modal-title">Add-on Management</h2>
<button class="modal-close-x" id="closeAddOnModalBtn"><i class="fas fa-times"></i></button>
</div>
<div class="modal-body">
<p class="modal-subtitle">Configure your installed add-ons.</p>
<div id="addOnCategoriesContainer"></div>
</div>
<div class="modal-footer">
<button id="saveAddOnPreferencesBtn" class="primary-button" style="display:none">Save Preferences</button>
</div>
<div style="height:1rem"></div>
</div>
</div>
<!-- Clear Cache Modal -->
<div id="clearCacheModal" class="modal-backdrop">
<div class="modal-content">
<div class="modal-handle"></div>
<div class="modal-header">
<h2 class="modal-title">Clear Cache</h2>
<button class="modal-close-x" id="closeCacheModalBtnCross"><i class="fas fa-times"></i></button>
</div>
<div class="modal-body">
<p class="modal-subtitle">This can help resolve display issues or problems with outdated content.</p>
</div>
<div class="modal-footer col">
<button id="clearPagesBtn" class="secondary-button">Clear Cached Pages &amp; Resources</button>
<button id="clearAllBtn" class="primary-button" style="background-color:var(--destructive)">Clear All Site Data (Logout)</button>
<button id="closeCacheModalBtn" class="secondary-button">Cancel</button>
</div>
<div style="height:0.5rem"></div>
</div>
</div>
<!-- ===========================
JAVASCRIPT
=========================== -->
<script>
document.addEventListener("DOMContentLoaded", () => {
let currentProfile = null;
const loadingScreen = document.getElementById("loadingScreen");
const settingsContainer = document.getElementById("settingsContainer");
const userAvatar = document.getElementById("userAvatar");
const avatarUpload = document.getElementById("avatarUpload");
const usernameDisplay = document.getElementById("usernameDisplay");
const switchProfileItem = document.getElementById("switchProfileItem");
const extensionServerItem = document.getElementById("extensionServerItem");
function getApiUrl(path) {
return "";
}
async function initialize() {
const profileId = localStorage.getItem("currentProfileId");
if (!profileId) { window.location.href = "login.html"; return; }
try {
const url = getApiUrl(`/profiles/${profileId}`);
if (!url) return;
const response = await fetch(url);
if (!response.ok) throw new Error("Profile not found");
currentProfile = await response.json();
renderSettingsForProfile();
setupEventListeners();
loadingScreen.style.display = "none";
settingsContainer.style.display = "block";
} catch (error) {
console.error("Error during initialization:", error);
window.parent.showToast("Failed to load profile. Please log in again.");
}
}
function renderSettingsForProfile() {
if (!currentProfile) return;
userAvatar.src = currentProfile.avatar_url;
usernameDisplay.textContent = currentProfile.name;
const { settings } = currentProfile;
document.body.dataset.theme = settings.theme;
document.getElementById("themeToggle").checked = settings.theme === "light";
document.getElementById("pushNotificationsToggle").checked = settings.pushNotifications;
updateDropdownSelection(settings.downloadQuality);
}
async function updateSetting(key, value) {
if (!currentProfile) return;
currentProfile.settings[key] = value;
if (key === "theme") document.body.dataset.theme = value;
try {
const url = getApiUrl(`/profiles/${currentProfile.id}`);
if (!url) return;
await fetch(url, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(currentProfile),
});
} catch (error) {
console.error("Failed to save settings:", error);
window.parent.showToast("Could not sync settings with the server.");
}
}
async function uploadAvatar(file) {
const formData = new FormData();
formData.append("avatar", file);
try {
const url = getApiUrl(`/profiles/${currentProfile.id}/avatar`);
if (!url) return;
const response = await fetch(url, { method: "POST", body: formData });
if (!response.ok) throw new Error("Avatar upload failed");
const updatedProfile = await response.json();
currentProfile.avatar_url = updatedProfile.avatar_url;
userAvatar.src = currentProfile.avatar_url;
} catch (error) {
console.error("Failed to upload avatar:", error);
window.parent.showToast("Could not upload new avatar.");
}
}
function setupEventListeners() {
document.getElementById("avatarEditBadge").addEventListener("click", () => avatarUpload.click());
userAvatar.addEventListener("click", () => avatarUpload.click());
avatarUpload.addEventListener("change", (e) => {
if (e.target.files && e.target.files[0]) uploadAvatar(e.target.files[0]);
});
switchProfileItem.addEventListener("click", () => {
window.parent.postMessage({ action: "switchProfile" }, "*");
});
document.getElementById("themeToggle").addEventListener("change", (e) => {
updateSetting("theme", e.target.checked ? "light" : "dark");
});
document.getElementById("pushNotificationsToggle").addEventListener("change", (e) => {
updateSetting("pushNotifications", e.target.checked);
});
setupAccentColorPicker();
setupCustomDropdown();
setupExtensionServerModal();
setupEditProfileModal();
setupModuleManagementModal();
setupAddOnManagementModal();
setupCacheModal();
setupAboutModal();
}
/* ── ACCENT COLOR PICKER ─────────────────────── */
function applyProfileAccent(color, rgb) {
document.documentElement.style.setProperty("--profile-accent", color);
document.documentElement.style.setProperty("--profile-accent-rgb", rgb);
const heroBg = document.getElementById("profileHeroBg");
if (heroBg) {
heroBg.style.background = `radial-gradient(ellipse 90% 70% at 50% 0%, rgba(${rgb}, 0.28) 0%, transparent 68%)`;
}
}
function setupAccentColorPicker() {
const swatches = document.querySelectorAll("#accentSwatches .swatch");
const savedColor = localStorage.getItem("profile_accent_color") || "#ff9500";
const savedRgb = localStorage.getItem("profile_accent_rgb") || "255,149,0";
applyProfileAccent(savedColor, savedRgb);
swatches.forEach(s => s.classList.toggle("active", s.dataset.color === savedColor));
swatches.forEach(swatch => {
swatch.addEventListener("click", () => {
const color = swatch.dataset.color;
const rgb = swatch.dataset.rgb;
applyProfileAccent(color, rgb);
localStorage.setItem("profile_accent_color", color);
localStorage.setItem("profile_accent_rgb", rgb);
swatches.forEach(s => s.classList.remove("active"));
swatch.classList.add("active");
});
});
}
/* ── ABOUT MODAL ─────────────────────────────── */
function setupAboutModal() {
const aboutItem = document.getElementById("aboutItem");
const aboutModal = document.getElementById("aboutModal");
const closeAboutBtn = document.getElementById("closeAboutModalBtn");
let tapCount = 0, tapTimer = null;
const TAP_WINDOW_MS = 500, REQUIRED_TAPS = 3;
function resetTapAndOpenAbout() {
tapCount = 0;
if (tapTimer) { clearTimeout(tapTimer); tapTimer = null; }
aboutModal.classList.add("active");
}
aboutItem.addEventListener("click", () => {
tapCount++;
if (tapTimer) clearTimeout(tapTimer);
if (tapCount === REQUIRED_TAPS) {
tapCount = 0; tapTimer = null;
if (window.parent && window.parent.openPopup) window.parent.openPopup("/portal.html");
else aboutModal.classList.add("active");
} else {
tapTimer = setTimeout(resetTapAndOpenAbout, TAP_WINDOW_MS);
}
});
const closeAboutModal = () => aboutModal.classList.remove("active");
closeAboutBtn.addEventListener("click", closeAboutModal);
aboutModal.addEventListener("click", (e) => { if (e.target === aboutModal) closeAboutModal(); });
}
/* ── CACHE MODAL ─────────────────────────────── */
function setupCacheModal() {
const modal = document.getElementById("clearCacheModal");
if (!modal) return;
const clearCacheItem = document.getElementById("clearCacheItem");
const clearPagesBtn = document.getElementById("clearPagesBtn");
const clearAllBtn = document.getElementById("clearAllBtn");
const closeBtn = document.getElementById("closeCacheModalBtn");
const closeBtnCross = document.getElementById("closeCacheModalBtnCross");
clearCacheItem.addEventListener("click", () => modal.classList.add("active"));
const closeModal = () => modal.classList.remove("active");
closeBtn.addEventListener("click", closeModal);
closeBtnCross.addEventListener("click", closeModal);
modal.addEventListener("click", (e) => { if (e.target === modal) closeModal(); });
clearPagesBtn.addEventListener("click", () => {
if (confirm("This will clear cached pages and resources, forcing them to be re-downloaded. Are you sure?")) {
window.parent.postMessage({ action: "clearPageCache" }, "*");
closeModal();
}
});
clearAllBtn.addEventListener("click", () => {
if (confirm("WARNING: This will delete ALL site data, including your login sessions, settings, and all cached content. You will be logged out. Are you sure?")) {
window.parent.postMessage({ action: "clearAllData" }, "*");
closeModal();
}
});
}
/* ── CUSTOM DROPDOWN ─────────────────────────── */
function setupCustomDropdown() {
const wrapper = document.getElementById("customDropdownWrapper");
const btn = document.getElementById("customDropdownBtn");
const list = document.getElementById("customDropdownList");
btn.addEventListener("click", () => wrapper.classList.toggle("is-open"));
list.querySelectorAll("li").forEach((opt) => {
opt.addEventListener("click", () => {
updateSetting("downloadQuality", opt.dataset.value);
updateDropdownSelection(opt.dataset.value);
wrapper.classList.remove("is-open");
});
});
document.addEventListener("click", (e) => {
if (!wrapper.contains(e.target)) wrapper.classList.remove("is-open");
});
}
/* ── EXTENSION SERVER MODAL ──────────────────── */
function setupExtensionServerModal() {
const modal = document.getElementById("extensionServerModal");
const ipInput = document.getElementById("ip-input");
const checkBtn = document.getElementById("checkConnectionBtn");
const saveBtn = document.getElementById("saveConnectionBtn");
const closeBtn = document.getElementById("closeModalBtn");
const statusIndicator = document.getElementById("status-indicator");
extensionServerItem.addEventListener("click", () => {
ipInput.value = localStorage.getItem("extension_server_ip") || "";
statusIndicator.textContent = "";
statusIndicator.className = "status-indicator";
saveBtn.disabled = true;
modal.classList.add("active");
});
closeBtn.addEventListener("click", () => modal.classList.remove("active"));
checkBtn.addEventListener("click", async () => {
const ip = ipInput.value.trim();
if (!ip) return;
statusIndicator.textContent = "Connecting...";
statusIndicator.className = "status-indicator";
saveBtn.disabled = true;
try {
const response = await fetch(`http://${ip}:7275/identify`, { signal: AbortSignal.timeout(5000) });
if (response.ok) {
const data = await response.json();
if ("Animex Extension API" === data.app) {
statusIndicator.textContent = "Connection successful!";
statusIndicator.className = "status-indicator connected";
localStorage.setItem("extension_server_ip", ip);
saveBtn.disabled = false;
} else throw new Error("Not a valid server.");
} else throw new Error("Connection failed.");
} catch (error) {
statusIndicator.textContent = "Connection failed. Check IP and server status.";
statusIndicator.className = "status-indicator error";
}
});
saveBtn.addEventListener("click", () => modal.classList.remove("active"));
}
/* ── EDIT PROFILE MODAL ──────────────────────── */
function setupEditProfileModal() {
const modal = document.getElementById("editProfileModal");
const editUsernameBtn = document.getElementById("editUsernameBtn");
const usernameInput = document.getElementById("username-input");
const saveBtn = document.getElementById("saveUsernameBtn");
const closeBtn = document.getElementById("closeEditProfileModalBtn");
editUsernameBtn.addEventListener("click", (e) => {
e.preventDefault();
usernameInput.value = currentProfile.name;
modal.classList.add("active");
});
closeBtn.addEventListener("click", () => modal.classList.remove("active"));
saveBtn.addEventListener("click", async () => {
const newName = usernameInput.value.trim();
if (newName && newName !== currentProfile.name) {
try {
const url = getApiUrl(`/profiles/${currentProfile.id}`);
if (!url) return;
const response = await fetch(url, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: newName }),
});
if (!response.ok) throw new Error("Failed to update username");
const updatedProfile = await response.json();
currentProfile.name = updatedProfile.name;
usernameDisplay.textContent = currentProfile.name;
modal.classList.remove("active");
} catch (error) {
console.error("Failed to update username:", error);
window.parent.showToast("Could not update username.");
}
} else modal.classList.remove("active");
});
}
/* ── MODULE MANAGEMENT MODAL ─────────────────── */
async function setupModuleManagementModal() {
const modal = document.getElementById("moduleManagementModal");
const moduleManagementItem = document.getElementById("moduleManagementItem");
const closeBtn = document.getElementById("closeModuleModalBtn");
const moduleCategoriesContainer = document.getElementById("moduleCategoriesContainer");
const saveModulePreferencesBtn = document.getElementById("saveModulePreferencesBtn");
moduleManagementItem.addEventListener("click", async () => {
modal.classList.add("active");
await renderModules();
});
closeBtn.addEventListener("click", () => modal.classList.remove("active"));
saveModulePreferencesBtn.addEventListener("click", async () => {
try {
const url = getApiUrl(`/profiles/${currentProfile.id}/module-preferences`);
if (!url) return;
const response = await fetch(url, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(currentProfile.settings.module_preferences),
});
if (!response.ok) throw new Error("Failed to save module preferences");
window.parent.showToast("Module preferences saved successfully!", "success");
} catch (error) {
console.error("Error saving module preferences:", error);
window.parent.showToast("Could not save module preferences.", "error");
}
modal.classList.remove("active");
});
async function renderModules() {
moduleCategoriesContainer.innerHTML = "Loading modules...";
try {
const url = getApiUrl("/modules");
if (!url) return;
const response = await fetch(url);
if (!response.ok) throw new Error("Failed to fetch modules");
const allModules = await response.json();
const categorizedModules = {
"Anime Streaming": [], "Anime Downloading": [], "Manga": [],
"Hentai (Anime) Streaming": [], "Hentai (Anime) Downloading": [],
"Hentai (Manga)": [], "Generic": [],
};
allModules.forEach((mod) => {
const modTypes = [].concat(mod.type || []);
let categorized = false;
if (modTypes.includes("ANIME_STREAMER")) {
mod.nsfw ? categorizedModules["Hentai (Anime) Streaming"].push(mod) : categorizedModules["Anime Streaming"].push(mod);
categorized = true;
}
if (modTypes.includes("ANIME_DOWNLOADER")) {
mod.nsfw ? categorizedModules["Hentai (Anime) Downloading"].push(mod) : categorizedModules["Anime Downloading"].push(mod);
categorized = true;
}
if (modTypes.includes("MANGA_READER")) {
mod.nsfw ? categorizedModules["Hentai (Manga)"].push(mod) : categorizedModules["Manga"].push(mod);
categorized = true;
}
if (!categorized) categorizedModules["Generic"].push(mod);
});
moduleCategoriesContainer.innerHTML = "";
for (const category in categorizedModules) {
if (categorizedModules[category].length === 0) continue;
(() => {
const preferredOrder = currentProfile.settings.module_preferences[category] || [];
categorizedModules[category].sort((a, b) => {
const iA = preferredOrder.indexOf(a.id), iB = preferredOrder.indexOf(b.id);
return iA === -1 && iB === -1 ? 0 : iA === -1 ? 1 : iB === -1 ? -1 : iA - iB;
});
const categoryDiv = document.createElement("div");
categoryDiv.classList.add("settings-group");
categoryDiv.innerHTML = `<h3 style="padding:.875rem 1rem;margin:0;border-bottom:1px solid var(--border-color);font-size:.85rem;font-weight:600;">${category}</h3><ul class="module-list" data-category-key="${category}"></ul>`;
moduleCategoriesContainer.appendChild(categoryDiv);
const moduleList = categoryDiv.querySelector(".module-list");
categorizedModules[category].forEach((mod) => {
const moduleItem = document.createElement("li");
moduleItem.classList.add("settings-item");
moduleItem.dataset.moduleId = mod.id;
moduleItem.innerHTML = `<span class="item-text">${mod.name}</span><div class="item-action"><label class="toggle-switch"><input type="checkbox" data-module-id="${mod.id}" ${mod.enabled ? "checked" : ""}><span class="slider"></span></label></div><div class="module-reorder-controls"><i class="fas fa-chevron-up reorder-up" data-module-id="${mod.id}"></i><i class="fas fa-chevron-down reorder-down" data-module-id="${mod.id}"></i></div>`;
moduleList.appendChild(moduleItem);
const toggle = moduleItem.querySelector('input[type="checkbox"]');
toggle.addEventListener("change", async (e) => {
const moduleId = e.target.dataset.moduleId, enable = e.target.checked;
try {
const url = getApiUrl(`/modules/toggle/${moduleId}/${enable}`);
if (!url) return;
const res = await fetch(url);
if (!res.ok) throw new Error("Failed to toggle module");
const um = allModules.find(m => m.id === moduleId);
if (um) um.enabled = enable;
} catch (err) {
console.error(`Error toggling module ${moduleId}:`, err);
window.parent.showToast(`Could not toggle module ${moduleId}.`);
e.target.checked = !enable;
}
});
moduleItem.querySelector(".reorder-up").addEventListener("click", () => reorderModule(mod.id, category, "up"));
moduleItem.querySelector(".reorder-down").addEventListener("click", () => reorderModule(mod.id, category, "down"));
});
})();
}
} catch (error) {
console.error("Error fetching modules:", error);
moduleCategoriesContainer.innerHTML = '<p style="color:var(--destructive)">Failed to load modules. Please check server connection.</p>';
}
}
function reorderModule(moduleId, category, direction) {
const moduleListEl = moduleCategoriesContainer.querySelector(`.module-list[data-category-key="${category}"]`);
if (!moduleListEl) return;
const items = Array.from(moduleListEl.children);
const index = items.findIndex(item => item.dataset.moduleId === moduleId);
if (index === -1) return;
let newIndex = index;
if (direction === "up" && index > 0) newIndex = index - 1;
else if (direction === "down" && index < items.length - 1) newIndex = index + 1;
else return;
const [moved] = items.splice(index, 1);
items.splice(newIndex, 0, moved);
moduleListEl.innerHTML = "";
items.forEach(item => moduleListEl.appendChild(item));
currentProfile.settings.module_preferences[category] = items.map(item => item.dataset.moduleId);
}
}
/* ── ADD-ON MANAGEMENT MODAL ─────────────────── */
async function setupAddOnManagementModal() {
const modal = document.getElementById("addOnManagementModal");
const addOnManagementItem = document.getElementById("addOnManagementItem");
const closeBtn = document.getElementById("closeAddOnModalBtn");
const addOnCategoriesContainer = document.getElementById("addOnCategoriesContainer");
const saveAddOnPreferencesBtn = document.getElementById("saveAddOnPreferencesBtn");
saveAddOnPreferencesBtn.style.display = "none";
addOnManagementItem.addEventListener("click", async () => {
modal.classList.add("active");
await renderAddOns();
});
closeBtn.addEventListener("click", () => modal.classList.remove("active"));
async function renderAddOns() {
addOnCategoriesContainer.innerHTML = "Loading add-ons...";
try {
const url = getApiUrl("/settings/add-ons");
if (!url) return;
const response = await fetch(url);
if (!response.ok) throw new Error("Failed to fetch add-ons");
const allAddOnsSettings = await response.json();
addOnCategoriesContainer.innerHTML = "";
if (allAddOnsSettings.length === 0) {
addOnCategoriesContainer.innerHTML = "<p>No add-ons with settings found.</p>";
return;
}
const groupedSettings = allAddOnsSettings.reduce((acc, setting) => {
const extId = setting.extension_id;
if (!acc[extId]) acc[extId] = { title: extId, settings: [] };
acc[extId].settings.push(setting);
return acc;
}, {});
for (const extensionId in groupedSettings) {
const { title, settings } = groupedSettings[extensionId];
const addOnGroupDiv = document.createElement("div");
addOnGroupDiv.classList.add("settings-group");
const settingsList = document.createElement("div");
settingsList.classList.add("add-on-settings-list");
settings.forEach((setting) => {
const settingItem = document.createElement("div");
settingItem.classList.add("settings-item");
settingItem.innerHTML = `<i class="item-icon ${setting.icon || "fas fa-cog"}" style="color:var(--foreground-secondary);width:24px;text-align:center"></i><span class="item-text">${setting.text}</span><div class="item-action"><label class="toggle-switch"><input type="checkbox" id="addon-${extensionId}-${setting.id}" data-setting-id="${setting.id}"><span class="slider"></span></label></div>`;
settingsList.appendChild(settingItem);
});
addOnGroupDiv.innerHTML = `<h3 style="padding:.875rem 1rem;margin:0;border-bottom:1px solid var(--border-color);font-size:.85rem;font-weight:600;">${title}</h3>`;
addOnGroupDiv.appendChild(settingsList);
addOnCategoriesContainer.appendChild(addOnGroupDiv);
settings.forEach((setting) => {
const toggle = document.getElementById(`addon-${extensionId}-${setting.id}`);
if (toggle) {
if (currentProfile && currentProfile.settings) toggle.checked = !!currentProfile.settings[setting.id];
toggle.addEventListener("change", (e) => updateSetting(e.target.dataset.settingId, e.target.checked));
}
});
}
} catch (error) {
console.error("Error fetching add-ons:", error);
addOnCategoriesContainer.innerHTML = '<p style="color:var(--destructive)">Failed to load add-ons. Please check server connection.</p>';
}
}
}
/* ── DROPDOWN SELECTION ──────────────────────── */
function updateDropdownSelection(value) {
const list = document.getElementById("customDropdownList");
const selectedSpan = document.getElementById("customDropdownSelected");
let found = false;
list.querySelectorAll("li").forEach((opt) => {
const isSelected = opt.dataset.value === value;
opt.classList.toggle("selected", isSelected);
if (isSelected) { selectedSpan.textContent = opt.textContent; found = true; }
});
if (!found) {
const firstOpt = list.querySelector("li");
if (firstOpt) { firstOpt.classList.add("selected"); selectedSpan.textContent = firstOpt.textContent; }
}
}
initialize();
});
</script>
</body>
</html>