Skip to content

Commit b674a80

Browse files
authored
Merge pull request #3 from rust17/feat_todo
feat(client): implement i18n, haptics, and UI optimizations
2 parents 440ba79 + a16e7e8 commit b674a80

15 files changed

Lines changed: 398 additions & 68 deletions

File tree

requirement/todo.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
edit:
2+
- status-bar.ts
3+
- [x] 将 StatusBar 中的 status 封装成枚举,remove 从枚举里通过 all 方法获取
4+
5+
---
6+
7+
add:
8+
- 配置
9+
- [x] 添加 i18n 设置,可以设置中文或英文,默认使用中文
10+
- [x] 使用 i18n key 替换项目中的文案,把 value 放到 lang 目录下
11+
12+
---
13+
14+
add:
15+
- ui
16+
- [ ] 在 Modifiers 区域上方添加一个横向滚动指示器:UI 为垂直滚动条的横向变体(仅保留滑块,隐藏轨道),支持手指拖拽左右滑动触发横向滚动,与 Modifiers 区域保持小间距
17+
18+
---
19+
20+
add:
21+
- 交互
22+
- [x] 增加震动反馈
23+
24+
---
25+
26+
edit:
27+
- [x] 除了:ctrl、shift、alt、win,Modifiers 键盘增加点击效果,点击时,触发 active 效果,离开后消失
28+
- [x] 当系统键盘呼出时,改成把 Modifiers 区域顶上去,使得可以同时操作 Modifiers 和键盘,同时取消掉图里有☑️这一行

web-client/index.html

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
<div id="status-bar">
1515
<div id="status-indicator">
1616
<div class="status-dot"></div>
17-
<span class="status-text">Connecting...</span>
17+
<span class="status-text" data-i18n="status.connecting">Connecting...</span>
1818
</div>
1919
</div>
2020

@@ -25,6 +25,18 @@
2525

2626
<!-- Function Keys Panel -->
2727
<div id="fn-panel">
28+
<!-- Floating Toolbar (Inside panel for relative positioning but absolutely floating above) -->
29+
<div id="toolbar-fab">
30+
<div class="fab-capsule">
31+
<button id="btn-settings" class="capsule-btn sub-action">
32+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33-1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>
33+
</button>
34+
<div class="capsule-divider"></div>
35+
<button id="btn-keyboard" class="capsule-btn main-action">
36+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="20" height="16" rx="2" ry="2"></rect><line x1="6" y1="8" x2="6" y2="8"></line><line x1="10" y1="8" x2="10" y2="8"></line><line x1="14" y1="8" x2="14" y2="8"></line><line x1="18" y1="8" x2="18" y2="8"></line><line x1="6" y1="12" x2="6" y2="12"></line><line x1="10" y1="12" x2="10" y2="12"></line><line x1="14" y1="12" x2="14" y2="12"></line><line x1="18" y1="12" x2="18" y2="12"></line><line x1="6" y1="16" x2="6" y2="16"></line><line x1="10" y1="16" x2="10" y2="16"></line><line x1="14" y1="16" x2="14" y2="16"></line></svg>
37+
</button>
38+
</div>
39+
</div>
2840
<!-- Row 1: System & Modifiers -->
2941
<button class="fn-btn" data-key="esc">Esc</button>
3042
<button class="fn-btn" data-key="tab"></button>
@@ -44,48 +56,35 @@
4456
<button class="fn-btn nav-key" data-key="right"></button>
4557
</div>
4658

