Add tag datatable

Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
This commit is contained in:
2023-07-14 19:35:07 +03:00
parent 498c3e77cc
commit 1e36e01ea1
16 changed files with 197 additions and 96 deletions

View File

@@ -1,23 +1,18 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { StyleSheet, Keyboard } from 'react-native'; import { Keyboard } from 'react-native';
import { FAB } from 'react-native-paper'; import { FAB } from 'react-native-paper';
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 { useDimensions } from '../contexts'; import { useDimensions } from '../contexts';
const styles = StyleSheet.create({
fab: {
position: 'absolute',
},
});
const FloatingActionButton = ({ visible = true }: { visible?: boolean }) => { const FloatingActionButton = ({ visible = true }: { visible?: boolean }) => {
const { navigate } = const { navigate } =
useNavigation<NativeStackNavigationProp<ParamListBase>>(); useNavigation<NativeStackNavigationProp<ParamListBase>>();
const dimensions = useDimensions(); const { responsive } = useDimensions();
const [state, setState] = useState(false); const [state, setState] = useState(false);
const [keyboardOpen, setKeyboardOpen] = useState(false); const [keyboardOpen, setKeyboardOpen] = useState(false);
useEffect(() => { useEffect(() => {
const keyboardDidShowListener = Keyboard.addListener( const keyboardDidShowListener = Keyboard.addListener(
'keyboardDidShow', 'keyboardDidShow',
@@ -29,8 +24,8 @@ const FloatingActionButton = ({ visible = true }: { visible?: boolean }) => {
); );
return () => { return () => {
keyboardDidHideListener.remove();
keyboardDidShowListener.remove(); keyboardDidShowListener.remove();
keyboardDidHideListener.remove();
}; };
}, []); }, []);
@@ -60,13 +55,10 @@ const FloatingActionButton = ({ visible = true }: { visible?: boolean }) => {
onPress={() => { onPress={() => {
if (state) navigate('Add Meme'); if (state) navigate('Add Meme');
}} }}
style={[ style={{
styles.fab, paddingBottom: responsive.verticalScale(75),
{ paddingRight: responsive.horizontalScale(10),
paddingRight: dimensions.responsive.horizontalScale(10), }}
paddingBottom: dimensions.responsive.verticalScale(75),
},
]}
/> />
); );
}; };

View File

@@ -2,4 +2,5 @@ export { default as FloatingActionButton } from './floatingActionButton';
export { default as LoadingView } from './loadingView'; export { default as LoadingView } from './loadingView';
export { default as RootScrollView } from './rootScrollView'; export { default as RootScrollView } from './rootScrollView';
export { default as RootView } from './rootView'; export { default as RootView } from './rootView';
export { default as TagChip } from './tagChip';
export { default as TagPreview } from './tagPreview'; export { default as TagPreview } from './tagPreview';

View File

