diff --git a/src/Drawer.tsx b/src/Drawer.tsx index b2ca064..b41a5c4 100644 --- a/src/Drawer.tsx +++ b/src/Drawer.tsx @@ -104,7 +104,6 @@ interface PropsType { export default function Drawer(props: PropsType) { const [showFingerprint, setShowFingerprint] = React.useState(false); const [showLogout, setShowLogout] = React.useState(false); - const cacheCollections = useSelector((state: StoreState) => state.cache.collections); const navigation = props.navigation as DrawerNavigationProp; const etebase = useCredentials(); const loggedIn = !!etebase; @@ -129,7 +128,7 @@ export default function Drawer(props: PropsType) { {loggedIn && ( <> { navigation.closeDrawer(); @@ -138,32 +137,6 @@ export default function Drawer(props: PropsType) { icon="note-multiple" /> - - {Array.from(cacheCollections - .sort((a, b) => (a.meta!.name!.toUpperCase() >= b.meta!.name!.toUpperCase()) ? 1 : -1) - .map(({ meta }, uid) => ( - { - navigation.closeDrawer(); - navigation.navigate("Collection", { colUid: uid }); - }} - icon="notebook" - /> - )) - .values() - )} - { - navigation.navigate("CollectionCreate"); - }} - icon="plus" - /> - )} <> diff --git a/src/components/NoteList.tsx b/src/components/NoteList.tsx new file mode 100644 index 0000000..0b6c03f --- /dev/null +++ b/src/components/NoteList.tsx @@ -0,0 +1,129 @@ +import * as React from "react"; +import moment from "moment"; +import { FlatList } from "react-native"; +import { List } from "react-native-paper"; +import { useSelector } from "react-redux"; + +import { useSyncGate } from "../SyncGate"; +import { CachedItem, StoreState } from "../store"; + +import NotFound from "../widgets/NotFound"; +import Link from "../widgets/Link"; +import { useTheme } from "../theme"; + +function sortMtime(aIn: CachedItem, bIn: CachedItem) { + const a = aIn.meta.mtime!; + const b = bIn.meta.mtime!; + return (a > b) ? -1 : (a < b) ? 1 : 0; +} + +function sortName(aIn: CachedItem, bIn: CachedItem) { + const a = aIn.meta.name!; + const b = bIn.meta.name!; + return a.localeCompare(b); +} + +function getSortFunction(sortOrder: string) { + const sortFunctions: (typeof sortName)[] = []; + + switch (sortOrder) { + case "mtime": + // Do nothing because it's the last sort function anyway + break; + case "name": + sortFunctions.push(sortName); + break; + } + + sortFunctions.push(sortMtime); + + return (a: CachedItem, b: CachedItem) => { + for (const sortFunction of sortFunctions) { + const ret = sortFunction(a, b); + if (ret !== 0) { + return ret; + } + } + + return 0; + }; +} + +interface PropsType { + colUid?: string; + sortBy: "name" | "mtime"; +} + +export default function NoteList(props: PropsType) { + const cacheCollections = useSelector((state: StoreState) => state.cache.collections); + const cacheItems = useSelector((state: StoreState) => state.cache.items); + const syncGate = useSyncGate(); + const theme = useTheme(); + + const { sortBy } = props; + const colUid = props.colUid || undefined; + const cacheCollection = (colUid) ? cacheCollections.get(colUid) : undefined; + + const entriesList = React.useMemo(() => { + const filterByUid = colUid; + + const ret: (CachedItem & { colUid: string, uid: string })[] = []; + for (const [colUid, itemLists] of cacheItems.entries()) { + if (filterByUid && (filterByUid !== colUid)) { + continue; + } + + for (const [uid, item] of itemLists.entries()) { + if (item.isDeleted) { + continue; + } + + ret.push({ ...item, uid, colUid }); + } + } + return ret.sort(getSortFunction(sortBy)); + }, [cacheItems, sortBy, colUid]); + + if (syncGate) { + return syncGate; + } + + if (colUid && !cacheCollection) { + return ; + } + + function renderEntry(param: { item: CachedItem & { colUid: string, uid: string } }) { + const item = param.item; + const name = item.meta.name!; + const mtime = (item.meta.mtime) ? moment(item.meta.mtime) : undefined; + + return ( + ( + + )} + /> + ); + } + + return ( + item.uid} + renderItem={renderEntry} + maxToRenderPerBatch={10} + ListEmptyComponent={() => ( + + )} + /> + ); +} diff --git a/src/screens/HomeScreen.tsx b/src/screens/HomeScreen.tsx index 8b63eb2..482e73a 100644 --- a/src/screens/HomeScreen.tsx +++ b/src/screens/HomeScreen.tsx @@ -8,6 +8,7 @@ import { RootStackParamList } from "../RootStackParamList"; import NoteListScreen from "./NoteListScreen"; import SearchScreen from "./SearchScreen"; +import NotebookListScreen from "./NotebookListScreen"; interface PropsType { route: RouteProp | RouteProp; @@ -16,6 +17,7 @@ interface PropsType { const routes = [ { key: "notes", title: "Notes", icon: "note-multiple" }, { key: "search", title: "Search", icon: "magnify" }, + { key: "notebooks", title: "Notebooks", icon: "notebook-multiple" }, ]; export default function HomeScreen(props: PropsType) { @@ -24,9 +26,11 @@ export default function HomeScreen(props: PropsType) { const renderScene = ({ route }: { route: { key: string } }) => { switch (route.key) { case "notes": - return ; + return ; case "search": return ; + case "notebooks": + return ; default: return null; } @@ -34,9 +38,14 @@ export default function HomeScreen(props: PropsType) { const colUid = props.route.params?.colUid || undefined; + React.useEffect(() => { + if (colUid) { + setIndex(2); + } + }, []); + return ( b) ? -1 : (a < b) ? 1 : 0; -} - -function sortName(aIn: CachedItem, bIn: CachedItem) { - const a = aIn.meta.name!; - const b = bIn.meta.name!; - return a.localeCompare(b); -} - -function getSortFunction(sortOrder: string) { - const sortFunctions: (typeof sortName)[] = []; - - switch (sortOrder) { - case "mtime": - // Do nothing because it's the last sort function anyway - break; - case "name": - sortFunctions.push(sortName); - break; - } - - sortFunctions.push(sortMtime); - - return (a: CachedItem, b: CachedItem) => { - for (const sortFunction of sortFunctions) { - const ret = sortFunction(a, b); - if (ret !== 0) { - return ret; - } - } - - return 0; - }; -} - interface PropsType { active: boolean; - colUid?: string; } export default function NoteListScreen(props: PropsType) { const viewSettings = useSelector((state: StoreState) => state.settings.viewSettings); const { sortBy } = viewSettings; - const cacheCollections = useSelector((state: StoreState) => state.cache.collections); - const cacheItems = useSelector((state: StoreState) => state.cache.items); const navigation = useNavigation(); const syncGate = useSyncGate(); const theme = useTheme(); - const { active, colUid } = props; - const cacheCollection = (colUid) ? cacheCollections.get(colUid) : undefined; + const { active } = props; React.useEffect(() => { if (!active) { @@ -86,74 +41,21 @@ export default function NoteListScreen(props: PropsType) { navigation.setOptions({ header: (props) => , - title: cacheCollection?.meta.name ?? "All Notes", + title: "Notes", headerRight: () => ( - + ), }); - }, [active, navigation, cacheCollections, colUid]); - - const entriesList = React.useMemo(() => { - const filterByUid = colUid; - - const ret: (CachedItem & { colUid: string, uid: string })[] = []; - for (const [colUid, itemLists] of cacheItems.entries()) { - if (filterByUid && (filterByUid !== colUid)) { - continue; - } - - for (const [uid, item] of itemLists.entries()) { - if (item.isDeleted) { - continue; - } - - ret.push({ ...item, uid, colUid }); - } - } - return ret.sort(getSortFunction(sortBy)); - }, [cacheItems, sortBy, colUid]); + }, [active, navigation]); if (syncGate) { return syncGate; } - if (colUid && !cacheCollection) { - return ; - } - - function renderEntry(param: { item: CachedItem & { colUid: string, uid: string } }) { - const item = param.item; - const name = item.meta.name!; - const mtime = (item.meta.mtime) ? moment(item.meta.mtime) : undefined; - - return ( - ( - - )} - /> - ); - } - return ( <> - item.uid} - renderItem={renderEntry} - maxToRenderPerBatch={10} - ListEmptyComponent={() => ( - - )} + navigation.navigate("NoteCreate", colUid ? { colUid } : undefined)} + onPress={() => navigation.navigate("NoteCreate")} /> ); diff --git a/src/screens/NotebookListScreen.tsx b/src/screens/NotebookListScreen.tsx new file mode 100644 index 0000000..6dfcc75 --- /dev/null +++ b/src/screens/NotebookListScreen.tsx @@ -0,0 +1,213 @@ +import * as React from "react"; +import { StyleSheet, FlatList, Platform, View, BackHandler } from "react-native"; +import { Appbar as PaperAppbar, List, FAB, Avatar } from "react-native-paper"; +import { useNavigation, useFocusEffect } from "@react-navigation/native"; +import { useDispatch, useSelector } from "react-redux"; +import * as Etebase from "etebase"; + +import { useSyncGate } from "../SyncGate"; +import { StoreState } from "../store"; +import { SyncManager } from "../sync/SyncManager"; +import { performSync } from "../store/actions"; +import { useCredentials } from "../credentials"; + +import NoteList from "../components/NoteList"; +import Appbar from "../widgets/Appbar"; +import AppbarAction from "../widgets/AppbarAction"; +import Menu from "../widgets/Menu"; +import MenuItem from "../widgets/MenuItem"; +import NotFound from "../widgets/NotFound"; +import { defaultColor } from "../helpers"; +import { DefaultNavigationProp } from "../RootStackParamList"; +import { useTheme } from "../theme"; + +interface PropsType { + colUid?: string; + active: boolean; +} + +type Notebook = { + meta: Etebase.ItemMetadata; + uid: string; +}; + +export default function NotebookListScreen(props: PropsType) { + const cacheCollections = useSelector((state: StoreState) => state.cache.collections); + const notebooks: Notebook[] = React.useMemo(() => Array.from(cacheCollections + .sort((a, b) => (a.meta!.name!.toUpperCase() >= b.meta!.name!.toUpperCase()) ? 1 : -1) + .map(({ meta }, uid) => {return { meta, uid }}) + .values() + ), [cacheCollections]); + const navigation = useNavigation(); + const syncGate = useSyncGate(); + const theme = useTheme(); + + const { colUid, active } = props; + const cacheCollection = (colUid) ? notebooks.find((col) => col.uid === colUid) : undefined; + const [notebook, setNotebook] = React.useState(cacheCollection); + + React.useEffect(() => { + if (!active) { + return; + } + + navigation.setOptions({ + header: (props) => , + title: notebook?.meta.name || "Notebooks", + headerLeft: (notebook) ? () => setNotebook(undefined)} /> : undefined, + headerRight: () => ( + + ), + }); + }, [active, navigation, notebook]); + + const onBackPress = React.useCallback(() => { + if (active && notebook) { + setNotebook(undefined); + return true; + } else { + return false; + } + }, [active, notebook]); + + React.useEffect(() => { + const backHandler = BackHandler.addEventListener("hardwareBackPress", onBackPress); + + return () => backHandler.remove(); + }, [onBackPress]); + + if (syncGate) { + return syncGate; + } + + if (colUid && !cacheCollection) { + return ; + } + + function renderItem({ item }: { item: Notebook }) { + return ( + ( + + + + )} + onPress={() => { + setNotebook(item); + }} + /> + ); + } + + return ( + <> + {notebook ? ( + + ) : ( + item.uid} + renderItem={renderItem} + maxToRenderPerBatch={10} + ListEmptyComponent={() => ( + + )} + /> + )} + + navigation.navigate("CollectionCreate")} + /> + + ); +} + +const styles = StyleSheet.create({ + fab: { + position: "absolute", + margin: 16, + right: 0, + bottom: 0, + }, +}); + +interface RightActionPropsType { + colUid?: string; +} + +function RightAction(props: RightActionPropsType) { + const etebase = useCredentials()!; + const [showMenu, setShowMenu] = React.useState(false); + const syncDispatch = useDispatch(); + const isSyncing = useSelector((state: StoreState) => state.syncCount) > 0; + const navigation = useNavigation(); + const { colUid } = props; + + async function refresh() { + const syncManager = SyncManager.getManager(etebase!); + syncDispatch(performSync(syncManager.sync())); // not awaiting on puprose + } + + useFocusEffect(React.useCallback(() => { + if (!etebase) { + return () => true; + } + + refresh(); + + if (Platform.OS !== "web") { + return () => true; + } + + function autoRefresh() { + if (navigator.onLine && etebase) { + refresh(); + } + } + + const interval = 5 * 60 * 1000; + const id = setInterval(autoRefresh, interval); + return () => clearInterval(id); + }, [etebase])); + + return ( + + { + setShowMenu(false); + refresh(); + }} + /> + {colUid && ( + setShowMenu(false)} + anchor={( + setShowMenu(true)} /> + )} + > + { + setShowMenu(false); + navigation.navigate("CollectionChangelog", { colUid }); + }} + /> + + )} + + ); +} \ No newline at end of file