mobile: reorder sidebar items

This commit is contained in:
Ammar Ahmed
2023-12-04 12:25:01 +05:00
parent 7bf2344f83
commit 501bd6917d
7 changed files with 334 additions and 34 deletions

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<T extends { id: string }>
extends Omit<DraxListProps<T>, "renderItem" | "data" | "renderItemContent"> {
onListOrderChanged: (data: string[]) => void;
renderDraggableItem: DraxListRenderItemContent<T>;
data: T[];
itemOrder: string[];
hiddenItems: string[];
onHiddenItemsChanged: (data: string[]) => void;
}
function ReorderableList<T extends { id: string }>({
renderDraggableItem,
data,
onListOrderChanged,
hiddenItems = [],
itemOrder = [],
onHiddenItemsChanged,
...restProps
}: ReorderableListProps<T>) {
const { colors } = useThemeColors();
const [itemOrderState, setItemsOrder] = useState(itemOrder);
const [hiddenItemsState, setHiddenItems] = useState(hiddenItems);
const dragging = useSideBarDraggingStore((state) => state.dragging);
const listRef = useRef<FlatList | null>(null);
if (dragging) {
tabBarRef.current?.lock();
} else {
tabBarRef.current?.unlock();
}
useEffect(() => {
setItemsOrder(itemOrder);
setHiddenItems(hiddenItems);
}, [itemOrder, hiddenItems]);
const renderItemContent: DraxListRenderItemContent<T> = React.useCallback(
(info, props) => {
const isHidden = hiddenItemsState.indexOf(info?.item?.id) > -1;
return isHidden && !dragging ? null : (
<View
style={{
flexDirection: "row",
opacity: isHidden ? 0.4 : 1,
paddingHorizontal: 12
}}
>
<View
style={{
flexGrow: 1
}}
>
{renderDraggableItem(info, props)}
</View>
{dragging ? (
<IconButton
name={!isHidden ? "minus" : "plus"}
color={colors.primary.icon}
size={SIZE.lg}
top={0}
bottom={0}
onPress={() => {
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}
</View>
);
},
[
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 (
<DraxProvider>
<View style={styles.container}>
<DraxList
{...restProps}
ref={listRef}
data={getOrderedItems()}
renderItemContent={renderItemContent}
itemStyles={{
hoverDragReleasedStyle: {
display: "none"
},
dragReleasedStyle: {
opacity: 1
},
hoverDraggingStyle: {
backgroundColor: colors.secondary.background
}
}}
longPressDelay={500}
onItemDragStart={() =>
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}
/>
</View>
</DraxProvider>
);
}
const styles = StyleSheet.create({
container: {
flex: 1
}
});
export default ReorderableList;

View File

@@ -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 <ColorItem key={item.id} item={item} />;
});
return (
<ReorderableList
onListOrderChanged={(data) => {
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 <ColorItem key={item.id} item={item} />;
}}
/>
);
},
() => 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 {

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
import create from "zustand";
export const useSideBarDraggingStore = create((set, get) => ({
dragging: false
}));

View File

@@ -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) => (
<MenuItem
key={item.name}
item={item}
testID={item.name}
index={index}
/>
))}
<ReorderableList
onListOrderChanged={(data) => {
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 (
<MenuItem
key={item.name}
item={item}
testID={item.name}
index={index}
/>
);
}}
/>
<ColorSection noTextMode={noTextMode} />
<TagsSection />
<PinnedSection />
</>
),
[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 ? (
<View
style={{
flexDirection: "row",
borderRadius: 5,
marginBottom: 12,
justifyContent: "space-between",
alignItems: "center",
borderBottomWidth: 1,
borderBottomColor: colors.primary.border,
paddingHorizontal: 12
}}
>
<Paragraph size={SIZE.xs + 1}>REORDER SIDEBAR</Paragraph>
<IconButton
name="close"
size={20}
onPress={() => {
useSideBarDraggingStore.setState({
dragging: false
});
}}
customStyle={{
width: 35,
height: 35
}}
/>
</View>
) : null}
<FlatList
alwaysBounceVertical={false}
contentContainerStyle={{
flexGrow: 1
}}
style={{
height: "100%",
width: "100%",
paddingHorizontal: 12
}}
showsVerticalScrollIndicator={false}
data={[0]}
keyExtractor={() => "mainMenuView"}

View File

@@ -20,7 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
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
}}
>
<FlatList
<ReorderableList
onListOrderChanged={(data) => {
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={
<Notice
size="small"
@@ -83,11 +100,6 @@ export const TagsSection = React.memo(
text="Add shortcuts for notebooks, topics and tags here."
/>
}
contentContainerStyle={{
flexGrow: 1
}}
keyExtractor={(item) => item.id}
renderItem={renderItem}
/>
</View>
);

View File

@@ -20,32 +20,38 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
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

View File

@@ -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