From 579e74b44d314ec5453d8193b4ac8416f5ca20e3 Mon Sep 17 00:00:00 2001 From: taylorcox75 Date: Sun, 20 Sep 2026 21:04:08 -0500 Subject: [PATCH] Fix select-mode header overlap/gap and Xcode 27 build crash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Torrents screen (select mode): - Bulk actions bar no longer covers the last card (measured height fed back as list bottom padding instead of a fixed value) - Fixed the large gap above the first card after scrolling up, caused by a hardcoded header paddingTop sized for the full normal-mode header rather than select mode's shorter one (now measured) - Header (Select All / Close) no longer scrolls away or gets scrolled over by cards during selection — it's pinned as a normal in-flow sibling instead of an absolute overlay while selectMode is active, and FlatList now declares flex: 1 explicitly rather than relying on being the sole flex participant Connectivity log viewer: - Fixed the header overflowing/clipping the Clear button on narrower screens by making the Copy button icon-only (matching the existing debug-info copy button convention) and letting the title truncate Build: - Xcode 27 / iOS 27 SDK hard-fails app launch unless the generated native project adopts the UIScene life cycle. Added expo-build-properties with ios.enableSceneSupport, and updated withNativeTorrentFileCopy's AppDelegate.swift template match for the resulting shape change (the cold-launch .torrent-copy path is weaker under scene lifecycle - documented in the plugin, needs on-device verification) - Added npm run simulator, targeting a specific iOS 26.5 sim device Changelog: added the 3.8.45 entry. --- app.config.js | 14 ++++++ app/(tabs)/(torrents)/index.tsx | 67 ++++++++++++++++++++++---- components/LogViewer.tsx | 32 ++++++------ constants/changelog.ts | 21 ++++++++ package-lock.json | 31 +++++++++++- package.json | 2 + plugins/withNativeTorrentFileCopy.js | 55 ++++++++++++--------- tests/rn/components/LogViewer.test.tsx | 4 +- 8 files changed, 174 insertions(+), 52 deletions(-) diff --git a/app.config.js b/app.config.js index d1a88156..4c0896b5 100644 --- a/app.config.js +++ b/app.config.js @@ -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: {}, diff --git a/app/(tabs)/(torrents)/index.tsx b/app/(tabs)/(torrents)/index.tsx index ed10bc01..d4c21d5a 100644 --- a/app/(tabs)/(torrents)/index.tsx +++ b/app/(tabs)/(torrents)/index.tsx @@ -110,6 +110,11 @@ export default function TorrentsScreen() { const [selectMode, setSelectMode] = useState(false); const [selectedHashes, setSelectedHashes] = useState>(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 @@ -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(null); @@ -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 @@ -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; @@ -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; @@ -1198,8 +1224,16 @@ export default function TorrentsScreen() { 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 }], @@ -1624,7 +1658,7 @@ export default function TorrentsScreen() { item.hash} - style={{ backgroundColor: colors.background }} + style={{ flex: 1, backgroundColor: colors.background }} renderItem={({ item }) => { const itemIsPaused = item.state === 'pausedDL' || @@ -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} @@ -1867,6 +1909,7 @@ export default function TorrentsScreen() { {selectMode && selectedHashes.size > 0 && ( setBulkActionsBarHeight(e.nativeEvent.layout.height)} style={[ styles.bulkActionsBar, { backgroundColor: colors.surface, borderTopColor: colors.surfaceOutline }, @@ -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, @@ -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, }, diff --git a/components/LogViewer.tsx b/components/LogViewer.tsx index be9441ae..0422cafe 100644 --- a/components/LogViewer.tsx +++ b/components/LogViewer.tsx @@ -113,19 +113,25 @@ export function LogViewer({ visible, onClose, onClear, refreshTrigger }: LogView { borderBottomColor: colors.surfaceOutline, backgroundColor: colors.surface }, ]} > - + {t('screens.settings.logsTitle')} - - - {t('common.copy')} - +