diff --git a/assets/translations/en.json b/assets/translations/en.json index 13fb2d18..4c235788 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -292,7 +292,12 @@ "subtitle_plural": "{{count}} files" }, "courseDirectoryScreen": { - "noResult": "No results found" + "clearSearch": "Clear search", + "emptyRootFolder": "There are no files", + "emptyFolder": "The folder is empty", + "noResult": "No results found", + "navigateRecentFiles": "View recent", + "search": "Search" }, "courseFileListItem": { "openFileError": "Cannot open the file.", @@ -300,7 +305,7 @@ }, "courseFilesTab": { "browseFiles": "Browse files", - "empty": "There are no files here", + "empty": "There are no files", "navigateFolders": "Navigate folders", "recentSectionTitle": "Recent files", "title": "Files" diff --git a/assets/translations/it.json b/assets/translations/it.json index 83794d25..03b23638 100644 --- a/assets/translations/it.json +++ b/assets/translations/it.json @@ -292,7 +292,12 @@ "subtitle_plural": "{{count}} file" }, "courseDirectoryScreen": { - "noResult": "Nessun risultato trovato" + "clearSearch": "Cancella ricerca", + "emptyRootFolder": "Non ci sono file", + "emptyFolder": "La cartella รจ vuota", + "noResult": "Nessun risultato trovato", + "navigateRecentFiles": "Sfoglia file recenti", + "search": "Cerca" }, "courseFileListItem": { "openFileError": "Impossibile aprire il file.", @@ -300,7 +305,7 @@ }, "courseFilesTab": { "browseFiles": "Esplora file", - "empty": "Non ci sono file qui", + "empty": "Non ci sono file", "navigateFolders": "Sfoglia le cartelle", "recentSectionTitle": "File recenti", "title": "Materiale" diff --git a/src/core/contexts/PreferencesContext.ts b/src/core/contexts/PreferencesContext.ts index 7921b0f9..c8888e83 100644 --- a/src/core/contexts/PreferencesContext.ts +++ b/src/core/contexts/PreferencesContext.ts @@ -19,6 +19,7 @@ export const editablePreferenceKeys = [ 'emailGuideRead', 'placesSearched', 'agendaScreen', + 'filesScreen', 'hideGrades', ] as const; @@ -34,6 +35,7 @@ export const objectPreferenceKeys = [ 'emailGuideRead', 'placesSearched', 'agendaScreen', + 'filesScreen', 'hideGrades', ]; @@ -62,6 +64,7 @@ export interface PreferencesContextBase { layout: 'weekly' | 'daily'; filters: AgendaTypesFilterState; }; + filesScreen: 'filesView' | 'directoryView'; hideGrades?: boolean; } diff --git a/src/core/providers/PreferencesProvider.tsx b/src/core/providers/PreferencesProvider.tsx index 8cf3a5a4..9a5731ff 100644 --- a/src/core/providers/PreferencesProvider.tsx +++ b/src/core/providers/PreferencesProvider.tsx @@ -33,6 +33,7 @@ export const PreferencesProvider = ({ children }: PropsWithChildren) => { lecture: false, }, }, + filesScreen: 'filesView', }); const preferencesInitialized = useRef(false); diff --git a/src/features/courses/navigation/CourseNavigator.tsx b/src/features/courses/navigation/CourseNavigator.tsx index 779a394d..dfa0502b 100644 --- a/src/features/courses/navigation/CourseNavigator.tsx +++ b/src/features/courses/navigation/CourseNavigator.tsx @@ -19,10 +19,10 @@ import { CourseIndicator } from '../components/CourseIndicator'; import { CourseContext } from '../contexts/CourseContext'; import { CourseFilesCacheProvider } from '../providers/CourseFilesCacheProvider'; import { CourseAssignmentsScreen } from '../screens/CourseAssignmentsScreen'; -import { CourseFilesScreen } from '../screens/CourseFilesScreen'; import { CourseInfoScreen } from '../screens/CourseInfoScreen'; import { CourseLecturesScreen } from '../screens/CourseLecturesScreen'; import { CourseNoticesScreen } from '../screens/CourseNoticesScreen'; +import { FileNavigator } from './FileNavigator'; type Props = NativeStackScreenProps; @@ -141,7 +141,7 @@ export const CourseNavigator = ({ route, navigation }: Props) => { /> diff --git a/src/features/courses/navigation/CourseSharedScreens.tsx b/src/features/courses/navigation/CourseSharedScreens.tsx index 2223303d..116b9476 100644 --- a/src/features/courses/navigation/CourseSharedScreens.tsx +++ b/src/features/courses/navigation/CourseSharedScreens.tsx @@ -28,12 +28,12 @@ export interface CourseSharedScreensParamList extends ParamListBase { Course: { id: number; animated?: boolean }; Notice: { noticeId: number; courseId: number }; CoursePreferences: { courseId: number; uniqueShortcode: string }; + CourseGuide: { courseId: number }; CourseDirectory: { courseId: number; directoryId?: string; directoryName?: string; }; - CourseGuide: { courseId: number }; CourseVideolecture: { courseId: number; lectureId: number; @@ -111,13 +111,10 @@ export const CourseSharedScreens = ( `${params.directoryId}`} + getId={({ params }) => `${params?.directoryId}`} options={{ headerBackTitleVisible: false, headerLargeTitle: false, - headerSearchBarOptions: { - hideWhenScrolling: false, - }, }} /> diff --git a/src/features/courses/navigation/FileNavigator.tsx b/src/features/courses/navigation/FileNavigator.tsx new file mode 100644 index 00000000..e52c9b9d --- /dev/null +++ b/src/features/courses/navigation/FileNavigator.tsx @@ -0,0 +1,50 @@ +import { useTheme } from '@lib/ui/hooks/useTheme'; +import { createNativeStackNavigator } from '@react-navigation/native-stack'; + +import { usePreferencesContext } from '../../../core/contexts/PreferencesContext'; +import { useTitlesStyles } from '../../../core/hooks/useTitlesStyles'; +import { useCourseContext } from '../contexts/CourseContext'; +import { CourseDirectoryScreen } from '../screens/CourseDirectoryScreen'; +import { CourseFilesScreen } from '../screens/CourseFilesScreen'; + +export type FileStackParamList = { + RecentFiles: { + courseId: number; + }; + DirectoryFiles: { + courseId: number; + directoryId?: string; + directoryName?: string; + }; +}; + +const Stack = createNativeStackNavigator(); +export const FileNavigator = () => { + const theme = useTheme(); + const { filesScreen } = usePreferencesContext(); + const courseId = useCourseContext(); + + return ( + + + + + ); +}; diff --git a/src/features/courses/screens/CourseDirectoryScreen.tsx b/src/features/courses/screens/CourseDirectoryScreen.tsx index 62c086fc..ce037d2d 100644 --- a/src/features/courses/screens/CourseDirectoryScreen.tsx +++ b/src/features/courses/screens/CourseDirectoryScreen.tsx @@ -2,9 +2,15 @@ import { useCallback, useContext, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { FlatList, Platform, StyleSheet } from 'react-native'; +import { faFile, faFolderOpen } from '@fortawesome/free-regular-svg-icons'; +import { faSearch } from '@fortawesome/free-solid-svg-icons'; +import { CtaButton } from '@lib/ui/components/CtaButton'; +import { EmptyState } from '@lib/ui/components/EmptyState'; import { IndentedDivider } from '@lib/ui/components/IndentedDivider'; import { RefreshControl } from '@lib/ui/components/RefreshControl'; +import { Row } from '@lib/ui/components/Row'; import { Text } from '@lib/ui/components/Text'; +import { TranslucentTextField } from '@lib/ui/components/TranslucentTextField'; import { useStylesheet } from '@lib/ui/hooks/useStylesheet'; import { Theme } from '@lib/ui/types/Theme'; import { CourseDirectory, CourseFileOverview } from '@polito/api-client'; @@ -14,11 +20,13 @@ import { NativeStackScreenProps } from '@react-navigation/native-stack'; import { DateTime } from 'luxon'; import { BottomBarSpacer } from '../../../core/components/BottomBarSpacer'; +import { usePreferencesContext } from '../../../core/contexts/PreferencesContext'; import { useSafeAreaSpacing } from '../../../core/hooks/useSafeAreaSpacing'; import { useGetCourseDirectory, useGetCourseFilesRecent, } from '../../../core/queries/courseHooks'; +import { GlobalStyles } from '../../../core/styles/GlobalStyles'; import { CourseFileOverviewWithLocation } from '../../../core/types/files'; import { TeachingStackParamList } from '../../teaching/components/TeachingNavigator'; import { CourseDirectoryListItem } from '../components/CourseDirectoryListItem'; @@ -26,10 +34,14 @@ import { CourseFileListItem } from '../components/CourseFileListItem'; import { CourseRecentFileListItem } from '../components/CourseRecentFileListItem'; import { CourseContext } from '../contexts/CourseContext'; import { CourseFilesCacheContext } from '../contexts/CourseFilesCacheContext'; +import { FileStackParamList } from '../navigation/FileNavigator'; import { CourseFilesCacheProvider } from '../providers/CourseFilesCacheProvider'; import { isDirectory } from '../utils/fs-entry'; -type Props = NativeStackScreenProps; +type Props = NativeStackScreenProps< + TeachingStackParamList & FileStackParamList, + 'CourseDirectory' | 'DirectoryFiles' +>; const FileCacheChecker = () => { const { refresh } = useContext(CourseFilesCacheContext); @@ -51,14 +63,14 @@ export const CourseDirectoryScreen = ({ route, navigation }: Props) => { const [searchFilter, setSearchFilter] = useState(''); const directoryQuery = useGetCourseDirectory(courseId, directoryId); const { paddingHorizontal } = useSafeAreaSpacing(); + const { updatePreference } = usePreferencesContext(); useEffect(() => { - navigation.setOptions({ - headerTitle: directoryName ?? t('common.file_plural'), - headerSearchBarOptions: { - onChangeText: e => setSearchFilter(e.nativeEvent.text), - }, - }); + if (navigation.getId() !== 'FileTabNavigator') { + navigation.setOptions({ + headerTitle: directoryName ?? t('common.file_plural'), + }); + } }, [directoryName, navigation, t]); directoryQuery.data?.sort((a, b) => { @@ -79,6 +91,14 @@ export const CourseDirectoryScreen = ({ route, navigation }: Props) => { + + {navigation.getId() === 'FileTabNavigator' && ( + + )} + {searchFilter ? ( { ios: IndentedDivider, })} ListFooterComponent={} + ListEmptyComponent={ + !directoryQuery.isLoading ? ( + + ) : null + } /> )} + {navigation.getId() === 'FileTabNavigator' && ( + { + navigation!.navigate('RecentFiles', { courseId }); + updatePreference('filesScreen', 'filesView'); + }} + /> + )} ); }; -interface SearchProps { +interface SearchFlatListProps { courseId: number; searchFilter: string; } -const CourseFileSearchFlatList = ({ courseId, searchFilter }: SearchProps) => { +interface SearchBarProps { + searchFilter: string; + setSearchFilter: (search: string) => void; +} + +const CourseSearchBar = ({ searchFilter, setSearchFilter }: SearchBarProps) => { + const { paddingHorizontal } = useSafeAreaSpacing(); + const styles = useStylesheet(createStyles); + const { t } = useTranslation(); + + return ( + + setSearchFilter('')} + onClearLabel={t('contactsScreen.clearSearch')} + /> + + ); +}; + +const CourseFileSearchFlatList = ({ + courseId, + searchFilter, +}: SearchFlatListProps) => { const styles = useStylesheet(createStyles); const { t } = useTranslation(); const [searchResults, setSearchResults] = useState< @@ -174,9 +248,17 @@ const CourseFileSearchFlatList = ({ courseId, searchFilter }: SearchProps) => { ); }; -const createStyles = ({ spacing }: Theme) => +const createStyles = ({ spacing, shapes, colors }: Theme) => StyleSheet.create({ noResultText: { padding: spacing[4], }, + textField: { + borderRadius: shapes.lg, + }, + searchBar: { + paddingBottom: spacing[2], + paddingTop: spacing[2], + backgroundColor: colors.background, + }, }); diff --git a/src/features/courses/screens/CourseFilesScreen.tsx b/src/features/courses/screens/CourseFilesScreen.tsx index aa802151..fa5eb6b4 100644 --- a/src/features/courses/screens/CourseFilesScreen.tsx +++ b/src/features/courses/screens/CourseFilesScreen.tsx @@ -8,32 +8,30 @@ import { EmptyState } from '@lib/ui/components/EmptyState'; import { IndentedDivider } from '@lib/ui/components/IndentedDivider'; import { RefreshControl } from '@lib/ui/components/RefreshControl'; import { CourseDirectory, CourseFileOverview } from '@polito/api-client'; -import { MaterialTopTabScreenProps } from '@react-navigation/material-top-tabs'; import { useFocusEffect } from '@react-navigation/native'; +import { NativeStackScreenProps } from '@react-navigation/native-stack'; import { BottomBarSpacer } from '../../../core/components/BottomBarSpacer'; +import { usePreferencesContext } from '../../../core/contexts/PreferencesContext'; import { useNotifications } from '../../../core/hooks/useNotifications'; import { useOnLeaveScreen } from '../../../core/hooks/useOnLeaveScreen'; import { useSafeAreaSpacing } from '../../../core/hooks/useSafeAreaSpacing'; import { useGetCourseFilesRecent } from '../../../core/queries/courseHooks'; import { CourseRecentFileListItem } from '../components/CourseRecentFileListItem'; -import { useCourseContext } from '../contexts/CourseContext'; import { CourseFilesCacheContext } from '../contexts/CourseFilesCacheContext'; -import { CourseTabsParamList } from '../navigation/CourseNavigator'; +import { FileStackParamList } from '../navigation/FileNavigator'; -type Props = MaterialTopTabScreenProps< - CourseTabsParamList, - 'CourseFilesScreen' ->; +type Props = NativeStackScreenProps; -export const CourseFilesScreen = ({ navigation }: Props) => { +export const CourseFilesScreen = ({ navigation, route }: Props) => { const { t } = useTranslation(); const [scrollEnabled, setScrollEnabled] = useState(true); const { refresh } = useContext(CourseFilesCacheContext); - const courseId = useCourseContext(); + const courseId = route.params.courseId; const recentFilesQuery = useGetCourseFilesRecent(courseId); const { paddingHorizontal } = useSafeAreaSpacing(); const { clearNotificationScope } = useNotifications(); + const { updatePreference } = usePreferencesContext(); useOnLeaveScreen(() => { clearNotificationScope(['teaching', 'courses', courseId, 'files']); @@ -87,11 +85,14 @@ export const CourseFilesScreen = ({ navigation }: Props) => { ) : null } /> - {recentFilesQuery.data && recentFilesQuery.data.length > 0 && ( + {recentFilesQuery.data && ( navigation!.navigate('CourseDirectory', { courseId })} + action={() => { + navigation!.navigate('DirectoryFiles', { courseId }); + updatePreference('filesScreen', 'directoryView'); + }} /> )}