Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions app.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,20 @@ module.exports = {
'expo-sharing',
'expo-status-bar',
'./plugins/withNativeTorrentFileCopy',
// Xcode 27 / iOS 27 SDK hard-fails app launch unless the generated
// native project adopts the UIScene life cycle — Expo SDK 57.0.23+
// ships that support, but only behind this opt-in flag (full default
// adoption doesn't land until SDK 58). Without it, `npm run xcode`
// produces a build that crashes instantly on any Xcode 27 toolchain,
// regardless of which simulator OS it's run on.
[
'expo-build-properties',
{
ios: {
enableSceneSupport: true,
},
},
],
],
extra: {
router: {},
Expand Down
67 changes: 58 additions & 9 deletions app/(tabs)/(torrents)/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,11 @@ export default function TorrentsScreen() {
const [selectMode, setSelectMode] = useState(false);
const [selectedHashes, setSelectedHashes] = useState<Set<string>>(new Set());
const [bulkLoading, setBulkLoading] = useState(false);
// The bulk actions bar is absolutely positioned over the list (see
// bulkActionsBar), so without this the last card(s) render underneath it
// and are partially hidden. Measured rather than hardcoded since its height
// varies with the safe-area inset and Dynamic Type text scaling.
const [bulkActionsBarHeight, setBulkActionsBarHeight] = useState(0);
const [bulkMenuVisible, setBulkMenuVisible] = useState(false);
const [showBulkCategoryPicker, setShowBulkCategoryPicker] = useState(false);
// Tag editing works as a draft: toggles accumulate in bulkTagDraft and are
Expand Down Expand Up @@ -178,6 +183,12 @@ export default function TorrentsScreen() {
const headerTranslateY = useRef(new Animated.Value(0)).current;
const isHeaderVisible = useRef(true);
const isAnimating = useRef(false);
// headerContainer is an absolute overlay (see styles.headerContainer), so
// the list's paddingTop must reserve exactly its rendered height or content
// either hides underneath it or leaves a gap above the first card. Its
// content differs by selectMode (fewer buttons/rows), so this is measured
// rather than a fixed constant — matches bulkActionsBarHeight below.
const [headerHeight, setHeaderHeight] = useState(136);

// Swipeable refs for closing open rows
const openSwipeableRef = useRef<Swipeable | null>(null);
Expand All @@ -194,9 +205,9 @@ export default function TorrentsScreen() {
// is focused.
useFocusEffect(
useCallback(() => {
setToastTopOffset(styles.listContent.paddingTop + spacing.xxl);
setToastTopOffset(headerHeight + spacing.xxl);
return () => setToastTopOffset(null);
}, [setToastTopOffset]),
}, [setToastTopOffset, headerHeight]),
);

// Check for filter + card view mode preference changes on screen focus
Expand Down Expand Up @@ -936,9 +947,13 @@ export default function TorrentsScreen() {
[refresh, showToast, t],
);

// Scroll handler — header show/hide only
// Scroll handler — header show/hide only. Pinned (never hides) in
// selectMode: Select All / Close must stay reachable without scrolling
// back up, per #252-adjacent feedback.
const handleScroll = useCallback(
(event: { nativeEvent: { contentOffset: { y: number } } }) => {
if (selectMode) return;

const currentScrollY = event.nativeEvent.contentOffset.y;
const scrollDifference = currentScrollY - lastScrollY.current;

Expand Down Expand Up @@ -990,9 +1005,20 @@ export default function TorrentsScreen() {

lastScrollY.current = currentScrollY;
},
[headerTranslateY],
[headerTranslateY, selectMode],
);

// Snap the header back to visible when entering selectMode — it may have
// been scrolled out of view before selection started, and while selected
// it must stay pinned (handleScroll no-ops during selectMode above).
useEffect(() => {
if (selectMode) {
isHeaderVisible.current = true;
isAnimating.current = false;
headerTranslateY.setValue(0);
}
}, [selectMode, headerTranslateY]);

// Whether any secondary (category/tag) filter is active
const hasSecondaryFilter = categoryFilter !== null || tagFilters.length > 0;

Expand Down Expand Up @@ -1198,8 +1224,16 @@ export default function TorrentsScreen() {
<FocusAwareStatusBar barStyle={isDark ? 'light-content' : 'dark-content'} />
<View style={[styles.container, { backgroundColor: colors.background }]}>
<Animated.View
onLayout={(e) => setHeaderHeight(e.nativeEvent.layout.height)}
style={[
styles.headerContainer,
// In selectMode the header is pinned (see the selectMode effect
// above) — switch it from an absolute overlay to a normal in-flow
// sibling so it physically pushes the list down instead of
// floating on top of it. Cards can only render underneath it (as
// in normal browsing, by design) when it's an overlay; pinned +
// overlay together is exactly the overlap this avoids.
selectMode && styles.headerContainerPinned,
{
backgroundColor: 'transparent',
transform: [{ translateY: headerTranslateY }],
Expand Down Expand Up @@ -1624,7 +1658,7 @@ export default function TorrentsScreen() {
<FlatList
data={filteredTorrents}
keyExtractor={(item) => item.hash}
style={{ backgroundColor: colors.background }}
style={{ flex: 1, backgroundColor: colors.background }}
renderItem={({ item }) => {
const itemIsPaused =
item.state === 'pausedDL' ||
Expand Down Expand Up @@ -1855,7 +1889,15 @@ export default function TorrentsScreen() {
tintColor={colors.primary}
/>
}
contentContainerStyle={styles.listContent}
contentContainerStyle={[
styles.listContent,
// Only the absolute-overlay (non-selectMode) header needs the
// list content pushed down by its height — in selectMode the
// header is a normal in-flow sibling (see headerContainerPinned)
// and already occupies that space in the layout itself.
selectMode ? { paddingTop: spacing.sm } : { paddingTop: headerHeight },
selectMode && selectedHashes.size > 0 && { paddingBottom: bulkActionsBarHeight },
]}
onScroll={handleScroll}
scrollEventThrottle={50}
removeClippedSubviews={false}
Expand All @@ -1867,6 +1909,7 @@ export default function TorrentsScreen() {

{selectMode && selectedHashes.size > 0 && (
<View
onLayout={(e) => setBulkActionsBarHeight(e.nativeEvent.layout.height)}
style={[
styles.bulkActionsBar,
{ backgroundColor: colors.surface, borderTopColor: colors.surfaceOutline },
Expand Down Expand Up @@ -2351,6 +2394,13 @@ const styles = StyleSheet.create({
right: 0,
zIndex: 1000,
},
// Cancels the absolute positioning above — see its usage in selectMode.
headerContainerPinned: {
position: 'relative',
top: undefined,
left: undefined,
right: undefined,
},
searchCard: {
borderRadius: borderRadius.medium,
paddingHorizontal: spacing.md,
Expand Down Expand Up @@ -2436,9 +2486,8 @@ const styles = StyleSheet.create({
opacity: 0.5,
},
listContent: {
// +36 over the skeleton's 100 to make room for the server-switcher row
// (#249) that only renders in this, the fully-connected header.
paddingTop: 136,
// paddingTop is applied dynamically (see headerHeight) — headerContainer
// is an absolute overlay whose height varies with selectMode.
paddingHorizontal: spacing.md,
borderRadius: borderRadius.large,
},
Expand Down
32 changes: 16 additions & 16 deletions components/LogViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,19 +113,25 @@ export function LogViewer({ visible, onClose, onClear, refreshTrigger }: LogView
{ borderBottomColor: colors.surfaceOutline, backgroundColor: colors.surface },
]}
>
<Text style={[styles.title, { color: colors.text }]}>
<Text
style={[styles.title, { color: colors.text }]}
numberOfLines={1}
ellipsizeMode="tail"
>
{t('screens.settings.logsTitle')}
</Text>
<View style={styles.headerButtons}>
<TouchableOpacity
onPress={handleCopy}
style={[styles.copyButton, { backgroundColor: colors.primary }]}
style={styles.copyButton}
disabled={logs.length === 0}
accessibilityLabel={t('common.copy')}
>
<Ionicons name="copy-outline" size={20} color={colors.onAccent} />
<Text style={[styles.copyButtonText, { color: colors.onAccent }]}>
{t('common.copy')}
</Text>
<Ionicons
name="copy-outline"
size={20}
color={logs.length === 0 ? colors.textSecondary : colors.primary}
/>
</TouchableOpacity>
<TouchableOpacity
onPress={handleClear}
Expand Down Expand Up @@ -234,23 +240,17 @@ const styles = StyleSheet.create({
...typography.h2,
fontSize: 24,
fontWeight: '700',
flexShrink: 1,
marginRight: spacing.sm,
},
headerButtons: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.sm,
flexShrink: 0,
},
copyButton: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.xs,
paddingHorizontal: spacing.md,
paddingVertical: spacing.sm,
borderRadius: borderRadius.small,
},
copyButtonText: {
...typography.bodyMedium,
fontWeight: '600',
padding: 8,
},
clearButton: {
flexDirection: 'row',
Expand Down
21 changes: 21 additions & 0 deletions constants/changelog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,27 @@ export interface ChangelogRelease {
sections?: ChangelogSection[];
}
export const CHANGELOG: ChangelogRelease[] = [
{
version: '3.8.45',
date: '2026-09-21',
sections: [
{
title: 'New Features',
items: ['Added a quick server switcher to jump between saved servers'],
},
{
title: 'Bugs Fixed',
items: [
'Fixed ETA being hidden while seeding or checking a completed torrent',
'Fixed requests hanging for up to two minutes when the server becomes unreachable',
'Fixed Disconnect not working when the server is unreachable',
'Fixed the screen flashing when connecting to a server from the not-connected screen',
'Fixed RSS feed articles displaying in random order instead of newest first',
'Fixed the connectivity logs screen not showing anything useful',
],
},
],
},
{
version: '3.8.44',
date: '2026-09-09',
Expand Down
31 changes: 29 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"scripts": {
"start": "expo start --dev-client",
"ios": "expo run:ios",
"simulator": "npx expo run:ios --device \"iPhone 17e\"",
"xcode": "npm install && npx expo prebuild -p ios && cd ios && LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 pod install && cd .. && open ios/qRemote.xcworkspace",
"web": "expo start --web",
"lint": "eslint . --ext .ts,.tsx",
Expand All @@ -19,6 +20,7 @@
"@tanstack/react-query": "^5.101.4",
"axios": "^1.18.1",
"expo": "~57.0.24",
"expo-build-properties": "~57.0.21",
"expo-clipboard": "~57.0.2",
"expo-constants": "~57.0.13",
"expo-dev-client": "~57.0.19",
Expand Down
Loading
Loading