Skip to content

Commit ab977d9

Browse files
lcompleteclaude
andcommitted
fix(server,client): normalize dataDir path separator and add mobile highlight support
- Add HuntlyEnvironmentPostProcessor to ensure huntly.dataDir always ends with a path separator before Spring resolves the datasource URL, fixing SQLite DB creation at wrong path on Windows (#155) - Register processor via META-INF/spring.factories; add 6 unit tests - Add touchend/pointerup/selectionchange listeners with 80ms debounce to TextHighlighter so mobile long-press selection triggers the highlight tooltip (#154) - Add WebkitUserSelect and touchAction styles for broader mobile compatibility Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 4437ff0 commit ab977d9

4 files changed

Lines changed: 147 additions & 6 deletions

File tree

app/client/src/components/highlights/TextHighlighter.tsx

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -481,10 +481,20 @@ const TextHighlighter: React.FC<TextHighlighterProps> = ({
481481
});
482482
};
483483

484-
// 监听鼠标抬起事件
484+
// 监听文本选择事件,兼容桌面与移动端
485485
useEffect(() => {
486-
const handleMouseUp = () => {
487-
setTimeout(handleSelection, 10); // 小延迟确保选择完成
486+
let selectionTimer: ReturnType<typeof setTimeout> | null = null;
487+
488+
// 防抖触发:移动端长按选词以及拖动选区把手时 selectionchange 会高频触发,
489+
// 需等浏览器最终落定后再计算偏移量
490+
const scheduleHandleSelection = () => {
491+
if (selectionTimer) {
492+
clearTimeout(selectionTimer);
493+
}
494+
selectionTimer = setTimeout(() => {
495+
selectionTimer = null;
496+
handleSelection();
497+
}, 80);
488498
};
489499

490500
const handleClickOutside = (e: MouseEvent) => {
@@ -495,14 +505,24 @@ const TextHighlighter: React.FC<TextHighlighterProps> = ({
495505

496506
const currentRef = contentRef.current;
497507
if (currentRef) {
498-
currentRef.addEventListener('mouseup', handleMouseUp);
499-
document.addEventListener('click', handleClickOutside);
508+
// mouseup 仅桌面触发;移动端依赖 touchend / pointerup / selectionchange
509+
currentRef.addEventListener('mouseup', scheduleHandleSelection);
510+
currentRef.addEventListener('touchend', scheduleHandleSelection);
511+
currentRef.addEventListener('pointerup', scheduleHandleSelection);
500512
}
513+
document.addEventListener('selectionchange', scheduleHandleSelection);
514+
document.addEventListener('click', handleClickOutside);
501515

502516
return () => {
517+
if (selectionTimer) {
518+
clearTimeout(selectionTimer);
519+
}
503520
if (currentRef) {
504-
currentRef.removeEventListener('mouseup', handleMouseUp);
521+
currentRef.removeEventListener('mouseup', scheduleHandleSelection);
522+
currentRef.removeEventListener('touchend', scheduleHandleSelection);
523+
currentRef.removeEventListener('pointerup', scheduleHandleSelection);
505524
}
525+
document.removeEventListener('selectionchange', scheduleHandleSelection);
506526
document.removeEventListener('click', handleClickOutside);
507527
};
508528
}, [handleSelection, selectionTooltip.show, highlightModeEnabled]);
@@ -610,6 +630,8 @@ const TextHighlighter: React.FC<TextHighlighterProps> = ({
610630
ref={contentRef}
611631
style={{
612632
userSelect: 'text',
633+
WebkitUserSelect: 'text',
634+
touchAction: 'auto',
613635
lineHeight: '1.6',
614636
fontSize: '16px'
615637
}}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package com.huntly.server.config;
2+
3+
import org.springframework.boot.SpringApplication;
4+
import org.springframework.boot.env.EnvironmentPostProcessor;
5+
import org.springframework.core.Ordered;
6+
import org.springframework.core.env.ConfigurableEnvironment;
7+
import org.springframework.core.env.MapPropertySource;
8+
9+
import java.util.Collections;
10+
11+
/**
12+
* Ensures that {@code huntly.dataDir} ends with a path separator so the
13+
* datasource URL (jdbc:sqlite:${huntly.dataDir:}db.sqlite) resolves to a
14+
* correct file path on every platform, including when users pass a
15+
* Windows path without a trailing slash (e.g. {@code C:\Users\name\huntly}).
16+
*
17+
* @author lcomplete
18+
*/
19+
public class HuntlyEnvironmentPostProcessor implements EnvironmentPostProcessor, Ordered {
20+
21+
private static final String DATA_DIR_PROPERTY = "huntly.dataDir";
22+
private static final String PROPERTY_SOURCE_NAME = "huntlyNormalizedProperties";
23+
24+
@Override
25+
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
26+
String dataDir = environment.getProperty(DATA_DIR_PROPERTY);
27+
if (dataDir == null || dataDir.isEmpty()) {
28+
return;
29+
}
30+
if (dataDir.endsWith("/") || dataDir.endsWith("\\")) {
31+
return;
32+
}
33+
// Forward slash works for SQLite JDBC URLs on every platform and avoids
34+
// backslash escaping pitfalls.
35+
String normalized = dataDir + "/";
36+
MapPropertySource source = new MapPropertySource(
37+
PROPERTY_SOURCE_NAME,
38+
Collections.singletonMap(DATA_DIR_PROPERTY, normalized)
39+
);
40+
environment.getPropertySources().addFirst(source);
41+
}
42+
43+
@Override
44+
public int getOrder() {
45+
return Ordered.LOWEST_PRECEDENCE;
46+
}
47+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
org.springframework.boot.env.EnvironmentPostProcessor=\
2+
com.huntly.server.config.HuntlyEnvironmentPostProcessor
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package com.huntly.server.config;
2+
3+
import org.junit.jupiter.api.Test;
4+
import org.springframework.mock.env.MockEnvironment;
5+
6+
import static org.assertj.core.api.Assertions.assertThat;
7+
8+
class HuntlyEnvironmentPostProcessorTest {
9+
10+
private final HuntlyEnvironmentPostProcessor processor = new HuntlyEnvironmentPostProcessor();
11+
12+
@Test
13+
void appendsForwardSlashWhenWindowsPathHasNoTrailingSeparator() {
14+
MockEnvironment env = new MockEnvironment();
15+
env.setProperty("huntly.dataDir", "C:\\Users\\name\\huntly");
16+
17+
processor.postProcessEnvironment(env, null);
18+
19+
assertThat(env.getProperty("huntly.dataDir")).isEqualTo("C:\\Users\\name\\huntly/");
20+
}
21+
22+
@Test
23+
void appendsForwardSlashWhenUnixPathHasNoTrailingSeparator() {
24+
MockEnvironment env = new MockEnvironment();
25+
env.setProperty("huntly.dataDir", "/var/lib/huntly");
26+
27+
processor.postProcessEnvironment(env, null);
28+
29+
assertThat(env.getProperty("huntly.dataDir")).isEqualTo("/var/lib/huntly/");
30+
}
31+
32+
@Test
33+
void leavesPathUntouchedWhenAlreadyEndsWithForwardSlash() {
34+
MockEnvironment env = new MockEnvironment();
35+
env.setProperty("huntly.dataDir", "/var/lib/huntly/");
36+
37+
processor.postProcessEnvironment(env, null);
38+
39+
assertThat(env.getProperty("huntly.dataDir")).isEqualTo("/var/lib/huntly/");
40+
}
41+
42+
@Test
43+
void leavesPathUntouchedWhenAlreadyEndsWithBackslash() {
44+
MockEnvironment env = new MockEnvironment();
45+
env.setProperty("huntly.dataDir", "C:\\Users\\name\\huntly\\");
46+
47+
processor.postProcessEnvironment(env, null);
48+
49+
assertThat(env.getProperty("huntly.dataDir")).isEqualTo("C:\\Users\\name\\huntly\\");
50+
}
51+
52+
@Test
53+
void doesNothingWhenPropertyIsAbsent() {
54+
MockEnvironment env = new MockEnvironment();
55+
56+
processor.postProcessEnvironment(env, null);
57+
58+
assertThat(env.getProperty("huntly.dataDir")).isNull();
59+
}
60+
61+
@Test
62+
void doesNothingWhenPropertyIsEmpty() {
63+
MockEnvironment env = new MockEnvironment();
64+
env.setProperty("huntly.dataDir", "");
65+
66+
processor.postProcessEnvironment(env, null);
67+
68+
assertThat(env.getProperty("huntly.dataDir")).isEmpty();
69+
}
70+
}

0 commit comments

Comments
 (0)