mirror of
https://github.com/streetwriters/notesnook.git
synced 2025-12-29 00:20:04 +01:00
feat: configure toolbar
This commit is contained in:
@@ -59,7 +59,7 @@ const DialogHeader = ({
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
<Heading style={{ textAlign: centered ? 'center' : 'left' }} size={SIZE.xl}>
|
||||
<Heading style={{ textAlign: centered ? 'center' : 'left' }} size={SIZE.lg}>
|
||||
{title} {titlePart ? <Text style={{ color: colors.accent }}>{titlePart}</Text> : null}
|
||||
</Heading>
|
||||
|
||||
|
||||
@@ -3,10 +3,9 @@ import { Linking, Platform, View } from 'react-native';
|
||||
import WebView from 'react-native-webview';
|
||||
import { notesnook } from '../../../e2e/test.ids';
|
||||
import { useUserStore } from '../../stores/use-user-store';
|
||||
import EditorHeader from './header';
|
||||
import { useEditor } from './tiptap/use-editor';
|
||||
import { editorController } from './tiptap/utils';
|
||||
import { useEditorEvents } from './tiptap/use-editor-events';
|
||||
import { editorController } from './tiptap/utils';
|
||||
|
||||
const sourceUri = '';
|
||||
|
||||
@@ -19,7 +18,7 @@ const style = {
|
||||
alignSelf: 'center',
|
||||
backgroundColor: 'transparent'
|
||||
};
|
||||
|
||||
const off = true;
|
||||
const Editor = React.memo(
|
||||
() => {
|
||||
const premiumUser = useUserStore(state => state.premium);
|
||||
@@ -38,7 +37,7 @@ const Editor = React.memo(
|
||||
return false;
|
||||
};
|
||||
|
||||
return editor.loading ? null : (
|
||||
return editor.loading || off ? null : (
|
||||
<>
|
||||
<View
|
||||
style={{
|
||||
@@ -76,7 +75,7 @@ const Editor = React.memo(
|
||||
allowUniversalAccessFromFileURLs={true}
|
||||
originWhitelist={['*']}
|
||||
source={{
|
||||
uri: 'http://192.168.10.3:3000'
|
||||
uri: 'http://192.168.10.7:3000'
|
||||
}}
|
||||
style={style}
|
||||
autoManageStatusBarEnabled={false}
|
||||
|
||||
13
apps/mobile/src/screens/settings/components.tsx
Normal file
13
apps/mobile/src/screens/settings/components.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import React, { ReactElement } from 'react';
|
||||
import { AccentColorPicker, HomagePageSelector } from './appearance';
|
||||
import { AutomaticBackupsSelector } from './backup-restore';
|
||||
import { ConfigureToolbar } from './editor/configure-toolbar';
|
||||
import { Subscription } from './subscription';
|
||||
|
||||
export const components: { [name: string]: ReactElement } = {
|
||||
colorpicker: <AccentColorPicker wrap={true} />,
|
||||
homeselector: <HomagePageSelector />,
|
||||
autobackups: <AutomaticBackupsSelector />,
|
||||
subscription: <Subscription />,
|
||||
configuretoolbar: <ConfigureToolbar />
|
||||
};
|
||||
21
apps/mobile/src/screens/settings/editor/common.tsx
Normal file
21
apps/mobile/src/screens/settings/editor/common.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { Group } from './group';
|
||||
import { DraggableItem } from './state';
|
||||
import { Tool } from './tool';
|
||||
|
||||
export const renderTool = ({ item, groupIndex, parentIndex }: DraggableItem) => {
|
||||
const data = item as string[];
|
||||
return data.map((item, index) => (
|
||||
<Tool
|
||||
key={Array.isArray(item) ? `subgroup-${index}` : item}
|
||||
item={item}
|
||||
index={index}
|
||||
groupIndex={groupIndex}
|
||||
parentIndex={parentIndex}
|
||||
/>
|
||||
));
|
||||
};
|
||||
|
||||
export const renderGroup = ({ item, index, parentIndex }: DraggableItem) => (
|
||||
<Group item={item} index={index} parentIndex={parentIndex} />
|
||||
);
|
||||
229
apps/mobile/src/screens/settings/editor/configuretoolbar.tsx
Normal file
229
apps/mobile/src/screens/settings/editor/configuretoolbar.tsx
Normal file
@@ -0,0 +1,229 @@
|
||||
import * as React from 'react';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
import { DraxProvider, DraxScrollView } from 'react-native-drax';
|
||||
import { Button } from '../../../components/ui/button';
|
||||
import { Notice } from '../../../components/ui/notice';
|
||||
import Paragraph from '../../../components/ui/typography/paragraph';
|
||||
import { useThemeStore } from '../../../stores/use-theme-store';
|
||||
import { SIZE } from '../../../utils/size';
|
||||
import { Group } from './group';
|
||||
import { useDragState } from './state';
|
||||
|
||||
export const ConfigureToolbar = () => {
|
||||
const data = useDragState(state => state.data);
|
||||
const preset = useDragState(state => state.preset);
|
||||
const colors = useThemeStore(state => state.colors);
|
||||
|
||||
const renderGroups = () => {
|
||||
return data.map((item, index) => <Group key={`group-${index}`} item={item} index={index} />);
|
||||
};
|
||||
|
||||
return (
|
||||
<DraxProvider>
|
||||
<View style={styles.container}>
|
||||
<View
|
||||
style={{
|
||||
paddingVertical: 12
|
||||
}}
|
||||
>
|
||||
<Notice
|
||||
text="Curate the toolbar that fits your needs and matches your personality."
|
||||
type="information"
|
||||
/>
|
||||
|
||||
<Paragraph
|
||||
style={{
|
||||
marginTop: 10
|
||||
}}
|
||||
size={SIZE.xs}
|
||||
color={colors.icon}
|
||||
>
|
||||
PRESETS
|
||||
</Paragraph>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
width: '100%',
|
||||
marginTop: 10
|
||||
}}
|
||||
>
|
||||
{[
|
||||
{
|
||||
id: 'default',
|
||||
name: 'Default'
|
||||
},
|
||||
{
|
||||
id: 'minimal',
|
||||
name: 'Minimal'
|
||||
},
|
||||
{
|
||||
id: 'custom',
|
||||
name: 'Custom'
|
||||
}
|
||||
].map(item => (
|
||||
<Button
|
||||
type={preset === item.id ? 'accent' : 'grayAccent'}
|
||||
style={{
|
||||
borderRadius: 100,
|
||||
height: 35,
|
||||
marginRight: 10
|
||||
}}
|
||||
onPress={() => {
|
||||
//@ts-ignore
|
||||
useDragState.getState().setPreset(item.id);
|
||||
}}
|
||||
fontSize={SIZE.sm - 1}
|
||||
key={item.name}
|
||||
title={item.name}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
<DraxScrollView>
|
||||
{renderGroups()}
|
||||
<View
|
||||
style={{
|
||||
height: 500
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
title="Create a group"
|
||||
type="grayAccent"
|
||||
icon="plus"
|
||||
style={{
|
||||
width: '100%'
|
||||
}}
|
||||
onPress={() => {
|
||||
const _data = data.slice();
|
||||
_data.push([]);
|
||||
useDragState.getState().setData(_data);
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</DraxScrollView>
|
||||
</View>
|
||||
</DraxProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
paddingHorizontal: 12,
|
||||
width: '100%'
|
||||
}
|
||||
});
|
||||
|
||||
// import React, { useEffect, useState } from 'react';
|
||||
// import { StyleSheet, Text, TouchableOpacity } from 'react-native';
|
||||
// import DraggableFlatList, {
|
||||
// RenderItemParams,
|
||||
// ScaleDecorator
|
||||
// } from 'react-native-draggable-flatlist';
|
||||
// import { useThemeStore } from '../../stores/use-theme-store';
|
||||
// import {
|
||||
// FlattenedToolbarItemType,
|
||||
// getFlattenedToolbarDefinition,
|
||||
// getToolbarDefinition,
|
||||
// moveGroup,
|
||||
// moveSubGroup,
|
||||
// moveTool,
|
||||
// toolbarDefinition
|
||||
// } from './toolbar-definition';
|
||||
// /**
|
||||
// *
|
||||
// * Flatten toolbar definition array with headers for each group.
|
||||
// * Add them to list
|
||||
// * Each list item gets it's data from tools
|
||||
// * reorder items.
|
||||
// * Save reordered data to database/storage.
|
||||
// */
|
||||
|
||||
// const flattened = getFlattenedToolbarDefinition(toolbarDefinition);
|
||||
// export function ConfigureToolbar() {
|
||||
// const [data, setData] = useState(flattened);
|
||||
// const colors = useThemeStore(state => state.colors);
|
||||
|
||||
// useEffect(() => {
|
||||
// console.log(getToolbarDefinition(data));
|
||||
// }, [data]);
|
||||
|
||||
// const onDragEnd = React.useCallback(
|
||||
// ({ data: _data, from, to }) => {
|
||||
// console.log(from, to);
|
||||
// let prevDraggedItem = data[from];
|
||||
// let nextDraggedItem = _data[to];
|
||||
|
||||
// switch (prevDraggedItem.type) {
|
||||
// case 'group':
|
||||
// _data = moveGroup(data, _data, to, from, prevDraggedItem, nextDraggedItem);
|
||||
// break;
|
||||
// case 'subgroup':
|
||||
// _data = moveSubGroup(data, _data, to, from, prevDraggedItem, nextDraggedItem);
|
||||
// break;
|
||||
// case 'tool':
|
||||
// _data = moveTool(data, _data, to, from, prevDraggedItem, nextDraggedItem);
|
||||
// break;
|
||||
// }
|
||||
|
||||
// setData(_data);
|
||||
// },
|
||||
// [data]
|
||||
// );
|
||||
|
||||
// const renderItem = React.useCallback(
|
||||
// ({ item, drag, isActive }: RenderItemParams<FlattenedToolbarItemType>) => {
|
||||
// return (
|
||||
// <ScaleDecorator>
|
||||
// <TouchableOpacity
|
||||
// onLongPress={drag}
|
||||
// disabled={isActive}
|
||||
// style={[
|
||||
// styles.rowItem,
|
||||
// {
|
||||
// backgroundColor: isActive ? colors.nav : colors.transGray,
|
||||
// borderWidth: 1,
|
||||
// borderColor: item.type === 'group' ? colors.accent : colors.nav
|
||||
// }
|
||||
// ]}
|
||||
// >
|
||||
// <Text style={styles.text}>
|
||||
// {item.title} - {item.groupId}
|
||||
// </Text>
|
||||
// </TouchableOpacity>
|
||||
// </ScaleDecorator>
|
||||
// );
|
||||
// },
|
||||
// []
|
||||
// );
|
||||
|
||||
// return (
|
||||
// <DraggableFlatList
|
||||
// data={data}
|
||||
// onDragEnd={onDragEnd}
|
||||
// style={{
|
||||
// paddingHorizontal: 12
|
||||
// }}
|
||||
// keyExtractor={item => item.id}
|
||||
// renderItem={renderItem}
|
||||
// />
|
||||
// );
|
||||
// }
|
||||
|
||||
// const styles = StyleSheet.create({
|
||||
// rowItem: {
|
||||
// height: 50,
|
||||
// width: '100%',
|
||||
// justifyContent: 'center',
|
||||
// marginBottom: 10,
|
||||
// borderRadius: 10,
|
||||
// paddingHorizontal: 12
|
||||
// },
|
||||
// text: {
|
||||
// color: 'black',
|
||||
// fontSize: 16,
|
||||
// textAlign: 'left'
|
||||
// }
|
||||
// });
|
||||
255
apps/mobile/src/screens/settings/editor/group.tsx
Normal file
255
apps/mobile/src/screens/settings/editor/group.tsx
Normal file
@@ -0,0 +1,255 @@
|
||||
import * as React from 'react';
|
||||
import { View } from 'react-native';
|
||||
import { DraxDragWithReceiverEventData, DraxView } from 'react-native-drax';
|
||||
import Animated, { Layout } from 'react-native-reanimated';
|
||||
import { presentDialog } from '../../../components/dialog/functions';
|
||||
import { IconButton } from '../../../components/ui/icon-button';
|
||||
import Paragraph from '../../../components/ui/typography/paragraph';
|
||||
import { useThemeStore } from '../../../stores/use-theme-store';
|
||||
import { getElevation } from '../../../utils';
|
||||
import { SIZE } from '../../../utils/size';
|
||||
import { renderTool } from './common';
|
||||
import { DraggableItem, useDragState } from './state';
|
||||
import ToolSheet from './tool-sheet';
|
||||
|
||||
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
export const Group = ({ item, index: groupIndex, parentIndex }: DraggableItem) => {
|
||||
const setData = useDragState(state => state.setData);
|
||||
const [dragged, setDragged] = useDragState(state => [state.dragged, state.setDragged]);
|
||||
const [recieving, setRecieving] = React.useState(false);
|
||||
const [recievePosition, setRecievePosition] = React.useState('above');
|
||||
const isDragged =
|
||||
dragged &&
|
||||
Array.isArray(dragged?.item) &&
|
||||
dragged?.item[0] === item[0] &&
|
||||
parentIndex === undefined;
|
||||
|
||||
const isSubgroup = parentIndex !== undefined;
|
||||
const dimensions = React.useRef({
|
||||
height: 0,
|
||||
width: 0
|
||||
});
|
||||
const colors = useThemeStore(state => state.colors);
|
||||
|
||||
if (isDragged) {
|
||||
console.log(dimensions.current.height, dimensions.current.width);
|
||||
}
|
||||
|
||||
const onDrop = (data: DraxDragWithReceiverEventData) => {
|
||||
const isDroppedAbove = data.receiver.receiveOffsetRatio.y < 0.5;
|
||||
const dragged = data.dragged.payload;
|
||||
const reciever = data.receiver.payload;
|
||||
let _data = useDragState.getState().data.slice();
|
||||
|
||||
if (dragged.type === 'group') {
|
||||
const fromIndex = dragged.index;
|
||||
const toIndex = isDroppedAbove ? Math.max(0, reciever.index) : reciever.index + 1;
|
||||
|
||||
_data.splice(toIndex > fromIndex ? toIndex - 1 : toIndex, 0, _data.splice(fromIndex, 1)[0]);
|
||||
}
|
||||
|
||||
// Always insert sub group at the end of the group.
|
||||
if (dragged.type === 'subgroup') {
|
||||
const fromIndex = dragged.index;
|
||||
const toIndex = isDroppedAbove ? Math.max(0, reciever.index) : reciever.index + 1;
|
||||
|
||||
const insertAt = _data[reciever.index] as string[];
|
||||
const insertFrom = _data[dragged.groupIndex] as string[];
|
||||
|
||||
if (typeof insertAt[insertAt.length - 1] !== 'string') {
|
||||
setRecieving(false);
|
||||
return data.dragAbsolutePosition;
|
||||
}
|
||||
insertAt.push(insertFrom.splice(fromIndex, 1)[0]);
|
||||
}
|
||||
|
||||
if (dragged.type === 'tool') {
|
||||
const insertFrom = dragged.parentIndex
|
||||
? (_data[dragged.parentIndex][dragged.groupIndex] as string[])
|
||||
: (_data[dragged.groupIndex] as string[]);
|
||||
|
||||
_data[groupIndex].push(insertFrom.splice(dragged.index, 1)[0]);
|
||||
}
|
||||
|
||||
setData(_data);
|
||||
setRecieving(false);
|
||||
return data.dragAbsolutePosition;
|
||||
};
|
||||
|
||||
const onRecieveData = (data: DraxDragWithReceiverEventData) => {
|
||||
setRecieving(true);
|
||||
if (data.dragged.payload.type !== 'group') return setRecievePosition('below');
|
||||
if (data.receiver.receiveOffsetRatio.y < 0.5) {
|
||||
setRecievePosition('above');
|
||||
} else {
|
||||
setRecievePosition('below');
|
||||
}
|
||||
};
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
name: 'minus',
|
||||
onPress: () => {
|
||||
presentDialog({
|
||||
context: 'global',
|
||||
title: 'Delete group?',
|
||||
positiveText: 'Delete',
|
||||
paragraph: 'All tools in the collapsed section will also be removed.',
|
||||
positivePress: () => {
|
||||
if (groupIndex === undefined) return;
|
||||
const _data = useDragState.getState().data.slice();
|
||||
console.log(groupIndex);
|
||||
_data.splice(groupIndex, 1);
|
||||
console.log(_data);
|
||||
setData(_data);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'plus',
|
||||
onPress: () => {
|
||||
ToolSheet.present({
|
||||
item,
|
||||
index: groupIndex
|
||||
});
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const renderGroup = (hover: boolean) => {
|
||||
const isSubgroup = parentIndex !== undefined;
|
||||
|
||||
return (
|
||||
<View
|
||||
onLayout={event => {
|
||||
if (hover) return;
|
||||
if (!isDragged) dimensions.current = event.nativeEvent.layout;
|
||||
}}
|
||||
style={[
|
||||
{
|
||||
width: isDragged ? dimensions.current?.width : '100%',
|
||||
backgroundColor: colors.bg,
|
||||
borderRadius: 10,
|
||||
...getElevation(hover ? 5 : 0),
|
||||
marginTop: isSubgroup ? 0 : 10
|
||||
}
|
||||
]}
|
||||
>
|
||||
{isSubgroup ? null : (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
height: 40,
|
||||
marginBottom: 5
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
<Icon size={SIZE.md} name="drag" color={colors.icon} />
|
||||
<Paragraph
|
||||
style={{
|
||||
marginLeft: 5
|
||||
}}
|
||||
color={colors.icon}
|
||||
size={SIZE.xs}
|
||||
>
|
||||
GROUP
|
||||
</Paragraph>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
{buttons.map(item => (
|
||||
<IconButton
|
||||
top={0}
|
||||
left={0}
|
||||
bottom={0}
|
||||
right={0}
|
||||
key={item.name}
|
||||
style={{
|
||||
marginLeft: 10
|
||||
}}
|
||||
onPress={item.onPress}
|
||||
name={item.name}
|
||||
color={colors.icon}
|
||||
size={SIZE.lg}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{isDragged && hover
|
||||
? null
|
||||
: renderTool({ item, index: groupIndex, groupIndex, parentIndex: parentIndex })}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Animated.View layout={Layout}>
|
||||
<DraxView
|
||||
longPressDelay={1000}
|
||||
receptive={
|
||||
(dragged.type === 'subgroup' && dragged.groupIndex === groupIndex) ||
|
||||
(dragged.type === 'tool' && item.length > 0) ||
|
||||
(dragged.type === 'group' && isSubgroup) ||
|
||||
(dragged.type === 'subgroup' && isSubgroup) ||
|
||||
(dragged.type === 'subgroup' && dragged.item && dragged.item[0] === item[0])
|
||||
? false
|
||||
: true
|
||||
}
|
||||
payload={{
|
||||
item,
|
||||
index: groupIndex,
|
||||
parentIndex,
|
||||
type: 'group'
|
||||
}}
|
||||
onDragStart={() => {
|
||||
setDragged({
|
||||
item,
|
||||
type: 'group',
|
||||
...dimensions.current
|
||||
});
|
||||
}}
|
||||
onDragDrop={data => {
|
||||
setDragged({});
|
||||
}}
|
||||
onDragEnd={data => {
|
||||
setDragged({});
|
||||
}}
|
||||
hoverDragReleasedStyle={{
|
||||
opacity: 0
|
||||
}}
|
||||
receivingStyle={{
|
||||
paddingBottom: recievePosition === 'below' ? 50 : 0,
|
||||
paddingTop: recievePosition === 'above' ? 50 : 0,
|
||||
backgroundColor: dragged.type === 'subgroup' ? colors.nav : undefined,
|
||||
marginTop: recievePosition === 'above' ? 10 : 0,
|
||||
marginBottom: recievePosition === 'below' ? 10 : 0,
|
||||
borderRadius: 10
|
||||
}}
|
||||
renderHoverContent={({ dimensions: { width } }) => renderGroup(true)}
|
||||
onReceiveDragDrop={onDrop}
|
||||
onReceiveDragOver={onRecieveData}
|
||||
onReceiveDragExit={() => {
|
||||
setRecieving(false);
|
||||
}}
|
||||
onReceiveDragEnter={onRecieveData}
|
||||
>
|
||||
{!isDragged ? renderGroup(false) : <View />}
|
||||
</DraxView>
|
||||
</Animated.View>
|
||||
);
|
||||
};
|
||||
44
apps/mobile/src/screens/settings/editor/state.ts
Normal file
44
apps/mobile/src/screens/settings/editor/state.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import create, { State } from 'zustand';
|
||||
import { presets, toolbarDefinition } from './toolbar-definition';
|
||||
|
||||
export type ToolDefinition = string | string[];
|
||||
|
||||
export type DraggedItem = {
|
||||
item?: ToolDefinition | ToolDefinition[];
|
||||
type?: 'tool' | 'group' | 'subgroup';
|
||||
height?: number;
|
||||
groupIndex?: number;
|
||||
};
|
||||
|
||||
export type DraggableItem = {
|
||||
item: ToolDefinition | ToolDefinition[];
|
||||
index: number;
|
||||
parentIndex?: number;
|
||||
groupIndex?: number;
|
||||
};
|
||||
|
||||
export interface DragState extends State {
|
||||
dragged: DraggedItem;
|
||||
setDragged: (dragged: DraggedItem) => void;
|
||||
data: ToolDefinition[][];
|
||||
setData: (data: ToolDefinition[][]) => void;
|
||||
preset: 'default' | 'minimal' | 'custom';
|
||||
setPreset: (preset: 'default' | 'minimal' | 'custom') => void;
|
||||
}
|
||||
|
||||
export const useDragState = create<DragState>((set, get) => ({
|
||||
preset: 'default',
|
||||
dragged: {},
|
||||
setDragged: dragged => {
|
||||
set({ dragged });
|
||||
},
|
||||
data: toolbarDefinition,
|
||||
setData: data => {
|
||||
//@ts-ignore
|
||||
presets['custom'] = data;
|
||||
set({ data, preset: 'custom' });
|
||||
},
|
||||
setPreset: preset => {
|
||||
set({ preset, data: presets[preset] });
|
||||
}
|
||||
}));
|
||||
300
apps/mobile/src/screens/settings/editor/tool.tsx
Normal file
300
apps/mobile/src/screens/settings/editor/tool.tsx
Normal file
@@ -0,0 +1,300 @@
|
||||
import * as React from 'react';
|
||||
import { View } from 'react-native';
|
||||
import { DraxDragWithReceiverEventData, DraxView } from 'react-native-drax';
|
||||
import Animated, { Layout } from 'react-native-reanimated';
|
||||
import { presentDialog } from '../../../components/dialog/functions';
|
||||
import { IconButton } from '../../../components/ui/icon-button';
|
||||
import { SvgView } from '../../../components/ui/svg';
|
||||
import Paragraph from '../../../components/ui/typography/paragraph';
|
||||
import { useThemeStore } from '../../../stores/use-theme-store';
|
||||
import { getElevation } from '../../../utils';
|
||||
import { SIZE } from '../../../utils/size';
|
||||
import { renderGroup } from './common';
|
||||
import { DraggableItem, useDragState } from './state';
|
||||
import { findToolById, getToolIcon } from './toolbar-definition';
|
||||
import ToolSheet from './tool-sheet';
|
||||
|
||||
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
export const Tool = ({ item, index, groupIndex, parentIndex }: DraggableItem) => {
|
||||
const setData = useDragState(state => state.setData);
|
||||
const [dragged, setDragged] = useDragState(state => [state.dragged, state.setDragged]);
|
||||
const [recieving, setRecieving] = React.useState(false);
|
||||
const [recievePosition, setRecievePosition] = React.useState('above');
|
||||
const colors = useThemeStore(state => state.colors);
|
||||
const isSubgroup = typeof item === 'object';
|
||||
const isDragged =
|
||||
dragged &&
|
||||
dragged.item &&
|
||||
((dragged.type === 'tool' && dragged?.item === item) ||
|
||||
(isSubgroup && dragged?.item[0] === item[0]));
|
||||
|
||||
const dimensions = React.useRef({
|
||||
height: 0,
|
||||
width: 0
|
||||
});
|
||||
//@ts-ignore
|
||||
const tool = isSubgroup ? null : findToolById(item);
|
||||
//@ts-ignore
|
||||
const iconSvgString = isSubgroup ? null : getToolIcon(tool.icon);
|
||||
|
||||
const buttons = isSubgroup
|
||||
? [
|
||||
{
|
||||
name: 'minus',
|
||||
onPress: () => {
|
||||
presentDialog({
|
||||
context: 'global',
|
||||
title: 'Delete collapsed section?',
|
||||
positiveText: 'Delete',
|
||||
paragraph: 'All tools in the collapsed section will also be removed.',
|
||||
positivePress: () => {
|
||||
if (!groupIndex) return;
|
||||
const _data = useDragState.getState().data.slice();
|
||||
_data[groupIndex].splice(index, 1);
|
||||
setData(_data);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'plus',
|
||||
onPress: () => {
|
||||
ToolSheet.present({
|
||||
item,
|
||||
index,
|
||||
groupIndex,
|
||||
parentIndex
|
||||
});
|
||||
}
|
||||
}
|
||||
]
|
||||
: [
|
||||
{
|
||||
name: 'minus',
|
||||
onPress: () => {
|
||||
if (!groupIndex) return;
|
||||
const _data = useDragState.getState().data.slice();
|
||||
if (!parentIndex) {
|
||||
_data[groupIndex].splice(index, 1);
|
||||
} else {
|
||||
//@ts-ignore
|
||||
_data[parentIndex][groupIndex].splice(index, 1);
|
||||
}
|
||||
setData(_data);
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
if (parentIndex === undefined && !isSubgroup) {
|
||||
buttons.unshift({
|
||||
name: 'unfold-less-horizontal',
|
||||
onPress: () => {
|
||||
if (groupIndex === undefined) return;
|
||||
const _data = useDragState.getState().data.slice();
|
||||
const hasSubGroup = Array.isArray(_data[groupIndex][_data[groupIndex].length - 1]);
|
||||
const _item = _data[groupIndex].splice(index, 1)[0];
|
||||
if (hasSubGroup) {
|
||||
const subgroup = _data[groupIndex][_data[groupIndex].length - 1] as string[];
|
||||
//@ts-ignore
|
||||
subgroup.unshift(_item);
|
||||
} else {
|
||||
_data[groupIndex].push([]);
|
||||
//@ts-ignore
|
||||
_data[groupIndex][_data[groupIndex].length - 1].unshift(_item);
|
||||
}
|
||||
|
||||
setData(_data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const renderChild = React.useCallback(
|
||||
(hover?: boolean) => (
|
||||
<>
|
||||
<View
|
||||
onLayout={event => {
|
||||
if (hover) return;
|
||||
if (!isDragged) dimensions.current = event.nativeEvent.layout;
|
||||
}}
|
||||
style={{
|
||||
backgroundColor: isSubgroup ? colors.bg : colors.nav,
|
||||
borderWidth: isSubgroup ? 0 : 1,
|
||||
borderColor: isSubgroup ? undefined : colors.nav,
|
||||
marginBottom: 10,
|
||||
width: isDragged ? dimensions.current.width : '100%',
|
||||
paddingTop: isSubgroup ? 15 : 0,
|
||||
height: 40,
|
||||
paddingHorizontal: isSubgroup ? 0 : 12,
|
||||
paddingRight: 0,
|
||||
borderRadius: 5,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingLeft: isSubgroup ? 30 : 12,
|
||||
...getElevation(hover ? 3 : 0)
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
{!isSubgroup && iconSvgString ? (
|
||||
<SvgView width={23} height={23} src={iconSvgString} />
|
||||
) : null}
|
||||
{isSubgroup && (
|
||||
<Icon style={{ marginRight: 5 }} size={SIZE.md} name="drag" color={colors.icon} />
|
||||
)}
|
||||
<Paragraph
|
||||
style={{
|
||||
marginLeft: iconSvgString ? 10 : 0
|
||||
}}
|
||||
color={isSubgroup ? colors.icon : colors.pri}
|
||||
size={isSubgroup ? SIZE.xs : SIZE.sm - 1}
|
||||
>
|
||||
{isSubgroup ? 'COLLAPSED' : tool?.title}
|
||||
</Paragraph>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
{buttons.map(item => (
|
||||
<IconButton
|
||||
top={0}
|
||||
left={0}
|
||||
bottom={0}
|
||||
right={0}
|
||||
key={item.name}
|
||||
onPress={item.onPress}
|
||||
style={{
|
||||
marginLeft: 10
|
||||
}}
|
||||
name={item.name}
|
||||
color={colors.icon}
|
||||
size={SIZE.lg}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{isSubgroup && !isDragged ? (
|
||||
<View
|
||||
style={{
|
||||
paddingLeft: 30
|
||||
}}
|
||||
>
|
||||
{renderGroup({ index, item, parentIndex: groupIndex })}
|
||||
</View>
|
||||
) : null}
|
||||
</>
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
const onDrop = (data: DraxDragWithReceiverEventData) => {
|
||||
const isDroppedAbove = data.receiver.receiveOffsetRatio.y < 0.5;
|
||||
const dragged = data.dragged.payload;
|
||||
const reciever = data.receiver.payload;
|
||||
let _data = useDragState.getState().data.slice();
|
||||
if (dragged.type === 'tool') {
|
||||
const fromIndex = dragged.index;
|
||||
const toIndex = isDroppedAbove ? Math.max(0, reciever.index) : reciever.index + 1;
|
||||
|
||||
const insertAt = reciever.parentIndex
|
||||
? (_data[reciever.parentIndex][reciever.groupIndex] as string[])
|
||||
: (_data[reciever.groupIndex] as string[]);
|
||||
const insertFrom = dragged.parentIndex
|
||||
? (_data[dragged.parentIndex][dragged.groupIndex] as string[])
|
||||
: (_data[dragged.groupIndex] as string[]);
|
||||
|
||||
insertAt.splice(
|
||||
toIndex > fromIndex ? toIndex - 1 : toIndex,
|
||||
0,
|
||||
insertFrom.splice(fromIndex, 1)[0]
|
||||
);
|
||||
|
||||
// Remove the group or subgroup if it is empty.
|
||||
if (insertFrom.length === 0) {
|
||||
dragged.parentIndex
|
||||
? _data[dragged.parentIndex].splice(_data[dragged.parentIndex].length - 1, 1)
|
||||
: _data.splice(dragged.groupIndex, 1);
|
||||
}
|
||||
}
|
||||
|
||||
setData(_data);
|
||||
setRecieving(false);
|
||||
return data.dragAbsolutePosition;
|
||||
};
|
||||
|
||||
const onRecieveData = (data: DraxDragWithReceiverEventData) => {
|
||||
setRecieving(true);
|
||||
if (data.receiver.receiveOffsetRatio.y < 0.5) {
|
||||
setRecievePosition('above');
|
||||
} else {
|
||||
setRecievePosition('below');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Animated.View layout={Layout}>
|
||||
<DraxView
|
||||
payload={{
|
||||
item,
|
||||
index,
|
||||
groupIndex,
|
||||
type: isSubgroup ? 'subgroup' : 'tool',
|
||||
parentIndex
|
||||
}}
|
||||
receptive={
|
||||
dragged.type === 'group' ||
|
||||
(dragged.type !== 'tool' && isSubgroup) ||
|
||||
(dragged.type === 'tool' && isSubgroup) ||
|
||||
(!isSubgroup && dragged.type === 'subgroup') ||
|
||||
(dragged.item && dragged.item?.indexOf(item as string) > -1)
|
||||
? false
|
||||
: true
|
||||
}
|
||||
longPressDelay={1000}
|
||||
onDragStart={() => {
|
||||
setDragged({
|
||||
item,
|
||||
type: isSubgroup ? 'subgroup' : 'tool',
|
||||
...dimensions.current,
|
||||
groupIndex: groupIndex
|
||||
});
|
||||
}}
|
||||
receivingStyle={{
|
||||
paddingBottom: recievePosition === 'below' ? 50 : 0,
|
||||
paddingTop: recievePosition === 'above' ? 50 : 0,
|
||||
backgroundColor: dragged.type === 'subgroup' ? colors.nav : undefined,
|
||||
marginTop: recievePosition === 'above' ? 5 : 0,
|
||||
marginBottom: recievePosition === 'below' ? 5 : 0,
|
||||
borderRadius: 10
|
||||
}}
|
||||
renderHoverContent={() => renderChild(true)}
|
||||
onDragDrop={data => {
|
||||
setDragged({});
|
||||
}}
|
||||
onDragEnd={data => {
|
||||
setDragged({});
|
||||
}}
|
||||
hoverDragReleasedStyle={{
|
||||
opacity: 0
|
||||
}}
|
||||
onReceiveDragDrop={onDrop}
|
||||
onReceiveDragOver={onRecieveData}
|
||||
onReceiveDragExit={() => {
|
||||
setRecieving(false);
|
||||
}}
|
||||
onReceiveDragEnter={onRecieveData}
|
||||
>
|
||||
{isDragged ? <View /> : renderChild()}
|
||||
</DraxView>
|
||||
</Animated.View>
|
||||
);
|
||||
};
|
||||
384
apps/mobile/src/screens/settings/editor/toolbardefinition.ts
Normal file
384
apps/mobile/src/screens/settings/editor/toolbardefinition.ts
Normal file
@@ -0,0 +1,384 @@
|
||||
import { Icons } from 'notesnook-editor/dist/toolbar/icons';
|
||||
import { useThemeStore } from '../../../stores/use-theme-store';
|
||||
|
||||
export const presets = {
|
||||
default: [
|
||||
['insertBlock'],
|
||||
[
|
||||
'bold',
|
||||
'italic',
|
||||
'underline',
|
||||
['strikethrough', 'code', 'subscript', 'superscript', 'highlight', 'textColor']
|
||||
],
|
||||
['fontSize'],
|
||||
['headings', 'fontFamily'],
|
||||
['numberedList', 'bulletList'],
|
||||
['link'],
|
||||
['alignCenter', ['alignLeft', 'alignRight', 'alignJustify', 'ltr', 'rtl']],
|
||||
['clearformatting']
|
||||
],
|
||||
minimal: [
|
||||
['insertBlock'],
|
||||
[
|
||||
'bold',
|
||||
'italic',
|
||||
'underline',
|
||||
['strikethrough', 'subscript', 'superscript', 'highlight', 'textColor']
|
||||
]
|
||||
],
|
||||
custom: []
|
||||
};
|
||||
|
||||
export function findToolById(id: keyof typeof tools): { title: string; icon: string } {
|
||||
return tools[id];
|
||||
}
|
||||
|
||||
export function getToolIcon(id: keyof typeof tools) {
|
||||
//@ts-ignore
|
||||
const icon = Icons[id];
|
||||
const colors = useThemeStore.getState().colors;
|
||||
//@ts-ignore
|
||||
return id === 'none'
|
||||
? null
|
||||
: `<svg width="20" height="20" >
|
||||
<path d="${icon}" fill="${colors.icon}" />
|
||||
</svg>`;
|
||||
}
|
||||
|
||||
export function getUngroupedTools(toolDefinition: (string | string[])[][]): string[] {
|
||||
let keys = Object.keys(tools);
|
||||
console.log(keys);
|
||||
const ungrouped = [];
|
||||
let toolString = JSON.stringify(toolDefinition);
|
||||
for (let key of keys) {
|
||||
if (!toolString.includes(key)) ungrouped.push(key);
|
||||
}
|
||||
console.log(ungrouped);
|
||||
return ungrouped;
|
||||
}
|
||||
|
||||
export const tools = {
|
||||
bold: {
|
||||
icon: 'bold',
|
||||
title: 'Bold'
|
||||
},
|
||||
italic: {
|
||||
icon: 'italic',
|
||||
title: 'Italic'
|
||||
},
|
||||
underline: {
|
||||
icon: 'underline',
|
||||
title: 'Underline'
|
||||
},
|
||||
strikethrough: {
|
||||
icon: 'strikethrough',
|
||||
title: 'Strikethrough'
|
||||
},
|
||||
link: {
|
||||
icon: 'link',
|
||||
title: 'Link'
|
||||
},
|
||||
code: {
|
||||
icon: 'code',
|
||||
title: 'Code'
|
||||
},
|
||||
clearformatting: {
|
||||
icon: 'formatClear',
|
||||
title: 'Clear all formatting'
|
||||
},
|
||||
subscript: {
|
||||
icon: 'subscript',
|
||||
title: 'Subscript'
|
||||
},
|
||||
superscript: {
|
||||
icon: 'superscript',
|
||||
title: 'Superscript'
|
||||
},
|
||||
insertBlock: {
|
||||
icon: 'plus',
|
||||
title: 'Insert'
|
||||
},
|
||||
bulletList: {
|
||||
icon: 'bulletList',
|
||||
title: 'Bullet list'
|
||||
},
|
||||
numberedList: {
|
||||
icon: 'numberedList',
|
||||
title: 'Numbered list'
|
||||
},
|
||||
fontFamily: {
|
||||
icon: 'none',
|
||||
title: 'Font family'
|
||||
},
|
||||
fontSize: {
|
||||
icon: 'none',
|
||||
title: 'Font size'
|
||||
},
|
||||
headings: {
|
||||
icon: 'none',
|
||||
title: 'Headings'
|
||||
},
|
||||
alignCenter: {
|
||||
icon: 'alignCenter',
|
||||
title: 'Align center'
|
||||
},
|
||||
alignLeft: {
|
||||
icon: 'alignLeft',
|
||||
title: 'Align left'
|
||||
},
|
||||
alignRight: {
|
||||
icon: 'alignRight',
|
||||
title: 'Align right'
|
||||
},
|
||||
alignJustify: {
|
||||
icon: 'alignJustify',
|
||||
title: 'Justify'
|
||||
},
|
||||
ltr: {
|
||||
icon: 'ltr',
|
||||
title: 'Left to right'
|
||||
},
|
||||
rtl: {
|
||||
icon: 'rtl',
|
||||
title: 'Right to left'
|
||||
},
|
||||
highlight: {
|
||||
icon: 'highlight',
|
||||
title: 'Highlight'
|
||||
},
|
||||
textColor: {
|
||||
icon: 'textColor',
|
||||
title: 'Text color'
|
||||
}
|
||||
};
|
||||
|
||||
export const toolbarDefinition = presets['default'];
|
||||
|
||||
export type FlattenedToolbarItemType = {
|
||||
id: string;
|
||||
type: 'group' | 'tool' | 'subgroup';
|
||||
icon?: string;
|
||||
title: string;
|
||||
count?: number;
|
||||
groupId?: string;
|
||||
};
|
||||
|
||||
export const getFlattenedToolbarDefinition = (
|
||||
toolbar: (string | string[])[][]
|
||||
): FlattenedToolbarItemType[] => {
|
||||
const flattenedToolbar: FlattenedToolbarItemType[] = [];
|
||||
for (let group = 0; group < toolbar.length; group++) {
|
||||
const groupId = `group-${group + 1}`;
|
||||
flattenedToolbar.push({
|
||||
id: groupId,
|
||||
type: 'group',
|
||||
title: `Group ${group + 1}`
|
||||
});
|
||||
for (let i = 0; i < toolbar[group].length; i++) {
|
||||
let tool = toolbar[group][i];
|
||||
if (typeof tool !== 'string') {
|
||||
let subGroupId = `subgroup-${i + 1}-group`;
|
||||
flattenedToolbar.push({
|
||||
id: subGroupId,
|
||||
type: 'subgroup',
|
||||
title: `SubGroup ${group + 1}`,
|
||||
count: tool.length,
|
||||
groupId: groupId
|
||||
});
|
||||
for (let it = 0; it < tool.length; it++) {
|
||||
const toolId = tool[it] as keyof typeof tools;
|
||||
flattenedToolbar.push({
|
||||
type: 'tool',
|
||||
id: toolId,
|
||||
groupId: subGroupId,
|
||||
...findToolById(toolId)
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const toolId = tool as keyof typeof tools;
|
||||
flattenedToolbar.push({
|
||||
type: 'tool',
|
||||
id: toolId,
|
||||
groupId: groupId,
|
||||
...findToolById(toolId)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return flattenedToolbar;
|
||||
};
|
||||
|
||||
export function getGroupIndexes(data: FlattenedToolbarItemType[]) {
|
||||
return data
|
||||
.map((item, index) => {
|
||||
if (item.type === 'group') return index;
|
||||
return -1;
|
||||
})
|
||||
.filter(item => item !== -1);
|
||||
}
|
||||
|
||||
export function moveGroup(
|
||||
data: FlattenedToolbarItemType[],
|
||||
_data: FlattenedToolbarItemType[],
|
||||
to: number,
|
||||
from: number,
|
||||
prevDraggedItem: FlattenedToolbarItemType,
|
||||
nextDraggedItem: FlattenedToolbarItemType
|
||||
) {
|
||||
const groupIndexes = getGroupIndexes(data);
|
||||
const nextGroupIndex = groupIndexes.findIndex(index => index === from) + 1;
|
||||
const prevGroupIndex = groupIndexes.findIndex(index => index === from) - 1;
|
||||
if (
|
||||
(to > groupIndexes[prevGroupIndex] && to < from) ||
|
||||
(to < groupIndexes[nextGroupIndex] && to > from) ||
|
||||
to === from
|
||||
) {
|
||||
console.log('invalid location');
|
||||
return data;
|
||||
}
|
||||
|
||||
const groupItems = data.slice(from + 1, groupIndexes[nextGroupIndex]);
|
||||
let { after, before } = getGroupBoundary(groupIndexes, to, from);
|
||||
console.log('group after drop index', after, 'group before drop index', before);
|
||||
let indexOfMovedGroup = _data.findIndex(item => item.id === prevDraggedItem.id);
|
||||
_data.splice(indexOfMovedGroup, 1);
|
||||
if (to < from) {
|
||||
// move up
|
||||
_data.splice(after === groupIndexes[0] ? after : before, 0, prevDraggedItem, ...groupItems);
|
||||
_data.splice(from + (groupItems.length + 1), groupItems.length);
|
||||
} else {
|
||||
// move down
|
||||
if (before === groupIndexes[groupIndexes.length - 1]) {
|
||||
if (after === before) before = _data.length - 1;
|
||||
_data.splice(before + 1, 0, prevDraggedItem, ...groupItems);
|
||||
_data.splice(from, groupItems.length);
|
||||
} else {
|
||||
_data.splice(before - 1, 0, prevDraggedItem, ...groupItems);
|
||||
_data.splice(from === 0 ? from : from - 1, groupItems.length);
|
||||
}
|
||||
}
|
||||
return _data;
|
||||
}
|
||||
|
||||
export function getGroupBoundary(indexes: number[], to: number, from: number) {
|
||||
let after = 0;
|
||||
let before = 0;
|
||||
for (let i = 0; i < indexes.length; i++) {
|
||||
before = indexes[i];
|
||||
if (to >= after && to <= before) {
|
||||
if (before === to) {
|
||||
after = before;
|
||||
before = indexes[i + 1];
|
||||
}
|
||||
|
||||
if (after === to && before === from) {
|
||||
before = after;
|
||||
}
|
||||
break;
|
||||
} else {
|
||||
after = before;
|
||||
}
|
||||
}
|
||||
return { after, before };
|
||||
}
|
||||
|
||||
export const getToolbarDefinition = (
|
||||
flattenedToolbar: FlattenedToolbarItemType[]
|
||||
): (string | string[])[][] => {
|
||||
const groupIndexes = flattenedToolbar
|
||||
.map((item, index) => {
|
||||
if (item.type === 'group') return index;
|
||||
return -1;
|
||||
})
|
||||
.filter(item => item !== -1);
|
||||
const toolbarDefinition = [];
|
||||
|
||||
for (let i = 0; i < groupIndexes.length; i++) {
|
||||
let groupIndex = groupIndexes[i];
|
||||
let nextGroupIndex =
|
||||
i + 1 === groupIndexes.length ? flattenedToolbar.length : groupIndexes[i + 1];
|
||||
|
||||
let groupDefinition = [];
|
||||
for (let i = groupIndex + 1; i < nextGroupIndex; i++) {
|
||||
let tool = flattenedToolbar[i];
|
||||
switch (tool.type) {
|
||||
case 'tool':
|
||||
if (!tool.groupId?.includes('subgroup')) {
|
||||
groupDefinition.push(tool.id);
|
||||
}
|
||||
break;
|
||||
case 'subgroup':
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
let subgroupDefinition = [];
|
||||
for (let index = i + 1; index < nextGroupIndex; index++) {
|
||||
let subgroupTool = flattenedToolbar[index];
|
||||
if (subgroupTool.groupId !== tool.id) continue;
|
||||
subgroupDefinition.push(subgroupTool.id);
|
||||
}
|
||||
groupDefinition.push(subgroupDefinition);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
toolbarDefinition.push(groupDefinition);
|
||||
}
|
||||
return toolbarDefinition;
|
||||
};
|
||||
|
||||
export const moveSubGroup = (
|
||||
data: FlattenedToolbarItemType[],
|
||||
_data: FlattenedToolbarItemType[],
|
||||
to: number,
|
||||
from: number,
|
||||
prevDraggedItem: FlattenedToolbarItemType,
|
||||
nextDraggedItem: FlattenedToolbarItemType
|
||||
) => {
|
||||
const groupIndexes = getGroupIndexes(data);
|
||||
let { after, before } = getGroupBoundary(groupIndexes, to, from);
|
||||
console.log(after, before, 'value');
|
||||
if ((from > after && from < before && to !== after && to !== before) || to === from || to === 0) {
|
||||
console.log('invalid move');
|
||||
return _data;
|
||||
}
|
||||
const groupItems = data.slice(from + 1, getGroupBoundary(groupIndexes, from, to).before);
|
||||
|
||||
let indexOfMovedGroup = _data.findIndex(item => item.id === prevDraggedItem.id);
|
||||
_data.splice(indexOfMovedGroup, 1);
|
||||
|
||||
if (to < from) {
|
||||
// move up
|
||||
_data.splice(after === to ? after : before, 0, prevDraggedItem, ...groupItems);
|
||||
_data.splice(from + (groupItems.length + 1), groupItems.length);
|
||||
} else {
|
||||
// move down
|
||||
_data.splice(before - 1, 0, prevDraggedItem, ...groupItems);
|
||||
_data.splice(from, groupItems.length);
|
||||
}
|
||||
|
||||
console.log(groupItems.map(i => i.id));
|
||||
|
||||
return _data;
|
||||
};
|
||||
|
||||
export const moveTool = (
|
||||
data: FlattenedToolbarItemType[],
|
||||
_data: FlattenedToolbarItemType[],
|
||||
to: number,
|
||||
from: number,
|
||||
prevDraggedItem: FlattenedToolbarItemType,
|
||||
nextDraggedItem: FlattenedToolbarItemType
|
||||
) => {
|
||||
const itemAboveDrop = _data[to - 1];
|
||||
const isSubGroupOrItem =
|
||||
itemAboveDrop.id.startsWith('subgroup') || itemAboveDrop.groupId?.startsWith('subgroup');
|
||||
console.log(itemAboveDrop.id);
|
||||
if (isSubGroupOrItem) {
|
||||
console.log('dropped in subgroup', itemAboveDrop.type);
|
||||
_data[to].groupId =
|
||||
itemAboveDrop.type === 'subgroup' ? itemAboveDrop.id : itemAboveDrop.groupId;
|
||||
} else {
|
||||
_data[to].groupId = itemAboveDrop.type === 'group' ? itemAboveDrop.id : itemAboveDrop.groupId;
|
||||
}
|
||||
|
||||
return _data;
|
||||
};
|
||||
116
apps/mobile/src/screens/settings/editor/toolsheet.tsx
Normal file
116
apps/mobile/src/screens/settings/editor/toolsheet.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
import React, { RefObject } from 'react';
|
||||
import { View } from 'react-native';
|
||||
import ActionSheet from 'react-native-actions-sheet';
|
||||
import { ScrollView } from 'react-native-gesture-handler';
|
||||
import { PressableButton } from '../../../components/ui/pressable';
|
||||
import { SvgView } from '../../../components/ui/svg';
|
||||
import Paragraph from '../../../components/ui/typography/paragraph';
|
||||
import { presentSheet } from '../../../services/event-manager';
|
||||
import { useThemeStore } from '../../../stores/use-theme-store';
|
||||
import { SIZE } from '../../../utils/size';
|
||||
import { DraggableItem, useDragState } from './state';
|
||||
import { findToolById, getToolIcon, getUngroupedTools } from './toolbar-definition';
|
||||
|
||||
export default function ToolSheet({
|
||||
group,
|
||||
fwdRef
|
||||
}: {
|
||||
group: DraggableItem;
|
||||
fwdRef: RefObject<ActionSheet>;
|
||||
}) {
|
||||
const colors = useThemeStore(state => state.colors);
|
||||
const data = useDragState(state => state.data);
|
||||
const ungrouped = getUngroupedTools(data);
|
||||
|
||||
const renderTool = React.useCallback((item: string) => {
|
||||
//@ts-ignore
|
||||
const tool = findToolById(item);
|
||||
//@ts-ignore
|
||||
const iconSvgString = tool ? getToolIcon(tool.icon) : null;
|
||||
return (
|
||||
<PressableButton
|
||||
key={item}
|
||||
type="grayBg"
|
||||
onPress={() => {
|
||||
const _data = useDragState.getState().data.slice();
|
||||
if (group.groupIndex !== undefined) {
|
||||
//@ts-ignore
|
||||
_data[group.groupIndex][group.index].unshift(item);
|
||||
} else {
|
||||
_data[group.index].unshift(item);
|
||||
}
|
||||
useDragState.getState().setData(_data);
|
||||
}}
|
||||
customStyle={{
|
||||
marginBottom: 10,
|
||||
width: '100%',
|
||||
height: 50,
|
||||
paddingHorizontal: 12,
|
||||
paddingRight: 0,
|
||||
borderRadius: 5,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-start'
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
{iconSvgString ? <SvgView width={23} height={23} src={iconSvgString} /> : null}
|
||||
<Paragraph
|
||||
style={{
|
||||
marginLeft: iconSvgString ? 10 : 0
|
||||
}}
|
||||
color={colors.pri}
|
||||
size={SIZE.sm}
|
||||
>
|
||||
{tool?.title}
|
||||
</Paragraph>
|
||||
</View>
|
||||
</PressableButton>
|
||||
);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
maxHeight: '100%',
|
||||
padding: 12
|
||||
}}
|
||||
>
|
||||
<ScrollView
|
||||
onMomentumScrollEnd={() => {
|
||||
fwdRef.current?.handleChildScrollEnd();
|
||||
}}
|
||||
nestedScrollEnabled={true}
|
||||
>
|
||||
{ungrouped.length === 0 ? (
|
||||
<Paragraph
|
||||
style={{
|
||||
alignSelf: 'center'
|
||||
}}
|
||||
color={colors.icon}
|
||||
>
|
||||
You have grouped all the tools.
|
||||
</Paragraph>
|
||||
) : (
|
||||
ungrouped.map(renderTool)
|
||||
)}
|
||||
<View
|
||||
style={{
|
||||
height: 200
|
||||
}}
|
||||
/>
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
ToolSheet.present = (payload: DraggableItem) => {
|
||||
presentSheet({
|
||||
component: ref => <ToolSheet fwdRef={ref} group={payload} />
|
||||
});
|
||||
};
|
||||
@@ -5,6 +5,7 @@ import DelayLayout from '../../components/delay-layout';
|
||||
import useNavigationStore from '../../stores/use-navigation-store';
|
||||
import { tabBarRef } from '../../utils/global-refs';
|
||||
import { useNavigationFocus } from '../../utils/hooks/use-navigation-focus';
|
||||
import { components } from './components';
|
||||
import { SectionItem } from './section-item';
|
||||
import { RouteParams, SettingSection } from './types';
|
||||
|
||||
@@ -37,7 +38,11 @@ const Group = ({ navigation, route }: NativeStackScreenProps<RouteParams, 'Setti
|
||||
|
||||
return (
|
||||
<DelayLayout>
|
||||
<View>
|
||||
<View
|
||||
style={{
|
||||
flex: 1
|
||||
}}
|
||||
>
|
||||
{route.params.sections ? (
|
||||
<FlatList
|
||||
data={route.params.sections}
|
||||
@@ -45,6 +50,7 @@ const Group = ({ navigation, route }: NativeStackScreenProps<RouteParams, 'Setti
|
||||
renderItem={renderItem}
|
||||
/>
|
||||
) : null}
|
||||
{route.params.component ? components[route.params.component] : null}
|
||||
</View>
|
||||
</DelayLayout>
|
||||
);
|
||||
|
||||
@@ -22,20 +22,14 @@ const Home = ({ navigation, route }: NativeStackScreenProps<RouteParams, 'Settin
|
||||
const colors = useThemeStore(state => state.colors);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const flatlistRef = useRef<FlatList<SettingSection>>(null);
|
||||
const isFocused = useNavigationFocus(navigation, {
|
||||
|
||||
useNavigationFocus(navigation, {
|
||||
onFocus: () => {
|
||||
useNavigationStore.getState().update({
|
||||
name: 'Settings'
|
||||
});
|
||||
return false;
|
||||
},
|
||||
onBlur: () => {
|
||||
flatlistRef.current?.scrollToOffset({
|
||||
offset: 0,
|
||||
animated: false
|
||||
});
|
||||
return false;
|
||||
},
|
||||
focusOnInit: true
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { NavigationProp, StackActions, useNavigation } from '@react-navigation/native';
|
||||
import React, { ReactElement } from 'react';
|
||||
import React from 'react';
|
||||
import { View } from 'react-native';
|
||||
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
import ToggleSwitch from 'toggle-switch-react-native';
|
||||
@@ -11,18 +11,9 @@ import useNavigationStore from '../../stores/use-navigation-store';
|
||||
import { useSettingStore } from '../../stores/use-setting-store';
|
||||
import { useThemeStore } from '../../stores/use-theme-store';
|
||||
import { SIZE } from '../../utils/size';
|
||||
import { AccentColorPicker, HomagePageSelector } from './appearance';
|
||||
import { AutomaticBackupsSelector } from './backup-restore';
|
||||
import { Subscription } from './subscription';
|
||||
import { components } from './components';
|
||||
import { RouteParams, SettingSection } from './types';
|
||||
|
||||
const components: { [name: string]: ReactElement } = {
|
||||
colorpicker: <AccentColorPicker wrap={true} />,
|
||||
homeselector: <HomagePageSelector />,
|
||||
autobackups: <AutomaticBackupsSelector />,
|
||||
subscription: <Subscription />
|
||||
};
|
||||
|
||||
export const SectionItem = React.memo(
|
||||
({ item }: { item: SettingSection }) => {
|
||||
const colors = useThemeStore(state => state.colors);
|
||||
@@ -37,6 +28,7 @@ export const SectionItem = React.memo(
|
||||
}
|
||||
if (!item.property) return;
|
||||
SettingsService.set({
|
||||
//@ts-ignore
|
||||
[item.property]: !settings[item.property]
|
||||
});
|
||||
};
|
||||
@@ -130,7 +122,7 @@ export const SectionItem = React.memo(
|
||||
</Paragraph>
|
||||
)}
|
||||
|
||||
{!!item.component && (
|
||||
{!!item.component && item.type !== 'screen' && (
|
||||
<>
|
||||
<Seperator half />
|
||||
{components[item.component]}
|
||||
|
||||
@@ -42,6 +42,7 @@ import AppLock from './app-lock';
|
||||
import { verifyUser } from './functions';
|
||||
import { SettingSection } from './types';
|
||||
import { getTimeLeft } from './user-section';
|
||||
import { ConfigureToolbar } from './editor/configure-toolbar';
|
||||
const format = (ver: number) => {
|
||||
let parts = ver.toString().split('');
|
||||
return `v${parts[0]}.${parts[1]}.${parts[2]?.startsWith('0') ? '' : parts[2]}${
|
||||
@@ -702,6 +703,20 @@ export const settingsGroups: SettingSection[] = [
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'editor',
|
||||
name: 'Editor',
|
||||
sections: [
|
||||
{
|
||||
id: 'configure-toolbar',
|
||||
type: 'screen',
|
||||
name: 'Configure toolbar',
|
||||
description: `Make the toolbar adaptable to your needs.`,
|
||||
icon: 'form-textbox',
|
||||
component: 'configuretoolbar'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'help-support',
|
||||
name: 'Help and support',
|
||||
|
||||
Reference in New Issue
Block a user