From 2c9072299e7fd3c5ba09fe13acd099a0b2c73934 Mon Sep 17 00:00:00 2001 From: AdityasahuX07 Date: Tue, 9 Dec 2025 23:17:19 +0530 Subject: [PATCH 1/3] Add Show/Hide Titles for Library. The Show/Hide Titles for Library because when the name of movie/show is large then it create uneven gap between library which not look good. And also If anyone want can toggle keep on for show titles in settings. --- src/screens/LibraryScreen.tsx | 110 ++++++++++++++++++++-------------- 1 file changed, 64 insertions(+), 46 deletions(-) diff --git a/src/screens/LibraryScreen.tsx b/src/screens/LibraryScreen.tsx index 470575e9..71f048dc 100644 --- a/src/screens/LibraryScreen.tsx +++ b/src/screens/LibraryScreen.tsx @@ -36,6 +36,7 @@ import { useTraktContext } from '../contexts/TraktContext'; import TraktIcon from '../../assets/rating-icons/trakt.svg'; import { traktService, TraktService, TraktImages } from '../services/traktService'; import { TraktLoadingSpinner } from '../components/common/TraktLoadingSpinner'; +import { useSettings } from '../hooks/useSettings'; // ADD THIS IMPORT // Define interfaces for proper typing interface LibraryItem extends StreamingContent { @@ -87,6 +88,7 @@ function getGridLayout(screenWidth: number): { numColumns: number; itemWidth: nu const TraktItem = React.memo(({ item, width, navigation, currentTheme }: { item: TraktDisplayItem; width: number; navigation: any; currentTheme: any }) => { const [posterUrl, setPosterUrl] = useState(null); + const { settings } = useSettings(); // ADD THIS useEffect(() => { let isMounted = true; @@ -107,7 +109,7 @@ const TraktItem = React.memo(({ item, width, navigation, currentTheme }: { item: navigation.navigate('Metadata', { id: item.imdbId, type: item.type }); } }, [navigation, item.imdbId, item.type]); - + return ( )} - - {item.name} - + {/* Conditionally render title based on settings.showPosterTitles */} + {settings.showPosterTitles && ( + + {item.name} + + )} ); @@ -168,17 +173,17 @@ const SkeletonLoader = () => { const renderSkeletonItem = () => ( - - ); @@ -212,7 +217,8 @@ const LibraryScreen = () => { const [selectedItem, setSelectedItem] = useState(null); const insets = useSafeAreaInsets(); const { currentTheme } = useTheme(); - + const { settings } = useSettings(); // ADD THIS + // Trakt integration const { isAuthenticated: traktAuthenticated, @@ -272,14 +278,14 @@ const LibraryScreen = () => { setLoading(true); try { const items = await catalogService.getLibraryItems(); - + // Sort by date added (most recent first) const sortedItems = items.sort((a, b) => { const timeA = (a as any).addedToLibraryAt || 0; const timeB = (b as any).addedToLibraryAt || 0; return timeB - timeA; // Descending order (newest first) }); - + // Load watched status for each item from AsyncStorage const updatedItems = await Promise.all(sortedItems.map(async (item) => { // Map StreamingContent to LibraryItem shape @@ -313,7 +319,7 @@ const LibraryScreen = () => { const timeB = (b as any).addedToLibraryAt || 0; return timeB - timeA; // Descending order (newest first) }); - + // Sync watched status on update const updatedItems = await Promise.all(sortedItems.map(async (item) => { // Map StreamingContent to LibraryItem shape @@ -403,7 +409,7 @@ const LibraryScreen = () => { activeOpacity={0.7} > - + { )} - - {item.name} - + {/* Conditionally render title based on settings.showPosterTitles */} + {settings.showPosterTitles && ( + + {item.name} + + )} ); @@ -444,11 +453,11 @@ const LibraryScreen = () => { > - {folder.name} @@ -458,6 +467,12 @@ const LibraryScreen = () => { + {/* Conditionally render folder title based on settings.showPosterTitles */} + {settings.showPosterTitles && ( + + {folder.name} + + )} ); @@ -489,9 +504,12 @@ const LibraryScreen = () => { )} - - Trakt collections - + {/* Conditionally render title based on settings.showPosterTitles */} + {settings.showPosterTitles && ( + + Trakt collections + + )} ); @@ -724,8 +742,8 @@ const LibraryScreen = () => { Your Trakt collections will appear here once you start using Trakt - { // Show content for specific folder const folderItems = getTraktFolderItems(selectedTraktFolder); - + if (folderItems.length === 0) { const folderName = traktFolders.find(f => f.id === selectedTraktFolder)?.name || 'Collection'; return ( @@ -767,8 +785,8 @@ const LibraryScreen = () => { This collection is empty - { const renderFilter = (filterType: 'trakt' | 'movies' | 'series', label: string, iconName: keyof typeof MaterialIcons.glyphMap) => { const isActive = filter === filterType; - + return ( { const emptySubtitle = 'Add some content to your library to see it here'; return ( - @@ -869,8 +887,8 @@ const LibraryScreen = () => { {emptySubtitle} - { {/* Fixed position header background to prevent shifts */} - + {/* Header Section with proper top spacing */} @@ -930,14 +948,14 @@ const LibraryScreen = () => { }} activeOpacity={0.7} > - - {selectedTraktFolder + {selectedTraktFolder ? traktFolders.find(f => f.id === selectedTraktFolder)?.name || 'Collection' : 'Trakt Collection' } @@ -951,10 +969,10 @@ const LibraryScreen = () => { onPress={() => navigation.navigate('Calendar')} activeOpacity={0.7} > - @@ -1352,4 +1370,4 @@ const styles = StyleSheet.create({ }, }); -export default LibraryScreen; +export default LibraryScreen; From 1c53e65b2681504551185b61f633b819d14eb912 Mon Sep 17 00:00:00 2001 From: AdityasahuX07 Date: Sun, 14 Dec 2025 15:31:10 +0530 Subject: [PATCH 2/3] Refactor LibraryScreen to include showTitles prop --- src/screens/LibraryScreen.tsx | 209 +++++++++++----------------------- 1 file changed, 65 insertions(+), 144 deletions(-) diff --git a/src/screens/LibraryScreen.tsx b/src/screens/LibraryScreen.tsx index 8b0e8e7f..83d11fd7 100644 --- a/src/screens/LibraryScreen.tsx +++ b/src/screens/LibraryScreen.tsx @@ -37,9 +37,8 @@ import { useTraktContext } from '../contexts/TraktContext'; import TraktIcon from '../../assets/rating-icons/trakt.svg'; import { traktService, TraktService, TraktImages } from '../services/traktService'; import { TraktLoadingSpinner } from '../components/common/TraktLoadingSpinner'; -import { useSettings } from '../hooks/useSettings'; // ADD THIS IMPORT +import { useSettings } from '../hooks/useSettings'; -// Define interfaces for proper typing interface LibraryItem extends StreamingContent { progress?: number; lastWatched?: string; @@ -73,10 +72,9 @@ interface TraktFolder { const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0; -// Compute responsive grid layout (more columns on tablets) function getGridLayout(screenWidth: number): { numColumns: number; itemWidth: number } { - const horizontalPadding = 16; // matches listContainer padding (approx) - const gutter = 12; // space between items (via space-between + marginBottom) + const horizontalPadding = 16; + const gutter = 12; let numColumns = 3; if (screenWidth >= 1200) numColumns = 5; else if (screenWidth >= 1000) numColumns = 4; @@ -87,9 +85,20 @@ function getGridLayout(screenWidth: number): { numColumns: number; itemWidth: nu return { numColumns, itemWidth }; } -const TraktItem = React.memo(({ item, width, navigation, currentTheme }: { item: TraktDisplayItem; width: number; navigation: any; currentTheme: any }) => { +const TraktItem = React.memo(({ + item, + width, + navigation, + currentTheme, + showTitles +}: { + item: TraktDisplayItem; + width: number; + navigation: any; + currentTheme: any; + showTitles: boolean; +}) => { const [posterUrl, setPosterUrl] = useState(null); - const { settings } = useSettings(); // ADD THIS useEffect(() => { let isMounted = true; @@ -131,8 +140,7 @@ const TraktItem = React.memo(({ item, width, navigation, currentTheme }: { item: )} - {/* Conditionally render title based on settings.showPosterTitles */} - {settings.showPosterTitles && ( + {showTitles && ( {item.name} @@ -189,7 +197,6 @@ const SkeletonLoader = () => { ); - // Render enough skeletons for at least two rows const skeletonCount = numColumns * 2; return ( @@ -213,14 +220,12 @@ const LibraryScreen = () => { const [showTraktContent, setShowTraktContent] = useState(false); const [selectedTraktFolder, setSelectedTraktFolder] = useState(null); const { showInfo, showError } = useToast(); - // DropUpMenu state const [menuVisible, setMenuVisible] = useState(false); const [selectedItem, setSelectedItem] = useState(null); const insets = useSafeAreaInsets(); const { currentTheme } = useTheme(); const { settings } = useSettings(); - // Trakt integration const { isAuthenticated: traktAuthenticated, isLoading: traktLoading, @@ -236,7 +241,6 @@ const LibraryScreen = () => { loadAllCollections } = useTraktContext(); - // Force consistent status bar settings useEffect(() => { const applyStatusBarConfig = () => { StatusBar.setBarStyle('light-content'); @@ -247,30 +251,24 @@ const LibraryScreen = () => { }; applyStatusBarConfig(); - - // Re-apply on focus const unsubscribe = navigation.addListener('focus', applyStatusBarConfig); return unsubscribe; }, [navigation]); - // Handle hardware back button and gesture navigation useEffect(() => { const backAction = () => { if (showTraktContent) { if (selectedTraktFolder) { - // If in a specific folder, go back to folder list setSelectedTraktFolder(null); } else { - // If in Trakt collections view, go back to main library setShowTraktContent(false); } - return true; // Prevent default back behavior + return true; } - return false; // Allow default back behavior (navigate back) + return false; }; const backHandler = BackHandler.addEventListener('hardwareBackPress', backAction); - return () => backHandler.remove(); }, [showTraktContent, selectedTraktFolder]); @@ -280,16 +278,13 @@ const LibraryScreen = () => { try { const items = await catalogService.getLibraryItems(); - // Sort by date added (most recent first) const sortedItems = items.sort((a, b) => { const timeA = (a as any).addedToLibraryAt || 0; const timeB = (b as any).addedToLibraryAt || 0; - return timeB - timeA; // Descending order (newest first) + return timeB - timeA; }); - // Load watched status for each item from AsyncStorage const updatedItems = await Promise.all(sortedItems.map(async (item) => { - // Map StreamingContent to LibraryItem shape const libraryItem: LibraryItem = { ...item, gradient: Array.isArray((item as any).gradient) ? (item as any).gradient : ['#222', '#444'], @@ -312,18 +307,14 @@ const LibraryScreen = () => { loadLibrary(); - // Subscribe to library updates const unsubscribe = catalogService.subscribeToLibraryUpdates(async (items) => { - // Sort by date added (most recent first) const sortedItems = items.sort((a, b) => { const timeA = (a as any).addedToLibraryAt || 0; const timeB = (b as any).addedToLibraryAt || 0; - return timeB - timeA; // Descending order (newest first) + return timeB - timeA; }); - // Sync watched status on update const updatedItems = await Promise.all(sortedItems.map(async (item) => { - // Map StreamingContent to LibraryItem shape const libraryItem: LibraryItem = { ...item, gradient: Array.isArray((item as any).gradient) ? (item as any).gradient : ['#222', '#444'], @@ -339,10 +330,7 @@ const LibraryScreen = () => { setLibraryItems(updatedItems); }); - // Listen for watched status changes const watchedSub = DeviceEventEmitter.addListener('watchedStatusChanged', loadLibrary); - - // Refresh when screen regains focus const focusSub = navigation.addListener('focus', loadLibrary); return () => { @@ -358,7 +346,6 @@ const LibraryScreen = () => { return true; }); - // Generate Trakt collection folders const traktFolders = useMemo((): TraktFolder[] => { if (!traktAuthenticated) return []; @@ -395,7 +382,6 @@ const LibraryScreen = () => { } ]; - // Only return folders that have content return folders.filter(folder => folder.itemCount > 0); }, [traktAuthenticated, watchedMovies, watchedShows, watchlistMovies, watchlistShows, collectionMovies, collectionShows, continueWatching, ratedContent]); @@ -411,7 +397,7 @@ const LibraryScreen = () => { > - { )} - {/* Conditionally render title based on settings.showPosterTitles */} {settings.showPosterTitles && ( {item.name} @@ -442,13 +427,12 @@ const LibraryScreen = () => { ); - // Render individual Trakt collection folder const renderTraktCollectionFolder = ({ folder }: { folder: TraktFolder }) => ( { setSelectedTraktFolder(folder.id); - loadAllCollections(); // Load all collections when entering a specific folder + loadAllCollections(); }} activeOpacity={0.7} > @@ -468,12 +452,6 @@ const LibraryScreen = () => { - {/* Conditionally render folder title based on settings.showPosterTitles */} - {settings.showPosterTitles && ( - - {folder.name} - - )} ); @@ -485,8 +463,8 @@ const LibraryScreen = () => { navigation.navigate('TraktSettings'); } else { setShowTraktContent(true); - setSelectedTraktFolder(null); // Reset to folder view - loadAllCollections(); // Load all collections when opening + setSelectedTraktFolder(null); + loadAllCollections(); } }} activeOpacity={0.7} @@ -505,7 +483,6 @@ const LibraryScreen = () => { )} - {/* Conditionally render title based on settings.showPosterTitles */} {settings.showPosterTitles && ( Trakt collections @@ -516,16 +493,20 @@ const LibraryScreen = () => { ); const renderTraktItem = useCallback(({ item }: { item: TraktDisplayItem }) => { - return ; - }, [itemWidth, navigation, currentTheme]); + return ; + }, [itemWidth, navigation, currentTheme, settings.showPosterTitles]); - // Get items for a specific Trakt folder const getTraktFolderItems = useCallback((folderId: string): TraktDisplayItem[] => { const items: TraktDisplayItem[] = []; switch (folderId) { case 'watched': - // Add watched movies if (watchedMovies) { for (const watchedMovie of watchedMovies) { const movie = watchedMovie.movie; @@ -536,7 +517,7 @@ const LibraryScreen = () => { type: 'movie', poster: 'placeholder', year: movie.year, - lastWatched: watchedMovie.last_watched_at, // Store raw timestamp for sorting + lastWatched: watchedMovie.last_watched_at, plays: watchedMovie.plays, imdbId: movie.ids.imdb, traktId: movie.ids.trakt, @@ -545,7 +526,6 @@ const LibraryScreen = () => { } } } - // Add watched shows if (watchedShows) { for (const watchedShow of watchedShows) { const show = watchedShow.show; @@ -556,7 +536,7 @@ const LibraryScreen = () => { type: 'series', poster: 'placeholder', year: show.year, - lastWatched: watchedShow.last_watched_at, // Store raw timestamp for sorting + lastWatched: watchedShow.last_watched_at, plays: watchedShow.plays, imdbId: show.ids.imdb, traktId: show.ids.trakt, @@ -568,7 +548,6 @@ const LibraryScreen = () => { break; case 'continue-watching': - // Add continue watching items if (continueWatching) { for (const item of continueWatching) { if (item.type === 'movie' && item.movie) { @@ -578,7 +557,7 @@ const LibraryScreen = () => { type: 'movie', poster: 'placeholder', year: item.movie.year, - lastWatched: item.paused_at, // Store raw timestamp for sorting + lastWatched: item.paused_at, imdbId: item.movie.ids.imdb, traktId: item.movie.ids.trakt, images: item.movie.images, @@ -590,7 +569,7 @@ const LibraryScreen = () => { type: 'series', poster: 'placeholder', year: item.show.year, - lastWatched: item.paused_at, // Store raw timestamp for sorting + lastWatched: item.paused_at, imdbId: item.show.ids.imdb, traktId: item.show.ids.trakt, images: item.show.images, @@ -601,7 +580,6 @@ const LibraryScreen = () => { break; case 'watchlist': - // Add watchlist movies if (watchlistMovies) { for (const watchlistMovie of watchlistMovies) { const movie = watchlistMovie.movie; @@ -612,7 +590,7 @@ const LibraryScreen = () => { type: 'movie', poster: 'placeholder', year: movie.year, - lastWatched: watchlistMovie.listed_at, // Store raw timestamp for sorting + lastWatched: watchlistMovie.listed_at, imdbId: movie.ids.imdb, traktId: movie.ids.trakt, images: movie.images, @@ -620,7 +598,6 @@ const LibraryScreen = () => { } } } - // Add watchlist shows if (watchlistShows) { for (const watchlistShow of watchlistShows) { const show = watchlistShow.show; @@ -631,7 +608,7 @@ const LibraryScreen = () => { type: 'series', poster: 'placeholder', year: show.year, - lastWatched: watchlistShow.listed_at, // Store raw timestamp for sorting + lastWatched: watchlistShow.listed_at, imdbId: show.ids.imdb, traktId: show.ids.trakt, images: show.images, @@ -642,7 +619,6 @@ const LibraryScreen = () => { break; case 'collection': - // Add collection movies if (collectionMovies) { for (const collectionMovie of collectionMovies) { const movie = collectionMovie.movie; @@ -653,7 +629,7 @@ const LibraryScreen = () => { type: 'movie', poster: 'placeholder', year: movie.year, - lastWatched: collectionMovie.collected_at, // Store raw timestamp for sorting + lastWatched: collectionMovie.collected_at, imdbId: movie.ids.imdb, traktId: movie.ids.trakt, images: movie.images, @@ -661,7 +637,6 @@ const LibraryScreen = () => { } } } - // Add collection shows if (collectionShows) { for (const collectionShow of collectionShows) { const show = collectionShow.show; @@ -672,7 +647,7 @@ const LibraryScreen = () => { type: 'series', poster: 'placeholder', year: show.year, - lastWatched: collectionShow.collected_at, // Store raw timestamp for sorting + lastWatched: collectionShow.collected_at, imdbId: show.ids.imdb, traktId: show.ids.trakt, images: show.images, @@ -683,7 +658,6 @@ const LibraryScreen = () => { break; case 'ratings': - // Add rated content if (ratedContent) { for (const ratedItem of ratedContent) { if (ratedItem.movie) { @@ -694,7 +668,7 @@ const LibraryScreen = () => { type: 'movie', poster: 'placeholder', year: movie.year, - lastWatched: ratedItem.rated_at, // Store raw timestamp for sorting + lastWatched: ratedItem.rated_at, rating: ratedItem.rating, imdbId: movie.ids.imdb, traktId: movie.ids.trakt, @@ -708,7 +682,7 @@ const LibraryScreen = () => { type: 'series', poster: 'placeholder', year: show.year, - lastWatched: ratedItem.rated_at, // Store raw timestamp for sorting + lastWatched: ratedItem.rated_at, rating: ratedItem.rating, imdbId: show.ids.imdb, traktId: show.ids.trakt, @@ -720,7 +694,6 @@ const LibraryScreen = () => { break; } - // Sort by last watched/added date (most recent first) using raw timestamps return items.sort((a, b) => { const dateA = a.lastWatched ? new Date(a.lastWatched).getTime() : 0; const dateB = b.lastWatched ? new Date(b.lastWatched).getTime() : 0; @@ -733,7 +706,6 @@ const LibraryScreen = () => { return ; } - // If no specific folder is selected, show the folder structure if (!selectedTraktFolder) { if (traktFolders.length === 0) { return ( @@ -759,7 +731,6 @@ const LibraryScreen = () => { ); } - // Show collection folders return ( { ); } - // Show content for specific folder const folderItems = getTraktFolderItems(selectedTraktFolder); if (folderItems.length === 0) { @@ -916,7 +886,6 @@ const LibraryScreen = () => { ); }; - // Tablet detection aligned with navigation tablet logic const isTablet = useMemo(() => { const smallestDimension = Math.min(width, height); return (Platform.OS === 'ios' ? (Platform as any).isPad === true : smallestDimension >= 768); @@ -924,72 +893,27 @@ const LibraryScreen = () => { return ( - {/* Fixed position header background to prevent shifts */} - + f.id === selectedTraktFolder)?.name || 'Collection' + : 'Trakt Collection') + : 'Library' + } + showBackButton={showTraktContent} + onBackPress={showTraktContent ? () => { + if (selectedTraktFolder) { + setSelectedTraktFolder(null); + } else { + setShowTraktContent(false); + } + } : undefined} + useMaterialIcons={showTraktContent} + rightActionIcon={!showTraktContent ? 'calendar' : undefined} + onRightActionPress={!showTraktContent ? () => navigation.navigate('Calendar') : undefined} + isTablet={isTablet} + /> - - {/* Header Section with proper top spacing */} - - - {showTraktContent ? ( - <> - { - if (selectedTraktFolder) { - setSelectedTraktFolder(null); - } else { - setShowTraktContent(false); - } - }} - activeOpacity={0.7} - > - - - - {selectedTraktFolder - ? traktFolders.find(f => f.id === selectedTraktFolder)?.name || 'Collection' - : 'Trakt Collection' - } - - - ) : ( - <> - Library - navigation.navigate('Calendar')} - activeOpacity={0.7} - > - - - - )} - - - - {/* Content Container */} - - {!showTraktContent && ( - // Replaced ScrollView with View and used the modified style - - {renderFilter('trakt', 'Trakt', 'pan-tool')} - {renderFilter('movies', 'Movies', 'movie')} - {renderFilter('series', 'TV Shows', 'live-tv')} - - )} - - {/* Content Container */} {!showTraktContent && ( @@ -1002,14 +926,13 @@ const LibraryScreen = () => { {showTraktContent ? renderTraktContent() : renderContent()} - {/* DropUpMenu integration */} {selectedItem && ( setMenuVisible(false)} item={selectedItem} isWatched={!!selectedItem.watched} - isSaved={true} // Since this is from library, it's always saved + isSaved={true} onOptionSelect={async (option) => { if (!selectedItem) return; switch (option) { @@ -1026,12 +949,10 @@ const LibraryScreen = () => { } case 'watched': { try { - // Use AsyncStorage to store watched status by key const key = `watched:${selectedItem.type}:${selectedItem.id}`; const newWatched = !selectedItem.watched; await mmkvStorage.setItem(key, newWatched ? 'true' : 'false'); showInfo(newWatched ? 'Marked as Watched' : 'Marked as Unwatched', newWatched ? 'Item marked as watched' : 'Item marked as unwatched'); - // Instantly update local state setLibraryItems(prev => prev.map(item => item.id === selectedItem.id && item.type === selectedItem.type ? { ...item, watched: newWatched } @@ -1330,7 +1251,7 @@ const styles = StyleSheet.create({ justifyContent: 'center', }, headerSpacer: { - width: 44, // Match the back button width + width: 44, }, traktContainer: { flex: 1, From af572f8b2971f89e733c934bf95048ba57008be2 Mon Sep 17 00:00:00 2001 From: AdityasahuX07 Date: Fri, 19 Dec 2025 23:44:17 +0530 Subject: [PATCH 3/3] Refactor renderItem for cleaner syntax --- src/screens/LibraryScreen.tsx | 78 +++++++++++++++++------------------ 1 file changed, 38 insertions(+), 40 deletions(-) diff --git a/src/screens/LibraryScreen.tsx b/src/screens/LibraryScreen.tsx index eaf75d3c..83d11fd7 100644 --- a/src/screens/LibraryScreen.tsx +++ b/src/screens/LibraryScreen.tsx @@ -385,49 +385,47 @@ const LibraryScreen = () => { return folders.filter(folder => folder.itemCount > 0); }, [traktAuthenticated, watchedMovies, watchedShows, watchlistMovies, watchlistShows, collectionMovies, collectionShows, continueWatching, ratedContent]); - const renderItem = ({ item }: { item: LibraryItem }) => { - const aspectRatio = item.posterShape === 'landscape' ? 16 / 9 : (item.posterShape === 'square' ? 1 : 2 / 3); - - return ( - navigation.navigate('Metadata', { id: item.id, type: item.type })} - onLongPress={() => { - setSelectedItem(item); - setMenuVisible(true); - }} - activeOpacity={0.7} - > - - - - {item.watched && ( - - - - )} - {item.progress !== undefined && item.progress < 1 && ( - - - - )} - + const renderItem = ({ item }: { item: LibraryItem }) => ( + navigation.navigate('Metadata', { id: item.id, type: item.type })} + onLongPress={() => { + setSelectedItem(item); + setMenuVisible(true); + }} + activeOpacity={0.7} + > + + + + {item.watched && ( + + + + )} + {item.progress !== undefined && item.progress < 1 && ( + + + + )} + + {settings.showPosterTitles && ( {item.name} - - - ); - }; + )} + + + ); const renderTraktCollectionFolder = ({ folder }: { folder: TraktFolder }) => (