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

View File

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

View File

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

View File

@@ -2,6 +2,7 @@ import React, { ReactNode } from 'react';
import { StyleProp, View, ViewStyle } from 'react-native';
import { useTheme } from 'react-native-paper';
import styles from '../styles';
import { useDimensions } from '../contexts';
const RootView = ({
children,
@@ -15,11 +16,21 @@ const RootView = ({
padded?: boolean;
}) => {
const { colors } = useTheme();
const { orientation } = useDimensions();
return (
<View
style={[
padded && styles.padding,
padded &&
orientation == 'portrait' && [
styles.paddingHorizontal,
styles.paddingVertical,
],
padded &&
orientation == 'landscape' && [
styles.paddingHorizontal,
styles.smallPaddingVertical,
],
centered && [styles.centered, styles.flex],
{ backgroundColor: colors.background },
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 { View } from 'react-native';
import { StyleSheet, View } from 'react-native';
import FontAwesome5 from 'react-native-vector-icons/FontAwesome5';
import { Chip } from 'react-native-paper';
import styles from '../styles';
import { useDimensions } from '../contexts';
import { getContrastColor } from '../utilities';
const tagPreviewStyles = StyleSheet.create({
chip: {
padding: 5,
},
text: {
fontSize: 18,
},
});
const TagPreview = (properties: { name: string; color: string }) => {
const dimensions = useDimensions();
const { responsive } = useDimensions();
const contrastColor = getContrastColor(properties.color);
return (
<View
style={[
styles.centeredHorizontal,
styles.justifyCenter,
styles.flexRow,
{
margin: dimensions.responsive.verticalScale(50),
margin: responsive.verticalScale(50),
},
]}>
<Chip
icon={() => {
return (
<FontAwesome5
name="tag"
size={dimensions.static.horizontalScale(12)}
color={getContrastColor(properties.color)}
/>
);
return <FontAwesome5 name="tag" size={14} color={contrastColor} />;
}}
elevated
style={[
tagPreviewStyles.chip,
{
backgroundColor: properties.color,
padding: dimensions.static.verticalScale(5),
},
]}
textStyle={[
{ fontSize: dimensions.static.horizontalScale(15) },
{ color: getContrastColor(properties.color) },
]}>
textStyle={[tagPreviewStyles.text, { color: contrastColor }]}>
{'#' + properties.name}
</Chip>
</View>

View File

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

View File

@@ -1,2 +1,2 @@
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?',
isFavorite: { type: 'bool', indexed: true, default: false },
tags: 'Tag[]',
dateCreated: 'date',
dateModified: 'date',
dateCreated: { type: 'date', default: new Date() },
dateModified: { type: 'date', default: new Date() },
dateUsed: 'date?',
timesUsed: { type: 'int', default: 0 },
},

View File

@@ -6,6 +6,10 @@ class Tag extends Realm.Object<Tag> {
name!: string;
color!: string;
memes!: Realm.List<Meme>;
dateCreated!: Date;
dateModified!: Date;
dateUsed?: Date;
timesUsed!: number;
static schema: Realm.ObjectSchema = {
name: 'Tag',
@@ -15,14 +19,25 @@ class Tag extends Realm.Object<Tag> {
name: 'string',
color: 'string',
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) => {
realm.write(() => {
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 { Home, Tags, Settings, AddMeme, AddTag } from './screens';
import { darkNavigationTheme, lightNavigationTheme } from './theme';
import { useDimensions } from './contexts';
import { FloatingActionButton } from './components';
const TabNavigator = () => {
const dimensions = useDimensions();
const [showFab, setShowFab] = React.useState(true);
const TabNavigatorBase = createBottomTabNavigator();
@@ -50,7 +47,7 @@ const TabNavigator = () => {
return options.tabBarIcon({
focused,
color,
size: dimensions.static.horizontalScale(20),
size: 22,
});
}
}}

View File

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

View File

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

View File

@@ -1,22 +1,55 @@
import React from 'react';
import { Button, Text } from 'react-native-paper';
import { RootScrollView } from '../components';
import React, { useState } from 'react';
import { DataTable, HelperText, Searchbar } from 'react-native-paper';
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 realm = useRealm();
const tags = useQuery<Tag>('Tag');
const [search, setSearch] = useState('');
const tags = useQuery<Tag>('Tag').filtered(`name CONTAINS[c] "${search}"`);
return (
<RootScrollView centered padded>
{tags.map(tag => (
<Text key={tag.id.toHexString()} style={{ color: tag.color }}>
{tag.name}
</Text>
))}
<Button onPress={() => deleteAllTags(realm)}>Delete All Tags</Button>
<RootScrollView padded style={styles.alignCenter}>
<Searchbar
placeholder="Search Tags"
value={search}
onChangeText={setSearch}
/>
<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>
);
};

View File

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

View File

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