<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title data-i18n="settings.title">Settings</title>
<link rel="stylesheet" href="lingxia://lxapp/public/downloads.css">
<style>
body { display: flex; min-height: 100vh; }
.sidebar {
width: 176px; flex-shrink: 0; background: #fff;
border-right: 1px solid #e5e5ea; padding: 20px 0;
position: sticky; top: 0; height: 100vh; overflow-y: auto;
}
.sidebar-title { font-size: 18px; font-weight: 700; padding: 0 16px 16px; }
.nav-item {
display: flex; align-items: center; gap: 10px;
padding: 9px 16px; font-size: 13px; font-weight: 500;
color: #6e6e73; cursor: pointer; transition: all 80ms;
border-left: 3px solid transparent; text-decoration: none;
}
.nav-item:hover { background: #f2f4f7; color: #1d1d1f; }
.nav-item.active { color: #007AFF; background: #f0f5ff; border-left-color: #007AFF; }
.nav-item svg { width: 16px; height: 16px; flex-shrink: 0; }
.main { flex: 1; min-width: 0; overflow-y: auto; }
.main-header {
position: sticky; top: 0; z-index: 10;
display: flex; align-items: center; justify-content: space-between;
padding: 14px 24px; background: rgba(245,245,247,0.9);
backdrop-filter: blur(12px); border-bottom: 1px solid #e5e5ea;
}
.main-header h2 { font-size: 17px; font-weight: 600; }
.panel { display: none; }
.panel.active { display: block; }
.settings-content { padding: 18px 24px 28px; display: flex; flex-direction: column; gap: 16px; }
.settings-content .section { background: #fff; border: 1px solid #e5e5ea; border-radius: 16px; overflow: hidden; }
.settings-content .row {
display: flex; align-items: center; justify-content: space-between; gap: 14px;
padding: 13px 16px; border-top: 1px solid #f2f2f7;
}
.settings-content .row:first-child { border-top: none; }
.settings-content .row-main { min-width: 0; flex: 1; }
.settings-content .row-title { font-size: 13px; font-weight: 600; }
.settings-content .row-value { font-size: 12px; color: #667085; margin-top: 3px; word-break: break-word; line-height: 1.45; }
.row-control { width: min(300px, 100%); flex-shrink: 0; }
.row-control.compact { width: 132px; }
.row-control.language { width: 304px; }
.row-actions { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
.settings-input {
width: 100%; height: 36px; border-radius: 11px; border: 1px solid #d0d5dd;
padding: 0 11px; background: #fff; color: #101828; font-size: 13px;
}
.settings-input:focus { outline: none; border-color: #84caff; box-shadow: 0 0 0 3px rgba(0,122,255,0.12); }
.settings-input:disabled { background: #f5f5f7; color: #98a2b3; cursor: not-allowed; }
.language-options {
display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 3px;
padding: 3px; border: 1px solid #d0d5dd; border-radius: 8px; background: #f2f4f7;
}
.language-option {
height: 32px; border: 0; border-radius: 5px; color: #667085; background: transparent;
font-size: 12px; font-weight: 600; transition: color 120ms, background 120ms, box-shadow 120ms;
}
.language-option:hover:not(:disabled) { color: #344054; background: rgba(255,255,255,0.62); }
.language-option:focus-visible { outline: 2px solid #84caff; outline-offset: 1px; }
.language-option.active {
color: #175cd3; background: #fff;
box-shadow: 0 1px 3px rgba(16,24,40,0.12), 0 0 0 1px rgba(16,24,40,0.04);
}
.language-option:disabled { opacity: 0.55; cursor: default; }
.row-action { padding: 8px 13px; border-radius: 999px; background: #007AFF; color: #fff; font-size: 12px; font-weight: 600; white-space: nowrap; }
.row-action.secondary { background: #eef1f6; color: #344054; }
.row-action:hover, .row-action:focus-visible { filter: brightness(0.97); }
.row-action:disabled { opacity: 0.55; cursor: default; filter: none; }
.pill { flex-shrink: 0; font-size: 11px; padding: 4px 9px; border-radius: 999px; font-weight: 600; }
.pill.is-active { background: #ecfdf3; color: #067647; }
.pill.is-success { background: #ecfdf3; color: #067647; }
.pill.is-disabled { background: #f2f4f7; color: #475467; }
.pill.is-error { background: #fff1f3; color: #c01048; }
.pill.is-unsupported { background: #fff7ed; color: #b54708; }
.pill.is-pending { background: #eef4ff; color: #175cd3; }
.pill.is-ready { background: #ecfdf3; color: #067647; }
.pill.is-empty { background: #f2f4f7; color: #475467; }
.proxy-stack { display: flex; flex-direction: column; gap: 16px; }
.proxy-block {
background:
radial-gradient(circle at top right, rgba(0,122,255,0.12), transparent 32%),
linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);
border: 1px solid #dbe7ff;
border-radius: 18px;
overflow: hidden;
}
.proxy-block-head {
display: flex; align-items: flex-start; justify-content: space-between; gap: 16px;
padding: 18px 18px 14px;
border-bottom: 1px solid rgba(219, 231, 255, 0.9);
}
.proxy-kicker { font-size: 11px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: #175cd3; }
.proxy-title { margin-top: 6px; font-size: 22px; font-weight: 700; letter-spacing: -0.02em; }
.proxy-subtitle { margin-top: 8px; max-width: 720px; font-size: 13px; line-height: 1.5; color: #475467; }
.proxy-block-body { padding: 16px 18px 18px; display: flex; flex-direction: column; gap: 16px; }
.proxy-mode-group {
display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px;
}
.proxy-mode {
text-align: left; padding: 14px 15px; border-radius: 16px;
border: 1px solid #d0d5dd; background: rgba(255,255,255,0.88);
transition: transform 120ms, border-color 120ms, box-shadow 120ms, background 120ms;
}
.proxy-mode:hover, .proxy-mode:focus-visible {
border-color: #84caff; box-shadow: 0 10px 24px rgba(15, 23, 42, 0.08); transform: translateY(-1px);
}
.proxy-mode.active {
border-color: #0b57d0; background: linear-gradient(180deg, #f0f7ff 0%, #e8f1ff 100%);
box-shadow: 0 14px 28px rgba(11, 87, 208, 0.14);
}
.proxy-mode:disabled {
cursor: not-allowed; opacity: 0.52; transform: none; box-shadow: none;
border-color: #d0d5dd; background: #f8fafc;
}
.proxy-mode-label { display: block; font-size: 13px; font-weight: 700; color: #101828; }
.proxy-mode-desc { display: block; margin-top: 6px; font-size: 12px; line-height: 1.45; color: #667085; }
.proxy-helper {
font-size: 12px; line-height: 1.45; color: #667085;
}
.proxy-helper.is-warning { color: #b54708; }
.proxy-mini {
background: #fff; border: 1px solid #e5e7eb; border-radius: 14px; overflow: hidden;
}
.proxy-mini .row { padding: 12px 14px; }
.autoswitch-grid { display: flex; flex-direction: column; gap: 16px; }
.autoswitch-table {
width: 100%; border-collapse: collapse; background: #fff;
border: 1px solid #e5e7eb; border-radius: 14px; overflow: hidden;
}
.autoswitch-table th, .autoswitch-table td {
padding: 12px 14px; border-top: 1px solid #edf0f4; text-align: left;
font-size: 12px; vertical-align: top;
}
.autoswitch-table thead th {
border-top: none; font-size: 11px; font-weight: 700; letter-spacing: 0.04em;
text-transform: uppercase; color: #667085; background: #f8fafc;
}
.autoswitch-profile {
display: inline-flex; align-items: center; gap: 8px; padding: 6px 10px;
border-radius: 999px; font-size: 12px; font-weight: 600;
border: 1px solid #d0d5dd; background: #fff;
}
.autoswitch-profile.is-proxy { color: #175cd3; background: #eef4ff; border-color: #bfd4ff; }
.autoswitch-profile.is-direct { color: #475467; background: #f8fafc; }
.autoswitch-card {
background: #fff; border: 1px solid #e5e7eb; border-radius: 14px; padding: 16px;
}
.autoswitch-card-title { font-size: 15px; font-weight: 700; color: #111827; }
.autoswitch-card-copy { margin-top: 6px; font-size: 12px; line-height: 1.5; color: #667085; }
.autoswitch-card-note {
margin-top: 12px; padding: 12px 14px; border-radius: 12px;
background: linear-gradient(180deg, #f8fbff 0%, #eef6ff 100%);
border: 1px solid #dbe7ff; color: #36536b; font-size: 12px; line-height: 1.55;
}
.autoswitch-card-note strong { color: #0f172a; }
.autoswitch-card-actions { margin-top: 14px; display: flex; align-items: center; gap: 10px; }
.autoswitch-rule-row td { background: #fff; }
.autoswitch-rule-input {
width: 100%; height: 34px; border-radius: 10px; border: 1px solid #d0d5dd;
padding: 0 10px; background: #fff; color: #101828; font-size: 12px;
}
.autoswitch-rule-input:focus {
outline: none; border-color: #84caff; box-shadow: 0 0 0 3px rgba(0,122,255,0.12);
}
.autoswitch-rule-cell {
display: flex; align-items: center; justify-content: space-between; gap: 10px;
}
.autoswitch-rule-segment {
display: inline-flex; align-items: center; gap: 4px; padding: 4px;
border-radius: 999px;
background: linear-gradient(180deg, #f8fafc 0%, #eef2f6 100%);
border: 1px solid #d7dee7;
box-shadow: inset 0 1px 0 rgba(255,255,255,0.8);
}
.autoswitch-rule-action {
min-width: 72px; height: 30px; padding: 0 14px; border-radius: 999px; border: none;
background: transparent; color: #526072; font-size: 12px; font-weight: 700;
letter-spacing: 0.01em; transition: background 120ms, color 120ms, box-shadow 120ms, transform 120ms;
}
.autoswitch-rule-action.active {
color: #175cd3;
background: linear-gradient(180deg, #ffffff 0%, #f5faff 100%);
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.08), 0 0 0 1px rgba(191, 212, 255, 0.7);
transform: translateY(-0.5px);
}
.autoswitch-rule-delete {
display: inline-flex; align-items: center; justify-content: center;
width: 32px; height: 32px; border-radius: 10px; color: #b42318; background: #fff1f3;
border: 1px solid #fecdd6;
}
.autoswitch-rule-empty {
color: #98a2b3; font-style: italic;
}
.mode-detail { display: none; }
.mode-detail.active { display: block; }
.mode-detail-card {
background: #fff; border: 1px solid #e5e7eb; border-radius: 14px; padding: 16px;
}
.mode-detail-title { font-size: 15px; font-weight: 700; color: #111827; }
.mode-detail-copy { margin-top: 6px; font-size: 12px; line-height: 1.55; color: #667085; }
.row-action.is-success { background: #12b76a; }
.proxy-toast {
display: none; align-items: center; gap: 10px; padding: 12px 14px;
border-radius: 14px; border: 1px solid #dbe7ff; background: #eef4ff; color: #175cd3;
font-size: 12px; font-weight: 600;
}
.proxy-toast.active { display: flex; }
.proxy-toast.is-success { background: #ecfdf3; border-color: #b7ebc6; color: #067647; }
.proxy-toast.is-error { background: #fff1f3; border-color: #fecdd6; color: #c01048; }
.row-action.danger { background: #fff1f3; color: #c01048; }
.row-action.danger:hover, .row-action.danger:focus-visible { background: #ffe4e8; filter: none; }
.confirm-overlay {
display: none; position: fixed; inset: 0; z-index: 40;
background: rgba(15, 23, 42, 0.32); backdrop-filter: blur(4px);
align-items: center; justify-content: center; padding: 24px;
}
.confirm-overlay.active { display: flex; }
.confirm-card {
width: min(100%, 400px); background: #fff; border-radius: 18px;
padding: 22px; box-shadow: 0 24px 64px rgba(15, 23, 42, 0.24);
border: 1px solid #e5e5ea;
}
.confirm-title { font-size: 15px; font-weight: 700; color: #101828; }
.confirm-copy { margin-top: 8px; font-size: 12.5px; line-height: 1.55; color: #667085; }
.confirm-actions { margin-top: 18px; display: flex; justify-content: flex-end; gap: 8px; }
.browsing-data-card { width: min(100%, 470px); }
.site-data-host {
margin-top: 12px; padding: 10px 12px; border-radius: 8px;
background: #f2f4f7; color: #344054; font-size: 12.5px; font-weight: 650;
overflow-wrap: anywhere;
}
.clear-section-label {
margin: 18px 0 8px; color: #667085; font-size: 11px;
font-weight: 700; letter-spacing: 0.05em; text-transform: uppercase;
}
.time-range-options {
display: grid; grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 5px; padding: 4px; border-radius: 12px; background: #f2f4f7;
}
.time-range-option {
min-height: 34px; padding: 4px 7px; border-radius: 9px;
color: #667085; font-size: 11px; font-weight: 620; line-height: 1.2;
}
.time-range-option:hover { color: #344054; background: rgba(255,255,255,0.6); }
.time-range-option.selected {
color: #175cd3; background: #fff;
box-shadow: 0 1px 4px rgba(16,24,40,0.10);
}
.data-options {
overflow: hidden; border: 1px solid #e5e7eb; border-radius: 13px;
}
.data-option {
display: flex; align-items: flex-start; gap: 11px; padding: 11px 12px;
border-top: 1px solid #f0f2f5; cursor: pointer;
}
.data-option:first-child { border-top: 0; }
.data-option:hover { background: #fafbfd; }
.data-option input { margin: 2px 0 0; accent-color: #007aff; }
.data-option-main { min-width: 0; }
.data-option-title { display: block; color: #344054; font-size: 12.5px; font-weight: 650; }
.data-option-copy { display: block; margin-top: 2px; color: #98a2b3; font-size: 11px; line-height: 1.4; }
.cookie-warning {
display: none; margin-top: 10px; padding: 10px 12px;
border: 1px solid #fedf89; border-radius: 11px;
color: #934f00; background: #fffaeb; font-size: 11.5px; line-height: 1.45;
}
.cookie-warning.active { display: block; }
.section-title {
padding: 14px 16px 0; font-size: 12px; font-weight: 700;
letter-spacing: 0.06em; text-transform: uppercase; color: #667085;
}
.section.is-muted { opacity: 0.72; }
.about-content { padding: 24px; }
.about-content .section { background: #fff; border: 1px solid #e5e5ea; border-radius: 14px; overflow: hidden; }
.about-content .row {
display: flex; align-items: center; justify-content: space-between; gap: 12px;
padding: 14px 16px; border-top: 1px solid #f2f2f7;
}
.about-content .row:first-child { border-top: none; }
.about-content .row-title { font-size: 13px; font-weight: 500; }
.about-content .row-value { font-size: 12px; color: #6e6e73; margin-top: 2px; }
.about-content .row-main { flex: 1; min-width: 0; }
.about-logo {
display: flex; flex-direction: column; align-items: center; gap: 8px;
padding: 32px 0 24px; text-align: center;
}
.about-logo-icon {
width: 72px; height: 72px; overflow: hidden;
border-radius: 18px; border: 1px solid #e5e5ea;
box-shadow: 0 12px 28px rgba(15, 23, 42, 0.12);
background: linear-gradient(180deg, #ffffff 0%, #f5f5f7 100%);
}
.about-logo-icon img { display: block; width: 100%; height: 100%; object-fit: cover; }
.about-logo h3 { font-size: 17px; font-weight: 600; }
.about-logo p { font-size: 12px; color: #8e8e93; }
@media (max-width: 980px) {
.proxy-mode-group { grid-template-columns: 1fr; }
.time-range-options { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 820px) {
body { display: block; }
.sidebar {
display: flex; flex-direction: row; align-items: center; gap: 4px;
width: auto; height: auto; position: static; padding: 8px 10px;
border-right: none; border-bottom: 1px solid #e5e5ea;
white-space: nowrap; overflow-x: auto;
}
.sidebar-title { display: none; }
.nav-item {
flex: 0 0 auto; padding: 8px 12px; border-left: none;
border-bottom: 2px solid transparent; border-radius: 8px;
}
.nav-item.active { border-left-color: transparent; border-bottom-color: #007AFF; }
.main-header { padding: 12px 16px; }
.settings-content, .about-content { padding: 12px 16px 20px; }
.settings-content .row, .about-content .row, .proxy-block-head { align-items: flex-start; flex-direction: column; }
.row-control, .row-control.compact, .row-control.language, .row-actions { width: 100%; }
}
@media (max-width: 600px) {
body { display: block; }
.sidebar {
display: flex; flex-direction: row; align-items: center; gap: 4px;
width: auto; height: auto; position: static;
border-right: none; border-bottom: 1px solid #e5e5ea;
padding: 8px 10px; white-space: nowrap; overflow-x: auto;
}
.sidebar-title { display: none; }
.nav-item {
flex: 0 0 auto; padding: 8px 12px; border-left: none;
border-bottom: 2px solid transparent; border-radius: 10px;
}
.nav-item.active { border-left-color: transparent; border-bottom-color: #007AFF; }
.main { width: 100%; }
.main-header { padding: 12px 14px; }
.settings-content, .about-content { padding: 12px 14px 20px; }
.settings-content .row, .about-content .row, .proxy-block-head, .proxy-mini .row {
align-items: flex-start; flex-direction: column;
}
.row-control, .row-control.compact, .row-control.language, .row-actions { width: 100%; }
.row-actions { flex-wrap: wrap; }
.row-action { width: 100%; text-align: center; }
.proxy-mode-group { grid-template-columns: 1fr; }
.proxy-block { border-radius: 14px; }
.proxy-block-head { padding: 14px 14px 12px; }
.proxy-title { font-size: 18px; }
.proxy-block-body { padding: 14px 14px 16px; }
.autoswitch-card { padding: 14px; }
.autoswitch-table { display: block; overflow-x: auto; }
.autoswitch-card-actions { flex-wrap: wrap; }
.autoswitch-rule-cell { flex-wrap: wrap; gap: 8px; }
}
</style>
</head>
<body>
<nav class="sidebar">
<div class="sidebar-title" data-i18n="settings.title">Settings</div>
<a class="nav-item active" data-panel="general">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round">
<circle cx="8" cy="8" r="6"/><path d="M2 8h12M8 2c1.7 1.6 2.5 3.6 2.5 6S9.7 12.4 8 14M8 2C6.3 3.6 5.5 5.6 5.5 8s.8 4.4 2.5 6"/>
</svg>
<span data-i18n="settings.general">General</span>
</a>
<a class="nav-item" data-panel="downloads">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round">
<path d="M8 2v8m0 0l-3-3m3 3l3-3"/><path d="M3 12h10"/>
</svg>
<span data-i18n="settings.downloads">Downloads</span>
</a>
<a class="nav-item" data-panel="proxy">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 5.5h5"/><path d="M8 5.5l1.8-1.8"/><path d="M8 5.5l1.8 1.8"/><path d="M13 10.5H8"/><path d="M8 10.5L6.2 8.7"/><path d="M8 10.5l-1.8 1.8"/>
</svg>
<span data-i18n="settings.proxy">Proxy</span>
</a>
<a class="nav-item" data-panel="privacy">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round">
<path d="M8 2.2l4.6 1.7v3.4c0 3-1.9 5.2-4.6 6.5-2.7-1.3-4.6-3.5-4.6-6.5V3.9z"/><path d="M6 8l1.4 1.4L10.2 6.6"/>
</svg>
<span data-i18n="settings.privacy">Privacy</span>
</a>
<a class="nav-item" data-panel="about">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round">
<circle cx="8" cy="8" r="6"/><path d="M8 11V7.5"/><circle cx="8" cy="5.25" r="0.01" stroke-width="2"/>
</svg>
<span data-i18n="settings.about">About</span>
</a>
</nav>
<div class="main">
<div class="panel active" id="panel-general">
<div class="main-header">
<div><h2 data-i18n="settings.general">General</h2></div>
</div>
<div class="settings-content">
<div class="section">
<div class="row">
<div class="row-main">
<div class="row-title" data-i18n="settings.language">Language</div>
<div class="row-value" id="languageStatus" data-i18n="settings.languageCopy">Choose the language used by LingXia browser pages.</div>
</div>
<div class="row-control language">
<div class="language-options" id="languageOptions" role="radiogroup" data-i18n-aria-label="settings.language" aria-label="Language">
<button class="language-option" data-language="auto" type="button" role="radio" data-i18n="settings.languageAuto">Automatic</button>
<button class="language-option" data-language="en-US" type="button" role="radio">English</button>
<button class="language-option" data-language="zh-CN" type="button" role="radio">ç®€ä½“ä¸æ–‡</button>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="panel" id="panel-downloads">
<div class="main-header">
<div><h2 data-i18n="settings.downloads">Downloads</h2></div>
</div>
<div class="settings-content">
<div class="section">
<div class="row">
<div class="row-main">
<div class="row-title" data-i18n="settings.location">Location</div>
<div class="row-value" id="downloadDir" data-i18n="downloads.loading">Loading...</div>
</div>
<span class="pill is-disabled" id="downloadMode">-</span>
</div>
<div class="row">
<div class="row-main"><div class="row-title" data-i18n="settings.changeLocation">Change location</div></div>
<button class="row-action" id="changeBtn" type="button" data-i18n="settings.change">Change</button>
</div>
<div class="row" id="resetRow" style="display:none">
<div class="row-main"><div class="row-title" data-i18n="settings.resetDefault">Reset to default</div></div>
<button class="row-action secondary" id="resetBtn" type="button" data-i18n="settings.reset">Reset</button>
</div>
</div>
</div>
</div>
<div class="panel" id="panel-proxy">
<div class="main-header">
<div><h2 data-i18n="settings.proxy">Proxy</h2></div>
</div>
<div class="settings-content">
<div class="proxy-stack">
<section class="proxy-block">
<div class="proxy-block-head">
<div>
<div class="proxy-kicker" data-i18n="settings.proxyServer">Proxy Server</div>
<div class="proxy-title">SOCKS</div>
<div class="proxy-subtitle" data-i18n="settings.proxyServerCopy">Configure the SOCKS server used by both Always Proxy and Auto Switch modes.</div>
</div>
</div>
<div class="proxy-block-body">
<div class="proxy-mini">
<div class="row">
<div class="row-main">
<div class="row-title" data-i18n="settings.socksHost">SOCKS host</div>
<div class="row-value" data-i18n="settings.socksHostCopy">Used by both Always Proxy and Auto Switch modes.</div>
</div>
<div class="row-control">
<input class="settings-input" id="proxyHost" type="text" placeholder="127.0.0.1" autocomplete="off">
</div>
</div>
<div class="row">
<div class="row-main">
<div class="row-title" data-i18n="settings.socksPort">SOCKS port</div>
<div class="row-value" data-i18n="settings.socksPortCopy">Default is 1080.</div>
</div>
<div class="row-control compact">
<input class="settings-input" id="proxyPort" type="text" inputmode="numeric" placeholder="1080" autocomplete="off">
</div>
</div>
<div class="row">
<div class="row-main">
<div class="row-title" data-i18n="settings.username">Username</div>
<div class="row-value" data-i18n="settings.usernameCopy">Optional SOCKS username.</div>
</div>
<div class="row-control">
<input class="settings-input" id="proxyUsername" type="text" autocomplete="off">
</div>
</div>
<div class="row">
<div class="row-main">
<div class="row-title" data-i18n="settings.password">Password</div>
<div class="row-value" data-i18n="settings.passwordCopy">Optional SOCKS password.</div>
</div>
<div class="row-control">
<input class="settings-input" id="proxyPassword" type="password" autocomplete="off">
</div>
</div>
<div class="row">
<div class="row-main">
<div class="row-title" data-i18n="settings.saveServerSettings">Save server settings</div>
<div class="row-value" data-i18n="settings.saveServerCopy">Save the server fields and reapply the current routing mode.</div>
</div>
<div class="row-actions">
<button class="row-action" id="saveServerBtn" type="button" data-i18n="settings.saveServer">Save Server</button>
</div>
</div>
</div>
</div>
</section>
<section class="proxy-block">
<div class="proxy-block-head">
<div>
<div class="proxy-kicker" data-i18n="settings.proxy">Proxy</div>
<div class="proxy-title" data-i18n="settings.routingMode">Routing Mode</div>
<div class="proxy-subtitle" id="proxySummary" data-i18n="settings.routingCopy">Choose how browser traffic should be routed: direct, auto switch by rule list, or always through the upstream proxy.</div>
</div>
<span class="pill is-pending" id="proxyStatusPill" data-i18n="settings.loading">Loading</span>
</div>
<div class="proxy-block-body">
<div class="proxy-mode-group">
<button class="proxy-mode" data-proxy-mode="direct" type="button">
<span class="proxy-mode-label" data-i18n="settings.direct">Direct</span>
<span class="proxy-mode-desc" data-i18n="settings.directDesc">No proxy. Browser traffic goes out normally.</span>
</button>
<button class="proxy-mode" data-proxy-mode="gfw_list" type="button">
<span class="proxy-mode-label" data-i18n="settings.autoSwitch">Auto Switch</span>
<span class="proxy-mode-desc" data-i18n="settings.autoSwitchDesc">Use the rule list below. Matched requests go to the proxy server, default traffic stays direct.</span>
</button>
<button class="proxy-mode" data-proxy-mode="global" type="button">
<span class="proxy-mode-label" data-i18n="settings.alwaysProxy">Always Proxy</span>
<span class="proxy-mode-desc" data-i18n="settings.alwaysProxyDesc">Send all browser traffic to the configured SOCKS server.</span>
</button>
</div>
<div class="proxy-toast" id="proxyToast"></div>
<div class="mode-detail active" id="modeDetailDirect">
<div class="mode-detail-card">
<div class="mode-detail-title" data-i18n="settings.direct">Direct</div>
<div class="mode-detail-copy" data-i18n="settings.directDetail">Browser traffic goes out normally. The proxy server configuration stays saved, but it is not used in this mode.</div>
</div>
</div>
<div class="mode-detail" id="modeDetailGlobal">
<div class="mode-detail-card">
<div class="mode-detail-title" data-i18n="settings.alwaysProxy">Always Proxy</div>
<div class="mode-detail-copy" data-i18n="settings.alwaysProxyDetail">All browser requests use the SOCKS server for newly created browser tabs.</div>
</div>
</div>
<div class="mode-detail autoswitch-grid section is-muted" id="gfwListSection">
<div class="autoswitch-card">
<div class="autoswitch-card-title" data-i18n="settings.switchRules">Switch Rules</div>
<div class="autoswitch-card-copy" data-i18n="settings.switchRulesCopy">Auto Switch checks custom rules first, then the downloaded rule list, and finally falls back to direct.</div>
<div class="autoswitch-card-note" data-i18n="settings.matchHelp">Enter a host pattern to match that host and its subdomains.</div>
<table class="autoswitch-table">
<thead>
<tr>
<th data-i18n="settings.conditionType">Condition Type</th>
<th data-i18n="settings.conditionDetails">Condition Details</th>
<th data-i18n="settings.profile">Profile</th>
</tr>
</thead>
<tbody>
<tr>
<td data-i18n="settings.ruleListRules">Rule list rules</td>
<td data-i18n="settings.ruleListRulesCopy">Any request matching the configured rule list.</td>
<td><span class="autoswitch-profile is-proxy">SOCKS</span></td>
</tr>
</tbody>
<tbody id="autoSwitchRulesBody">
<tr>
<td colspan="3" class="autoswitch-rule-empty" data-i18n="settings.noCustomRules">No custom rules yet.</td>
</tr>
</tbody>
<tbody>
<tr>
<td data-i18n="settings.default">Default</td>
<td data-i18n="settings.defaultRuleCopy">Requests that do not match the rule list.</td>
<td><span class="autoswitch-profile is-direct" data-i18n="settings.direct">Direct</span></td>
</tr>
</tbody>
</table>
<div class="autoswitch-card-actions">
<button class="row-action secondary" id="addAutoSwitchRuleBtn" type="button" data-i18n="settings.addRule">Add Rule</button>
</div>
</div>
<div class="autoswitch-card">
<div class="autoswitch-card-title" data-i18n="settings.ruleListConfig">Rule List Config</div>
<div class="autoswitch-card-copy" id="gfwListStatusMessage">
<span data-i18n="downloads.loading">Loading...</span>
</div>
<div class="autoswitch-card-actions">
<span class="pill is-empty" id="gfwListStatusPill" data-i18n="settings.empty">Empty</span>
</div>
<div class="proxy-mini" style="margin-top:14px;">
<div class="row">
<div class="row-main">
<div class="row-title" data-i18n="settings.ruleListUrl">Rule List URL</div>
<div class="row-value" data-i18n="settings.ruleListUrlCopy">HTTPS URL used when downloading the rule list.</div>
</div>
<div class="row-control">
<input class="settings-input" id="gfwListSourceInput" type="url" placeholder="https://raw.githubusercontent.com/gfwlist/gfwlist/master/gfwlist.txt" autocomplete="off">
</div>
</div>
<div class="row">
<div class="row-main">
<div class="row-title" data-i18n="settings.lastUpdated">Last updated</div>
<div class="row-value" id="gfwListUpdatedAt" data-i18n="settings.never">Never</div>
</div>
<div class="row-actions">
<button class="row-action secondary" id="refreshGfwListBtn" type="button" data-i18n="settings.downloadRules">Download Rules Now</button>
</div>
</div>
</div>
</div>
</div>
<div class="proxy-helper" id="proxyModeHelper" data-i18n="settings.proxyHelper">Choose a mode, then save when you want to update the proxy configuration used by browser tabs.</div>
</div>
</section>
</div>
</div>
</div>
<div class="panel" id="panel-privacy">
<div class="main-header">
<div><h2 data-i18n="settings.privacy">Privacy</h2></div>
</div>
<div class="settings-content">
<div class="section">
<div class="section-title" data-i18n="settings.privacyBrowsingData">Browsing Data</div>
<div class="row">
<div class="row-main">
<div class="row-title" data-i18n="settings.browsingHistory">Browsing history</div>
<div class="row-value" id="privacyHistoryUsage" data-i18n="downloads.loading">Loading...</div>
</div>
<div class="row-actions">
<a class="row-action secondary" href="lingxia://history" data-i18n="settings.viewHistory">View History</a>
</div>
</div>
<div class="row">
<div class="row-main">
<div class="row-title" data-i18n="settings.cachedFiles">Cached images and files</div>
<div class="row-value" id="privacyCacheUsage" data-i18n="downloads.loading">Loading...</div>
</div>
</div>
<div class="row">
<div class="row-main">
<div class="row-title" data-i18n="settings.cookies">Cookies and site data</div>
<div class="row-value" id="privacyCookieUsage" data-i18n="downloads.loading">Loading...</div>
</div>
</div>
<div class="row">
<div class="row-main">
<div class="row-title" data-i18n="settings.clearBrowsingData">Clear browsing data</div>
<div class="row-value" data-i18n="settings.clearBrowsingDataCopy">Choose a time range and the data you want to remove.</div>
</div>
<div class="row-actions">
<button class="row-action danger" id="openClearDataBtn" type="button" data-i18n="settings.clearBrowsingDataMenu">Clear Browsing Data...</button>
</div>
</div>
</div>
<div class="proxy-toast" id="privacyToast"></div>
</div>
</div>
<div class="confirm-overlay" id="clearBrowsingDataDialog">
<div class="confirm-card browsing-data-card" role="dialog" aria-modal="true" aria-labelledby="clearBrowsingDataTitle">
<div class="confirm-title" id="clearBrowsingDataTitle" data-i18n="settings.clearBrowsingData">Clear browsing data</div>
<div class="confirm-copy" data-i18n="settings.clearDialogCopy">Select a time range and the information to remove from this browser profile.</div>
<div class="clear-section-label" data-i18n="settings.timeRange">Time range</div>
<div class="time-range-options" id="timeRangeOptions" role="radiogroup" data-i18n-aria-label="settings.timeRange" aria-label="Time range">
<button class="time-range-option" data-time-range="lastHour" type="button" role="radio" data-i18n="settings.lastHour">Last hour</button>
<button class="time-range-option" data-time-range="last24Hours" type="button" role="radio" data-i18n="settings.last24Hours">24 hours</button>
<button class="time-range-option" data-time-range="last7Days" type="button" role="radio" data-i18n="settings.last7Days">7 days</button>
<button class="time-range-option" data-time-range="last4Weeks" type="button" role="radio" data-i18n="settings.last4Weeks">4 weeks</button>
<button class="time-range-option selected" data-time-range="allTime" type="button" role="radio" aria-checked="true" data-i18n="settings.allTime">All time</button>
</div>
<div class="clear-section-label" data-i18n="settings.dataToRemove">Data to remove</div>
<div class="data-options">
<label class="data-option">
<input id="clearHistoryCheck" type="checkbox" checked>
<span class="data-option-main">
<span class="data-option-title" data-i18n="settings.browsingHistory">Browsing history</span>
<span class="data-option-copy" data-i18n="settings.historyDataCopy">Visited pages and their timestamps.</span>
</span>
</label>
<label class="data-option">
<input id="clearCacheCheck" type="checkbox" checked>
<span class="data-option-main">
<span class="data-option-title" data-i18n="settings.cachedFiles">Cached images and files</span>
<span class="data-option-copy" data-i18n="settings.cacheDataCopy">Sites may load more slowly on your next visit.</span>
</span>
</label>
<label class="data-option">
<input id="clearCookiesCheck" type="checkbox">
<span class="data-option-main">
<span class="data-option-title" data-i18n="settings.cookies">Cookies and site data</span>
<span class="data-option-copy" data-i18n="settings.cookieDataCopy">Sign-in sessions, local storage, and databases.</span>
</span>
</label>
</div>
<div class="cookie-warning" id="cookieWarning" data-i18n="settings.cookieWarning">You will be signed out of most websites.</div>
<div class="confirm-actions">
<button class="row-action secondary" id="clearDataCancelBtn" type="button" data-i18n="common.cancel">Cancel</button>
<button class="row-action danger" id="clearDataConfirmBtn" type="button" data-i18n="settings.clearData">Clear Data</button>
</div>
</div>
</div>
<div class="confirm-overlay" id="clearSiteDataDialog">
<div class="confirm-card browsing-data-card" role="dialog" aria-modal="true" aria-labelledby="clearSiteDataTitle">
<div class="confirm-title" id="clearSiteDataTitle" data-i18n="settings.clearSiteData">Clear data for this site</div>
<div class="confirm-copy" data-i18n="settings.clearSiteDataCopy">Remove data saved by this website without affecting other sites.</div>
<div class="site-data-host" id="clearSiteDataHost" data-i18n="settings.loading">Loading</div>
<div class="clear-section-label" data-i18n="settings.dataToRemove">Data to remove</div>
<div class="data-options">
<label class="data-option">
<input id="clearSiteCacheCheck" type="checkbox" checked>
<span class="data-option-main">
<span class="data-option-title" data-i18n="settings.cachedFiles">Cached images and files</span>
<span class="data-option-copy" data-i18n="settings.siteCacheDataCopy">Only cache storage that can be isolated to this site is removed.</span>
</span>
</label>
<label class="data-option">
<input id="clearSiteCookiesCheck" type="checkbox">
<span class="data-option-main">
<span class="data-option-title" data-i18n="settings.cookies">Cookies and site data</span>
<span class="data-option-copy" data-i18n="settings.siteCookieDataCopy">You will be signed out of this website.</span>
</span>
</label>
</div>
<div class="cookie-warning" id="siteCookieWarning" data-i18n="settings.siteCookieWarning">You will be signed out of this website.</div>
<div class="confirm-actions">
<button class="row-action secondary" id="clearSiteDataCancelBtn" type="button" data-i18n="common.cancel">Cancel</button>
<button class="row-action danger" id="clearSiteDataConfirmBtn" type="button" data-i18n="settings.clearSiteDataAction">Clear Site Data</button>
</div>
</div>
</div>
<div class="panel" id="panel-about">
<div class="main-header">
<div><h2 data-i18n="settings.about">About</h2></div>
</div>
<div class="about-content">
<div class="about-logo">
<div class="about-logo-icon">
<img src="lingxia://lxapp/public/LingXia.png" alt="LingXia app icon">
</div>
<h3 id="aboutProductName">LingXia</h3>
<p id="aboutVersion">App Version -</p>
</div>
<div class="section">
<div class="row">
<div class="row-main">
<div class="row-title" data-i18n="settings.appVersion">App Version</div>
<div class="row-value" id="aboutAppVersion">-</div>
</div>
</div>
<div class="row">
<div class="row-main">
<div class="row-title" data-i18n="settings.lingxiaVersion">LingXia Version</div>
<div class="row-value" id="aboutSdkVersion">-</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="lingxia://lxapp/public/i18n.js"></script>
<script>
(function () {
var i18n = window.LingXiaI18n;
var t = i18n.t;
i18n.apply();
var languageStatus = document.getElementById('languageStatus');
var languageOptions = Array.prototype.slice.call(document.querySelectorAll('[data-language]'));
var configuredLanguage = null;
function syncLanguageOptions(language, disabled) {
languageOptions.forEach(function (option) {
var selected = option.dataset.language === language;
option.classList.toggle('active', selected);
option.setAttribute('aria-checked', selected ? 'true' : 'false');
option.disabled = !!disabled;
});
}
syncLanguageOptions('auto', false);
callHost('settings.getLanguage').then(function (result) {
configuredLanguage = result && result.language || null;
syncLanguageOptions(configuredLanguage || 'auto', false);
}, function () {
syncLanguageOptions('auto', false);
});
languageOptions.forEach(function (option) {
option.addEventListener('click', function () {
var next = option.dataset.language;
if (next === (configuredLanguage || 'auto')) return;
syncLanguageOptions(next, true);
callHost('settings.setLanguage', { language: next }).then(function (result) {
configuredLanguage = result && result.language || null;
if (configuredLanguage) {
i18n.setLocale(configuredLanguage);
} else {
i18n.useSystemLocale();
}
window.location.reload();
}, function (error) {
console.error('[Settings] settings.setLanguage error:', error);
syncLanguageOptions(configuredLanguage || 'auto', false);
languageStatus.textContent = t('settings.languageSaveFailed');
languageStatus.style.color = '#c01048';
});
});
});
document.getElementById('aboutVersion').textContent = t('settings.appVersionValue', { version: '-' });
var navItems = document.querySelectorAll('.nav-item[data-panel]');
var modeButtons = document.querySelectorAll('.proxy-mode[data-proxy-mode]');
var DEFAULT_PROXY_PORT = 1080;
var DEFAULT_GFWLIST_SOURCE_URL = 'https://raw.githubusercontent.com/gfwlist/gfwlist/master/gfwlist.txt';
var proxyWatchHandle = null;
var proxyToastTimer = null;
var autoSwitchSaveTimer = null;
var settingsState = {
downloadSettings: null,
proxySettings: null,
proxyHidden: false,
proxyBusy: false,
serverDirty: false,
autoSwitchDirty: false,
serverSavedFeedback: false
};
function bridge() {
var api = window.LingXiaBridge;
if (!api || typeof api.invoke !== 'function' || typeof api.stream !== 'function') {
throw new Error('LingXiaBridge is not available');
}
return api;
}
function callHost(route, input) {
return bridge().invoke(route, input);
}
function streamHost(route, input) {
return bridge().stream(route, input);
}
var appApi = function () {
return {
getInfo: function () { return callHost('app.getInfo'); }
};
};
var downloadsApi = function () {
return {
getSettings: function () { return callHost('downloads.getSettings'); },
chooseDirectory: function () { return callHost('downloads.chooseDirectory'); },
resetDirectory: function () { return callHost('downloads.resetDirectory'); }
};
};
var privacyApi = function () {
return {
getUsage: function () { return callHost('privacy.getUsage'); },
clearBrowsingData: function (input) { return callHost('privacy.clearBrowsingData', input); },
getSiteDataContext: function (input) { return callHost('privacy.getSiteDataContext', input); },
clearSiteData: function (input) { return callHost('privacy.clearSiteData', input); }
};
};
var proxyApi = function () {
return {
getSettings: function () { return callHost('proxy.getSettings'); },
updateSettings: function (input) { return callHost('proxy.updateSettings', input); },
refreshGfwList: function () { return callHost('proxy.refreshGfwList'); },
watch: function () { return streamHost('proxy.watch'); }
};
};
navItems.forEach(function (nav) {
nav.addEventListener('click', function (e) {
e.preventDefault();
switchPanel(nav.dataset.panel);
});
});
function switchPanel(name) {
navItems.forEach(function (n) { n.classList.toggle('active', n.dataset.panel === name); });
document.querySelectorAll('.panel').forEach(function (p) { p.classList.toggle('active', p.id === 'panel-' + name); });
history.replaceState(null, '', '#' + name);
}
function isRouteUnavailableError(err) {
if (!err) return false;
var code = typeof err === 'object' ? err.code : null;
if (code === 'BRIDGE_METHOD_NOT_FOUND' || code === 'BRIDGE_TOPIC_NOT_FOUND') return true;
var message = typeof err === 'string' ? err : (err && err.message) || '';
return /method not found|channel not found/i.test(message);
}
function hideProxyPanel() {
if (settingsState.proxyHidden) return;
settingsState.proxyHidden = true;
var proxyNav = document.querySelector('.nav-item[data-panel="proxy"]');
if (proxyNav) proxyNav.style.display = 'none';
var proxyPanel = document.getElementById('panel-proxy');
var wasActive = proxyPanel && proxyPanel.classList.contains('active');
if (proxyPanel) proxyPanel.style.display = 'none';
if (wasActive || (proxyNav && proxyNav.classList.contains('active'))) {
switchPanel('downloads');
}
}
function proxyField(id) {
return document.getElementById(id);
}
function formatTimestamp(ts) {
if (!ts) return t('settings.never');
try {
return new Date(ts).toLocaleString(i18n.locale);
} catch (_) {
return t('settings.never');
}
}
function selectedProxyMode() {
var active = document.querySelector('.proxy-mode.active');
return active ? active.dataset.proxyMode : 'direct';
}
function hasValidUpstreamConfig() {
var payload = proxyPayloadFromForm('global');
return !validateProxyPayload(payload);
}
function getEffectiveGfwListSource() {
var value = String(proxyField('gfwListSourceInput').value || '').trim();
return value || DEFAULT_GFWLIST_SOURCE_URL;
}
function collectAutoSwitchRules(includeEmpty) {
var rows = document.querySelectorAll('#autoSwitchRulesBody tr[data-rule-index]');
var rules = Array.prototype.map.call(rows, function (row) {
return {
pattern: String(row.querySelector('[data-rule-pattern]').value || '').trim(),
action: String(row.querySelector('[data-rule-action]').value || 'proxy')
};
});
if (includeEmpty) return rules;
return rules.filter(function (rule) { return !!rule.pattern; });
}
function renderAutoSwitchRules(rules) {
var body = document.getElementById('autoSwitchRulesBody');
var list = Array.isArray(rules) ? rules : [];
if (!list.length) {
body.innerHTML = '<tr><td colspan="3" class="autoswitch-rule-empty">' + t('settings.noCustomRules') + '</td></tr>';
return;
}
body.innerHTML = list.map(function (rule, index) {
var pattern = String(rule.pattern || '')
.replace(/&/g, '&')
.replace(/"/g, '"')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/'/g, ''');
var action = rule.action === 'direct' ? 'direct' : 'proxy';
return [
'<tr class="autoswitch-rule-row" data-rule-index="', index, '">',
'<td>', t('settings.matchSite'), '</td>',
'<td><input class="autoswitch-rule-input" data-rule-pattern type="text" placeholder="site.test" value="', pattern, '"></td>',
'<td><div class="autoswitch-rule-cell">',
'<div class="autoswitch-rule-segment">',
'<button class="autoswitch-rule-action', action === 'proxy' ? ' active' : '', '" data-rule-action-btn data-rule-action-value="proxy" type="button">SOCKS</button>',
'<button class="autoswitch-rule-action', action === 'direct' ? ' active' : '', '" data-rule-action-btn data-rule-action-value="direct" type="button">', t('settings.direct'), '</button>',
'<input type="hidden" data-rule-action value="', action, '">',
'</div>',
'<button class="autoswitch-rule-delete" data-rule-delete type="button">×</button>',
'</div></td>',
'</tr>'
].join('');
}).join('');
}
function scheduleAutoSwitchAutosave() {
if (autoSwitchSaveTimer) window.clearTimeout(autoSwitchSaveTimer);
autoSwitchSaveTimer = window.setTimeout(function () {
autoSwitchSaveTimer = null;
if (collectAutoSwitchRules(true).some(function (rule) { return !rule.pattern; })) {
return;
}
saveAutoSwitchConfig();
}, 700);
}
function showProxyToast(kind, message) {
var toast = document.getElementById('proxyToast');
if (proxyToastTimer) window.clearTimeout(proxyToastTimer);
toast.className = 'proxy-toast active' + (kind === 'error' ? ' is-error' : kind === 'success' ? ' is-success' : '');
toast.textContent = message;
proxyToastTimer = window.setTimeout(function () {
toast.className = 'proxy-toast';
toast.textContent = '';
proxyToastTimer = null;
}, 2600);
}
function setSelectedProxyMode(mode) {
modeButtons.forEach(function (btn) {
btn.classList.toggle('active', btn.dataset.proxyMode === mode);
});
document.getElementById('modeDetailDirect').classList.toggle('active', mode === 'direct');
document.getElementById('modeDetailGlobal').classList.toggle('active', mode === 'global');
document.getElementById('gfwListSection').classList.toggle('active', mode === 'gfw_list');
document.getElementById('gfwListSection').classList.toggle('is-muted', mode !== 'gfw_list');
var summaryMap = {
direct: t('settings.directDetail'),
global: t('settings.alwaysProxyDetail'),
gfw_list: t('settings.autoSwitchDesc')
};
document.getElementById('proxySummary').textContent = summaryMap[mode] || summaryMap.direct;
}
function syncProxyFormState() {
var disabled = settingsState.proxyBusy;
['proxyHost', 'proxyPort', 'proxyUsername', 'proxyPassword'].forEach(function (id) {
proxyField(id).disabled = disabled;
});
proxyField('gfwListSourceInput').disabled = disabled;
var hasUpstream = hasValidUpstreamConfig();
modeButtons.forEach(function (btn) {
btn.disabled = disabled;
});
var saveServerBtn = document.getElementById('saveServerBtn');
saveServerBtn.disabled = disabled || !settingsState.serverDirty;
document.getElementById('refreshGfwListBtn').disabled = disabled;
saveServerBtn.classList.toggle('is-success', settingsState.serverSavedFeedback && !settingsState.serverDirty);
saveServerBtn.textContent = t(settingsState.serverSavedFeedback && !settingsState.serverDirty ? 'settings.saved' : 'settings.saveServer');
var helper = document.getElementById('proxyModeHelper');
if (disabled) {
helper.className = 'proxy-helper';
helper.textContent = t('settings.applying');
} else if (!hasUpstream) {
helper.className = 'proxy-helper is-warning';
helper.textContent = t('settings.invalidServer');
} else {
helper.className = 'proxy-helper';
helper.textContent = t('settings.saveAfterEdit');
}
}
function setProxyBusy(busy) {
settingsState.proxyBusy = !!busy;
syncProxyFormState();
}
function setProxyStatus(status, message, localAddr) {
var pill = document.getElementById('proxyStatusPill');
var text = (status || '').toLowerCase();
var labelMap = {
saved: t('settings.saved'),
active: t('settings.active'),
disabled: t('settings.direct'),
unsupported: t('settings.unsupported'),
error: t('settings.error'),
pending: t('settings.pending')
};
var classMap = {
saved: 'is-success',
active: 'is-active',
disabled: 'is-disabled',
unsupported: 'is-unsupported',
error: 'is-error',
pending: 'is-pending'
};
pill.className = 'pill ' + (classMap[text] || 'is-pending');
pill.textContent = labelMap[text] || t('settings.loading');
var summary = document.getElementById('proxySummary');
if (summary && message) summary.textContent = message;
}
function setGfwListStatus(status, message, source, updatedAt) {
var pill = document.getElementById('gfwListStatusPill');
var text = (status || '').toLowerCase();
var labelMap = {
ready: t('settings.ready'),
empty: t('settings.empty'),
error: t('settings.error')
};
var classMap = {
ready: 'is-ready',
empty: 'is-empty',
error: 'is-error'
};
pill.className = 'pill ' + (classMap[text] || 'is-empty');
pill.textContent = labelMap[text] || t('settings.empty');
document.getElementById('gfwListStatusMessage').textContent = message || t('settings.rulesNotDownloaded');
if (source != null) proxyField('gfwListSourceInput').value = source || '';
document.getElementById('gfwListUpdatedAt').textContent = formatTimestamp(updatedAt);
}
function renderDownloadSettings() {
var s = settingsState.downloadSettings;
var dir = document.getElementById('downloadDir');
var mode = document.getElementById('downloadMode');
var resetRow = document.getElementById('resetRow');
if (!s) {
dir.textContent = t('downloads.loading');
mode.textContent = '-';
resetRow.style.display = 'none';
return;
}
var isDefault = !!s.usesDefaultDir;
dir.textContent = s.downloadDir || '-';
mode.textContent = t(isDefault ? 'settings.default' : 'settings.custom');
mode.className = 'pill ' + (isDefault ? 'is-disabled' : 'is-active');
resetRow.style.display = isDefault ? 'none' : '';
}
function renderProxySettings() {
var s = settingsState.proxySettings;
if (!s) {
setProxyStatus('pending', t('downloads.loading'), null);
setGfwListStatus('empty', t('downloads.loading'), null, null);
return;
}
setSelectedProxyMode(s.mode || 'direct');
proxyField('proxyHost').value = s.socks5Host || '';
proxyField('proxyPort').value = s.socks5Port || DEFAULT_PROXY_PORT;
proxyField('proxyUsername').value = s.username || '';
proxyField('proxyPassword').value = s.password || '';
proxyField('gfwListSourceInput').value = s.gfwlistSourceUrl || DEFAULT_GFWLIST_SOURCE_URL;
renderAutoSwitchRules(s.autoSwitchRules || []);
setProxyStatus(s.status, s.statusMessage, s.localProxyAddr);
setGfwListStatus(
s.gfwlistStatus,
s.gfwlistStatusMessage,
s.gfwlistSourceUrl,
s.gfwlistUpdatedAtMs
);
syncProxyFormState();
}
function proxyPayloadFromForm(overrideMode) {
var portRaw = String(proxyField('proxyPort').value || '').trim();
var port = portRaw ? Number(portRaw) : DEFAULT_PROXY_PORT;
return {
mode: overrideMode || selectedProxyMode(),
socks5Host: String(proxyField('proxyHost').value || '').trim(),
socks5Port: port,
username: String(proxyField('proxyUsername').value || ''),
password: String(proxyField('proxyPassword').value || ''),
gfwlistSourceUrl: getEffectiveGfwListSource(),
autoSwitchRules: collectAutoSwitchRules(false)
};
}
function proxyErrorMessage(err) {
if (!err) return t('settings.unknownProxyError');
if (typeof err === 'string') return err;
if (err.message) return err.message;
try {
return JSON.stringify(err);
} catch (_) {
return t('settings.unknownProxyError');
}
}
function withTimeout(promise, timeoutMs, label) {
return new Promise(function (resolve, reject) {
var settled = false;
var timer = window.setTimeout(function () {
if (settled) return;
settled = true;
reject(new Error(label + ' timed out after ' + timeoutMs + 'ms'));
}, timeoutMs);
promise.then(function (value) {
if (settled) return;
settled = true;
window.clearTimeout(timer);
resolve(value);
}, function (error) {
if (settled) return;
settled = true;
window.clearTimeout(timer);
reject(error);
});
});
}
function ensureProxyWatch() {
if (proxyWatchHandle || settingsState.proxyHidden) return;
proxyWatchHandle = proxyApi().watch();
proxyWatchHandle.onEvent(function (event) {
console.info('[Settings] proxy.watch event:', event);
var previousStatus = settingsState.proxySettings && settingsState.proxySettings.status;
var normalizedEvent = event;
if (event && event.status === 'active' && /new webviews only/i.test(event.statusMessage || '')) {
normalizedEvent = Object.assign({}, event, {
status: 'saved',
statusMessage: t('settings.savedForNewTabs')
});
}
settingsState.proxySettings = normalizedEvent;
renderProxySettings();
if (normalizedEvent && normalizedEvent.status !== previousStatus) {
if (normalizedEvent.status === 'saved') showProxyToast('success', normalizedEvent.statusMessage || t('settings.settingsSaved'));
else if (normalizedEvent.status === 'active') showProxyToast('success', normalizedEvent.statusMessage || t('settings.proxyActive'));
else if (normalizedEvent.status === 'error') showProxyToast('error', normalizedEvent.statusMessage || t('settings.proxyFailed'));
else if (normalizedEvent.status === 'pending') showProxyToast('', normalizedEvent.statusMessage || t('settings.applying'));
}
});
proxyWatchHandle.onError(function (err) {
console.error('[Settings] proxy.watch error:', err);
});
if (proxyWatchHandle.result && typeof proxyWatchHandle.result.then === 'function') {
proxyWatchHandle.result.then(function () {
console.warn('[Settings] proxy.watch ended');
proxyWatchHandle = null;
}, function () {});
}
}
function validateProxyPayload(payload) {
if (payload.mode === 'direct') return null;
if (!payload.socks5Host) return t('settings.hostRequired');
if (!Number.isInteger(payload.socks5Port) || payload.socks5Port <= 0 || payload.socks5Port > 65535) {
return t('settings.portInvalid');
}
return null;
}
function saveAutoSwitchConfig() {
var payload = proxyPayloadFromForm();
var validationError = validateProxyPayload(payload);
if (validationError) {
setProxyStatus('error', validationError, null);
return;
}
submitProxySettings(payload, { source: 'auto_switch', autosave: true });
}
function submitProxySettings(payload, options) {
var submitOptions = options || {};
var validationError = validateProxyPayload(payload);
if (validationError) {
setSelectedProxyMode(payload.mode);
setProxyStatus('error', validationError, null);
return;
}
console.info('[Settings] submitProxySettings:', payload);
setProxyBusy(true);
withTimeout(proxyApi().updateSettings(payload), 10000, 'proxy.updateSettings').then(function (result) {
console.info('[Settings] proxy.updateSettings result:', result);
settingsState.proxySettings = result;
if (submitOptions.source === 'server') {
settingsState.serverDirty = false;
settingsState.serverSavedFeedback = true;
showProxyToast('success', t('settings.serverSaved'));
} else if (submitOptions.source === 'auto_switch') {
settingsState.autoSwitchDirty = false;
if (!submitOptions.autosave) showProxyToast('success', t('settings.autoSaved'));
}
if (payload.mode === 'gfw_list' && !result.gfwlistReady) {
setProxyStatus('pending', t('settings.downloadingAutoRules'), result.localProxyAddr);
setGfwListStatus('empty', t('settings.downloadingRules'), payload.gfwlistSourceUrl, result.gfwlistUpdatedAtMs);
withTimeout(proxyApi().refreshGfwList(), 15000, 'proxy.refreshGfwList').then(function (refreshResult) {
settingsState.proxySettings = refreshResult;
renderProxySettings();
setProxyBusy(false);
}, function (refreshErr) {
console.error('[Settings] proxy.refreshGfwList after update error:', refreshErr);
settingsState.proxySettings = result;
renderProxySettings();
setProxyStatus('error', proxyErrorMessage(refreshErr), result.localProxyAddr);
setGfwListStatus(
'error',
proxyErrorMessage(refreshErr),
payload.gfwlistSourceUrl,
result.gfwlistUpdatedAtMs
);
setProxyBusy(false);
});
return;
}
renderProxySettings();
setProxyBusy(false);
}, function (err) {
console.error('[Settings] proxy.updateSettings error:', err);
setProxyStatus('error', proxyErrorMessage(err), null);
setProxyBusy(false);
});
}
modeButtons.forEach(function (btn) {
btn.addEventListener('click', function () {
submitProxySettings(proxyPayloadFromForm(btn.dataset.proxyMode), { source: 'mode' });
});
});
document.getElementById('saveServerBtn').addEventListener('click', function () {
submitProxySettings(proxyPayloadFromForm(), { source: 'server' });
});
['proxyHost', 'proxyPort', 'proxyUsername', 'proxyPassword'].forEach(function (id) {
proxyField(id).addEventListener('input', function () {
settingsState.serverDirty = true;
settingsState.serverSavedFeedback = false;
syncProxyFormState();
});
});
['gfwListSourceInput'].forEach(function (id) {
proxyField(id).addEventListener('input', function () {
settingsState.autoSwitchDirty = true;
syncProxyFormState();
scheduleAutoSwitchAutosave();
});
});
document.getElementById('addAutoSwitchRuleBtn').addEventListener('click', function () {
var nextRules = collectAutoSwitchRules(true);
nextRules.push({ pattern: '', action: 'proxy' });
renderAutoSwitchRules(nextRules);
settingsState.autoSwitchDirty = true;
syncProxyFormState();
});
document.getElementById('autoSwitchRulesBody').addEventListener('click', function (event) {
var target = event.target;
if (!target) return;
if (target.hasAttribute('data-rule-action-btn')) {
var rowForAction = target.closest('tr[data-rule-index]');
if (!rowForAction) return;
Array.prototype.forEach.call(rowForAction.querySelectorAll('[data-rule-action-btn]'), function (btn) {
btn.classList.toggle('active', btn === target);
});
rowForAction.querySelector('[data-rule-action]').value = target.getAttribute('data-rule-action-value') || 'proxy';
settingsState.autoSwitchDirty = true;
syncProxyFormState();
scheduleAutoSwitchAutosave();
return;
}
if (!target.hasAttribute('data-rule-delete')) return;
var row = target.closest('tr[data-rule-index]');
if (!row) return;
var index = Number(row.getAttribute('data-rule-index'));
var rules = collectAutoSwitchRules(true);
if (Number.isInteger(index) && index >= 0 && index < rules.length) {
rules.splice(index, 1);
renderAutoSwitchRules(rules);
settingsState.autoSwitchDirty = true;
syncProxyFormState();
scheduleAutoSwitchAutosave();
}
});
document.getElementById('autoSwitchRulesBody').addEventListener('input', function () {
settingsState.autoSwitchDirty = true;
syncProxyFormState();
scheduleAutoSwitchAutosave();
});
document.getElementById('refreshGfwListBtn').addEventListener('click', function () {
var validationError = validateProxyPayload(proxyPayloadFromForm('gfw_list'));
if (validationError) {
setSelectedProxyMode('gfw_list');
setGfwListStatus('error', validationError, proxyField('gfwListSourceInput').value, null);
return;
}
setProxyBusy(true);
withTimeout(proxyApi().refreshGfwList(), 15000, 'proxy.refreshGfwList').then(function (result) {
settingsState.proxySettings = result;
renderProxySettings();
setProxyBusy(false);
}, function (err) {
console.error('[Settings] proxy.refreshGfwList error:', err);
setGfwListStatus(
'error',
proxyErrorMessage(err),
proxyField('gfwListSourceInput').value,
settingsState.proxySettings ? settingsState.proxySettings.gfwlistUpdatedAtMs : null
);
setProxyBusy(false);
});
});
var privacyToastTimer = null;
function showPrivacyToast(kind, message) {
var toast = document.getElementById('privacyToast');
if (privacyToastTimer) window.clearTimeout(privacyToastTimer);
toast.className = 'proxy-toast active' + (kind === 'error' ? ' is-error' : ' is-success');
toast.textContent = message;
privacyToastTimer = window.setTimeout(function () {
toast.className = 'proxy-toast';
toast.textContent = '';
privacyToastTimer = null;
}, 2600);
}
function hidePrivacyPanel() {
var nav = document.querySelector('.nav-item[data-panel="privacy"]');
if (nav) nav.style.display = 'none';
var panel = document.getElementById('panel-privacy');
var wasActive = panel && panel.classList.contains('active');
if (panel) panel.style.display = 'none';
if (wasActive || (nav && nav.classList.contains('active'))) switchPanel('downloads');
}
function sitesLabel(n) {
if (!n) return t('settings.noSites');
return n === 1 ? t('settings.siteCountOne') : t('settings.siteCountMany', { count: n });
}
function renderPrivacyUsage(usage) {
var historyUsage = document.getElementById('privacyHistoryUsage');
var cache = document.getElementById('privacyCacheUsage');
var cookies = document.getElementById('privacyCookieUsage');
if (!usage) {
historyUsage.textContent = t('downloads.loading');
cache.textContent = t('downloads.loading');
cookies.textContent = t('downloads.loading');
return;
}
historyUsage.textContent = usage.historyEntries
? t(usage.historyEntries === 1 ? 'history.countOne' : 'history.countMany', { count: usage.historyEntries })
: t('settings.noSavedHistory');
cache.textContent = usage.cacheSites
? t('settings.cacheSites', { sites: sitesLabel(usage.cacheSites) })
: t('settings.nothingCached');
cookies.textContent = usage.cookies || usage.siteDataSites
? t('settings.cookieUsage', { count: usage.cookies, sites: sitesLabel(usage.siteDataSites) })
: t('settings.noCookies');
}
function refreshPrivacyUsage() {
privacyApi().getUsage().then(renderPrivacyUsage, function (err) {
if (isRouteUnavailableError(err) || /not supported/i.test((err && err.message) || '')) {
hidePrivacyPanel();
return;
}
console.error('[Settings] privacy.getUsage error:', err);
document.getElementById('privacyHistoryUsage').textContent = t('settings.unavailable');
document.getElementById('privacyCacheUsage').textContent = t('settings.unavailable');
document.getElementById('privacyCookieUsage').textContent = t('settings.unavailable');
});
}
var clearDataState = { timeRange: 'allTime' };
var clearDataDialog = document.getElementById('clearBrowsingDataDialog');
var clearDataChecks = [
document.getElementById('clearHistoryCheck'),
document.getElementById('clearCacheCheck'),
document.getElementById('clearCookiesCheck')
];
function updateClearDataDialog() {
document.querySelectorAll('[data-time-range]').forEach(function (button) {
var selected = button.dataset.timeRange === clearDataState.timeRange;
button.classList.toggle('selected', selected);
button.setAttribute('aria-checked', selected ? 'true' : 'false');
});
document.getElementById('cookieWarning').classList.toggle(
'active',
document.getElementById('clearCookiesCheck').checked
);
document.getElementById('clearDataConfirmBtn').disabled =
!clearDataChecks.some(function (check) { return check.checked; });
}
function openClearDataDialog() {
clearDataDialog.classList.add('active');
updateClearDataDialog();
document.querySelector('[data-time-range].selected').focus();
}
function closeClearDataDialog() {
clearDataDialog.classList.remove('active');
}
document.getElementById('openClearDataBtn').addEventListener('click', openClearDataDialog);
document.getElementById('clearDataCancelBtn').addEventListener('click', closeClearDataDialog);
document.getElementById('timeRangeOptions').addEventListener('click', function (event) {
var option = event.target.closest('[data-time-range]');
if (!option) return;
clearDataState.timeRange = option.dataset.timeRange;
updateClearDataDialog();
});
clearDataChecks.forEach(function (check) {
check.addEventListener('change', updateClearDataDialog);
});
clearDataDialog.addEventListener('click', function (event) {
if (event.target === clearDataDialog) closeClearDataDialog();
});
document.addEventListener('keydown', function (event) {
if (event.key === 'Escape' && clearDataDialog.classList.contains('active')) {
closeClearDataDialog();
} else if (event.key === 'Escape' && clearSiteDataDialog.classList.contains('active')) {
closeClearSiteDataDialog();
}
});
document.getElementById('clearDataConfirmBtn').addEventListener('click', function () {
var button = document.getElementById('clearDataConfirmBtn');
button.disabled = true;
privacyApi().clearBrowsingData({
timeRange: clearDataState.timeRange,
history: document.getElementById('clearHistoryCheck').checked,
cache: document.getElementById('clearCacheCheck').checked,
siteData: document.getElementById('clearCookiesCheck').checked
}).then(function () {
button.disabled = false;
closeClearDataDialog();
showPrivacyToast('success', t('settings.dataCleared'));
refreshPrivacyUsage();
}, function (err) {
button.disabled = false;
console.error('[Settings] privacy.clearBrowsingData error:', err);
showPrivacyToast('error', t('settings.clearFailed'));
});
});
var clearSiteDataDialog = document.getElementById('clearSiteDataDialog');
var clearSiteDataState = { tabId: null, context: null };
var clearSiteChecks = [
document.getElementById('clearSiteCacheCheck'),
document.getElementById('clearSiteCookiesCheck')
];
function updateClearSiteDataDialog() {
document.getElementById('siteCookieWarning').classList.toggle(
'active',
document.getElementById('clearSiteCookiesCheck').checked
);
document.getElementById('clearSiteDataConfirmBtn').disabled =
!clearSiteDataState.context ||
!clearSiteChecks.some(function (check) { return check.checked; });
}
function closeClearSiteDataDialog() {
clearSiteDataDialog.classList.remove('active');
clearSiteDataState = { tabId: null, context: null };
if (location.hash.indexOf('#clear-site-data') === 0) {
history.replaceState(null, '', '#privacy');
}
}
function openClearSiteDataDialog(tabId) {
clearSiteDataState = { tabId: tabId, context: null };
document.getElementById('clearSiteDataHost').textContent = t('settings.loading');
clearSiteDataDialog.classList.add('active');
updateClearSiteDataDialog();
privacyApi().getSiteDataContext({ tabId: tabId }).then(function (context) {
clearSiteDataState.context = context;
document.getElementById('clearSiteDataHost').textContent = context.host || context.url;
updateClearSiteDataDialog();
document.getElementById('clearSiteCacheCheck').focus();
}, function (err) {
console.error('[Settings] privacy.getSiteDataContext error:', err);
closeClearSiteDataDialog();
showPrivacyToast('error', t('settings.siteUnavailable'));
});
}
clearSiteChecks.forEach(function (check) {
check.addEventListener('change', updateClearSiteDataDialog);
});
document.getElementById('clearSiteDataCancelBtn').addEventListener('click', closeClearSiteDataDialog);
clearSiteDataDialog.addEventListener('click', function (event) {
if (event.target === clearSiteDataDialog) closeClearSiteDataDialog();
});
document.getElementById('clearSiteDataConfirmBtn').addEventListener('click', function () {
var button = document.getElementById('clearSiteDataConfirmBtn');
var cacheRequested = document.getElementById('clearSiteCacheCheck').checked;
button.disabled = true;
privacyApi().clearSiteData({
tabId: clearSiteDataState.tabId,
cache: cacheRequested,
siteData: document.getElementById('clearSiteCookiesCheck').checked
}).then(function (result) {
closeClearSiteDataDialog();
showPrivacyToast(
'success',
cacheRequested && !result.cacheCleared
? t('settings.siteDataClearedSharedCacheKept')
: t('settings.siteDataCleared')
);
refreshPrivacyUsage();
}, function (err) {
button.disabled = false;
console.error('[Settings] privacy.clearSiteData error:', err);
showPrivacyToast('error', t('settings.clearSiteDataFailed'));
updateClearSiteDataDialog();
});
});
refreshPrivacyUsage();
function applyLocationHash() {
var hash = location.hash.replace('#', '');
if (hash === 'clear-browsing-data') {
switchPanel('privacy');
openClearDataDialog();
} else if (hash.indexOf('clear-site-data?') === 0) {
var query = new URLSearchParams(hash.slice(hash.indexOf('?') + 1));
var tabId = query.get('tabId');
switchPanel('privacy');
if (tabId) openClearSiteDataDialog(tabId);
} else if (hash && document.getElementById('panel-' + hash)) {
switchPanel(hash);
}
}
window.addEventListener('hashchange', applyLocationHash);
applyLocationHash();
document.getElementById('changeBtn').addEventListener('click', function () {
downloadsApi().chooseDirectory().then(function (s) {
settingsState.downloadSettings = s;
renderDownloadSettings();
}, function (err) {
console.error('[Settings] chooseDirectory error:', err);
});
});
document.getElementById('resetBtn').addEventListener('click', function () {
downloadsApi().resetDirectory().then(function (s) {
settingsState.downloadSettings = s;
renderDownloadSettings();
}, function (err) {
console.error('[Settings] resetDirectory error:', err);
});
});
appApi().getInfo().then(function (info) {
if (!info) return;
if (info.productName) document.getElementById('aboutProductName').textContent = info.productName;
if (info.version) {
document.getElementById('aboutVersion').textContent = t('settings.appVersionValue', { version: info.version });
document.getElementById('aboutAppVersion').textContent = info.version;
}
if (info.sdkVersion) {
document.getElementById('aboutSdkVersion').textContent = info.sdkVersion;
}
}).catch(function () {});
downloadsApi().getSettings().then(function (s) {
settingsState.downloadSettings = s;
renderDownloadSettings();
});
proxyApi().getSettings().then(function (s) {
settingsState.proxySettings = s;
renderProxySettings();
ensureProxyWatch();
}, function (err) {
if (isRouteUnavailableError(err)) {
console.info('[Settings] proxy capability unavailable; hiding Proxy panel.');
hideProxyPanel();
return;
}
console.error('[Settings] proxy.getSettings error:', err);
setProxyStatus('error', proxyErrorMessage(err), null);
ensureProxyWatch();
});
renderDownloadSettings();
renderProxySettings();
})();
</script>
</body>
</html>