@wendongfly/myhi 1.3.190 → 1.3.192
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.
- package/dist/admin.html +182 -67
- package/dist/chat.html +88 -65
- package/dist/grid.html +2 -1
- package/dist/index.html +403 -30
- package/dist/index.js +4 -4
- package/dist/index.min.js +66 -65
- package/dist/lib/machine.js +5 -2
- package/dist/lib/prefs.js +51 -0
- package/dist/login.html +175 -163
- package/package.json +1 -1
package/dist/lib/machine.js
CHANGED
|
@@ -11,8 +11,11 @@
|
|
|
11
11
|
|
|
12
12
|
function fmt(t) {
|
|
13
13
|
if (sessionPage) {
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
// 会话页设的标题是「任务名 — myhi」,只留任务名(任务名本身叫 myhi 也照样显示 myhi);
|
|
15
|
+
// 还没加载出任务时是页面自带的「myhi 会话」,先显示机器名
|
|
16
|
+
const m = t.match(/^(.*?)\s*—\s*myhi$/);
|
|
17
|
+
if (m && m[1].trim()) return m[1].trim();
|
|
18
|
+
return /^myhi (会话|终端)$/.test(t) ? (name || 'myhi') : t; // 只认页面自带的占位标题,改写后的结果再进来保持不动
|
|
16
19
|
}
|
|
17
20
|
if (!name || t.startsWith(name + ' · ')) return t;
|
|
18
21
|
const rest = t.replace(/\s*—\s*myhi$/, '').replace(/^myhi\s*/, '').trim();
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// 个人偏好跨设备同步:外观(主题/字号)、会话页的聊天显示与快捷栏。
|
|
2
|
+
// 各页面照旧读写 localStorage(myhi_theme / myhi_font / myhi_chat_settings),本脚本负责和服务端 /api/prefs 对齐:
|
|
3
|
+
// · 打开页面:服务端那份比本机新 → 写回 localStorage,有差异就刷新一次让页面按新设置渲染(每个标签页最多一次);
|
|
4
|
+
// 本机比服务端新(或服务端还没有)→ 推上去。
|
|
5
|
+
// · 页面改了设置后调 myhiPrefsPush()(防抖 800ms)推上去。
|
|
6
|
+
(function () {
|
|
7
|
+
const LS = { theme: 'myhi_theme', font: 'myhi_font', chat: 'myhi_chat_settings' };
|
|
8
|
+
const AT = 'myhi_prefs_at'; // 本机这份偏好对应的服务端时间戳
|
|
9
|
+
const get = (k) => { try { return localStorage.getItem(k); } catch { return null; } };
|
|
10
|
+
const set = (k, v) => { try { v == null ? localStorage.removeItem(k) : localStorage.setItem(k, v); } catch {} };
|
|
11
|
+
const dirty = () => get(AT + '_dirty') === '1';
|
|
12
|
+
|
|
13
|
+
function snapshot() {
|
|
14
|
+
let chat = null;
|
|
15
|
+
try { chat = JSON.parse(get(LS.chat) || 'null'); } catch {}
|
|
16
|
+
return { theme: get(LS.theme), font: get(LS.font), chat };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let timer = null;
|
|
20
|
+
window.myhiPrefsPush = function () {
|
|
21
|
+
set(AT + '_dirty', '1'); // 先记「本机有未推送的改动」,推失败/页面关了下次打开再推
|
|
22
|
+
clearTimeout(timer);
|
|
23
|
+
timer = setTimeout(async () => {
|
|
24
|
+
try {
|
|
25
|
+
const r = await fetch('/api/prefs', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(snapshot()) });
|
|
26
|
+
const d = await r.json();
|
|
27
|
+
if (d.ok) { set(AT, String(d.updatedAt)); set(AT + '_dirty', null); }
|
|
28
|
+
} catch {}
|
|
29
|
+
}, 800);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
fetch('/api/prefs').then((r) => (r.ok ? r.json() : null)).then((d) => {
|
|
33
|
+
if (!d?.ok) return;
|
|
34
|
+
const p = d.prefs;
|
|
35
|
+
const localAt = Number(get(AT) || 0);
|
|
36
|
+
if (!p || dirty() || (p.updatedAt || 0) <= localAt) {
|
|
37
|
+
// 服务端没有 / 本机有没推的改动 / 本机已是最新:本机为准(有值才推,空白新设备不推空值覆盖)
|
|
38
|
+
if ((!p || dirty()) && (get(LS.theme) || get(LS.font) || get(LS.chat))) window.myhiPrefsPush();
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const before = JSON.stringify(snapshot());
|
|
42
|
+
if (p.theme !== undefined) set(LS.theme, p.theme);
|
|
43
|
+
if (p.font !== undefined) set(LS.font, p.font);
|
|
44
|
+
if (p.chat !== undefined) set(LS.chat, p.chat == null ? null : JSON.stringify(p.chat));
|
|
45
|
+
set(AT, String(p.updatedAt));
|
|
46
|
+
if (JSON.stringify(snapshot()) !== before) {
|
|
47
|
+
try { if (sessionStorage.getItem('myhi_prefs_reloaded')) return; sessionStorage.setItem('myhi_prefs_reloaded', '1'); } catch {}
|
|
48
|
+
location.reload();
|
|
49
|
+
}
|
|
50
|
+
}).catch(() => {});
|
|
51
|
+
})();
|
package/dist/login.html
CHANGED
|
@@ -1,163 +1,175 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html lang="zh-CN">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, viewport-fit=cover">
|
|
6
|
-
<meta name="theme-color" content="#0d1117">
|
|
7
|
-
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
8
|
-
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
|
9
|
-
<link rel="apple-touch-icon" href="/icon.png">
|
|
10
|
-
<link rel="manifest" href="/manifest.json">
|
|
11
|
-
<title>myhi</title>
|
|
12
|
-
<
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
align-items: center;
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, viewport-fit=cover">
|
|
6
|
+
<meta name="theme-color" content="#0d1117">
|
|
7
|
+
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
8
|
+
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
|
9
|
+
<link rel="apple-touch-icon" href="/icon.png">
|
|
10
|
+
<link rel="manifest" href="/manifest.json">
|
|
11
|
+
<title>myhi</title>
|
|
12
|
+
<script src="/theme.js"></script>
|
|
13
|
+
<script>
|
|
14
|
+
// 主题:浏览器选过的优先,否则用服务端默认(新装浅色、老机器深色),与其他页面一致
|
|
15
|
+
(function(){try{var t=localStorage.getItem('myhi_theme')||window.MYHI_DEFAULT_THEME||'dark';
|
|
16
|
+
if(t==='system') t=matchMedia('(prefers-color-scheme: light)').matches?'light':'dark';
|
|
17
|
+
if(t!=='dark') document.documentElement.setAttribute('data-theme',t);
|
|
18
|
+
var m=document.querySelector('meta[name="theme-color"]'); if(m) m.setAttribute('content',t==='light'?'#ffffff':t==='black'?'#000000':'#0d1117');
|
|
19
|
+
}catch(e){}})();
|
|
20
|
+
</script>
|
|
21
|
+
<style>
|
|
22
|
+
:root { --bg:#0d1117; --surface:#161b22; --border:#21262d; --text:#e6edf3; --muted:#4a5568; --accent:#7c3aed; --accent2:#6d28d9; }
|
|
23
|
+
:root[data-theme="light"] { --bg:#ffffff; --surface:#f6f8fa; --border:#d0d7de; --text:#1f2328; --muted:#8c959f; --accent:#8250df; --accent2:#6f42c1; }
|
|
24
|
+
:root[data-theme="black"] { --bg:#000000; --surface:#0a0a0a; --border:#262626; }
|
|
25
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
26
|
+
html, body {
|
|
27
|
+
height: 100%;
|
|
28
|
+
background: var(--bg);
|
|
29
|
+
color: var(--text);
|
|
30
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
31
|
+
}
|
|
32
|
+
body {
|
|
33
|
+
display: flex;
|
|
34
|
+
align-items: center;
|
|
35
|
+
justify-content: center;
|
|
36
|
+
padding: env(safe-area-inset-top) env(safe-area-inset-right)
|
|
37
|
+
env(safe-area-inset-bottom) env(safe-area-inset-left);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.card {
|
|
41
|
+
width: 100%;
|
|
42
|
+
max-width: 340px;
|
|
43
|
+
padding: 2.5rem 2rem;
|
|
44
|
+
display: flex;
|
|
45
|
+
flex-direction: column;
|
|
46
|
+
align-items: center;
|
|
47
|
+
gap: 1.5rem;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.logo {
|
|
51
|
+
font-size: 2.2rem;
|
|
52
|
+
font-weight: 800;
|
|
53
|
+
letter-spacing: -1px;
|
|
54
|
+
}
|
|
55
|
+
.logo span { color: var(--accent); }
|
|
56
|
+
|
|
57
|
+
form {
|
|
58
|
+
width: 100%;
|
|
59
|
+
display: flex;
|
|
60
|
+
flex-direction: column;
|
|
61
|
+
gap: 0.85rem;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
input[type=password] {
|
|
65
|
+
width: 100%;
|
|
66
|
+
background: var(--surface);
|
|
67
|
+
border: 1.5px solid var(--border);
|
|
68
|
+
border-radius: 10px;
|
|
69
|
+
padding: 0.85rem 1rem;
|
|
70
|
+
color: var(--text);
|
|
71
|
+
font-size: 1.1rem;
|
|
72
|
+
text-align: center;
|
|
73
|
+
letter-spacing: 0.2em;
|
|
74
|
+
outline: none;
|
|
75
|
+
transition: border-color 0.15s;
|
|
76
|
+
-webkit-appearance: none;
|
|
77
|
+
}
|
|
78
|
+
input[type=password]:focus { border-color: var(--accent); }
|
|
79
|
+
input[type=password]::placeholder { letter-spacing: normal; font-size: 0.9rem; color: var(--muted); }
|
|
80
|
+
|
|
81
|
+
button {
|
|
82
|
+
width: 100%;
|
|
83
|
+
background: var(--accent);
|
|
84
|
+
color: #fff;
|
|
85
|
+
border: none;
|
|
86
|
+
border-radius: 10px;
|
|
87
|
+
padding: 0.85rem;
|
|
88
|
+
font-size: 1rem;
|
|
89
|
+
font-weight: 600;
|
|
90
|
+
cursor: pointer;
|
|
91
|
+
transition: background 0.15s;
|
|
92
|
+
-webkit-appearance: none;
|
|
93
|
+
}
|
|
94
|
+
button:active { background: var(--accent2); }
|
|
95
|
+
button:disabled { background: #4a5568; cursor: not-allowed; }
|
|
96
|
+
|
|
97
|
+
.error {
|
|
98
|
+
font-size: 0.82rem;
|
|
99
|
+
color: #f85149;
|
|
100
|
+
text-align: center;
|
|
101
|
+
min-height: 1.2em;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.occupied {
|
|
105
|
+
font-size: 0.9rem;
|
|
106
|
+
color: #f0883e;
|
|
107
|
+
text-align: center;
|
|
108
|
+
line-height: 1.6;
|
|
109
|
+
padding: 0.8rem;
|
|
110
|
+
background: rgba(240, 136, 62, 0.1);
|
|
111
|
+
border-radius: 10px;
|
|
112
|
+
display: none;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.hint {
|
|
116
|
+
font-size: 0.75rem;
|
|
117
|
+
color: var(--muted);
|
|
118
|
+
text-align: center;
|
|
119
|
+
line-height: 1.5;
|
|
120
|
+
}
|
|
121
|
+
</style>
|
|
122
|
+
</head>
|
|
123
|
+
<body>
|
|
124
|
+
<div class="card">
|
|
125
|
+
<div class="logo">my<span>hi</span></div>
|
|
126
|
+
|
|
127
|
+
<div class="occupied" id="occupied"></div>
|
|
128
|
+
|
|
129
|
+
<form method="POST" action="/login">
|
|
130
|
+
<input type="password" name="password" placeholder="输入密码" autofocus autocomplete="current-password">
|
|
131
|
+
<div class="error" id="err"></div>
|
|
132
|
+
<button type="submit" id="btn">进入</button>
|
|
133
|
+
</form>
|
|
134
|
+
|
|
135
|
+
<div class="hint">密码见服务器启动日志<br>或编辑 ~/.myhi/password</div>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
<script>
|
|
139
|
+
const errType = new URLSearchParams(location.search).get('error');
|
|
140
|
+
const errUser = new URLSearchParams(location.search).get('user');
|
|
141
|
+
if (errType === 'locked') {
|
|
142
|
+
document.getElementById('err').textContent = '登录尝试过多,请 5 分钟后重试';
|
|
143
|
+
} else if (errType === 'occupied' && errUser) {
|
|
144
|
+
document.getElementById('err').textContent = errUser + ' 正在使用中,同账号可直接登录,不同账号请让对方退出';
|
|
145
|
+
} else if (errType) {
|
|
146
|
+
document.getElementById('err').textContent = '密码错误,请重试';
|
|
147
|
+
}
|
|
148
|
+
document.querySelector('input').focus();
|
|
149
|
+
|
|
150
|
+
// 轮询登录状态(独占模式)
|
|
151
|
+
const occupiedEl = document.getElementById('occupied');
|
|
152
|
+
const btnEl = document.getElementById('btn');
|
|
153
|
+
const inputEl = document.querySelector('input[type=password]');
|
|
154
|
+
|
|
155
|
+
async function checkStatus() {
|
|
156
|
+
try {
|
|
157
|
+
const res = await fetch('/api/status');
|
|
158
|
+
const data = await res.json();
|
|
159
|
+
if (data.occupied) {
|
|
160
|
+
occupiedEl.textContent = data.userName + ' 正在使用中' + (data.releasing ? '\n对方已离线,即将自动释放…' : '\n同账号可直接登录');
|
|
161
|
+
occupiedEl.style.display = 'block';
|
|
162
|
+
} else if (data.onlineUsers && data.onlineUsers.length > 0) {
|
|
163
|
+
occupiedEl.textContent = '当前在线: ' + data.onlineUsers.join(', ');
|
|
164
|
+
occupiedEl.style.display = 'block';
|
|
165
|
+
} else {
|
|
166
|
+
occupiedEl.style.display = 'none';
|
|
167
|
+
}
|
|
168
|
+
} catch {}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
checkStatus();
|
|
172
|
+
setInterval(checkStatus, 3000);
|
|
173
|
+
</script>
|
|
174
|
+
</body>
|
|
175
|
+
</html>
|
package/package.json
CHANGED