diff --git a/apps/mobile/app/components/list/reorderable-list.tsx b/apps/mobile/app/components/list/reorderable-list.tsx new file mode 100644 index 000000000..3ecc57d0d --- /dev/null +++ b/apps/mobile/app/components/list/reorderable-list.tsx @@ -0,0 +1,186 @@ +/* +This file is part of the Notesnook project (https://notesnook.com/) + +Copyright (C) 2023 Streetwriters (Private) Limited + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +import { useThemeColors } from "@notesnook/theme"; +import React, { useEffect, useRef, useState } from "react"; +import { FlatList, StyleSheet, View } from "react-native"; +import { + DraxList, + DraxListProps, + DraxListRenderItemContent, + DraxProvider +} from "react-native-drax"; +import { tabBarRef } from "../../utils/global-refs"; +import { SIZE } from "../../utils/size"; +import { IconButton } from "../ui/icon-button"; +import Paragraph from "../ui/typography/paragraph"; +import { useSideBarDraggingStore } from "../side-menu/dragging-store"; + +interface ReorderableListProps + extends Omit, "renderItem" | "data" | "renderItemContent"> { + onListOrderChanged: (data: string[]) => void; + renderDraggableItem: DraxListRenderItemContent; + data: T[]; + itemOrder: string[]; + hiddenItems: string[]; + onHiddenItemsChanged: (data: string[]) => void; +} + +function ReorderableList({ + renderDraggableItem, + data, + onListOrderChanged, + hiddenItems = [], + itemOrder = [], + onHiddenItemsChanged, + ...restProps +}: ReorderableListProps) { + const { colors } = useThemeColors(); + const [itemOrderState, setItemsOrder] = useState(itemOrder); + const [hiddenItemsState, setHiddenItems] = useState(hiddenItems); + const dragging = useSideBarDraggingStore((state) => state.dragging); + const listRef = useRef(null); + + if (dragging) { + tabBarRef.current?.lock(); + } else { + tabBarRef.current?.unlock(); + } + + useEffect(() => { + setItemsOrder(itemOrder); + setHiddenItems(hiddenItems); + }, [itemOrder, hiddenItems]); + + const renderItemContent: DraxListRenderItemContent = React.useCallback( + (info, props) => { + const isHidden = hiddenItemsState.indexOf(info?.item?.id) > -1; + return isHidden && !dragging ? null : ( + + + {renderDraggableItem(info, props)} + + {dragging ? ( + { + const _hiddenItems = hiddenItemsState.slice(); + const index = _hiddenItems.indexOf(info.item.id); + if (index === -1) { + _hiddenItems.push(info.item?.id); + } else { + _hiddenItems.splice(index, 1); + } + onHiddenItemsChanged(_hiddenItems); + setHiddenItems(_hiddenItems); + }} + /> + ) : null} + + ); + }, + [ + colors.primary.icon, + dragging, + hiddenItemsState, + onHiddenItemsChanged, + renderDraggableItem + ] + ); + + function getOrderedItems() { + const items: T[] = []; + data.forEach((item) => { + const index = itemOrderState.indexOf(item?.id); + if (index === -1) { + items.push(item); + } else { + items.splice(index, 0, item); + } + }); + console.log(items.map((item) => item.id)); + return items; + } + + return ( + + + + useSideBarDraggingStore.setState({ + dragging: true + }) + } + lockItemDragsToMainAxis + onItemReorder={({ fromIndex, fromItem, toIndex, toItem }) => { + const newOrder = getOrderedItems().map((item) => item.id); + const element = newOrder.splice(fromIndex, 1)[0]; + if (toIndex === 0) { + newOrder.unshift(element); + } else { + newOrder.splice(toIndex, 0, element); + } + console.log(newOrder); + setItemsOrder(newOrder); + onListOrderChanged?.(newOrder); + }} + keyExtractor={(item) => (item as any).id} + /> + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1 + } +}); + +export default ReorderableList; diff --git a/apps/mobile/app/components/side-menu/color-section.tsx b/apps/mobile/app/components/side-menu/color-section.tsx index 1c96cda5d..7a9a3dd39 100644 --- a/apps/mobile/app/components/side-menu/color-section.tsx +++ b/apps/mobile/app/components/side-menu/color-section.tsx @@ -32,6 +32,7 @@ import { PressableButton } from "../ui/pressable"; import Heading from "../ui/typography/heading"; import Paragraph from "../ui/typography/paragraph"; import { Color } from "@notesnook/core"; +import ReorderableList from "../list/reorderable-list"; export const ColorSection = React.memo( function ColorSection() { @@ -45,9 +46,27 @@ export const ColorSection = React.memo( } }, [loading, setColorNotes]); - return colorNotes.map((item) => { - return ; - }); + return ( + { + db.settings.setSideBarOrder("colors", data); + }} + onHiddenItemsChanged={(data) => { + db.settings.setSideBarHiddenItems("colors", data); + }} + itemOrder={db.settings.getSideBarOrder("colors")} + hiddenItems={db.settings.getSideBarHiddenItems("colors")} + alwaysBounceVertical={false} + data={colorNotes} + style={{ + width: "100%" + }} + showsVerticalScrollIndicator={false} + renderDraggableItem={({ item }) => { + return ; + }} + /> + ); }, () => true ); @@ -66,7 +85,7 @@ const ColorItem = React.memo( const onHeaderStateChange = useCallback( (state: any) => { setTimeout(() => { - let id = state.focusedRouteId; + const id = state.focusedRouteId; if (id === item.id) { setHeaderTextState({ id: state.currentScreen.id }); } else { diff --git a/apps/mobile/app/components/side-menu/dragging-store.ts b/apps/mobile/app/components/side-menu/dragging-store.ts new file mode 100644 index 000000000..121b14e93 --- /dev/null +++ b/apps/mobile/app/components/side-menu/dragging-store.ts @@ -0,0 +1,23 @@ +/* +This file is part of the Notesnook project (https://notesnook.com/) + +Copyright (C) 2023 Streetwriters (Private) Limited + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ +import create from "zustand"; + +export const useSideBarDraggingStore = create((set, get) => ({ + dragging: false +})); diff --git a/apps/mobile/app/components/side-menu/index.js b/apps/mobile/app/components/side-menu/index.js index bdf212e93..780036e7f 100644 --- a/apps/mobile/app/components/side-menu/index.js +++ b/apps/mobile/app/components/side-menu/index.js @@ -33,9 +33,15 @@ import { SUBSCRIPTION_STATUS } from "../../utils/constants"; import { eOpenPremiumDialog } from "../../utils/events"; import { ColorSection } from "./color-section"; import { MenuItem } from "./menu-item"; -import { TagsSection } from "./pinned-section"; +import { PinnedSection } from "./pinned-section"; import { UserStatus } from "./user-status"; import { useThemeStore } from "../../stores/use-theme-store"; +import ReorderableList from "../list/reorderable-list"; +import { db } from "../../common/database"; +import { useSideBarDraggingStore } from "./dragging-store"; +import Paragraph from "../ui/typography/paragraph"; +import { IconButton } from "../ui/icon-button"; +import { SIZE } from "../../utils/size"; export const SideMenu = React.memo( function SideMenu() { @@ -45,6 +51,8 @@ export const SideMenu = React.memo( (state) => state.user?.subscription?.type ); const loading = useNoteStore((state) => state.loading); + const dragging = useSideBarDraggingStore((state) => state.dragging); + const introCompleted = useSettingStore( (state) => state.settings.introCompleted ); @@ -77,20 +85,38 @@ export const SideMenu = React.memo( eSendEvent(eOpenPremiumDialog); } }; - const renderItem = useCallback( () => ( <> - {MenuItemsList.map((item, index) => ( - - ))} + { + console.log(data); + db.settings.setSideBarOrder("menu", data); + }} + onHiddenItemsChanged={(data) => { + db.settings.setSideBarHiddenItems("menu", data); + }} + itemOrder={db.settings.getSideBarOrder("menu")} + hiddenItems={db.settings.getSideBarHiddenItems("menu")} + alwaysBounceVertical={false} + data={MenuItemsList} + style={{ + width: "100%" + }} + showsVerticalScrollIndicator={false} + renderDraggableItem={({ item, index }) => { + return ( + + ); + }} + /> - + ), [noTextMode] @@ -109,22 +135,45 @@ export const SideMenu = React.memo( height: "100%", width: "100%", backgroundColor: colors.primary.background, - paddingTop: insets.top, - borderRadius: 10, - borderTopLeftRadius: 0, - borderBottomLeftRadius: 0 + paddingTop: insets.top }} > + {dragging ? ( + + REORDER SIDEBAR + + { + useSideBarDraggingStore.setState({ + dragging: false + }); + }} + customStyle={{ + width: 35, + height: 35 + }} + /> + + ) : null} + "mainMenuView"} diff --git a/apps/mobile/app/components/side-menu/pinned-section.tsx b/apps/mobile/app/components/side-menu/pinned-section.tsx index 6b2df225f..f41c05e03 100644 --- a/apps/mobile/app/components/side-menu/pinned-section.tsx +++ b/apps/mobile/app/components/side-menu/pinned-section.tsx @@ -20,7 +20,7 @@ along with this program. If not, see . import { Notebook, Tag } from "@notesnook/core"; import { useThemeColors } from "@notesnook/theme"; import React, { useCallback, useEffect, useRef, useState } from "react"; -import { FlatList, View } from "react-native"; +import { View } from "react-native"; import Icon from "react-native-vector-icons/MaterialCommunityIcons"; import { db } from "../../common/database"; import NotebookScreen from "../../screens/notebook"; @@ -30,6 +30,7 @@ import { useMenuStore } from "../../stores/use-menu-store"; import useNavigationStore from "../../stores/use-navigation-store"; import { useNoteStore } from "../../stores/use-notes-store"; import { SIZE, normalize } from "../../utils/size"; +import ReorderableList from "../list/reorderable-list"; import { Properties } from "../properties"; import { Button } from "../ui/button"; import { Notice } from "../ui/notice"; @@ -39,8 +40,8 @@ import SheetWrapper from "../ui/sheet"; import Heading from "../ui/typography/heading"; import Paragraph from "../ui/typography/paragraph"; -export const TagsSection = React.memo( - function TagsSection() { +export const PinnedSection = React.memo( + function PinnedSection() { const menuPins = useMenuStore((state) => state.menuPins); const loading = useNoteStore((state) => state.loading); const setMenuPins = useMenuStore((state) => state.setMenuPins); @@ -71,11 +72,27 @@ export const TagsSection = React.memo( flexGrow: 1 }} > - { + db.settings.setSideBarOrder("pinned", data); + }} + onHiddenItemsChanged={(data) => { + db.settings.setSideBarHiddenItems("pinned", data); + }} + itemOrder={db.settings.getSideBarOrder("pinned")} + hiddenItems={db.settings.getSideBarHiddenItems("pinned")} + alwaysBounceVertical={false} data={menuPins} style={{ + flexGrow: 1, + width: "100%", + paddingHorizontal: 12 + }} + contentContainerStyle={{ flexGrow: 1 }} + showsVerticalScrollIndicator={false} + renderDraggableItem={renderItem} ListEmptyComponent={ } - contentContainerStyle={{ - flexGrow: 1 - }} - keyExtractor={(item) => item.id} - renderItem={renderItem} /> ); diff --git a/apps/mobile/app/utils/menu-items.ts b/apps/mobile/app/utils/menu-items.ts index 1ace6d653..efd9c7f3a 100644 --- a/apps/mobile/app/utils/menu-items.ts +++ b/apps/mobile/app/utils/menu-items.ts @@ -20,32 +20,38 @@ along with this program. If not, see . import { Monographs } from "../screens/notes/monographs"; export const MenuItemsList = [ { + id: "notes", name: "Notes", icon: "home-variant-outline", close: true }, { + id: "notebooks", name: "Notebooks", icon: "book-outline", close: true }, { + id: "favorites", name: "Favorites", icon: "star-outline", close: true }, { + id: "tags", name: "Tags", icon: "pound", close: true }, { + id: "reminders", name: "Reminders", icon: "bell", close: true, isBeta: true }, { + id: "monographs", name: "Monographs", icon: "text-box-multiple-outline", close: true, @@ -54,6 +60,7 @@ export const MenuItemsList = [ } }, { + id: "trash", name: "Trash", icon: "delete-outline", close: true diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 1b2543a41..d5d0e21cb 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -33,6 +33,8 @@ export type SortOptions = { sortDirection: "desc" | "asc"; }; +export type SideBarSectionKey = "menu" | "colors" | "pinned"; + export type GroupOptions = SortOptions & { groupBy: "none" | "abc" | "year" | "month" | "week" | "default"; }; @@ -422,7 +424,9 @@ export type SettingItemMap = { dateFormat: string; defaultNotebook: string | undefined; } & Record<`groupOptions:${GroupingKey}`, GroupOptions> & - Record<`toolbarConfig:${ToolbarConfigPlatforms}`, ToolbarConfig | undefined>; + Record<`toolbarConfig:${ToolbarConfigPlatforms}`, ToolbarConfig | undefined> & + Record<`sideBarOrder:${SideBarSectionKey}`, string[]> & + Record<`sideBarHiddenItems:${SideBarSectionKey}`, string[]>; export interface SettingItem< TKey extends keyof SettingItemMap = keyof SettingItemMap