Skip to content
Open

aaa #272

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 35 additions & 8 deletions functions/modules/utils/node-cleaner.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,20 @@ export function fixNodeUrlEncoding(nodeUrl) {
}
};

const decodeRepeated = (value) => {
let decoded = safeDecode(value);
if (decoded.includes('%')) {
const decodedTwice = safeDecode(decoded);
if (decodedTwice !== decoded) {
decoded = decodedTwice;
}
}
return decoded;
};

// 辅助函数:判断是否需要保持原样(即解码后出现乱码)
const shouldKeepRaw = (decoded) => {
return decoded.includes('');
return decoded.includes('');
};

let fixedUrl = nodeUrl;
Expand All @@ -42,7 +53,7 @@ export function fixNodeUrlEncoding(nodeUrl) {
// 修复 hash (节点名称)
if (urlObj.hash) {
const rawHash = urlObj.hash.substring(1);
const decodedHash = safeDecode(rawHash);
const decodedHash = decodeRepeated(rawHash);
if (!shouldKeepRaw(decodedHash)) {
urlObj.hash = '#' + encodeURIComponent(decodedHash);
}
Expand All @@ -68,13 +79,29 @@ export function fixSSEncoding(nodeUrl) {
if (!nodeUrl.startsWith('ss://')) return nodeUrl;

try {
const urlObj = new URL(nodeUrl);
if (urlObj.hash) {
try {
urlObj.hash = '#' + encodeURIComponent(decodeURIComponent(urlObj.hash.substring(1)));
} catch (e) { }
const hashIndex = nodeUrl.indexOf('#');
const baseUrl = hashIndex === -1 ? nodeUrl : nodeUrl.substring(0, hashIndex);
const hashPart = hashIndex === -1 ? '' : nodeUrl.substring(hashIndex + 1);

let fixedBase = baseUrl;
const prefix = 'ss://';
if (baseUrl.startsWith(prefix)) {
const afterScheme = baseUrl.substring(prefix.length);
const atIndex = afterScheme.indexOf('@');
if (atIndex !== -1) {
const base64Part = afterScheme.substring(0, atIndex);
const rest = afterScheme.substring(atIndex);
const decodedBase64 = base64Part.includes('%') ? decodeURIComponent(base64Part) : base64Part;
fixedBase = `${prefix}${decodedBase64}${rest}`;
}
}
return urlObj.toString();

if (!hashPart) {
return fixedBase;
}

const decodedName = decodeURIComponent(hashPart);
return `${fixedBase}#${encodeURIComponent(decodedName)}`;
} catch (e) {
const parts = nodeUrl.split('#');
if (parts.length > 1) {
Expand Down
4 changes: 3 additions & 1 deletion public/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@
"name": "MiSub - 订阅转换器",
"short_name": "MiSub",
"description": "基于 Cloudflare 的订阅转换和管理工具",
"start_url": "/",
"start_url": "/?source=pwa",
"display": "standalone",
"display_override": ["standalone", "minimal-ui", "browser"],
"background_color": "#0f172a",
"theme_color": "#4f46e5",
"orientation": "portrait-primary",
"scope": "/",
"id": "/?source=pwa",
"lang": "zh-CN",
"categories": ["productivity", "utilities"],
"icons": [
Expand Down
172 changes: 126 additions & 46 deletions public/offline.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,127 +3,207 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#0f172a" />
<title>MiSub - 离线模式</title>
<style>
:root {
color-scheme: light dark;
--bg: #f8fafc;
--card: rgba(255, 255, 255, 0.85);
--text: #0f172a;
--muted: #64748b;
--primary: #4f46e5;
--border: rgba(148, 163, 184, 0.2);
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #0b1120;
--card: rgba(15, 23, 42, 0.75);
--text: #f8fafc;
--muted: #94a3b8;
--primary: #818cf8;
--border: rgba(148, 163, 184, 0.2);
}
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-family: 'Plus Jakarta Sans', 'Outfit', 'Segoe UI', sans-serif;
margin: 0;
padding: 0;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
color: white;
background:
radial-gradient(circle at 0% 0%, rgba(99, 102, 241, 0.12), transparent 55%),
radial-gradient(circle at 100% 100%, rgba(14, 165, 233, 0.12), transparent 55%),
var(--bg);
color: var(--text);
}
.container {
text-align: center;
padding: 2rem;
max-width: 400px;
padding: 2.5rem 2.25rem;
max-width: 440px;
border-radius: 28px;
background: var(--card);
border: 1px solid var(--border);
box-shadow: 0 30px 60px -40px rgba(15, 23, 42, 0.6);
backdrop-filter: blur(12px);
}
.icon {
width: 80px;
height: 80px;
margin: 0 auto 2rem;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
width: 88px;
height: 88px;
margin: 0 auto 1.5rem;
border-radius: 24px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(99, 102, 241, 0.12);
color: var(--primary);
}
h1 {
font-size: 1.5rem;
margin-bottom: 1rem;
font-weight: 600;
margin-bottom: 0.75rem;
font-weight: 700;
}
p {
font-size: 1rem;
opacity: 0.9;
color: var(--muted);
line-height: 1.6;
margin-bottom: 2rem;
margin-bottom: 1.5rem;
}
.actions {
display: flex;
gap: 0.75rem;
justify-content: center;
flex-wrap: wrap;
margin-bottom: 1.5rem;
}
.btn {
background: rgba(255, 255, 255, 0.2);
border: 2px solid rgba(255, 255, 255, 0.3);
background: var(--primary);
border: none;
color: white;
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
font-size: 1rem;
padding: 0.7rem 1.4rem;
border-radius: 12px;
font-size: 0.95rem;
cursor: pointer;
transition: all 0.3s ease;
transition: transform 0.2s ease, box-shadow 0.2s ease, opacity 0.2s ease;
text-decoration: none;
display: inline-block;
display: inline-flex;
align-items: center;
justify-content: center;
box-shadow: 0 12px 30px -16px rgba(79, 70, 229, 0.7);
}
.btn.secondary {
background: transparent;
border: 1px solid var(--border);
color: var(--text);
box-shadow: none;
}
.btn:hover {
background: rgba(255, 255, 255, 0.3);
transform: translateY(-1px);
opacity: 0.95;
}
.features {
margin-top: 2rem;
text-align: left;
border-top: 1px solid var(--border);
padding-top: 1.25rem;
margin-top: 1.25rem;
}
.feature {
display: flex;
align-items: center;
margin-bottom: 0.75rem;
font-size: 0.9rem;
color: var(--muted);
}
.feature:last-child {
margin-bottom: 0;
}
.feature-icon {
width: 16px;
height: 16px;
margin-right: 0.5rem;
opacity: 0.8;
color: var(--primary);
}
.status {
display: inline-flex;
align-items: center;
gap: 0.5rem;
font-size: 0.85rem;
color: var(--muted);
}
.dot {
width: 8px;
height: 8px;
border-radius: 999px;
background: #f97316;
}
.dot.online {
background: #22c55e;
}
</style>
</head>
<body>
<div class="container">
<div class="icon">
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
<svg width="42" height="42" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M2 12s3.5-6.5 10-6.5 10 6.5 10 6.5-3.5 6.5-10 6.5-10-6.5-10-6.5z"></path>
<circle cx="12" cy="12" r="3"></circle>
</svg>
</div>

<h1>您当前处于离线状态</h1>
<p>网络连接似乎出现了问题,但您仍然可以查看已缓存的内容。</p>

<button class="btn" onclick="window.location.reload()">重新连接</button>


<h1>当前离线</h1>
<p>网络暂不可用,但已缓存的页面仍可浏览。恢复连接后将自动同步最新数据。</p>

<div class="actions">
<button class="btn" onclick="window.location.reload()">重新连接</button>
<a class="btn secondary" href="/">返回主页</a>
</div>

<div class="status">
<span class="dot" id="statusDot"></span>
<span id="statusText">离线状态</span>
</div>

<div class="features">
<div class="feature">
<svg class="feature-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="20,6 9,17 4,12"></polyline>
</svg>
离线时可查看已缓存的订阅
已缓存的订阅和配置可继续查看
</div>
<div class="feature">
<svg class="feature-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="20,6 9,17 4,12"></polyline>
</svg>
数据会在连接恢复后同步
恢复网络后自动尝试同步
</div>
<div class="feature">
<svg class="feature-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="20,6 9,17 4,12"></polyline>
</svg>
PWA应用离线体验优化
建议保持应用常驻以加速更新
</div>
</div>
</div>

<script>
// 定期检查网络连接
setInterval(() => {
if (navigator.onLine) {
window.location.reload();
const statusDot = document.getElementById('statusDot');
const statusText = document.getElementById('statusText');

const updateStatus = () => {
const online = navigator.onLine;
statusDot.classList.toggle('online', online);
statusText.textContent = online ? '已恢复连接' : '离线状态';
if (online) {
setTimeout(() => window.location.reload(), 600);
}
}, 5000);
// 监听网络状态变化
window.addEventListener('online', () => {
window.location.reload();
});
};

updateStatus();
window.addEventListener('online', updateStatus);
window.addEventListener('offline', updateStatus);
setInterval(updateStatus, 5000);
</script>
</body>
</html>
</html>
Loading