@@ -29,9 +29,30 @@ const BLUR_CLEAR_MS = 150;
2929 * root of whatever drive qBittorrent happens to be running on (typically C:) —
3030 * there's no API to enumerate other drives. But the endpoint does accept a
3131 * drive-letter path like "D:/" directly, so once a user types one, suggestions
32- * should still kick in instead of silently doing nothing (#180).
32+ * should still kick in instead of silently doing nothing (#180). Accept either
33+ * separator after the colon (`D:/` or `D:\`) since Windows users naturally
34+ * type backslashes.
3335 */
34- const WINDOWS_DRIVE_PATH = / ^ [ A - Z a - z ] : \/ / ;
36+ const WINDOWS_DRIVE_PATH = / ^ [ A - Z a - z ] : [ / \\ ] / ;
37+ /** A UNC network path, e.g. `\\nas\share\`. Windows-only; always backslash. */
38+ const UNC_PATH = / ^ \\ \\ / ;
39+
40+ /**
41+ * True for paths that are unambiguously Windows-style (drive letter or UNC
42+ * share) — the only case where `\` should be treated as a path separator.
43+ * A bare `/`-rooted path (Linux/macOS host) never gets this treatment, so a
44+ * literal backslash in a Linux directory name is never misread as a separator.
45+ */
46+ export function isWindowsStylePath ( text : string ) : boolean {
47+ return WINDOWS_DRIVE_PATH . test ( text ) || UNC_PATH . test ( text ) ;
48+ }
49+
50+ /** Index of the last path separator, respecting `isWindowsStylePath`. */
51+ function lastSeparatorIndex ( text : string ) : number {
52+ return isWindowsStylePath ( text )
53+ ? Math . max ( text . lastIndexOf ( '/' ) , text . lastIndexOf ( '\\' ) )
54+ : text . lastIndexOf ( '/' ) ;
55+ }
3556
3657/**
3758 * qBittorrent's getDirectoryContent returns absolute paths (QDirIterator::next),
@@ -116,13 +137,13 @@ export function PathAutocompleteInput({
116137 setSuggestions ( [ ] ) ;
117138 return ;
118139 }
119- const lastSlash = text . lastIndexOf ( '/' ) ;
120- if ( ( ! text . startsWith ( '/' ) && ! WINDOWS_DRIVE_PATH . test ( text ) ) || lastSlash < 0 ) {
140+ const lastSep = lastSeparatorIndex ( text ) ;
141+ if ( ( ! text . startsWith ( '/' ) && ! isWindowsStylePath ( text ) ) || lastSep < 0 ) {
121142 setSuggestions ( [ ] ) ;
122143 return ;
123144 }
124- const parentDir = text . slice ( 0 , lastSlash + 1 ) ;
125- const partial = text . slice ( lastSlash + 1 ) . toLowerCase ( ) ;
145+ const parentDir = text . slice ( 0 , lastSep + 1 ) ;
146+ const partial = text . slice ( lastSep + 1 ) . toLowerCase ( ) ;
126147 const fetchId = ++ fetchIdRef . current ;
127148 debounceRef . current = setTimeout ( async ( ) => {
128149 try {
@@ -150,7 +171,7 @@ export function PathAutocompleteInput({
150171
151172 const applySuggestion = ( path : string ) => {
152173 cancelPendingBlurClear ( ) ;
153- const newValue = `${ path } / ` ;
174+ const newValue = `${ path } ${ isWindowsStylePath ( path ) ? '\\' : '/' } ` ;
154175 onChangeText ( newValue ) ;
155176 // Immediately list the directory just selected instead of leaving the
156177 // dropdown empty until the user types another character.
0 commit comments