Add searchbar to home screen
Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
This commit is contained in:
1687
package-lock.json
generated
1687
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -23,13 +23,13 @@
|
|||||||
"@reduxjs/toolkit": "^1.9.5",
|
"@reduxjs/toolkit": "^1.9.5",
|
||||||
"@shopify/flash-list": "^1.4.3",
|
"@shopify/flash-list": "^1.4.3",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-native": "0.72.1",
|
"react-native": "0.72.2",
|
||||||
"react-native-document-picker": "^9.0.1",
|
"react-native-document-picker": "^9.0.1",
|
||||||
"react-native-fast-image": "^8.6.3",
|
"react-native-fast-image": "^8.6.3",
|
||||||
"react-native-file-access": "^3.0.4",
|
"react-native-file-access": "^3.0.4",
|
||||||
"react-native-gesture-handler": "^2.12.0",
|
"react-native-gesture-handler": "^2.12.0",
|
||||||
"react-native-paper": "^5.9.1",
|
"react-native-paper": "^5.9.1",
|
||||||
"react-native-reanimated": "2.2.4",
|
"react-native-reanimated": "^3.3.0",
|
||||||
"react-native-safe-area-context": "^4.6.4",
|
"react-native-safe-area-context": "^4.6.4",
|
||||||
"react-native-scoped-storage": "^1.9.3",
|
"react-native-scoped-storage": "^1.9.3",
|
||||||
"react-native-screens": "^3.22.1",
|
"react-native-screens": "^3.22.1",
|
||||||
|
26
src/app.tsx
26
src/app.tsx
@@ -12,6 +12,8 @@ import NavigationContainer from './navigation';
|
|||||||
import { store, persistor, validateSettings } from './state';
|
import { store, persistor, validateSettings } from './state';
|
||||||
import { LoadingView } from './components';
|
import { LoadingView } from './components';
|
||||||
import { Welcome } from './screens';
|
import { Welcome } from './screens';
|
||||||
|
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
||||||
|
import styles from './styles';
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
const [showWelcome, setShowWelcome] = useState(false);
|
const [showWelcome, setShowWelcome] = useState(false);
|
||||||
@@ -47,17 +49,19 @@ const App = () => {
|
|||||||
persistor={persistor}
|
persistor={persistor}
|
||||||
onBeforeLift={onBeforeLift}>
|
onBeforeLift={onBeforeLift}>
|
||||||
<RealmProvider schema={[Meme, Tag]}>
|
<RealmProvider schema={[Meme, Tag]}>
|
||||||
<SafeAreaProvider>
|
<GestureHandlerRootView style={styles.flex}>
|
||||||
<StatusBar
|
<SafeAreaProvider>
|
||||||
barStyle={isDarkMode ? 'light-content' : 'dark-content'}
|
<StatusBar
|
||||||
backgroundColor={theme.colors.background}
|
barStyle={isDarkMode ? 'light-content' : 'dark-content'}
|
||||||
/>
|
backgroundColor={theme.colors.background}
|
||||||
{showWelcome ? (
|
/>
|
||||||
<Welcome onWelcomeComplete={() => setShowWelcome(false)} />
|
{showWelcome ? (
|
||||||
) : (
|
<Welcome onWelcomeComplete={() => setShowWelcome(false)} />
|
||||||
<NavigationContainer />
|
) : (
|
||||||
)}
|
<NavigationContainer />
|
||||||
</SafeAreaProvider>
|
)}
|
||||||
|
</SafeAreaProvider>
|
||||||
|
</GestureHandlerRootView>
|
||||||
</RealmProvider>
|
</RealmProvider>
|
||||||
</PersistGate>
|
</PersistGate>
|
||||||
</Provider>
|
</Provider>
|
||||||
|
@@ -1,26 +1,46 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { StyleSheet } from 'react-native';
|
import { StyleSheet, Keyboard } from 'react-native';
|
||||||
import { FAB, Portal } from 'react-native-paper';
|
import { FAB, Portal } from 'react-native-paper';
|
||||||
import { verticalScale } from '../styles';
|
import { horizontalScale, verticalScale } from '../styles';
|
||||||
import { ParamListBase, useNavigation } from '@react-navigation/native';
|
import { ParamListBase, useNavigation } from '@react-navigation/native';
|
||||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
fab: {
|
fab: {
|
||||||
paddingBottom: verticalScale(75),
|
position: 'absolute',
|
||||||
|
right: horizontalScale(10),
|
||||||
|
bottom: verticalScale(75),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const FloatingActionButton = ({ visible }: { visible?: boolean }) => {
|
const FloatingActionButton = ({ visible = true }: { visible?: boolean }) => {
|
||||||
const [state, setState] = React.useState(false);
|
const [state, setState] = useState(false);
|
||||||
const { navigate } =
|
const { navigate } =
|
||||||
useNavigation<NativeStackNavigationProp<ParamListBase>>();
|
useNavigation<NativeStackNavigationProp<ParamListBase>>();
|
||||||
|
|
||||||
|
const [keyboardOpen, setKeyboardOpen] = useState(false);
|
||||||
|
useEffect(() => {
|
||||||
|
const keyboardDidShowListener = Keyboard.addListener(
|
||||||
|
'keyboardDidShow',
|
||||||
|
() => setKeyboardOpen(true),
|
||||||
|
);
|
||||||
|
const keyboardDidHideListener = Keyboard.addListener(
|
||||||
|
'keyboardDidHide',
|
||||||
|
() => setKeyboardOpen(false),
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
keyboardDidHideListener.remove();
|
||||||
|
keyboardDidShowListener.remove();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Portal>
|
<Portal>
|
||||||
<FAB.Group
|
<FAB.Group
|
||||||
open={state}
|
open={state}
|
||||||
visible={visible ?? true}
|
visible={visible && !keyboardOpen}
|
||||||
icon={state ? 'image' : 'plus'}
|
icon={state ? 'image' : 'plus'}
|
||||||
actions={[
|
actions={[
|
||||||
{
|
{
|
||||||
|
@@ -1,2 +1,2 @@
|
|||||||
export { MEME_TYPE, Meme } from './meme';
|
export { MEME_TYPE, memeTypePlural, Meme } from './meme';
|
||||||
export { Tag } from './tag';
|
export { Tag } from './tag';
|
||||||
|
@@ -2,18 +2,27 @@ import { Realm } from '@realm/react';
|
|||||||
import { Tag } from './tag';
|
import { Tag } from './tag';
|
||||||
|
|
||||||
enum MEME_TYPE {
|
enum MEME_TYPE {
|
||||||
IMAGE = 'IMAGE',
|
IMAGE = 'Image',
|
||||||
GIF = 'GIF',
|
GIF = 'GIF',
|
||||||
ALMBUM = 'ALMBUM',
|
VIDEO = 'Video',
|
||||||
VIDEO = 'VIDEO',
|
AUDIO = 'Audio',
|
||||||
AUDIO = 'AUDIO',
|
ALBUM = 'Album',
|
||||||
TEXT = 'TEXT',
|
TEXT = 'Text',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const memeTypePlural = {
|
||||||
|
[MEME_TYPE.IMAGE]: 'Images',
|
||||||
|
[MEME_TYPE.GIF]: 'GIFs',
|
||||||
|
[MEME_TYPE.VIDEO]: 'Videos',
|
||||||
|
[MEME_TYPE.AUDIO]: 'Audio',
|
||||||
|
[MEME_TYPE.ALBUM]: 'Albums',
|
||||||
|
[MEME_TYPE.TEXT]: 'Text',
|
||||||
|
};
|
||||||
|
|
||||||
class Meme extends Realm.Object<Meme> {
|
class Meme extends Realm.Object<Meme> {
|
||||||
id!: Realm.BSON.UUID;
|
id!: Realm.BSON.UUID;
|
||||||
type!: MEME_TYPE;
|
type!: MEME_TYPE;
|
||||||
uri!: string;
|
uri!: Realm.List<string>;
|
||||||
size!: number;
|
size!: number;
|
||||||
title!: string;
|
title!: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
@@ -30,7 +39,7 @@ class Meme extends Realm.Object<Meme> {
|
|||||||
properties: {
|
properties: {
|
||||||
id: 'uuid',
|
id: 'uuid',
|
||||||
type: { type: 'string', indexed: true },
|
type: { type: 'string', indexed: true },
|
||||||
uri: 'string',
|
uri: 'string[]',
|
||||||
size: 'int',
|
size: 'int',
|
||||||
title: 'string',
|
title: 'string',
|
||||||
description: 'string?',
|
description: 'string?',
|
||||||
@@ -44,4 +53,4 @@ class Meme extends Realm.Object<Meme> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export { MEME_TYPE, Meme };
|
export { MEME_TYPE, memeTypePlural, Meme };
|
||||||
|
@@ -7,7 +7,7 @@ import FontAwesome5 from 'react-native-vector-icons/FontAwesome5';
|
|||||||
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
|
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
|
||||||
import { createNativeStackNavigator } from '@react-navigation/native-stack';
|
import { createNativeStackNavigator } from '@react-navigation/native-stack';
|
||||||
import { BottomNavigation, Portal, useTheme } from 'react-native-paper';
|
import { BottomNavigation, Portal, useTheme } from 'react-native-paper';
|
||||||
import { Home, Search, Tags, Settings, AddItem } from './screens';
|
import { Home, Tags, Settings, AddItem } from './screens';
|
||||||
import { horizontalScale } from './styles';
|
import { horizontalScale } from './styles';
|
||||||
import { FloatingActionButton } from './components';
|
import { FloatingActionButton } from './components';
|
||||||
import { darkNavigationTheme, lightNavigationTheme } from './theme';
|
import { darkNavigationTheme, lightNavigationTheme } from './theme';
|
||||||
@@ -70,15 +70,6 @@ const TabNavigator = () => {
|
|||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<TabNavigatorBase.Screen
|
|
||||||
name="Search"
|
|
||||||
component={Search}
|
|
||||||
options={{
|
|
||||||
tabBarIcon: ({ color, size }) => (
|
|
||||||
<FontAwesome5 name="search" color={color} size={size} />
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<TabNavigatorBase.Screen
|
<TabNavigatorBase.Screen
|
||||||
name="Tags"
|
name="Tags"
|
||||||
component={Tags}
|
component={Tags}
|
||||||
|
@@ -6,6 +6,7 @@ import {
|
|||||||
IconButton,
|
IconButton,
|
||||||
Divider,
|
Divider,
|
||||||
useTheme,
|
useTheme,
|
||||||
|
Searchbar,
|
||||||
} from 'react-native-paper';
|
} from 'react-native-paper';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { PaddedView } from '../components';
|
import { PaddedView } from '../components';
|
||||||
@@ -19,7 +20,9 @@ import {
|
|||||||
setSortDirection,
|
setSortDirection,
|
||||||
toggleFavoritesOnly,
|
toggleFavoritesOnly,
|
||||||
setSort,
|
setSort,
|
||||||
|
setFilter,
|
||||||
} from '../state';
|
} from '../state';
|
||||||
|
import { MEME_TYPE, memeTypePlural } from '../database';
|
||||||
|
|
||||||
const Home = () => {
|
const Home = () => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
@@ -32,9 +35,11 @@ const Home = () => {
|
|||||||
const favoritesOnly = useSelector(
|
const favoritesOnly = useSelector(
|
||||||
(state: RootState) => state.home.favoritesOnly,
|
(state: RootState) => state.home.favoritesOnly,
|
||||||
);
|
);
|
||||||
|
const filter = useSelector((state: RootState) => state.home.filter);
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const [sortMenuVisible, setSortMenuVisible] = useState(false);
|
const [sortMenuVisible, setSortMenuVisible] = useState(false);
|
||||||
|
const [filterMenuVisible, setFilterMenuVisible] = useState(false);
|
||||||
|
|
||||||
const handleSortModeChange = (newSort: SORT) => {
|
const handleSortModeChange = (newSort: SORT) => {
|
||||||
if (newSort === sort) {
|
if (newSort === sort) {
|
||||||
@@ -50,45 +55,86 @@ const Home = () => {
|
|||||||
setSortMenuVisible(false);
|
setSortMenuVisible(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleFilterChange = (newFilter: MEME_TYPE | undefined) => {
|
||||||
|
dispatch(setFilter(newFilter));
|
||||||
|
setFilterMenuVisible(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PaddedView>
|
<PaddedView>
|
||||||
|
<Searchbar placeholder="Search" value={search} onChangeText={setSearch} />
|
||||||
<View style={[styles.flexRowSpaceBetween, styles.centeredVertical]}>
|
<View style={[styles.flexRowSpaceBetween, styles.centeredVertical]}>
|
||||||
<Menu
|
<View style={[styles.flexRow, styles.centeredVertical]}>
|
||||||
visible={sortMenuVisible}
|
<Menu
|
||||||
onDismiss={() => setSortMenuVisible(false)}
|
visible={sortMenuVisible}
|
||||||
anchor={
|
onDismiss={() => setSortMenuVisible(false)}
|
||||||
<Button
|
anchor={
|
||||||
onPress={() => setSortMenuVisible(true)}
|
<Button
|
||||||
icon={getSortIcon(sort, sortDirection)}
|
onPress={() => setSortMenuVisible(true)}
|
||||||
contentStyle={styles.flexRowReverse}>
|
icon={getSortIcon(sort, sortDirection)}
|
||||||
Sort By: {sort}
|
contentStyle={styles.flexRowReverse}
|
||||||
</Button>
|
compact>
|
||||||
}>
|
Sort By: {sort}
|
||||||
{Object.keys(SORT).map(key => {
|
</Button>
|
||||||
return (
|
}>
|
||||||
<Menu.Item
|
{Object.keys(SORT).map(key => {
|
||||||
key={key}
|
return (
|
||||||
onPress={() =>
|
<Menu.Item
|
||||||
handleSortModeChange(SORT[key as keyof typeof SORT])
|
key={key}
|
||||||
}
|
onPress={() =>
|
||||||
title={SORT[key as keyof typeof SORT]}
|
handleSortModeChange(SORT[key as keyof typeof SORT])
|
||||||
/>
|
}
|
||||||
);
|
title={SORT[key as keyof typeof SORT]}
|
||||||
})}
|
/>
|
||||||
</Menu>
|
);
|
||||||
|
})}
|
||||||
|
</Menu>
|
||||||
|
</View>
|
||||||
<View style={styles.flexRow}>
|
<View style={styles.flexRow}>
|
||||||
<IconButton
|
|
||||||
icon={favoritesOnly ? 'heart' : 'heart-outline'}
|
|
||||||
iconColor={theme.colors.primary}
|
|
||||||
size={verticalScale(16)}
|
|
||||||
onPress={() => dispatch(toggleFavoritesOnly())}
|
|
||||||
/>
|
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={getViewIcon(view)}
|
icon={getViewIcon(view)}
|
||||||
iconColor={theme.colors.primary}
|
iconColor={theme.colors.primary}
|
||||||
size={verticalScale(16)}
|
size={verticalScale(16)}
|
||||||
onPress={() => dispatch(cycleView())}
|
onPress={() => dispatch(cycleView())}
|
||||||
/>
|
/>
|
||||||
|
<IconButton
|
||||||
|
icon={favoritesOnly ? 'heart' : 'heart-outline'}
|
||||||
|
iconColor={theme.colors.primary}
|
||||||
|
size={verticalScale(16)}
|
||||||
|
onPress={() => dispatch(toggleFavoritesOnly())}
|
||||||
|
/>
|
||||||
|
<Menu
|
||||||
|
visible={filterMenuVisible}
|
||||||
|
onDismiss={() => setFilterMenuVisible(false)}
|
||||||
|
anchor={
|
||||||
|
<IconButton
|
||||||
|
onPress={() => setFilterMenuVisible(true)}
|
||||||
|
icon={filter ? 'filter' : 'filter-outline'}
|
||||||
|
iconColor={theme.colors.primary}
|
||||||
|
size={verticalScale(16)}
|
||||||
|
/>
|
||||||
|
}>
|
||||||
|
<Menu.Item
|
||||||
|
// eslint-disable-next-line unicorn/no-useless-undefined
|
||||||
|
onPress={() => handleFilterChange(undefined)}
|
||||||
|
title="All"
|
||||||
|
/>
|
||||||
|
{Object.keys(MEME_TYPE).map(key => {
|
||||||
|
return (
|
||||||
|
<Menu.Item
|
||||||
|
key={key}
|
||||||
|
onPress={() =>
|
||||||
|
handleFilterChange(MEME_TYPE[key as keyof typeof MEME_TYPE])
|
||||||
|
}
|
||||||
|
title={
|
||||||
|
memeTypePlural[MEME_TYPE[key as keyof typeof MEME_TYPE]]
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Menu>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<Divider />
|
<Divider />
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
export { default as AddItem } from './addItem';
|
export { default as AddItem } from './addItem';
|
||||||
export { default as Home } from './home';
|
export { default as Home } from './home';
|
||||||
export { default as Search } from './search';
|
|
||||||
export { default as Settings } from './settings';
|
export { default as Settings } from './settings';
|
||||||
export { default as Tags } from './tags';
|
export { default as Tags } from './tags';
|
||||||
export { default as Welcome } from './welcome';
|
export { default as Welcome } from './welcome';
|
||||||
|
@@ -1,14 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { Text } from 'react-native-paper';
|
|
||||||
import { PaddedView } from '../components';
|
|
||||||
|
|
||||||
const Search = () => {
|
|
||||||
return (
|
|
||||||
<PaddedView centered>
|
|
||||||
<Text>Search</Text>
|
|
||||||
</PaddedView>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Search;
|
|
@@ -1,11 +1,13 @@
|
|||||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||||
import { SORT, SORT_DIRECTION, VIEW } from '../types';
|
import { SORT, SORT_DIRECTION, VIEW } from '../types';
|
||||||
|
import { MEME_TYPE } from '../database';
|
||||||
|
|
||||||
interface HomeState {
|
interface HomeState {
|
||||||
sort: SORT;
|
sort: SORT;
|
||||||
sortDirection: SORT_DIRECTION;
|
sortDirection: SORT_DIRECTION;
|
||||||
view: VIEW;
|
view: VIEW;
|
||||||
favoritesOnly: boolean;
|
favoritesOnly: boolean;
|
||||||
|
filter: MEME_TYPE | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: HomeState = {
|
const initialState: HomeState = {
|
||||||
@@ -13,6 +15,7 @@ const initialState: HomeState = {
|
|||||||
sortDirection: SORT_DIRECTION.ASCENDING,
|
sortDirection: SORT_DIRECTION.ASCENDING,
|
||||||
view: VIEW.MASONRY,
|
view: VIEW.MASONRY,
|
||||||
favoritesOnly: false,
|
favoritesOnly: false,
|
||||||
|
filter: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
const homeSlice = createSlice({
|
const homeSlice = createSlice({
|
||||||
@@ -53,6 +56,9 @@ const homeSlice = createSlice({
|
|||||||
toggleFavoritesOnly: state => {
|
toggleFavoritesOnly: state => {
|
||||||
state.favoritesOnly = !state.favoritesOnly;
|
state.favoritesOnly = !state.favoritesOnly;
|
||||||
},
|
},
|
||||||
|
setFilter: (state, action: PayloadAction<MEME_TYPE | undefined>) => {
|
||||||
|
state.filter = action.payload;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -64,6 +70,7 @@ const {
|
|||||||
cycleView,
|
cycleView,
|
||||||
setFavoritesOnly,
|
setFavoritesOnly,
|
||||||
toggleFavoritesOnly,
|
toggleFavoritesOnly,
|
||||||
|
setFilter,
|
||||||
} = homeSlice.actions;
|
} = homeSlice.actions;
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@@ -75,5 +82,6 @@ export {
|
|||||||
cycleView,
|
cycleView,
|
||||||
setFavoritesOnly,
|
setFavoritesOnly,
|
||||||
toggleFavoritesOnly,
|
toggleFavoritesOnly,
|
||||||
|
setFilter,
|
||||||
};
|
};
|
||||||
export default homeSlice.reducer;
|
export default homeSlice.reducer;
|
||||||
|
@@ -58,4 +58,5 @@ export {
|
|||||||
cycleView,
|
cycleView,
|
||||||
setFavoritesOnly,
|
setFavoritesOnly,
|
||||||
toggleFavoritesOnly,
|
toggleFavoritesOnly,
|
||||||
|
setFilter,
|
||||||
} from './home';
|
} from './home';
|
||||||
|
@@ -37,6 +37,9 @@ const styles = StyleSheet.create({
|
|||||||
centerText: {
|
centerText: {
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
},
|
},
|
||||||
|
flex: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
flexRow: {
|
flexRow: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
},
|
},
|
||||||
|
Reference in New Issue
Block a user