@@ -1,11 +1,8 @@
import React, { ReactNode } from 'react'; import React, { ReactNode } from 'react';
import { import { StyleProp, ScrollView, ViewStyle } from 'react-native';
StyleProp,
ScrollView,
ViewStyle,
} from 'react-native';
import { useTheme } from 'react-native-paper'; import { useTheme } from 'react-native-paper';
import styles from '../styles'; import styles from '../styles';
import { useDimensions } from '../contexts';
const RootScrollView = ({ const RootScrollView = ({
children, children,
@@ -19,11 +16,21 @@ const RootScrollView = ({
padded?: boolean; padded?: boolean;
}) => { }) => {
const { colors } = useTheme(); const { colors } = useTheme();
const { orientation } = useDimensions();
return ( return (
<ScrollView <ScrollView
contentContainerStyle={[ contentContainerStyle={[
padded && styles.padding, padded &&
orientation == 'portrait' && [
styles.paddingHorizontal,
styles.paddingVertical,
],
padded &&
orientation == 'landscape' && [
styles.paddingHorizontal,
styles.smallPaddingVertical,
],
centered && [styles.centered, styles.flex], centered && [styles.centered, styles.flex],
{ backgroundColor: colors.background }, { backgroundColor: colors.background },
style, style,

View File

@@ -2,6 +2,7 @@ import React, { ReactNode } from 'react';
import { StyleProp, View, ViewStyle } from 'react-native'; import { StyleProp, View, ViewStyle } from 'react-native';
import { useTheme } from 'react-native-paper'; import { useTheme } from 'react-native-paper';
import styles from '../styles'; import styles from '../styles';
import { useDimensions } from '../contexts';
const RootView = ({ const RootView = ({
children, children,
@@ -15,11 +16,21 @@ const RootView = ({
padded?: boolean; padded?: boolean;
}) => { }) => {
const { colors } = useTheme(); const { colors } = useTheme();
const { orientation } = useDimensions();
return ( return (
<View <View
style={[ style={[
padded && styles.padding, padded &&
orientation == 'portrait' && [
styles.paddingHorizontal,
styles.paddingVertical,
],
padded &&
orientation == 'landscape' && [
styles.paddingHorizontal,
styles.smallPaddingVertical,
],
centered && [styles.centered, styles.flex], centered && [styles.centered, styles.flex],
{ backgroundColor: colors.background }, { backgroundColor: colors.background },
style, style,

View File

@@ -0,0 +1,27 @@
import React from 'react';
import { getContrastColor } from '../utilities';
import { Chip } from 'react-native-paper';
import { Tag } from '../database';
import FontAwesome5 from 'react-native-vector-icons/FontAwesome5';
const TagChip = (properties: { tag: Tag }) => {
const contrastColor = getContrastColor(properties.tag.color);
return (
<Chip
icon={() => {
return <FontAwesome5 name="tag" color={contrastColor} />;
}}
compact
style={[
{
backgroundColor: properties.tag.color,
},
]}
textStyle={{ color: contrastColor }}>
{'#' + properties.tag.name}
</Chip>
);
};
export default TagChip;

View File

@@ -1,44 +1,45 @@
import React from 'react'; import React from 'react';
import { View } from 'react-native'; import { StyleSheet, View } from 'react-native';
import FontAwesome5 from 'react-native-vector-icons/FontAwesome5'; import FontAwesome5 from 'react-native-vector-icons/FontAwesome5';
import { Chip } from 'react-native-paper'; import { Chip } from 'react-native-paper';
import styles from '../styles'; import styles from '../styles';
import { useDimensions } from '../contexts'; import { useDimensions } from '../contexts';
import { getContrastColor } from '../utilities'; import { getContrastColor } from '../utilities';
const tagPreviewStyles = StyleSheet.create({
chip: {
padding: 5,
},
text: {
fontSize: 18,
},
});
const TagPreview = (properties: { name: string; color: string }) => { const TagPreview = (properties: { name: string; color: string }) => {
const dimensions = useDimensions(); const { responsive } = useDimensions();
const contrastColor = getContrastColor(properties.color);
return ( return (
<View <View
style={[ style={[
styles.centeredHorizontal, styles.justifyCenter,
styles.flexRow, styles.flexRow,
{ {
margin: dimensions.responsive.verticalScale(50), margin: responsive.verticalScale(50),
}, },
]}> ]}>
<Chip <Chip
icon={() => { icon={() => {
return ( return <FontAwesome5 name="tag" size={14} color={contrastColor} />;
<FontAwesome5
name="tag"
size={dimensions.static.horizontalScale(12)}
color={getContrastColor(properties.color)}
/>
);
}} }}
elevated elevated
style={[ style={[
tagPreviewStyles.chip,
{ {
backgroundColor: properties.color, backgroundColor: properties.color,
padding: dimensions.static.verticalScale(5),
}, },
]} ]}
textStyle={[ textStyle={[tagPreviewStyles.text, { color: contrastColor }]}>
{ fontSize: dimensions.static.horizontalScale(15) },
{ color: getContrastColor(properties.color) },
]}>
{'#' + properties.name} {'#' + properties.name}
</Chip> </Chip>
</View> </View>

View File

@@ -3,6 +3,7 @@ import React, {
createContext, createContext,
useContext, useContext,
useEffect, useEffect,
useMemo,
useState, useState,
} from 'react'; } from 'react';
import { Dimensions, ScaledSize } from 'react-native'; import { Dimensions, ScaledSize } from 'react-native';
@@ -18,6 +19,7 @@ interface ScaleFunctions {
interface DimensionsContext { interface DimensionsContext {
orientation: 'portrait' | 'landscape'; orientation: 'portrait' | 'landscape';
dimensions: ScaledSize;
responsive: ScaleFunctions; responsive: ScaleFunctions;
static: ScaleFunctions; static: ScaleFunctions;
} }
@@ -42,15 +44,16 @@ const DimensionsProvider = ({ children }: { children: ReactNode }) => {
const orientation = const orientation =
dimensions.width > dimensions.height ? 'landscape' : 'portrait'; dimensions.width > dimensions.height ? 'landscape' : 'portrait';
const [initialDimensions, setInitialDimensions] = useState(dimensions); const initialDimensions = useMemo(() => {
const [initialOrientation] = useState(orientation); if (orientation === 'landscape') {
return {
if (initialOrientation === 'landscape') { width: dimensions.height,
setInitialDimensions({ height: dimensions.width,
width: initialDimensions.height, } as ScaledSize;
height: initialDimensions.width, }
} as ScaledSize); return dimensions;
} // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const responsiveScale = createScaleFunctions(dimensions); const responsiveScale = createScaleFunctions(dimensions);
const staticScale = createScaleFunctions(initialDimensions); const staticScale = createScaleFunctions(initialDimensions);
@@ -71,6 +74,7 @@ const DimensionsProvider = ({ children }: { children: ReactNode }) => {
<DimensionsContext.Provider <DimensionsContext.Provider
value={{ value={{
orientation, orientation,
dimensions,
responsive: responsiveScale, responsive: responsiveScale,
static: staticScale, static: staticScale,
}}> }}>

View File

@@ -1,2 +1,2 @@
export { MEME_TYPE, memeTypePlural, Meme } from './meme'; export { MEME_TYPE, memeTypePlural, Meme } from './meme';
export { Tag, deleteAllTags } from './tag'; export { Tag, deleteTag, deleteAllTags } from './tag';

View File

@@ -45,8 +45,8 @@ class Meme extends Realm.Object<Meme> {
description: 'string?', description: 'string?',
isFavorite: { type: 'bool', indexed: true, default: false }, isFavorite: { type: 'bool', indexed: true, default: false },
tags: 'Tag[]', tags: 'Tag[]',
dateCreated: 'date', dateCreated: { type: 'date', default: new Date() },
dateModified: 'date', dateModified: { type: 'date', default: new Date() },
dateUsed: 'date?', dateUsed: 'date?',
timesUsed: { type: 'int', default: 0 }, timesUsed: { type: 'int', default: 0 },
}, },

View File

@@ -6,6 +6,10 @@ class Tag extends Realm.Object<Tag> {
name!: string; name!: string;
color!: string; color!: string;
memes!: Realm.List<Meme>; memes!: Realm.List<Meme>;
dateCreated!: Date;
dateModified!: Date;
dateUsed?: Date;
timesUsed!: number;
static schema: Realm.ObjectSchema = { static schema: Realm.ObjectSchema = {
name: 'Tag', name: 'Tag',
@@ -15,14 +19,25 @@ class Tag extends Realm.Object<Tag> {
name: 'string', name: 'string',
color: 'string', color: 'string',
memes: 'Meme[]', memes: 'Meme[]',
dateCreated: { type: 'date', default: new Date() },
dateModified: { type: 'date', default: new Date() },
dateUsed: 'date?',
timesUsed: { type: 'int', default: 0 },
}, },
}; };
} }
const deleteTag = (realm: Realm, tag: Tag) => {
realm.write(() => {
realm.delete(tag);
});
};
const deleteAllTags = (realm: Realm) => { const deleteAllTags = (realm: Realm) => {
realm.write(() => { realm.write(() => {
realm.delete(realm.objects<Tag>('Tag')); realm.delete(realm.objects<Tag>('Tag'));
}); });
}; };
export { Tag, deleteAllTags };
export { Tag, deleteTag, deleteAllTags };

View File

@@ -9,12 +9,9 @@ import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { BottomNavigation, useTheme } from 'react-native-paper'; import { BottomNavigation, useTheme } from 'react-native-paper';
import { Home, Tags, Settings, AddMeme, AddTag } from './screens'; import { Home, Tags, Settings, AddMeme, AddTag } from './screens';
import { darkNavigationTheme, lightNavigationTheme } from './theme'; import { darkNavigationTheme, lightNavigationTheme } from './theme';
import { useDimensions } from './contexts';
import { FloatingActionButton } from './components'; import { FloatingActionButton } from './components';
const TabNavigator = () => { const TabNavigator = () => {
const dimensions = useDimensions();
const [showFab, setShowFab] = React.useState(true); const [showFab, setShowFab] = React.useState(true);
const TabNavigatorBase = createBottomTabNavigator(); const TabNavigatorBase = createBottomTabNavigator();
@@ -50,7 +47,7 @@ const TabNavigator = () => {
return options.tabBarIcon({ return options.tabBarIcon({
focused, focused,
color, color,
size: dimensions.static.horizontalScale(20), size: 22,
}); });
} }
}} }}

View File

@@ -23,11 +23,9 @@ import {
setFilter, setFilter,
} from '../state'; } from '../state';
import { MEME_TYPE, memeTypePlural } from '../database'; import { MEME_TYPE, memeTypePlural } from '../database';
import { useDimensions } from '../contexts';
const Home = () => { const Home = () => {
const theme = useTheme(); const theme = useTheme();
const dimensions = useDimensions();
const sort = useSelector((state: RootState) => state.home.sort); const sort = useSelector((state: RootState) => state.home.sort);
const sortDirection = useSelector( const sortDirection = useSelector(
(state: RootState) => state.home.sortDirection, (state: RootState) => state.home.sortDirection,
@@ -65,9 +63,13 @@ const Home = () => {
return ( return (
<RootScrollView padded> <RootScrollView padded>
<Searchbar placeholder="Search" value={search} onChangeText={setSearch} /> <Searchbar
<View style={[styles.flexRowSpaceBetween, styles.centeredVertical]}> placeholder="Search Memes"
<View style={[styles.flexRow, styles.centeredVertical]}> value={search}
onChangeText={setSearch}
/>
<View style={[styles.flexRowSpaceBetween, styles.alignCenter]}>
<View style={[styles.flexRow, styles.alignCenter]}>
<Menu <Menu
visible={sortMenuVisible} visible={sortMenuVisible}
onDismiss={() => setSortMenuVisible(false)} onDismiss={() => setSortMenuVisible(false)}
@@ -97,13 +99,13 @@ const Home = () => {
<IconButton <IconButton
icon={getViewIcon(view)} icon={getViewIcon(view)}
iconColor={theme.colors.primary} iconColor={theme.colors.primary}
size={dimensions.static.verticalScale(16)} size={18}
onPress={() => dispatch(cycleView())} onPress={() => dispatch(cycleView())}
/> />
<IconButton <IconButton
icon={favoritesOnly ? 'heart' : 'heart-outline'} icon={favoritesOnly ? 'heart' : 'heart-outline'}
iconColor={theme.colors.primary} iconColor={theme.colors.primary}
size={dimensions.static.verticalScale(16)} size={18}
onPress={() => dispatch(toggleFavoritesOnly())} onPress={() => dispatch(toggleFavoritesOnly())}
/> />
<Menu <Menu
@@ -114,7 +116,7 @@ const Home = () => {
onPress={() => setFilterMenuVisible(true)} onPress={() => setFilterMenuVisible(true)}
icon={filter ? 'filter' : 'filter-outline'} icon={filter ? 'filter' : 'filter-outline'}
iconColor={theme.colors.primary} iconColor={theme.colors.primary}
size={dimensions.static.verticalScale(16)} size={18}
/> />
}> }>
<Menu.Item <Menu.Item

View File

@@ -1,5 +1,5 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { View } from 'react-native'; import { StyleSheet, View } from 'react-native';
import { import {
Button, Button,
List, List,
@@ -12,18 +12,20 @@ import { openDocumentTree } from 'react-native-scoped-storage';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { RootScrollView } from '../components'; import { RootScrollView } from '../components';
import styles from '../styles'; import styles from '../styles';
import { import { RootState, updateNoMedia, updateStorageUri } from '../state';
RootState,
updateNoMedia,
updateStorageUri,
} from '../state';
import type {} from 'redux-thunk/extend-redux'; import type {} from 'redux-thunk/extend-redux';
import { useDimensions } from '../contexts'; import { useDimensions } from '../contexts';
const settingsScreenStyles = StyleSheet.create({
snackbar: {
marginBottom: 90,
},
});
const SettingsScreen = () => { const SettingsScreen = () => {
const noMedia = useSelector((state: RootState) => state.settings.noMedia); const noMedia = useSelector((state: RootState) => state.settings.noMedia);
const dispatch = useDispatch(); const dispatch = useDispatch();
const dimensions = useDimensions(); const { responsive } = useDimensions();
const [optimizingDatabase, setOptimizingDatabase] = useState(false); const [optimizingDatabase, setOptimizingDatabase] = useState(false);
const [snackbarVisible, setSnackbarVisible] = useState(false); const [snackbarVisible, setSnackbarVisible] = useState(false);
@@ -46,7 +48,7 @@ const SettingsScreen = () => {
<Button <Button
mode="elevated" mode="elevated"
style={{ style={{
marginBottom: dimensions.responsive.verticalScale(15), marginBottom: responsive.verticalScale(15),
}} }}
loading={optimizingDatabase} loading={optimizingDatabase}
onPress={optimizeDatabase}> onPress={optimizeDatabase}>
@@ -58,7 +60,7 @@ const SettingsScreen = () => {
<Button <Button
mode="elevated" mode="elevated"
style={{ style={{
marginBottom: dimensions.responsive.verticalScale(15), marginBottom: responsive.verticalScale(15),
}} }}
onPress={async () => { onPress={async () => {
const { uri } = await openDocumentTree(true); const { uri } = await openDocumentTree(true);
@@ -71,7 +73,7 @@ const SettingsScreen = () => {
styles.flexRowSpaceBetween, styles.flexRowSpaceBetween,
styles.smallPaddingHorizontal, styles.smallPaddingHorizontal,
{ {
marginBottom: dimensions.responsive.verticalScale(15), marginBottom: responsive.verticalScale(15),
}, },
]}> ]}>
<Text>Hide media from gallery</Text> <Text>Hide media from gallery</Text>
@@ -89,7 +91,7 @@ const SettingsScreen = () => {
<Snackbar <Snackbar
visible={snackbarVisible} visible={snackbarVisible}
onDismiss={() => setSnackbarVisible(false)} onDismiss={() => setSnackbarVisible(false)}
style={{ marginBottom: dimensions.static.verticalScale(75) }} style={settingsScreenStyles.snackbar}
action={{ action={{
label: 'Dismiss', label: 'Dismiss',
onPress: () => setSnackbarVisible(false), onPress: () => setSnackbarVisible(false),

View File

@@ -1,22 +1,55 @@
import React from 'react'; import React, { useState } from 'react';
import { DataTable, HelperText, Searchbar } from 'react-native-paper';
import { Button, Text } from 'react-native-paper';
import { RootScrollView } from '../components';
import { useQuery, useRealm } from '@realm/react'; import { useQuery, useRealm } from '@realm/react';
import { Tag, deleteAllTags } from '../database'; import { RootScrollView, TagChip } from '../components';
import { Tag, deleteTag } from '../database';
import { StyleSheet, View } from 'react-native';
import styles from '../styles';
const tagStyles = StyleSheet.create({
helperText: {
marginVertical: 10,
},
view: {
justifyContent: 'center',
maxWidth: '75%',
},
});
const Tags = () => { const Tags = () => {
const realm = useRealm(); const realm = useRealm();
const tags = useQuery<Tag>('Tag');
const [search, setSearch] = useState('');
const tags = useQuery<Tag>('Tag').filtered(`name CONTAINS[c] "${search}"`);
return ( return (
<RootScrollView centered padded> <RootScrollView padded style={styles.alignCenter}>
{tags.map(tag => ( <Searchbar
<Text key={tag.id.toHexString()} style={{ color: tag.color }}> placeholder="Search Tags"
{tag.name} value={search}
</Text> onChangeText={setSearch}
))} />
<Button onPress={() => deleteAllTags(realm)}>Delete All Tags</Button> <DataTable>
<DataTable.Header>
<DataTable.Title>Tag</DataTable.Title>
<DataTable.Title numeric>Items</DataTable.Title>
</DataTable.Header>
{tags.map(tag => (
<DataTable.Row
key={tag.id.toHexString()}
onPress={() => deleteTag(realm, tag)}>
<View style={tagStyles.view}>
<TagChip tag={tag} />
</View>
<DataTable.Cell numeric>{tag.memes.length}</DataTable.Cell>
</DataTable.Row>
))}
</DataTable>
{tags.length === 0 && (
<HelperText type={'info'} style={tagStyles.helperText}>
No tags found
</HelperText>
)}
</RootScrollView> </RootScrollView>
); );
}; };

View File

@@ -10,7 +10,7 @@ import { useDimensions } from '../contexts';
const Welcome = ({ onWelcomeComplete }: { onWelcomeComplete: () => void }) => { const Welcome = ({ onWelcomeComplete }: { onWelcomeComplete: () => void }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const dimensions = useDimensions(); const { responsive } = useDimensions();
const selectStorageLocation = async () => { const selectStorageLocation = async () => {
const uri = await openDocumentTree(true).catch(noOp); const uri = await openDocumentTree(true).catch(noOp);
@@ -25,7 +25,7 @@ const Welcome = ({ onWelcomeComplete }: { onWelcomeComplete: () => void }) => {
variant="displayMedium" variant="displayMedium"
style={[ style={[
{ {
marginBottom: dimensions.responsive.verticalScale(30), marginBottom: responsive.verticalScale(30),
}, },
styles.centerText, styles.centerText,
]}> ]}>
@@ -35,7 +35,7 @@ const Welcome = ({ onWelcomeComplete }: { onWelcomeComplete: () => void }) => {
mode="contained" mode="contained"
onPress={selectStorageLocation} onPress={selectStorageLocation}
style={{ style={{
marginBottom: dimensions.responsive.verticalScale(100), marginBottom: responsive.verticalScale(100),
}}> }}>
Select Storage Location Select Storage Location
</Button> </Button>

View File

@@ -2,22 +2,31 @@ import { StyleSheet } from 'react-native';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
smallPadding: { smallPadding: {
padding: '2.5%', padding: '2%',
},
smallPaddingHorizontal: {
paddingHorizontal: '2%',
},
smallPaddingVertical: {
paddingVertical: '2%',
}, },
padding: { padding: {
padding: '5%', padding: '5%',
}, },
smallPaddingHorizontal: { paddingHorizontal: {
paddingHorizontal: '2.5%', paddingHorizontal: '5%',
},
paddingVertical: {
paddingVertical: '5%',
}, },
centered: { centered: {
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
}, },
centeredVertical: { alignCenter: {
alignItems: 'center', alignItems: 'center',
}, },
centeredHorizontal: { justifyCenter: {
justifyContent: 'center', justifyContent: 'center',
}, },
centerText: { centerText: {