47-
<!-- Floating Toolbar -->
48-
<div id="toolbar-fab">
49-
<div class="fab-capsule">
50-
<button id="btn-settings" class="capsule-btn sub-action">
51-
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>
52-
</button>
53-
<div class="capsule-divider"></div>
54-
<button id="btn-keyboard" class="capsule-btn main-action">
55-
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="20" height="16" rx="2" ry="2"></rect><line x1="6" y1="8" x2="6" y2="8"></line><line x1="10" y1="8" x2="10" y2="8"></line><line x1="14" y1="8" x2="14" y2="8"></line><line x1="18" y1="8" x2="18" y2="8"></line><line x1="6" y1="12" x2="6" y2="12"></line><line x1="10" y1="12" x2="10" y2="12"></line><line x1="14" y1="12" x2="14" y2="12"></line><line x1="18" y1="12" x2="18" y2="12"></line><line x1="6" y1="16" x2="6" y2="16"></line><line x1="10" y1="16" x2="10" y2="16"></line><line x1="14" y1="16" x2="14" y2="16"></line></svg>
56-
</button>
57-
</div>
58-
</div>
59-
6059
<!-- Settings Modal -->
6160
<div id="settings-modal" class="modal hidden">
6261
<div class="modal-content">
6362
<div class="modal-header">
64-
<h3>Settings</h3>
63+
<h3 data-i18n="settings.title">Settings</h3>
6564
<button id="btn-close-settings" class="icon-btn">
6665
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
6766
</button>
6867
</div>
6968
<div class="modal-body">
7069
<div class="setting-item">
7170
<div class="setting-label">
72-
<span>Mouse Sensitivity</span>
71+
<span data-i18n="settings.mouse_sensitivity">Mouse Sensitivity</span>
7372
<span id="sensitivity-value">2.0</span>
7473
</div>
7574
<input type="range" id="sensitivity-slider" min="0.5" max="10.0" step="0.1" value="2.0">
7675
</div>
7776

7877
<div class="setting-item">
7978
<div class="setting-label">
80-
<span>Scroll Sensitivity</span>
79+
<span data-i18n="settings.scroll_sensitivity">Scroll Sensitivity</span>
8180
<span id="scroll-sensitivity-value">1.0</span>
8281
</div>
8382
<input type="range" id="scroll-sensitivity-slider" min="0.1" max="10.0" step="0.1" value="1.0">
8483
</div>
8584

8685
<div class="setting-item">
8786
<div class="setting-label" style="align-items: center;">
88-
<span>Light Mode</span>
87+
<span data-i18n="settings.light_mode">Light Mode</span>
8988
<label class="switch">
9089
<input type="checkbox" id="theme-toggle">
9190
<span class="slider"></span>
@@ -95,7 +94,7 @@ <h3>Settings</h3>
9594

9695
<div class="setting-item">
9796
<div class="setting-label" style="align-items: center;">
98-
<span>Scroll Bar Right</span>
97+
<span data-i18n="settings.scroll_bar_right">Scroll Bar Right</span>
9998
<label class="switch">
10099
<input type="checkbox" id="scroll-pos-toggle">
101100
<span class="slider"></span>
@@ -105,13 +104,23 @@ <h3>Settings</h3>
105104

106105
<div class="setting-item">
107106
<div class="setting-label" style="align-items: center;">
108-
<span>Rate Monitor</span>
107+
<span data-i18n="settings.rate_monitor">Rate Monitor</span>
109108
<label class="switch">
110109
<input type="checkbox" id="rate-monitor-toggle">
111110
<span class="slider"></span>
112111
</label>
113112
</div>
114113
</div>
114+
115+
<div class="setting-item">
116+
<div class="setting-label" style="align-items: center;">
117+
<span data-i18n="settings.language">Language</span>
118+
<select id="lang-select">
119+
<option value="zh">中文</option>
120+
<option value="en">English</option>
121+
</select>
122+
</div>
123+
</div>
115124
</div>
116125
</div>
117126
</div>
@@ -121,7 +130,13 @@ <h3>Settings</h3>
121130
<div>BPS: <span id="rate-bps">0</span></div>
122131
</div>
123132

124-
<textarea id="keyboard-input" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"></textarea>
133+
<input type="text" id="keyboard-input"
134+
inputmode="text"
135+
enterkeyhint="done"
136+
autocomplete="one-time-code"
137+
autocorrect="off"
138+
autocapitalize="off"
139+
spellcheck="false">
125140
</div>
126141
<script type="module" src="/src/main.ts"></script>
127142
</body>

web-client/package-lock.json

Lines changed: 29 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

web-client/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,8 @@
1515
"vite": "^7.2.4",
1616
"vite-plugin-pwa": "^1.2.0",
1717
"vitest": "^4.0.16"
18+
},
19+
"dependencies": {
20+
"web-haptics": "^0.0.6"
1821
}
1922
}

