@swimmingliu/autovpn 1.8.0 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,76 @@
1
+ import { icon } from './icons.js';
2
+
3
+ const THEME_KEY = 'autovpn.ui.theme';
4
+ const THEMES = ['system', 'light', 'dark'];
5
+
6
+ // Presentation preferences must work when browser storage is disabled, too.
7
+ export function readPreference(key, fallback) {
8
+ try {
9
+ return JSON.parse(localStorage.getItem(key)) ?? fallback;
10
+ } catch {
11
+ return fallback;
12
+ }
13
+ }
14
+
15
+ export function writePreference(key, value) {
16
+ try {
17
+ localStorage.setItem(key, JSON.stringify(value));
18
+ } catch {
19
+ // A blocked/full store only disables persistence; the current UI still works.
20
+ }
21
+ }
22
+
23
+ export function getThemePreference() {
24
+ const saved = readPreference(THEME_KEY, 'system');
25
+ return THEMES.includes(saved) ? saved : 'system';
26
+ }
27
+
28
+ export function applyTheme(preference = getThemePreference()) {
29
+ const choice = THEMES.includes(preference) ? preference : 'system';
30
+ const dark = choice === 'dark' || (choice === 'system' && matchMedia('(prefers-color-scheme: dark)').matches);
31
+ document.documentElement.dataset.theme = dark ? 'dark' : 'light';
32
+ document.documentElement.dataset.themePreference = choice;
33
+ for (const button of document.querySelectorAll('[data-theme-choice]')) {
34
+ button.setAttribute('aria-pressed', String(button.dataset.themeChoice === choice));
35
+ }
36
+ }
37
+
38
+ export function selectTheme(preference) {
39
+ writePreference(THEME_KEY, preference);
40
+ applyTheme(preference);
41
+ }
42
+
43
+ export function toggleSidebar() {
44
+ const collapsed = document.documentElement.dataset.sidebarCollapsed !== 'true';
45
+ writePreference('autovpn.ui.sidebarCollapsed', collapsed);
46
+ applySidebar(collapsed);
47
+ }
48
+
49
+ function applySidebar(collapsed) {
50
+ document.documentElement.dataset.sidebarCollapsed = String(collapsed);
51
+ const button = document.querySelector('#sidebarToggle');
52
+ if (!button) return;
53
+ const label = collapsed ? '展开导航' : '收起导航';
54
+ button.setAttribute('aria-label', label);
55
+ button.setAttribute('aria-expanded', String(!collapsed));
56
+ button.title = label;
57
+ button.innerHTML = icon(collapsed ? 'right' : 'left');
58
+ }
59
+
60
+ export function initializeAppearance() {
61
+ applySidebar(readPreference('autovpn.ui.sidebarCollapsed', false) === true);
62
+ applyTheme();
63
+ matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
64
+ if (document.documentElement.dataset.themePreference === 'system') applyTheme('system');
65
+ });
66
+ const timers = new WeakMap();
67
+ // Keep native scrolling and drag behavior. Only the thumb's paint changes,
68
+ // so appearing/disappearing scrollbars cannot shift a table column.
69
+ document.addEventListener('scroll', (event) => {
70
+ const target = event.target === document ? document.documentElement : event.target;
71
+ if (!(target instanceof Element)) return;
72
+ clearTimeout(timers.get(target));
73
+ target.setAttribute('data-scroll-active', '');
74
+ timers.set(target, setTimeout(() => target.removeAttribute('data-scroll-active'), 800));
75
+ }, true);
76
+ }
@@ -5,7 +5,7 @@ const ZH_MESSAGES = {
5
5
  locale: 'zh-CN',
6
6
  appTitle: 'AutoVPN',
7
7
  sidebarTitle: 'AutoVPN',
8
- sidebarVersion: 'v.1.8.0',
8
+ sidebarVersion: 'v.2.0.2',
9
9
  brandSubtitle: '概览、运行、结果、订阅、日志、设置统一管理',
10
10
  languageLabel: '',
11
11
  saveButton: '保存配置',
@@ -0,0 +1,38 @@
1
+ // Shared SVG geometry replaces font-dependent symbols, which had different
2
+ // weight and alignment across the desktop and web renderers.
3
+ const paths = {
4
+ qr: '<path d="M3 3h6v6H3zm12 0h6v6h-6zM3 15h6v6H3zm12 0h3v3h3v3h-6zM12 3v2m0 4v3H9m-6 0h2m7 3v6m9-9h-6"/>',
5
+ dashboard: '<rect x="3" y="3" width="7" height="7" rx="1.5"/><rect x="14" y="3" width="7" height="7" rx="1.5"/><rect x="3" y="14" width="7" height="7" rx="1.5"/><rect x="14" y="14" width="7" height="7" rx="1.5"/>',
6
+ runs: '<path d="m9 4 12 8-12 8z"/><path d="M3 4v16"/>',
7
+ results: '<path d="M8 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-3M9 15l3-3 3 2 6-8"/><path d="M16 6h5v5"/>',
8
+ subscriptions: '<path d="M10 13a5 5 0 0 0 7 .1l3-3a5 5 0 0 0-7-7l-2 2M14 11a5 5 0 0 0-7-.1l-3 3a5 5 0 0 0 7 7l2-2"/>',
9
+ logs: '<rect x="3" y="4" width="18" height="16" rx="2"/><path d="m7 9 3 3-3 3m6 0h4"/>',
10
+ settings: '<path d="M12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8Z"/><path d="m9 3 1-1h4l1 1 .5 2 2 .8 2-.6 2 3.6-.5 1.2-1.5 1.5v2l1.5 1.5.5 1.2-2 3.6-2-.6-2 .8-.5 2-1 1h-4l-1-1-.5-2-2-.8-2 .6-2-3.6.5-1.2L4.5 13v-2L3 9.5 2.5 8.3l2-3.6 2 .6 2-.8Z"/>',
11
+ copy: '<rect x="8" y="8" width="12" height="13" rx="2"/><path d="M16 8V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h3"/>',
12
+ folder: '<path d="M3 7V5a2 2 0 0 1 2-2h5l2 3h7a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7Z"/>',
13
+ refresh: '<path d="M20 7v5h-5M4 17v-5h5"/><path d="M6 6a8 8 0 0 1 14 6M4 12a8 8 0 0 0 14 6"/>',
14
+ download: '<path d="M12 3v12m-5-5 5 5 5-5M4 16v4h16v-4"/>',
15
+ upload: '<path d="M12 16V4m-5 5 5-5 5 5M4 16v4h16v-4"/>',
16
+ external: '<path d="M14 3h7v7m0-7L10 14M9 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-4"/>',
17
+ close: '<path d="m6 6 12 12M6 18 18 6"/>',
18
+ trash: '<path d="M3 6h18M9 6V3h6v3M5 6l1 15h12l1-15M10 10v7m4-7v7"/>',
19
+ play: '<path d="m8 4 12 8-12 8z"/>',
20
+ stop: '<rect x="5" y="5" width="14" height="14" rx="2"/>',
21
+ check: '<path d="m5 12 4 4L19 6"/>',
22
+ chevron: '<path d="m9 5 7 7-7 7"/>',
23
+ sun: '<circle cx="12" cy="12" r="4"/><path d="M12 2v2m0 16v2M2 12h2m16 0h2M5 5l1.5 1.5m11 11L19 19M5 19l1.5-1.5m11-11L19 5"/>',
24
+ moon: '<path d="M21 13A9 9 0 0 1 11 3 9 9 0 1 0 21 13Z"/>',
25
+ monitor: '<rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8m-4-4v4"/>',
26
+ sources: '<circle cx="12" cy="5" r="3"/><circle cx="5" cy="18" r="3"/><circle cx="19" cy="18" r="3"/><path d="M12 8v4m0 0-7 3m7-3 7 3"/>',
27
+ speed_test: '<path d="M4 19a10 10 0 1 1 16 0M12 13l5-6M4 12h2m12 0h2M7 6l1 2"/><circle cx="12" cy="13" r="2"/>',
28
+ availability_targets: '<circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3a17 17 0 0 1 0 18 17 17 0 0 1 0-18Z"/>',
29
+ deploy: '<path d="M7 18H6a4 4 0 0 1-1-8 7 7 0 0 1 13-2 5 5 0 0 1 0 10h-1m-5 3V11m-4 4 4-4 4 4"/>',
30
+ columns: '<rect x="3" y="4" width="18" height="16" rx="2"/><path d="M9 4v16m6-16v16"/>',
31
+ grip: '<path d="M9 5h.01M15 5h.01M9 12h.01M15 12h.01M9 19h.01M15 19h.01" stroke-width="3"/>',
32
+ left: '<path d="m14 6-6 6 6 6"/>',
33
+ right: '<path d="m10 6 6 6-6 6"/>'
34
+ };
35
+
36
+ export function icon(name, className = '') {
37
+ return `<svg class="ui-icon ${className}" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">${paths[name] ?? paths.settings}</svg>`;
38
+ }
@@ -4,16 +4,36 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
6
6
  <title>AutoVPN</title>
7
+ <link rel="icon" type="image/png" href="./assets/vpn-auto-logo-v3.png" />
8
+ <!-- Resolve appearance before paint, including while the profile loads.
9
+ Storage can be blocked in a browser, so system mode is the fallback. -->
10
+ <script>
11
+ try {
12
+ const saved = JSON.parse(localStorage.getItem('autovpn.ui.theme'));
13
+ const choice = ['light', 'dark'].includes(saved) ? saved : 'system';
14
+ document.documentElement.dataset.theme = choice === 'dark' || (choice === 'system' && matchMedia('(prefers-color-scheme: dark)').matches) ? 'dark' : 'light';
15
+ document.documentElement.dataset.themePreference = choice;
16
+ } catch {
17
+ document.documentElement.dataset.theme = matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
18
+ }
19
+ </script>
7
20
  <link rel="stylesheet" href="./styles.css" />
21
+ <link rel="stylesheet" href="./workspace.css" />
22
+ <link rel="stylesheet" href="./analytics.bundle.css" />
8
23
  </head>
9
24
  <body data-page="dashboard">
25
+ <script>
26
+ // Only a native file window needs Electron's draggable title area.
27
+ // Plain H5 previews previously inherited it and showed a blank top bar.
28
+ document.body.dataset.runtime = location.protocol === 'file:' && window.vpnAutomation ? 'electron' : 'web';
29
+ </script>
10
30
  <div class="app-frame">
11
31
  <div class="window-titlebar" aria-hidden="true"></div>
12
32
  <div class="app-content-shell">
13
33
  <div class="workspace-shell">
14
34
  <aside class="sidebar">
15
35
  <div class="sidebar-brand">
16
- <img class="brand-mark" src="./assets/vpn-auto-logo-v2-minimal.svg" alt="" aria-hidden="true" />
36
+ <img class="brand-mark" src="./assets/vpn-auto-logo-v3.png" alt="" aria-hidden="true" />
17
37
  <div class="brand-copy">
18
38
  <div id="sidebarTitle" class="sidebar-title"></div>
19
39
  <div id="sidebarVersion" class="sidebar-version"></div>
@@ -21,6 +41,7 @@
21
41
  </div>
22
42
 
23
43
  <nav id="sidebarNav" class="sidebar-nav"></nav>
44
+ <button id="sidebarToggle" class="icon-btn sidebar-toggle" type="button" aria-label="收起导航" aria-expanded="true" aria-controls="sidebarNav" title="收起导航"></button>
24
45
  </aside>
25
46
 
26
47
  <div class="main-shell">
@@ -30,7 +51,8 @@
30
51
  <h1 id="pageTitle"></h1>
31
52
  <span id="runStateBadge" class="badge neutral"></span>
32
53
  </div>
33
- <p id="pageSubtitle"></p>
54
+ <!-- Repeated feature descriptions previously pushed the page
55
+ tools below the fold. The navigation supplies context. -->
34
56
  </div>
35
57
 
36
58
  <div id="pageActions" class="topbar-actions"></div>
@@ -15,7 +15,6 @@ export const PAGE_ORDER = [
15
15
  'dashboard',
16
16
  'runs',
17
17
  'results',
18
- 'subscriptions',
19
18
  'logs',
20
19
  'settings'
21
20
  ];