From a8314eefa40d29d4f8bc0e6be632d6f8428755f8 Mon Sep 17 00:00:00 2001 From: ReturnFI <151555003+ReturnFI@users.noreply.github.com> Date: Wed, 17 Sep 2025 11:14:37 +0000 Subject: [PATCH] Feat: Refactor subscription page templates --- core/scripts/normalsub/normalsub.py | 35 +- core/scripts/normalsub/template.html | 519 --------------------- core/scripts/normalsub/template/index.html | 171 +++++++ core/scripts/normalsub/template/script.js | 119 +++++ core/scripts/normalsub/template/style.css | 118 +++++ 5 files changed, 438 insertions(+), 524 deletions(-) delete mode 100644 core/scripts/normalsub/template.html create mode 100644 core/scripts/normalsub/template/index.html create mode 100644 core/scripts/normalsub/template/script.js create mode 100644 core/scripts/normalsub/template/style.css diff --git a/core/scripts/normalsub/normalsub.py b/core/scripts/normalsub/normalsub.py index 425bb87..3284951 100644 --- a/core/scripts/normalsub/normalsub.py +++ b/core/scripts/normalsub/normalsub.py @@ -12,7 +12,7 @@ from io import BytesIO from aiohttp import web from aiohttp.web_middlewares import middleware -from urllib.parse import unquote, parse_qs, urlparse, urljoin +from urllib.parse import unquote, parse_qs, urlparse, urljoin, quote from dotenv import load_dotenv import qrcode from jinja2 import Environment, FileSystemLoader @@ -127,9 +127,14 @@ class TemplateContext: expiration_date: str sublink_qrcode: str sub_link: str + sub_link_encoded: str blocked: bool = False local_uris: List[NodeURI] = field(default_factory=list) node_uris: List[NodeURI] = field(default_factory=list) + singbox_qrcode: Optional[str] = None + hiddify_qrcode: Optional[str] = None + streisand_qrcode: Optional[str] = None + nekobox_qrcode: Optional[str] = None class Utils: @@ -398,14 +403,14 @@ class SubscriptionManager: f"total={user_info.max_download_bytes}; " f"expire={user_info.expiration_timestamp}\n" ) - profile_lines = f"//profile-title: {username}-Hysteria2 🚀\n//profile-update-interval: 1\n" + profile_lines = f"//profile-title: {username}-Blitz ⚡\n//profile-update-interval: 1\n" return profile_lines + subscription_info + "\n".join(all_processed_uris) class TemplateRenderer: def __init__(self, template_dir: str, config: AppConfig): self.env = Environment(loader=FileSystemLoader(template_dir), autoescape=True) - self.html_template = self.env.get_template('template.html') + self.html_template = self.env.get_template('index.html') self.config = config def render(self, context: TemplateContext) -> str: @@ -430,6 +435,8 @@ class HysteriaServer: safe_subpath = self.validate_and_escape_subpath(self.config.subpath) base_path = f'/{safe_subpath}' + self.app.router.add_get(f'{base_path}/sub/normal/style.css', self.handle_style) + self.app.router.add_get(f'{base_path}/sub/normal/script.js', self.handle_script) self.app.router.add_get(f'{base_path}/sub/normal/{{password_token}}', self.handle) self.app.router.add_get(f'{base_path}/robots.txt', self.robots_handler) self.app.router.add_route('*', f'{base_path}/{{tail:.*}}', self.handle_404_subpath) @@ -452,7 +459,7 @@ class HysteriaServer: extra_config_path = '/etc/hysteria/extra.json' rate_limit = 100 rate_limit_window = 60 - template_dir = os.path.dirname(__file__) + template_dir = os.path.join(os.path.dirname(__file__), 'template') sni = self._load_sni_from_env(sni_file) return AppConfig(domain=domain, external_port=external_port, @@ -565,6 +572,7 @@ class HysteriaServer: expiration_date=user_info.expiration_date, sublink_qrcode="", sub_link="#blocked", + sub_link_encoded="", blocked=True, local_uris=[ NodeURI( @@ -603,8 +611,14 @@ class HysteriaServer: print(f"Warning: Constructed base URL '{base_url}' might be invalid. Check domain and port config.") sub_link = f"{base_url}/{self.config.subpath}/sub/normal/{user_info.password}" + sub_link_encoded = quote(sub_link, safe='') sublink_qrcode = Utils.generate_qrcode_base64(sub_link) + singbox_qrcode = Utils.generate_qrcode_base64(f"sing-box://import-remote-profile?url={sub_link_encoded}") + hiddify_qrcode = Utils.generate_qrcode_base64(f"hiddify://import/{sub_link_encoded}") + streisand_qrcode = Utils.generate_qrcode_base64(f"streisand://import/sub?url={sub_link_encoded}") + nekobox_qrcode = Utils.generate_qrcode_base64(f"nekobox://import?url={sub_link_encoded}") + local_uris = [] node_uris = [] @@ -626,9 +640,14 @@ class HysteriaServer: expiration_date=user_info.expiration_date, sublink_qrcode=sublink_qrcode, sub_link=sub_link, + sub_link_encoded=sub_link_encoded, blocked=user_info.blocked, local_uris=local_uris, - node_uris=node_uris + node_uris=node_uris, + singbox_qrcode=singbox_qrcode, + hiddify_qrcode=hiddify_qrcode, + streisand_qrcode=streisand_qrcode, + nekobox_qrcode=nekobox_qrcode ) async def robots_handler(self, request: web.Request) -> web.Response: @@ -637,6 +656,12 @@ class HysteriaServer: async def handle_404_subpath(self, request: web.Request) -> web.Response: print(f"404 Not Found (within subpath, unhandled by specific routes): {request.path}") return web.Response(status=404, text="Not Found within Subpath") + + async def handle_style(self, request: web.Request) -> web.Response: + return web.FileResponse(os.path.join(self.config.template_dir, 'style.css')) + + async def handle_script(self, request: web.Request) -> web.Response: + return web.FileResponse(os.path.join(self.config.template_dir, 'script.js')) def run(self): print(f"Starting Hysteria Normalsub server on {self.config.aiohttp_listen_address}:{self.config.aiohttp_listen_port}") diff --git a/core/scripts/normalsub/template.html b/core/scripts/normalsub/template.html deleted file mode 100644 index 0912c67..0000000 --- a/core/scripts/normalsub/template.html +++ /dev/null @@ -1,519 +0,0 @@ - - - - - - - - Hysteria2 Subscription - - - - - -
-
-
-
- -
-
-

Welcome {{ username }} 🚀❤️

- -
- -
-
-
- - Username -
-
-

{{ username }}

-
-
- -
-
- - Used / Total -
-
-

- {{ usage }} -

-
-
- -
-
- - Expiration Date -
-
-

{{ expiration_date }}

-
-
-
- -
-
- - Subscription Link -
-
-
- {% if blocked %} -
-

Account Suspended

-

Your subscription link is disabled. Please contact support.

-
- {% else %} -
-

Universal Subscription Link

- Subscription QR Code -
- -
-
- {% endif %} -
-
-
- -
-
- - {% if blocked %} - Connection Status - {% else %} - Local Server Connections - {% endif %} -
-
-
- {% if blocked %} - {% for item in local_uris %} -
-

{{ item.label }}

-

{{ usage_raw }}

-
- {% endfor %} - {% else %} - {% for item in local_uris %} -
-

{{ item.label }} URI

- {% if item.qrcode %} - {{ item.label }} QR Code -
- -
- {% else %} -

{{ item.label }} URI not available

- {% endif %} -
- {% endfor %} - {% endif %} -
-
-
- - {% if node_uris and not blocked %} -
-
- - External Nodes -
-
-
- {% for item in node_uris %} -
-

{{ item.label }}

- {% if item.qrcode %} - {{ item.label }} QR Code -
- -
- {% else %} -

{{ item.label }} URI not available

- {% endif %} -
- {% endfor %} -
-
-
- {% endif %} - -
- - - - - \ No newline at end of file diff --git a/core/scripts/normalsub/template/index.html b/core/scripts/normalsub/template/index.html new file mode 100644 index 0000000..ab171fe --- /dev/null +++ b/core/scripts/normalsub/template/index.html @@ -0,0 +1,171 @@ + + + + + + + {{ username }} + + + + +
+
+
+
+ +
+
+

Welcome, {{ username }}

+ +
+ +
+
+
+
+
+

Username

+

{{ username }}

+
+
+ +
+
+
+

Data Usage

+

{{ usage }}

+
+
+ +
+
+
+

Expires On

+

{{ expiration_date }}

+
+
+
+ +
+
+ +

Client Import

+
+
+ {% if blocked %} +
+ +

Account Suspended

+

Your subscription is currently disabled.

+
+ {% else %} +
+
+ + + + + +
+
+
+ Universal Subscription QR Code + +
+ + + + +
+
+ {% endif %} +
+
+ +
+
+ +

Manual Connection URIs

+
+
+ {% if blocked %} +
+ +

Connections Disabled

+

{{ usage_raw }}

+
+ {% else %} +
+ {% for item in local_uris %} +
+

{{ item.label }}

+ {% if item.qrcode %} + {{ item.label }} QR Code + + {% else %} +

URI not available

+ {% endif %} +
+ {% endfor %} +
+ {% endif %} +
+
+ + {% if node_uris and not blocked %} +
+
+ +

External Node URIs

+
+
+
+ {% for item in node_uris %} +
+

{{ item.label }}

+ {% if item.qrcode %} + {{ item.label }} QR Code + + {% else %} +

URI not available

+ {% endif %} +
+ {% endfor %} +
+
+
+ {% endif %} +
+
+ + + \ No newline at end of file diff --git a/core/scripts/normalsub/template/script.js b/core/scripts/normalsub/template/script.js new file mode 100644 index 0000000..b3c5767 --- /dev/null +++ b/core/scripts/normalsub/template/script.js @@ -0,0 +1,119 @@ +document.addEventListener('DOMContentLoaded', () => { + const themeToggle = document.getElementById('dark-mode-toggle'); + const themeIcon = document.getElementById('theme-icon'); + let isDarkMode = localStorage.getItem('darkMode') === 'enabled'; + + const enableDarkMode = () => { + document.body.classList.add('dark-mode'); + themeIcon.classList.replace('fa-sun', 'fa-moon'); + localStorage.setItem('darkMode', 'enabled'); + isDarkMode = true; + }; + + const disableDarkMode = () => { + document.body.classList.remove('dark-mode'); + themeIcon.classList.replace('fa-moon', 'fa-sun'); + localStorage.setItem('darkMode', 'disabled'); + isDarkMode = false; + }; + + if (isDarkMode) { + enableDarkMode(); + } else { + const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; + if (prefersDark && localStorage.getItem('darkMode') === null) { + enableDarkMode(); + } + } + + themeToggle.addEventListener('click', () => { + isDarkMode ? disableDarkMode() : enableDarkMode(); + }); + + // --- Loading Indicator --- + const loadingIndicator = document.getElementById('loading-indicator'); + window.addEventListener('load', () => { + loadingIndicator.style.opacity = '0'; + setTimeout(() => { + loadingIndicator.style.display = 'none'; + }, 300); + }); + + // --- App Import Tabs --- + const tabsContainer = document.querySelector('.app-tabs'); + if (tabsContainer) { + const tabButtons = tabsContainer.querySelectorAll('.app-tab-btn'); + const tabPanes = tabsContainer.querySelectorAll('.app-tab-pane'); + + tabButtons.forEach(button => { + button.addEventListener('click', () => { + const targetId = button.getAttribute('data-target'); + + tabButtons.forEach(btn => btn.classList.remove('active')); + button.classList.add('active'); + + tabPanes.forEach(pane => { + if ('#' + pane.id === targetId) { + pane.classList.add('active'); + } else { + pane.classList.remove('active'); + } + }); + }); + }); + } +}); + +function copyToClipboard(text) { + if (!navigator.clipboard) { + alert('Clipboard API not available.'); + return; + } + navigator.clipboard.writeText(text).then(() => { + showToast('Copied to clipboard!'); + }).catch(err => { + console.error('Failed to copy text: ', err); + alert('Failed to copy.'); + }); +} + +function showToast(message) { + let toast = document.querySelector('.toast'); + if (toast) { + toast.remove(); + } + + toast = document.createElement('div'); + toast.className = 'toast'; + toast.textContent = message; + + document.body.appendChild(toast); + + const toastStyles = ` + position: fixed; + bottom: 2rem; + left: 50%; + transform: translateX(-50%) translateY(20px); + background-color: var(--text-light-primary); + color: var(--bg-light); + padding: 0.75rem 1.5rem; + border-radius: 0.75rem; + font-weight: 500; + z-index: 1001; + opacity: 0; + transition: opacity 0.3s ease, transform 0.3s ease; + box-shadow: var(--shadow-lg); + `; + toast.style.cssText = toastStyles; + + setTimeout(() => { + toast.style.opacity = '1'; + toast.style.transform = 'translateX(-50%) translateY(0)'; + }, 10); + + setTimeout(() => { + toast.style.opacity = '0'; + toast.style.transform = 'translateX(-50%) translateY(20px)'; + toast.addEventListener('transitionend', () => toast.remove()); + }, 2000); +} \ No newline at end of file diff --git a/core/scripts/normalsub/template/style.css b/core/scripts/normalsub/template/style.css new file mode 100644 index 0000000..0fd5b08 --- /dev/null +++ b/core/scripts/normalsub/template/style.css @@ -0,0 +1,118 @@ +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'); + +:root { + /* Color Palette */ + --bg-light: #f9fafb; + --bg-dark: #111827; + --card-bg-light: #ffffff; + --card-bg-dark: #1f2937; + --text-light-primary: #1f2937; + --text-light-secondary: #6b7280; + --text-dark-primary: #f9fafb; + --text-dark-secondary: #9ca3af; + --accent-primary: #4f46e5; + --accent-secondary: #3b82f6; + --accent-danger: #e11d48; + --border-light: #e5e7eb; + --border-dark: #374151; + --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1); + --gradient: linear-gradient(135deg, var(--accent-secondary) 0%, var(--accent-primary) 100%); + + /* Layout & Design */ + --radius: 0.75rem; + --transition-speed: 0.3s; +} + +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } + +body { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + background-color: var(--bg-light); + color: var(--text-light-primary); + transition: background-color var(--transition-speed) ease, color var(--transition-speed) ease; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body.dark-mode { + --bg-light: var(--bg-dark); + --card-bg-light: var(--card-bg-dark); + --text-light-primary: var(--text-dark-primary); + --text-light-secondary: var(--text-dark-secondary); + --border-light: var(--border-dark); + --shadow-sm: none; --shadow-md: none; --shadow-lg: none; +} + +.background-animation { position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: -1; overflow: hidden; background: var(--bg-light); transition: background var(--transition-speed) ease; } +.background-animation::before, .background-animation::after { content: ''; position: absolute; width: 40vw; height: 40vw; max-width: 500px; max-height: 500px; border-radius: 50%; background: var(--gradient); opacity: 0.1; filter: blur(120px); animation: float 30s infinite ease-in-out; } +.background-animation::before { top: 5%; left: 5%; } +.background-animation::after { bottom: 5%; right: 5%; animation-delay: -15s; } +@keyframes float { 50% { transform: translate(20%, -20%) scale(1.1); } } + +.container { max-width: 900px; margin: 0 auto; padding: 2rem; } +.header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 3.5rem; } +.header-title { font-size: 1.75rem; font-weight: 600; } +.header-title strong { font-weight: 700; background: var(--gradient); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; } +.theme-toggle { background-color: transparent; border: 1px solid var(--border-light); color: var(--text-light-secondary); width: 40px; height: 40px; border-radius: 50%; cursor: pointer; display: grid; place-items: center; font-size: 1.125rem; transition: all var(--transition-speed) ease; } +.theme-toggle:hover { color: var(--accent-primary); border-color: var(--accent-primary); transform: rotate(15deg); } + +.card { background: var(--card-bg-light); border: 1px solid var(--border-light); border-radius: var(--radius); margin-bottom: 1rem; margin-top: 1rem; box-shadow: var(--shadow-md); transition: transform var(--transition-speed) ease, box-shadow var(--transition-speed) ease; } +.dark-mode .card { background: var(--card-bg-dark); } +.card:hover { transform: translateY(-5px); box-shadow: var(--shadow-lg); } +.card:last-child { margin-bottom: 0; } +.card-header { display: flex; align-items: center; gap: 0.75rem; padding: 1.25rem 1.5rem; border-bottom: 1px solid var(--border-light); font-weight: 600; font-size: 1.125rem; } +.card-header i { color: var(--accent-primary); } +.card-body { padding: 1.5rem; } + +.info-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 1.5rem; } +.info-card { display: flex; align-items: center; gap: 1rem; padding: 1.5rem; margin-bottom: 0; } +.card-icon { background: var(--gradient); color: white; width: 48px; height: 48px; border-radius: 50%; display: grid; place-items: center; font-size: 1.25rem; flex-shrink: 0; } +.card-title { font-size: 0.875rem; font-weight: 500; color: var(--text-light-secondary); margin-bottom: 0.25rem; } +.card-value { font-size: 1.25rem; font-weight: 600; } + +[data-tooltip] { position: relative; cursor: help; } +[data-tooltip]::after { content: attr(data-tooltip); position: absolute; bottom: 110%; left: 50%; transform: translateX(-50%); background: var(--text-light-primary); color: var(--bg-light); padding: 0.5rem 0.75rem; border-radius: 0.5rem; font-size: 0.875rem; font-weight: 500; white-space: nowrap; opacity: 0; visibility: hidden; transition: opacity var(--transition-speed) ease; box-shadow: var(--shadow-lg); } +[data-tooltip]:hover::after { opacity: 1; visibility: visible; } + +.app-import-section.blocked, .uri-section.blocked { background-color: var(--accent-danger); color: white; } +.app-import-section.blocked .card-header, .uri-section.blocked .card-header { border-color: transparent; color: white; } +.app-import-section.blocked .card-header i, .uri-section.blocked .card-header i { color: white; } +.blocked-message { text-align: center; } +.blocked-message i { font-size: 2.5rem; margin-bottom: 1rem; } +.blocked-message h3 { font-size: 1.25rem; } +.blocked-message p { opacity: 0.9; } + +.app-tabs-nav { display: flex; overflow-x: auto; margin-bottom: 1.5rem; border-bottom: 1px solid var(--border-light); gap: 0.5rem; } +.app-tab-btn { font-family: inherit; font-size: 0.9rem; font-weight: 600; color: var(--text-light-secondary); background: transparent; border: none; padding: 0.75rem 1rem; cursor: pointer; transition: all var(--transition-speed) ease; border-bottom: 2px solid transparent; } +.app-tab-btn:hover { color: var(--text-light-primary); } +.app-tab-btn.active { color: var(--accent-primary); border-bottom-color: var(--accent-primary); } +.app-tabs-content { padding-top: 1rem; } +.app-tab-pane { display: none; flex-direction: column; align-items: center; gap: 1.25rem; } +.app-tab-pane.active { display: flex; } + +.qr-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 2.5rem; place-items: center; } +.qr-item { display: flex; flex-direction: column; align-items: center; gap: 1.25rem; text-align: center; } +.qr-title { font-weight: 600; font-size: 1rem; } +.qrcode { max-width: 200px; width: 100%; padding: 0.75rem; background: white; border-radius: 0.75rem; box-shadow: var(--shadow-md); } + +.btn { font-family: inherit; text-decoration: none; font-size: 1rem; font-weight: 600; padding: 0.75rem 1.5rem; border: none; border-radius: 0.5rem; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; gap: 0.5rem; transition: all var(--transition-speed) ease; } +.btn-primary { background: var(--gradient); color: white; box-shadow: var(--shadow-md); } +.btn-primary:hover { transform: translateY(-3px); box-shadow: var(--shadow-lg); } +.btn-secondary { background: var(--card-bg-light); color: var(--accent-primary); border: 1px solid var(--border-light); } +.btn-secondary:hover { background-color: var(--accent-primary); color: white; border-color: var(--accent-primary); } + +.uri-unavailable { color: var(--text-light-secondary); } + +.loading-indicator { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: var(--bg-light); display: grid; place-items: center; z-index: 1000; transition: opacity var(--transition-speed) ease; } +.spinner { width: 3rem; height: 3rem; border: 4px solid var(--accent-primary); border-top-color: transparent; border-radius: 50%; animation: spin 0.8s linear infinite; } +@keyframes spin { to { transform: rotate(360deg); } } + +@media (max-width: 768px) { + .container { padding: 1rem; } + .header { flex-direction: column; gap: 1.5rem; align-items: flex-start; margin-bottom: 2.5rem; } + .header-title { font-size: 1.5rem; } + .card-body { padding: 1rem; } + .info-grid { grid-template-columns: 1fr; } +} \ No newline at end of file