web-client/src/core/i18n.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { zh } from '../lang/zh';
2+
import type { TranslationKeys } from '../lang/zh';
3+
import { en } from '../lang/en';
4+
5+
type Language = 'zh' | 'en';
6+
7+
class I18nManager {
8+
private currentLang: Language = 'zh';
9+
private translations: Record<Language, TranslationKeys> = { zh, en };
10+
11+
constructor() {
12+
const savedLang = localStorage.getItem('remote-mouse-lang') as Language;
13+
if (savedLang && (savedLang === 'zh' || savedLang === 'en')) {
14+
this.currentLang = savedLang;
15+
}
16+
}
17+
18+
public setLanguage(lang: Language) {
19+
this.currentLang = lang;
20+
localStorage.setItem('remote-mouse-lang', lang);
21+
this.updateDOM();
22+
}
23+
24+
public getLanguage(): Language {
25+
return this.currentLang;
26+
}
27+
28+
public t<K1 extends keyof TranslationKeys, K2 extends keyof TranslationKeys[K1]>(
29+
key: `${K1}.${K2 & string}`
30+
): string {
31+
const [k1, k2] = key.split('.') as [K1, K2 & string];
32+
return this.translations[this.currentLang][k1][k2] as unknown as string;
33+
}
34+
35+
public updateDOM() {
36+
const elements = document.querySelectorAll('[data-i18n]');
37+
elements.forEach(el => {
38+
const key = el.getAttribute('data-i18n');
39+
if (key) {
40+
// @ts-ignore - simplified access
41+
const text = this.t(key as any);
42+
if (text) el.textContent = text;
43+
}
44+
});
45+
46+
// Update titles if necessary
47+
const titles = document.querySelectorAll('[data-i18n-title]');
48+
titles.forEach(el => {
49+
const key = el.getAttribute('data-i18n-title');
50+
if (key) {
51+
// @ts-ignore
52+
const text = this.t(key as any);
53+
if (text) (el as HTMLElement).title = text;
54+
}
55+
});
56+
}
57+
}
58+
59+
export const i18n = new I18nManager();

web-client/src/core/protocol.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,15 @@ export const OP_SCROLL = 0x03;
44
export const OP_DRAG = 0x04;
55
export const OP_TEXT = 0x05;
66
export const OP_KEY_ACTION = 0x06;
7+
8+
export const ConnectionStatus = {
9+
Connected: 'connected',
10+
Disconnected: 'disconnected',
11+
Connecting: 'connecting',
12+
} as const;
13+
14+
export type ConnectionStatus = (typeof ConnectionStatus)[keyof typeof ConnectionStatus];
15+
16+
export const allConnectionStatuses = Object.values(ConnectionStatus);
17+
18+

web-client/src/core/transport.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
type ConnectionState = 'connected' | 'disconnected' | 'connecting';
1+
import { ConnectionStatus } from './protocol';
22

33
interface TransportOptions {
4-
onStateChange?: (state: ConnectionState, statusText: string) => void;
4+
onStateChange?: (state: ConnectionStatus, statusText: string) => void;
55
}
66

77
export class Transport {
@@ -28,31 +28,31 @@ export class Transport {
2828

2929
public connect(url: string) {
3030
this.isExplicitlyClosed = false;
31-
this.updateState('connecting', '正在连接...');
31+
this.updateState(ConnectionStatus.Connecting, 'status.connecting');
3232

3333
try {
3434
this.ws = new WebSocket(url);
3535
this.ws.binaryType = 'arraybuffer';
3636

3737
this.ws.onopen = () => {
38-
this.updateState('connected', '已连接');
38+
this.updateState(ConnectionStatus.Connected, 'status.connected');
3939
console.log('WebSocket opened');
4040
};
4141

4242
this.ws.onclose = () => {
43-
this.updateState('disconnected', '连接断开');
43+
this.updateState(ConnectionStatus.Disconnected, 'status.disconnected');
4444
this.scheduleReconnect(url);
4545
};
4646

4747
this.ws.onerror = (error) => {
4848
console.error('WebSocket error:', error);
49-
this.updateState('disconnected', '连接错误');
49+
this.updateState(ConnectionStatus.Disconnected, 'status.error');
5050
// onerror usually is followed by onclose, so we let onclose handle reconnect
5151
};
5252

5353
} catch (e) {
5454
console.error('Connection failed synchronously', e);
55-
this.updateState('disconnected', '连接失败');
55+
this.updateState(ConnectionStatus.Disconnected, 'status.failed');
5656
this.scheduleReconnect(url);
5757
}
5858
}
@@ -88,9 +88,10 @@ export class Transport {
8888
}
8989
}
9090

91-
private updateState(state: ConnectionState, text: string) {
91+
private updateState(state: ConnectionStatus, text: string) {
9292
if (this.options.onStateChange) {
9393
this.options.onStateChange(state, text);
9494
}
9595
}
9696
}
97+

0 commit comments

Comments
 (0)