Skip to content

Commit 768541d

Browse files
authored
Fix Windows path autocomplete, and enhance search options (#204)
1. Autocomplete for windows path 2. Search indexers sort options
1 parent f98f7f1 commit 768541d

16 files changed

Lines changed: 151 additions & 15 deletions

app/(tabs)/search.tsx

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ import { haptics } from '@/utils/haptics';
5555
const ALL = 'all';
5656
const ENABLED = 'enabled';
5757

58-
type SortKey = 'seeders' | 'size' | 'name' | 'leechers';
58+
type SortKey = 'seeders' | 'size' | 'name' | 'leechers' | 'date';
5959

6060
const SORT_OPTIONS: Array<{
6161
key: SortKey;
@@ -66,6 +66,7 @@ const SORT_OPTIONS: Array<{
6666
{ key: 'leechers', labelKey: 'screens.search.sortLeechers', icon: 'arrow-down-outline' },
6767
{ key: 'size', labelKey: 'screens.search.sortSize', icon: 'cube-outline' },
6868
{ key: 'name', labelKey: 'screens.search.sortName', icon: 'text-outline' },
69+
{ key: 'date', labelKey: 'screens.search.sortDate', icon: 'calendar-outline' },
6970
];
7071

7172
const TAG_MATCH_ATTEMPTS = 8;
@@ -234,6 +235,13 @@ export default function SearchScreen() {
234235
const [pendingAddUrl, setPendingAddUrl] = useState<string | null>(null);
235236
const [actionResult, setActionResult] = useState<SearchResult | null>(null);
236237

238+
// pubDate is a qBit 5.0+ (WebAPI >= 2.11.0) field — hide the option on older
239+
// servers rather than offering a sort that silently does nothing.
240+
const visibleSortOptions = useMemo(
241+
() => SORT_OPTIONS.filter((opt) => opt.key !== 'date' || features.supportsSearchPubDate),
242+
[features.supportsSearchPubDate],
243+
);
244+
237245
// Load remembered plugin/category once at mount. The query text itself is
238246
// deliberately NOT restored — it should reset on a fresh app launch, and
239247
// React state already keeps it intact when just switching tabs within the
@@ -388,6 +396,16 @@ export default function SearchScreen() {
388396
case 'name':
389397
cmp = (a.fileName || '').localeCompare(b.fileName || '');
390398
break;
399+
case 'date': {
400+
// qBittorrent always sends pubDate, using -1 as the "plugin didn't
401+
// report one" sentinel (same convention as nbLeechers) — never
402+
// actually absent. Normalize any non-positive value to 0 so
403+
// unknown dates group together instead of comparing as -1.
404+
const aDate = a.pubDate && a.pubDate > 0 ? a.pubDate : 0;
405+
const bDate = b.pubDate && b.pubDate > 0 ? b.pubDate : 0;
406+
cmp = aDate - bDate;
407+
break;
408+
}
391409
}
392410
return sortDirection === 'asc' ? cmp : -cmp;
393411
});
@@ -1013,7 +1031,7 @@ export default function SearchScreen() {
10131031
},
10141032
]}
10151033
>
1016-
{SORT_OPTIONS.map((opt) => {
1034+
{visibleSortOptions.map((opt) => {
10171035
const isActive = sortBy === opt.key;
10181036
return (
10191037
<TouchableOpacity

app/(tabs)/settings/rss-rule.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { Ionicons } from '@expo/vector-icons';
2727
import { SafeAreaView } from 'react-native-safe-area-context';
2828
import { useQuery } from '@tanstack/react-query';
2929
import { FocusAwareStatusBar } from '@/components/FocusAwareStatusBar';
30+
import { PathAutocompleteInput } from '@/components/PathAutocompleteInput';
3031
import { SettingRow } from '@/components/SettingRow';
3132
import { OptionPicker, OptionPickerItem } from '@/components/OptionPicker';
3233
import { MultiSelectPicker, MultiSelectPickerItem } from '@/components/MultiSelectPicker';
@@ -450,7 +451,7 @@ export default function RssRuleEditorScreen() {
450451
<View style={[styles.separator, { backgroundColor: colors.surfaceOutline }]} />
451452

452453
<SettingRow label={t('screens.rss.savePath')} hint={t('screens.rss.savePathHint')}>
453-
<TextInput
454+
<PathAutocompleteInput
454455
style={[
455456
styles.rowInput,
456457
{

app/(tabs)/settings/torrent-defaults.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { useToast } from '@/context/ToastContext';
2020
import { FocusAwareStatusBar } from '@/components/FocusAwareStatusBar';
2121
import { OptionPicker, OptionPickerItem } from '@/components/OptionPicker';
2222
import { InputModal } from '@/components/InputModal';
23-
import { PathAutocompleteInput } from '@/components/PathAutocompleteInput';
23+
import { PathAutocompleteInput, isWindowsStylePath } from '@/components/PathAutocompleteInput';
2424
import { storageService } from '@/services/storage';
2525
import { applicationApi } from '@/services/api/application';
2626
import { apiClient } from '@/services/api/client';
@@ -1988,7 +1988,12 @@ export default function TorrentDefaultsScreen() {
19881988
placeholder={
19891989
// Illustrative only — the field's actual value never gets set
19901990
// to this string; an empty save path really is sent as "".
1991-
`${defaultSavePath || t('screens.settings.categorySavePathPlaceholderDefault')}/${editCategoryName || editingCategory} ${t('screens.settings.categorySavePathPlaceholderSuffix')}`
1991+
(() => {
1992+
const base =
1993+
defaultSavePath || t('screens.settings.categorySavePathPlaceholderDefault');
1994+
const sep = isWindowsStylePath(base) ? '\\' : '/';
1995+
return `${base}${sep}${editCategoryName || editingCategory} ${t('screens.settings.categorySavePathPlaceholderSuffix')}`;
1996+
})()
19921997
}
19931998
placeholderTextColor={colors.textSecondary}
19941999
/>

components/PathAutocompleteInput.tsx

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -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-Za-z]:\//;
36+
const WINDOWS_DRIVE_PATH = /^[A-Za-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.

components/SearchResultRow.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { Ionicons } from '@expo/vector-icons';
1616
import { useTranslation } from 'react-i18next';
1717
import { useTheme } from '@/context/ThemeContext';
1818
import { SearchResult } from '@/types/api';
19-
import { formatSize } from '@/utils/format';
19+
import { formatSize, formatDate } from '@/utils/format';
2020
import { resultTrackerLabel } from '@/utils/searchResult';
2121
import { spacing, borderRadius } from '@/constants/spacing';
2222
import { typography } from '@/constants/typography';
@@ -85,13 +85,19 @@ export function SearchResultRow({
8585
{/* Line 2: health dot + meta */}
8686
<View style={styles.statusRow}>
8787
<View style={[styles.stateDot, { backgroundColor: dotColor }]} />
88-
<Text style={[styles.statusText, { color: colors.textSecondary }]} numberOfLines={1}>
88+
{/* numberOfLines=2, not 1: size/seeders/leechers/host/date can
89+
overflow one line once a date is present — wrap instead of
90+
silently truncating the date off the end. */}
91+
<Text style={[styles.statusText, { color: colors.textSecondary }]} numberOfLines={2}>
8992
{formatSize(result.fileSize)}
9093
{' · '}
9194
<Text style={{ color: colors.success }}>{seeders}</Text>
9295
{' · '}
9396
<Text>{leechers}</Text>
9497
{host ? ` · ${host}` : ''}
98+
{/* qBittorrent sends -1 (not undefined) when the plugin didn't
99+
report a date — only render when it's a real timestamp. */}
100+
{result.pubDate && result.pubDate > 0 ? ` · ${formatDate(result.pubDate)}` : ''}
95101
</Text>
96102
{/* Chevron hints that the row expands */}
97103
<Ionicons

constants/changelog.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export const CHANGELOG: ChangelogRelease[] = [
2828
items: [
2929
'Global seeding limits (ratio, seeding time, and what happens when reached) can now be set from the Transfer tab',
3030
'Added an Unlimited shortcut to the Max Ratio and Max Seeding Time editors on the Transfer tab',
31-
'File path now suggested from existing torrent paths and support for windows'
31+
'File path now suggested from existing torrent paths and support for windows',
3232
],
3333
},
3434
{

locales/de/translation.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,7 @@
512512
"sortLeechers": "Leechers",
513513
"sortSize": "Size",
514514
"sortName": "Name",
515+
"sortDate": "Date",
515516
"categoryLabel": "Kategorie",
516517
"indexerLabel": "Indexer",
517518
"allTrackers": "Alle Indexer",

locales/en/translation.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,7 @@
533533
"sortLeechers": "Leechers",
534534
"sortSize": "Size",
535535
"sortName": "Name",
536+
"sortDate": "Date",
536537
"categoryLabel": "Category",
537538
"indexerLabel": "Indexer",
538539
"allTrackers": "All indexers",

locales/es/translation.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,7 @@
512512
"sortLeechers": "Leechers",
513513
"sortSize": "Size",
514514
"sortName": "Name",
515+
"sortDate": "Date",
515516
"categoryLabel": "Categoría",
516517
"indexerLabel": "Indexador",
517518
"allTrackers": "Todos los indexadores",

locales/fr/translation.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,7 @@
512512
"sortLeechers": "Leechers",
513513
"sortSize": "Size",
514514
"sortName": "Name",
515+
"sortDate": "Date",
515516
"categoryLabel": "Catégorie",
516517
"indexerLabel": "Indexeur",
517518
"allTrackers": "Tous les indexeurs",

0 commit comments

Comments
 (0)