Fix layout bugs

Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
This commit is contained in:
2023-07-22 12:28:02 +03:00
parent a4726eb89a
commit fa0a89f324
25 changed files with 561 additions and 400 deletions

View File

@@ -1,17 +1,28 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Keyboard } from 'react-native'; import { Keyboard, StyleSheet } 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 { pick } from 'react-native-document-picker'; import { pick } from 'react-native-document-picker';
import { useDimensions } from '../contexts'; import { ORIENTATION, useDimensions } from '../contexts';
import { ROUTE } from '../types'; import { ROUTE } from '../types';
import { allowedMimeTypes, noOp } from '../utilities'; import { allowedMimeTypes, noOp } from '../utilities';
const floatingActionButtonStyles = StyleSheet.create({
fab: {
paddingBottom: 90,
paddingRight: 10,
},
fabLandscape: {
paddingBottom: 40,
paddingRight: 12,
},
});
const FloatingActionButton = ({ visible = true }: { visible?: boolean }) => { const FloatingActionButton = ({ visible = true }: { visible?: boolean }) => {
const { navigate } = const { navigate } =
useNavigation<NativeStackNavigationProp<ParamListBase>>(); useNavigation<NativeStackNavigationProp<ParamListBase>>();
const { responsive } = useDimensions(); const { orientation } = useDimensions();
const [state, setState] = useState(false); const [state, setState] = useState(false);
const [keyboardOpen, setKeyboardOpen] = useState(false); const [keyboardOpen, setKeyboardOpen] = useState(false);
@@ -50,18 +61,6 @@ const FloatingActionButton = ({ visible = true }: { visible?: boolean }) => {
throw new Error('Not yet implemented'); throw new Error('Not yet implemented');
}, },
}, },
{
icon: 'image-album',
label: 'Album',
onPress: async () => {
const res = await pick({
allowMultiSelection: true,
type: allowedMimeTypes,
}).catch(noOp);
if (!res) return;
navigate(ROUTE.ADD_MEME, { uri: res });
},
},
]} ]}
onStateChange={({ open }) => setState(open)} onStateChange={({ open }) => setState(open)}
onPress={async () => { onPress={async () => {
@@ -70,10 +69,11 @@ const FloatingActionButton = ({ visible = true }: { visible?: boolean }) => {
if (!res) return; if (!res) return;
navigate(ROUTE.ADD_MEME, { uri: res }); navigate(ROUTE.ADD_MEME, { uri: res });
}} }}
style={{ style={
paddingBottom: responsive.verticalScale(75), orientation === ORIENTATION.PORTRAIT
paddingRight: responsive.horizontalScale(10), ? floatingActionButtonStyles.fab
}} : floatingActionButtonStyles.fabLandscape
}
/> />
); );
}; };

View File

@@ -1,6 +1,6 @@
import React, { useEffect, useRef } from 'react'; import React, { useEffect, useRef } from 'react';
import { Animated, StyleSheet } from 'react-native'; import { Animated, StyleSheet } from 'react-native';
import { useDimensions } from '../contexts'; import { ORIENTATION, useDimensions } from '../contexts';
import styles from '../styles'; import styles from '../styles';
import { useTheme } from 'react-native-paper'; import { useTheme } from 'react-native-paper';
@@ -38,8 +38,9 @@ const HideableHeader = ({
<Animated.View <Animated.View
style={[ style={[
hideableHeaderStyles.headerView, hideableHeaderStyles.headerView,
orientation == 'portrait' && styles.paddingTop, orientation === ORIENTATION.PORTRAIT
orientation == 'landscape' && styles.smallPaddingTop, ? styles.paddingTop
: styles.smallPaddingTop,
styles.paddingHorizontal, styles.paddingHorizontal,
{ {
transform: [ transform: [

View File

@@ -1,5 +1,10 @@
export { MemeEditor } from './memes'; export {
export { TagChip, TagEditor, TagPreview } from './tags'; MemeEditor,
MemesHeader,
MemeTagSearchModal,
MemeTagSelector,
} from './memes';
export { TagChip, TagEditor, TagPreview, TagRow, TagsHeader } from './tags';
export { default as FloatingActionButton } from './floatingActionButton'; export { default as FloatingActionButton } from './floatingActionButton';
export { default as HideableBottomNavigationBar } from './hideableBottomNavigationBar'; export { default as HideableBottomNavigationBar } from './hideableBottomNavigationBar';
export { default as HideableHeader } from './hideableHeader'; export { default as HideableHeader } from './hideableHeader';

View File

@@ -1,3 +1,4 @@
export { default as MemeEditor } from './memeEditor'; export { default as MemeEditor } from './memeEditor';
export { default as MemesHeader } from './memesHeader';
export { default as MemeTagSearchModal } from './memeTagSearchModal'; export { default as MemeTagSearchModal } from './memeTagSearchModal';
export { default as MemeTagSelector } from './memeTagSelector'; export { default as MemeTagSelector } from './memeTagSelector';

View File

@@ -11,6 +11,19 @@ import {
validateMemeTitle, validateMemeTitle,
} from '../../utilities'; } from '../../utilities';
const memeEditorStyles = {
image: {
marginBottom: 15,
borderRadius: 5,
},
memeTagSelector: {
marginBottom: 10,
},
description: {
marginBottom: 10,
},
};
const MemeEditor = ({ const MemeEditor = ({
imageUri, imageUri,
memeTitle, memeTitle,
@@ -20,7 +33,7 @@ const MemeEditor = ({
memeTags, memeTags,
setMemeTags, setMemeTags,
}: { }: {
imageUri: string[]; imageUri: string;
memeTitle: StringValidationResult; memeTitle: StringValidationResult;
setMemeTitle: (name: StringValidationResult) => void; setMemeTitle: (name: StringValidationResult) => void;
memeDescription: StringValidationResult; memeDescription: StringValidationResult;
@@ -28,14 +41,14 @@ const MemeEditor = ({
memeTags: Map<string, Tag>; memeTags: Map<string, Tag>;
setMemeTags: (tags: Map<string, Tag>) => void; setMemeTags: (tags: Map<string, Tag>) => void;
}) => { }) => {
const { dimensions, fixed, responsive } = useDimensions(); const { dimensions } = useDimensions();
const [imageWidth, setImageWidth] = useState<number>(); const [imageWidth, setImageWidth] = useState<number>();
const [imageHeight, setImageHeight] = useState<number>(); const [imageHeight, setImageHeight] = useState<number>();
useEffect(() => { useEffect(() => {
Image.getSize(imageUri[0], (width, height) => { Image.getSize(imageUri, (width, height) => {
const paddedWidth = dimensions.width - dimensions.width * 0.08; const paddedWidth = dimensions.width * 0.92;
setImageWidth(paddedWidth); setImageWidth(paddedWidth);
setImageHeight((paddedWidth / width) * height); setImageHeight((paddedWidth / width) * height);
}); });
@@ -57,33 +70,31 @@ const MemeEditor = ({
{memeTitle.error} {memeTitle.error}
</HelperText> </HelperText>
<Image <Image
source={{ uri: imageUri[0] }} source={{ uri: imageUri }}
style={{ style={[
{
width: imageWidth, width: imageWidth,
height: imageHeight, height: imageHeight,
marginBottom: fixed.verticalScale(10), },
borderRadius: fixed.verticalScale(5), memeEditorStyles.image,
}} ]}
/> />
<MemeTagSelector <MemeTagSelector
memeTags={memeTags} memeTags={memeTags}
setMemeTags={setMemeTags} setMemeTags={setMemeTags}
style={{ style={memeEditorStyles.memeTagSelector}
marginBottom: responsive.verticalScale(10),
}}
/> />
<TextInput <TextInput
mode="outlined" mode="outlined"
label="Description" label="Description"
multiline multiline
numberOfLines={6} numberOfLines={6}
style={{
marginBottom: responsive.verticalScale(15),
}}
value={memeDescription.raw} value={memeDescription.raw}
style={memeEditorStyles.description}
onChangeText={description => onChangeText={description =>
setMemeDescription(validateMemeDescription(description)) setMemeDescription(validateMemeDescription(description))
} }
error={!memeDescription.valid}
/> />
</> </>
); );

View File

@@ -5,7 +5,6 @@ import { useQuery, useRealm } from '@realm/react';
import { TAG_SORT, tagSortQuery } from '../../types'; import { TAG_SORT, tagSortQuery } from '../../types';
import { Chip, Modal, Portal, Searchbar, useTheme } from 'react-native-paper'; import { Chip, Modal, Portal, Searchbar, useTheme } from 'react-native-paper';
import { StyleSheet } from 'react-native'; import { StyleSheet } from 'react-native';
import { useDimensions } from '../../contexts';
import styles from '../../styles'; import styles from '../../styles';
import { FlashList } from '@shopify/flash-list'; import { FlashList } from '@shopify/flash-list';
import { validateTagName } from '../../utilities'; import { validateTagName } from '../../utilities';
@@ -14,6 +13,15 @@ const memeTagSearchModalStyles = StyleSheet.create({
modal: { modal: {
position: 'absolute', position: 'absolute',
bottom: 0, bottom: 0,
padding: 10,
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
},
searchbar: {
marginBottom: 12,
},
tagChip: {
marginRight: 8,
}, },
}); });
@@ -29,7 +37,6 @@ const MemeTagSearchModal = ({
setMemeTags: (tags: Map<string, Tag>) => void; setMemeTags: (tags: Map<string, Tag>) => void;
}) => { }) => {
const { colors } = useTheme(); const { colors } = useTheme();
const { fixed, responsive } = useDimensions();
const realm = useRealm(); const realm = useRealm();
const flashListRef = useRef<FlashList<Tag>>(null); const flashListRef = useRef<FlashList<Tag>>(null);
@@ -90,9 +97,6 @@ const MemeTagSearchModal = ({
visible={visible} visible={visible}
contentContainerStyle={[ contentContainerStyle={[
{ {
padding: fixed.horizontalScale(10),
borderTopLeftRadius: fixed.verticalScale(20),
borderTopRightRadius: fixed.verticalScale(20),
backgroundColor: colors.surface, backgroundColor: colors.surface,
}, },
styles.fullWidth, styles.fullWidth,
@@ -103,9 +107,7 @@ const MemeTagSearchModal = ({
placeholder="Search or Create Tags" placeholder="Search or Create Tags"
onChangeText={handleSearch} onChangeText={handleSearch}
value={search} value={search}
style={{ style={memeTagSearchModalStyles.searchbar}
marginBottom: responsive.verticalScale(10),
}}
autoFocus autoFocus
/> />
<FlashList <FlashList
@@ -119,9 +121,7 @@ const MemeTagSearchModal = ({
renderItem={({ item: tag }) => ( renderItem={({ item: tag }) => (
<TagChip <TagChip
tag={tag} tag={tag}
style={{ style={memeTagSearchModalStyles.tagChip}
marginRight: fixed.horizontalScale(5),
}}
onPress={() => handleTagPress(tag)} onPress={() => handleTagPress(tag)}
active={memeTags.has(tag.id.toHexString())} active={memeTags.has(tag.id.toHexString())}
/> />

View File

@@ -1,5 +1,5 @@
import React, { ComponentProps, useState } from 'react'; import React, { ComponentProps, useState } from 'react';
import { View } from 'react-native'; import { StyleSheet, View } from 'react-native';
import { Chip } from 'react-native-paper'; import { Chip } from 'react-native-paper';
import { TagChip } from '../tags'; import { TagChip } from '../tags';
import { Tag } from '../../database'; import { Tag } from '../../database';
@@ -7,6 +7,12 @@ import { useDimensions } from '../../contexts';
import { MemeTagSearchModal } from '.'; import { MemeTagSearchModal } from '.';
import { FlashList } from '@shopify/flash-list'; import { FlashList } from '@shopify/flash-list';
const memeTagSelectorStyles = StyleSheet.create({
tagChip: {
marginRight: 8,
},
});
const MemeTagSelector = ({ const MemeTagSelector = ({
memeTags, memeTags,
setMemeTags, setMemeTags,
@@ -15,8 +21,9 @@ const MemeTagSelector = ({
memeTags: Map<string, Tag>; memeTags: Map<string, Tag>;
setMemeTags: (tags: Map<string, Tag>) => void; setMemeTags: (tags: Map<string, Tag>) => void;
} & ComponentProps<typeof View>) => { } & ComponentProps<typeof View>) => {
const { fixed, dimensions } = useDimensions(); const { dimensions } = useDimensions();
const [flashListMargin, setFlashListMargin] = useState(0);
const [tagSearchModalVisible, setTagSearchModalVisible] = useState(false); const [tagSearchModalVisible, setTagSearchModalVisible] = useState(false);
const handleTagPress = (tag: Tag) => { const handleTagPress = (tag: Tag) => {
@@ -38,9 +45,7 @@ const MemeTagSelector = ({
<TagChip <TagChip
tag={tag} tag={tag}
onPress={() => handleTagPress(tag)} onPress={() => handleTagPress(tag)}
style={{ style={memeTagSelectorStyles.tagChip}
marginRight: fixed.horizontalScale(5),
}}
/> />
)} )}
ListFooterComponent={() => ( ListFooterComponent={() => (
@@ -48,8 +53,13 @@ const MemeTagSelector = ({
icon="plus" icon="plus"
mode="outlined" mode="outlined"
onPress={() => setTagSearchModalVisible(true)} onPress={() => setTagSearchModalVisible(true)}
onLayout={event =>
setFlashListMargin(
dimensions.width * 0.92 - event.nativeEvent.layout.width,
)
}
style={{ style={{
marginRight: dimensions.width * 0.92 - 105.8, marginRight: flashListMargin,
}}> }}>
Add Tag Add Tag
</Chip> </Chip>

View File

@@ -0,0 +1,167 @@
import React, { ComponentProps, useState } from 'react';
import { StyleSheet, View } from 'react-native';
import {
Button,
Divider,
IconButton,
Menu,
Searchbar,
useTheme,
} from 'react-native-paper';
import { useDispatch, useSelector } from 'react-redux';
import { MEME_TYPE, memeTypePlural } from '../../database';
import {
RootState,
cycleMemesView,
setMemesFilter,
setMemesSort,
setMemesSortDirection,
toggleMemesFavoritesOnly,
toggleMemesSortDirection,
} from '../../state';
import styles from '../../styles';
import { MEME_SORT, SORT_DIRECTION } from '../../types';
import { getSortIcon, getViewIcon } from '../../utilities';
const memesHeaderStyles = StyleSheet.create({
buttonView: {
height: 50,
},
});
const MemesHeader = ({
search,
setSearch,
...props
}: {
search: string;
setSearch: (search: string) => void;
} & ComponentProps<typeof View>) => {
const { colors } = useTheme();
const sort = useSelector((state: RootState) => state.memes.sort);
const sortDirection = useSelector(
(state: RootState) => state.memes.sortDirection,
);
const view = useSelector((state: RootState) => state.memes.view);
const favoritesOnly = useSelector(
(state: RootState) => state.memes.favoritesOnly,
);
const filter = useSelector((state: RootState) => state.memes.filter);
const dispatch = useDispatch();
const [sortMenuVisible, setSortMenuVisible] = useState(false);
const [filterMenuVisible, setFilterMenuVisible] = useState(false);
const handleSortModeChange = (newSort: MEME_SORT) => {
if (newSort === sort) {
dispatch(toggleMemesSortDirection());
} else {
dispatch(setMemesSort(newSort));
if (newSort === MEME_SORT.TITLE) {
dispatch(setMemesSortDirection(SORT_DIRECTION.ASCENDING));
} else {
dispatch(setMemesSortDirection(SORT_DIRECTION.DESCENDING));
}
}
setSortMenuVisible(false);
};
const handleFilterChange = (newFilter: MEME_TYPE | undefined) => {
dispatch(setMemesFilter(newFilter));
setFilterMenuVisible(false);
};
return (
<View {...props}>
<Searchbar
placeholder="Search Memes"
value={search}
onChangeText={setSearch}
/>
<View
style={[
styles.flexRowSpaceBetween,
styles.alignCenter,
memesHeaderStyles.buttonView,
]}>
<View style={[styles.flexRow, styles.alignCenter]}>
<Menu
visible={sortMenuVisible}
onDismiss={() => setSortMenuVisible(false)}
anchor={
<Button
onPress={() => setSortMenuVisible(true)}
icon={getSortIcon(sort, sortDirection)}
contentStyle={styles.flexRowReverse}
compact>
Sort By: {sort}
</Button>
}>
{Object.keys(MEME_SORT).map(key => {
return (
<Menu.Item
key={key}
onPress={() =>
handleSortModeChange(
MEME_SORT[key as keyof typeof MEME_SORT],
)
}
title={MEME_SORT[key as keyof typeof MEME_SORT]}
/>
);
})}
</Menu>
</View>
<View style={[styles.flexRow, styles.alignCenter]}>
<IconButton
icon={getViewIcon(view)}
iconColor={colors.primary}
size={16}
animated
onPress={() => dispatch(cycleMemesView())}
/>
<IconButton
icon={favoritesOnly ? 'heart' : 'heart-outline'}
iconColor={colors.primary}
size={16}
animated
onPress={() => dispatch(toggleMemesFavoritesOnly())}
/>
<Menu
visible={filterMenuVisible}
onDismiss={() => setFilterMenuVisible(false)}
anchor={
<IconButton
onPress={() => setFilterMenuVisible(true)}
icon={filter ? 'filter' : 'filter-outline'}
iconColor={colors.primary}
size={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>
<Divider />
</View>
);
};
export default MemesHeader;

View File

@@ -1,3 +1,5 @@
export { default as TagChip } from './tagChip'; export { default as TagChip } from './tagChip';
export { default as TagEditor } from './tagEditor'; export { default as TagEditor } from './tagEditor';
export { default as TagPreview } from './tagPreview'; export { default as TagPreview } from './tagPreview';
export { default as TagRow } from './tagRow';
export { default as TagsHeader } from './tagsHeader';

View File

@@ -1,4 +1,4 @@
import React, { useMemo } from 'react'; import React, { ComponentProps, useMemo } from 'react';
import { getContrastColor } from '../../utilities'; import { getContrastColor } from '../../utilities';
import { Chip, useTheme } from 'react-native-paper'; import { Chip, useTheme } from 'react-native-paper';
import { Tag } from '../../database'; import { Tag } from '../../database';
@@ -20,7 +20,7 @@ const TagChip = ({
tag: Tag; tag: Tag;
active?: boolean; active?: boolean;
onPress?: () => void; onPress?: () => void;
} & Omit<React.ComponentProps<typeof Chip>, 'children'>) => { } & Omit<ComponentProps<typeof Chip>, 'children'>) => {
const theme = useTheme(); const theme = useTheme();
const chipTheme = useMemo(() => { const chipTheme = useMemo(() => {

View File

@@ -35,7 +35,6 @@ const TagEditor = ({
value={tagName.raw} value={tagName.raw}
onChangeText={name => setTagName(validateTagName(name))} onChangeText={name => setTagName(validateTagName(name))}
error={!tagName.valid} error={!tagName.valid}
autoCapitalize="none"
selectTextOnFocus selectTextOnFocus
/> />
<HelperText type="error" visible={!tagName.valid}> <HelperText type="error" visible={!tagName.valid}>

View File

@@ -0,0 +1,37 @@
import React from 'react';
import { StyleSheet, View } from 'react-native';
import { TouchableRipple, Text } from 'react-native-paper';
import { useNavigation, NavigationProp } from '@react-navigation/native';
import { Tag } from '../../database';
import { ROUTE, RootStackParamList } from '../../types';
import { TagChip } from '.';
const tagRowStyles = StyleSheet.create({
view: {
justifyContent: 'space-between',
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 10,
paddingHorizontal: 15,
},
tagChip: {
flexShrink: 1,
maxWidth: '80%',
},
});
const TagRow = ({ tag }: { tag: Tag }) => {
const { navigate } = useNavigation<NavigationProp<RootStackParamList>>();
return (
<TouchableRipple
onPress={() => navigate(ROUTE.EDIT_TAG, { id: tag.id.toHexString() })}>
<View style={tagRowStyles.view}>
<TagChip tag={tag} style={tagRowStyles.tagChip} />
<Text>{tag.memesLength}</Text>
</View>
</TouchableRipple>
);
};
export default TagRow;

View File

@@ -0,0 +1,96 @@
import React, { ComponentProps, useState } from 'react';
import { StyleSheet, View } from 'react-native';
import { Button, Divider, Menu, Searchbar } from 'react-native-paper';
import styles from '../../styles';
import { useDispatch, useSelector } from 'react-redux';
import {
RootState,
setTagsSort,
setTagsSortDirection,
toggleTagsSortDirection,
} from '../../state';
import { SORT_DIRECTION, TAG_SORT } from '../../types';
import { getSortIcon } from '../../utilities';
const tagsHeaderStyles = StyleSheet.create({
buttonView: {
height: 50,
},
});
const TagsHeader = ({
search,
setSearch,
...props
}: {
search: string;
setSearch: (search: string) => void;
} & ComponentProps<typeof View>) => {
const sort = useSelector((state: RootState) => state.tags.sort);
const sortDirection = useSelector(
(state: RootState) => state.tags.sortDirection,
);
const dispatch = useDispatch();
const [sortMenuVisible, setSortMenuVisible] = useState(false);
const handleSortModeChange = (newSort: TAG_SORT) => {
if (newSort === sort) {
dispatch(toggleTagsSortDirection());
} else {
dispatch(setTagsSort(newSort));
if (newSort === TAG_SORT.NAME) {
dispatch(setTagsSortDirection(SORT_DIRECTION.ASCENDING));
} else {
dispatch(setTagsSortDirection(SORT_DIRECTION.DESCENDING));
}
}
setSortMenuVisible(false);
};
return (
<View {...props}>
<Searchbar
placeholder="Search Tags"
value={search}
onChangeText={(value: string) => {
setSearch(value);
}}
/>
<View
style={[
styles.flexRow,
styles.alignCenter,
tagsHeaderStyles.buttonView,
]}>
<Menu
visible={sortMenuVisible}
onDismiss={() => setSortMenuVisible(false)}
anchor={
<Button
onPress={() => setSortMenuVisible(true)}
icon={getSortIcon(sort, sortDirection)}
contentStyle={styles.flexRowReverse}
compact>
Sort By: {sort}
</Button>
}>
{Object.keys(TAG_SORT).map(key => {
return (
<Menu.Item
key={key}
onPress={() =>
handleSortModeChange(TAG_SORT[key as keyof typeof TAG_SORT])
}
title={TAG_SORT[key as keyof typeof TAG_SORT]}
/>
);
})}
</Menu>
</View>
<Divider />
</View>
);
};
export default TagsHeader;

View File

@@ -11,17 +11,22 @@ import { Dimensions, ScaledSize } from 'react-native';
const guidelineBaseWidth = 350; const guidelineBaseWidth = 350;
const guidelineBaseHeight = 680; const guidelineBaseHeight = 680;
enum ORIENTATION {
PORTRAIT = 'portrait',
LANDSCAPE = 'landscape',
}
interface ScaleFunctions { interface ScaleFunctions {
horizontalScale: (size: number) => number; horizontalScale: (size: number) => number;
verticalScale: (size: number) => number; verticalScale: (size: number) => number;
moderateScale: (size: number, factor?: number) => number; moderateHorizontalScale: (size: number, factor?: number) => number;
moderateVerticalScale: (size: number, factor?: number) => number;
} }
interface DimensionsContext { interface DimensionsContext {
orientation: 'portrait' | 'landscape'; orientation: ORIENTATION;
dimensions: ScaledSize; dimensions: ScaledSize;
responsive: ScaleFunctions; responsive: ScaleFunctions;
fixed: ScaleFunctions;
} }
const createScaleFunctions = (dimensionsIn: ScaledSize) => { const createScaleFunctions = (dimensionsIn: ScaledSize) => {
@@ -29,10 +34,17 @@ const createScaleFunctions = (dimensionsIn: ScaledSize) => {
(dimensionsIn.width / guidelineBaseWidth) * size; (dimensionsIn.width / guidelineBaseWidth) * size;
const verticalScale = (size: number) => const verticalScale = (size: number) =>
(dimensionsIn.height / guidelineBaseHeight) * size; (dimensionsIn.height / guidelineBaseHeight) * size;
const moderateScale = (size: number, factor = 0.5) => const moderateHorizontalScale = (size: number, factor = 0.5) =>
size + (horizontalScale(size) - size) * factor; size + (horizontalScale(size) - size) * factor;
const moderateVerticalScale = (size: number, factor = 0.5) =>
size + (verticalScale(size) - size) * factor;
return { horizontalScale, verticalScale, moderateScale }; return {
horizontalScale,
verticalScale,
moderateHorizontalScale,
moderateVerticalScale,
};
}; };
const DimensionsContext = createContext<DimensionsContext | undefined>( const DimensionsContext = createContext<DimensionsContext | undefined>(
@@ -41,22 +53,14 @@ const DimensionsContext = createContext<DimensionsContext | undefined>(
const DimensionsProvider = ({ children }: { children: ReactNode }) => { const DimensionsProvider = ({ children }: { children: ReactNode }) => {
const [dimensions, setDimensions] = useState(Dimensions.get('window')); const [dimensions, setDimensions] = useState(Dimensions.get('window'));
const orientation =
dimensions.width > dimensions.height ? 'landscape' : 'portrait';
const initialDimensions = useMemo(() => { const orientation = useMemo(() => {
if (orientation === 'landscape') { return dimensions.width > dimensions.height
return { ? ORIENTATION.LANDSCAPE
width: dimensions.height, : ORIENTATION.PORTRAIT;
height: dimensions.width, }, [dimensions]);
} as ScaledSize;
}
return dimensions;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const responsiveScale = createScaleFunctions(dimensions); const responsiveScale = createScaleFunctions(dimensions);
const fixedScale = createScaleFunctions(initialDimensions);
useEffect(() => { useEffect(() => {
const onChange = ({ window }: { window: ScaledSize }) => { const onChange = ({ window }: { window: ScaledSize }) => {
@@ -76,7 +80,6 @@ const DimensionsProvider = ({ children }: { children: ReactNode }) => {
orientation, orientation,
dimensions, dimensions,
responsive: responsiveScale, responsive: responsiveScale,
fixed: fixedScale,
}}> }}>
{children} {children}
</DimensionsContext.Provider> </DimensionsContext.Provider>
@@ -91,4 +94,4 @@ const useDimensions = (): DimensionsContext => {
return context; return context;
}; };
export { DimensionsProvider, useDimensions }; export { ORIENTATION, DimensionsProvider, useDimensions };

View File

@@ -1 +1 @@
export { DimensionsProvider, useDimensions } from './dimensions'; export { ORIENTATION, DimensionsProvider, useDimensions } from './dimensions';

View File

@@ -6,7 +6,6 @@ enum MEME_TYPE {
GIF = 'GIF', GIF = 'GIF',
VIDEO = 'Video', VIDEO = 'Video',
AUDIO = 'Audio', AUDIO = 'Audio',
ALBUM = 'Album',
TEXT = 'Text', TEXT = 'Text',
} }
@@ -15,7 +14,6 @@ const memeTypePlural = {
[MEME_TYPE.GIF]: 'GIFs', [MEME_TYPE.GIF]: 'GIFs',
[MEME_TYPE.VIDEO]: 'Videos', [MEME_TYPE.VIDEO]: 'Videos',
[MEME_TYPE.AUDIO]: 'Audio', [MEME_TYPE.AUDIO]: 'Audio',
[MEME_TYPE.ALBUM]: 'Albums',
[MEME_TYPE.TEXT]: 'Text', [MEME_TYPE.TEXT]: 'Text',
}; };

View File

@@ -82,6 +82,7 @@ const TabNavigator = () => {
const NavigationContainer = () => { const NavigationContainer = () => {
const theme = useTheme(); const theme = useTheme();
const StackNavigatorBase = createNativeStackNavigator<RootStackParamList>(); const StackNavigatorBase = createNativeStackNavigator<RootStackParamList>();
return ( return (

View File

@@ -1,7 +1,7 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Appbar, Button, useTheme } from 'react-native-paper'; import { Appbar, Button, useTheme } from 'react-native-paper';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import { useDimensions } from '../contexts'; import { ORIENTATION, useDimensions } from '../contexts';
import { ScrollView, View } from 'react-native'; import { ScrollView, View } from 'react-native';
import { NativeStackScreenProps } from '@react-navigation/native-stack'; import { NativeStackScreenProps } from '@react-navigation/native-stack';
import { useRealm } from '@realm/react'; import { useRealm } from '@realm/react';
@@ -11,7 +11,7 @@ import { useSelector } from 'react-redux';
import { extension } from 'react-native-mime-types'; import { extension } from 'react-native-mime-types';
import styles from '../styles'; import styles from '../styles';
import { ROUTE, RootStackParamList } from '../types'; import { ROUTE, RootStackParamList } from '../types';
import { MEME_TYPE, Meme, Tag } from '../database'; import { Meme, Tag } from '../database';
import { RootState } from '../state'; import { RootState } from '../state';
import { import {
getMemeType, getMemeType,
@@ -32,11 +32,9 @@ const AddMeme = ({
(state: RootState) => state.settings.storageUri, (state: RootState) => state.settings.storageUri,
)!; )!;
const { uri } = route.params; const uri = route.params.uri[0].uri;
const memeType =
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
uri.length > 1 ? MEME_TYPE.ALBUM : getMemeType(uri[0].type!); const memeType = getMemeType(route.params.uri[0].type!);
const [memeTitle, setMemeTitle] = useState(validateMemeTitle('New Meme')); const [memeTitle, setMemeTitle] = useState(validateMemeTitle('New Meme'));
const [memeDescription, setMemeDescription] = useState( const [memeDescription, setMemeDescription] = useState(
@@ -55,7 +53,7 @@ const AddMeme = ({
const hash: string[] = []; const hash: string[] = [];
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const fileExtension = extension(uri[0].type!); const fileExtension = extension(memeType!);
if (!fileExtension) navigation.goBack(); if (!fileExtension) navigation.goBack();
savedUri.push( savedUri.push(
@@ -65,7 +63,7 @@ const AddMeme = ({
), ),
); );
await FileSystem.cp(uri[0].uri, savedUri[0]); await FileSystem.cp(uri, savedUri[0]);
const { size } = await FileSystem.stat(savedUri[0]); const { size } = await FileSystem.stat(savedUri[0]);
hash.push(await FileSystem.hash(savedUri[0], 'MD5')); hash.push(await FileSystem.hash(savedUri[0], 'MD5'));
@@ -107,8 +105,9 @@ const AddMeme = ({
</Appbar.Header> </Appbar.Header>
<ScrollView <ScrollView
contentContainerStyle={[ contentContainerStyle={[
orientation == 'portrait' && styles.paddingVertical, orientation === ORIENTATION.PORTRAIT
orientation == 'landscape' && styles.smallPaddingVertical, ? styles.paddingVertical
: styles.smallPaddingVertical,
styles.paddingHorizontal, styles.paddingHorizontal,
styles.flexGrow, styles.flexGrow,
styles.flexColumnSpaceBetween, styles.flexColumnSpaceBetween,
@@ -116,7 +115,7 @@ const AddMeme = ({
]}> ]}>
<View style={[styles.flex, styles.justifyStart]}> <View style={[styles.flex, styles.justifyStart]}>
<MemeEditor <MemeEditor
imageUri={uri.map(uriIn => uriIn.uri)} imageUri={uri}
memeTitle={memeTitle} memeTitle={memeTitle}
setMemeTitle={setMemeTitle} setMemeTitle={setMemeTitle}
memeDescription={memeDescription} memeDescription={memeDescription}

View File

@@ -9,7 +9,7 @@ import {
validateColor, validateColor,
validateTagName, validateTagName,
} from '../utilities'; } from '../utilities';
import { useDimensions } from '../contexts'; import { ORIENTATION, useDimensions } from '../contexts';
import { Tag } from '../database'; import { Tag } from '../database';
import { TagEditor } from '../components'; import { TagEditor } from '../components';
@@ -43,8 +43,9 @@ const AddTag = () => {
</Appbar.Header> </Appbar.Header>
<ScrollView <ScrollView
contentContainerStyle={[ contentContainerStyle={[
orientation == 'portrait' && styles.paddingVertical, orientation === ORIENTATION.PORTRAIT
orientation == 'landscape' && styles.smallPaddingVertical, ? styles.paddingVertical
: styles.smallPaddingVertical,
styles.paddingHorizontal, styles.paddingHorizontal,
styles.flexGrow, styles.flexGrow,
styles.flexColumnSpaceBetween, styles.flexColumnSpaceBetween,

View File

@@ -2,7 +2,7 @@ import React from 'react';
import { ScrollView } from 'react-native'; import { ScrollView } from 'react-native';
import { Appbar, useTheme } from 'react-native-paper'; import { Appbar, useTheme } from 'react-native-paper';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import { useDimensions } from '../contexts'; import { ORIENTATION, useDimensions } from '../contexts';
import styles from '../styles'; import styles from '../styles';
const EditMeme = () => { const EditMeme = () => {
@@ -18,8 +18,9 @@ const EditMeme = () => {
</Appbar.Header> </Appbar.Header>
<ScrollView <ScrollView
contentContainerStyle={[ contentContainerStyle={[
orientation == 'portrait' && styles.paddingVertical, orientation === ORIENTATION.PORTRAIT
orientation == 'landscape' && styles.smallPaddingVertical, ? styles.paddingVertical
: styles.smallPaddingVertical,
styles.paddingHorizontal, styles.paddingHorizontal,
styles.flexGrow, styles.flexGrow,
styles.flexColumnSpaceBetween, styles.flexColumnSpaceBetween,

View File

@@ -7,7 +7,7 @@ import { BSON } from 'realm';
import { useRealm } from '@realm/react'; import { useRealm } from '@realm/react';
import { TagEditor } from '../components'; import { TagEditor } from '../components';
import styles from '../styles'; import styles from '../styles';
import { useDimensions } from '../contexts'; import { ORIENTATION, useDimensions } from '../contexts';
import { ROUTE, RootStackParamList } from '../types'; import { ROUTE, RootStackParamList } from '../types';
import { Tag } from '../database'; import { Tag } from '../database';
import { validateColor, validateTagName } from '../utilities'; import { validateColor, validateTagName } from '../utilities';
@@ -63,8 +63,9 @@ const EditTag = ({
</Appbar.Header> </Appbar.Header>
<ScrollView <ScrollView
contentContainerStyle={[ contentContainerStyle={[
orientation == 'portrait' && styles.paddingVertical, orientation === ORIENTATION.PORTRAIT
orientation == 'landscape' && styles.smallPaddingVertical, ? styles.paddingVertical
: styles.smallPaddingVertical,
styles.paddingHorizontal, styles.paddingHorizontal,
styles.flexGrow, styles.flexGrow,
styles.flexColumnSpaceBetween, styles.flexColumnSpaceBetween,

View File

@@ -1,35 +1,24 @@
import React, { useState } from 'react'; import React, { useCallback, useRef, useState } from 'react';
import { StyleSheet, View } from 'react-native'; import {
BackHandler,
NativeScrollEvent,
NativeSyntheticEvent,
StyleSheet,
View,
} from 'react-native';
import { useQuery } from '@realm/react'; import { useQuery } from '@realm/react';
import { import { useTheme, HelperText } from 'react-native-paper';
Button,
Menu,
IconButton,
Divider,
useTheme,
Searchbar,
HelperText,
} from 'react-native-paper';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { FlashList } from '@shopify/flash-list';
import { useFocusEffect } from '@react-navigation/native';
import styles from '../styles'; import styles from '../styles';
import { MEME_SORT, SORT_DIRECTION, memesSortQuery } from '../types'; import { SORT_DIRECTION, memesSortQuery } from '../types';
import { getSortIcon, getViewIcon } from '../utilities'; import { RootState, setNavVisible } from '../state';
import { import { Meme, Tag } from '../database';
RootState,
cycleMemesView,
toggleMemesSortDirection,
setMemesSortDirection,
toggleMemesFavoritesOnly,
setMemesSort,
setMemesFilter,
} from '../state';
import { MEME_TYPE, Meme, memeTypePlural } from '../database';
import { useDimensions } from '../contexts'; import { useDimensions } from '../contexts';
import { HideableHeader, MemesHeader } from '../components';
const memesStyles = StyleSheet.create({ const memesStyles = StyleSheet.create({
headerButtonView: {
height: 50,
},
helperText: { helperText: {
marginVertical: 10, marginVertical: 10,
}, },
@@ -47,30 +36,12 @@ const Memes = () => {
(state: RootState) => state.memes.favoritesOnly, (state: RootState) => state.memes.favoritesOnly,
); );
const filter = useSelector((state: RootState) => state.memes.filter); const filter = useSelector((state: RootState) => state.memes.filter);
const navVisisble = useSelector(
(state: RootState) => state.navigation.navVisible,
);
const dispatch = useDispatch(); const dispatch = useDispatch();
const [sortMenuVisible, setSortMenuVisible] = useState(false); const [flashListPadding, setFlashListPadding] = useState(0);
const [filterMenuVisible, setFilterMenuVisible] = useState(false);
const handleSortModeChange = (newSort: MEME_SORT) => {
if (newSort === sort) {
dispatch(toggleMemesSortDirection());
} else {
dispatch(setMemesSort(newSort));
if (newSort === MEME_SORT.TITLE) {
dispatch(setMemesSortDirection(SORT_DIRECTION.ASCENDING));
} else {
dispatch(setMemesSortDirection(SORT_DIRECTION.DESCENDING));
}
}
setSortMenuVisible(false);
};
const handleFilterChange = (newFilter: MEME_TYPE | undefined) => {
dispatch(setMemesFilter(newFilter));
setFilterMenuVisible(false);
};
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const memes = useQuery<Meme>( const memes = useQuery<Meme>(
@@ -94,110 +65,56 @@ const Memes = () => {
[sort, sortDirection, favoritesOnly, filter, search], [sort, sortDirection, favoritesOnly, filter, search],
); );
const [scrollOffset, setScrollOffset] = useState(0);
const handleScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
const currentOffset = event.nativeEvent.contentOffset.y;
if (currentOffset <= 150) {
dispatch(setNavVisible(true));
} else {
const diff = currentOffset - scrollOffset;
if (Math.abs(diff) > 50) dispatch(setNavVisible(diff < 0));
}
setScrollOffset(currentOffset);
};
const flashListRef = useRef<FlashList<Tag>>(null);
useFocusEffect(
useCallback(() => {
const handleBackPress = () => {
if (scrollOffset > 0) {
flashListRef.current?.scrollToOffset({ offset: 0, animated: true });
return true;
}
return false;
};
BackHandler.addEventListener('hardwareBackPress', handleBackPress);
return () =>
BackHandler.removeEventListener('hardwareBackPress', handleBackPress);
}, [flashListRef, scrollOffset]),
);
return ( return (
<View <View
style={[ style={[
orientation == 'portrait' && styles.paddingTop,
orientation == 'landscape' && styles.smallPaddingTop,
styles.paddingHorizontal, styles.paddingHorizontal,
styles.fullSize, styles.fullSize,
{ backgroundColor: colors.background }, { backgroundColor: colors.background },
]}> ]}>
<Searchbar <HideableHeader visible={navVisisble}>
placeholder="Search Memes" <MemesHeader
value={search} search={search}
onChangeText={setSearch} setSearch={setSearch}
onLayout={event => {
setFlashListPadding(event.nativeEvent.layout.height);
}}
/> />
<View </HideableHeader>
style={[
styles.flexRowSpaceBetween,
styles.alignCenter,
memesStyles.headerButtonView,
]}>
<View style={[styles.flexRow, styles.alignCenter]}>
<Menu
visible={sortMenuVisible}
onDismiss={() => setSortMenuVisible(false)}
anchor={
<Button
onPress={() => setSortMenuVisible(true)}
icon={getSortIcon(sort, sortDirection)}
contentStyle={styles.flexRowReverse}
compact>
Sort By: {sort}
</Button>
}>
{Object.keys(MEME_SORT).map(key => {
return (
<Menu.Item
key={key}
onPress={() =>
handleSortModeChange(
MEME_SORT[key as keyof typeof MEME_SORT],
)
}
title={MEME_SORT[key as keyof typeof MEME_SORT]}
/>
);
})}
</Menu>
</View>
<View style={[styles.flexRow, styles.alignCenter]}>
<IconButton
icon={getViewIcon(view)}
iconColor={colors.primary}
size={16}
animated
onPress={() => dispatch(cycleMemesView())}
/>
<IconButton
icon={favoritesOnly ? 'heart' : 'heart-outline'}
iconColor={colors.primary}
size={16}
animated
onPress={() => dispatch(toggleMemesFavoritesOnly())}
/>
<Menu
visible={filterMenuVisible}
onDismiss={() => setFilterMenuVisible(false)}
anchor={
<IconButton
onPress={() => setFilterMenuVisible(true)}
icon={filter ? 'filter' : 'filter-outline'}
iconColor={colors.primary}
size={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>
<Divider />
{/* TODO: Meme Views */}
{memes.length === 0 && (
<HelperText
type={'info'}
style={[memesStyles.helperText, styles.centerText]}>
No memes found
</HelperText>
)}
</View> </View>
); );
}; };

View File

@@ -13,12 +13,8 @@ import { openDocumentTree } from 'react-native-scoped-storage';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import type {} from 'redux-thunk/extend-redux'; import type {} from 'redux-thunk/extend-redux';
import styles from '../styles'; import styles from '../styles';
import { import { RootState, setNoMedia, setStorageUri } from '../state';
RootState, import { ORIENTATION, useDimensions } from '../contexts';
setNoMedia,
setStorageUri,
} from '../state';
import { useDimensions } from '../contexts';
const settingsScreenStyles = StyleSheet.create({ const settingsScreenStyles = StyleSheet.create({
snackbar: { snackbar: {
@@ -48,8 +44,9 @@ const SettingsScreen = () => {
<> <>
<ScrollView <ScrollView
contentContainerStyle={[ contentContainerStyle={[
orientation == 'portrait' && styles.paddingTop, orientation === ORIENTATION.PORTRAIT
orientation == 'landscape' && styles.smallPaddingTop, ? styles.paddingTop
: styles.smallPaddingTop,
styles.paddingHorizontal, styles.paddingHorizontal,
{ backgroundColor: colors.background }, { backgroundColor: colors.background },
]}> ]}>

View File

@@ -6,70 +6,30 @@ import {
NativeScrollEvent, NativeScrollEvent,
BackHandler, BackHandler,
} from 'react-native'; } from 'react-native';
import { import { Divider, HelperText, useTheme } from 'react-native-paper';
Button,
Divider,
HelperText,
Menu,
Searchbar,
Text,
TouchableRipple,
useTheme,
} from 'react-native-paper';
import { useQuery } from '@realm/react'; import { useQuery } from '@realm/react';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { FlashList } from '@shopify/flash-list'; import { FlashList } from '@shopify/flash-list';
import { import { useFocusEffect } from '@react-navigation/native';
NavigationProp, import { HideableHeader, TagRow, TagsHeader } from '../components';
useFocusEffect,
useNavigation,
} from '@react-navigation/native';
import { HideableHeader, TagChip } from '../components';
import { Tag } from '../database'; import { Tag } from '../database';
import styles from '../styles'; import styles from '../styles';
import { import { RootState, setNavVisible } from '../state';
RootState, import { SORT_DIRECTION, tagSortQuery } from '../types';
setNavVisible, import { ORIENTATION, useDimensions } from '../contexts';
setTagsSort,
setTagsSortDirection,
toggleTagsSortDirection,
} from '../state';
import {
ROUTE,
RootStackParamList,
SORT_DIRECTION,
TAG_SORT,
tagSortQuery,
} from '../types';
import { getSortIcon } from '../utilities';
const tagsStyles = StyleSheet.create({ const tagsStyles = StyleSheet.create({
headerButtonView: {
height: 50,
},
tagRow: {
justifyContent: 'space-between',
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 10,
paddingHorizontal: 15,
},
tagChip: {
flexShrink: 1,
maxWidth: '80%',
},
helperText: { helperText: {
marginVertical: 10, marginVertical: 10,
}, },
flashList: { flashList: {
paddingTop: 122, paddingBottom: 100,
paddingBottom: 25,
}, },
}); });
const Tags = () => { const Tags = () => {
const { colors } = useTheme(); const { colors } = useTheme();
const { navigate } = useNavigation<NavigationProp<RootStackParamList>>(); const { dimensions, orientation } = useDimensions();
const sort = useSelector((state: RootState) => state.tags.sort); const sort = useSelector((state: RootState) => state.tags.sort);
const sortDirection = useSelector( const sortDirection = useSelector(
(state: RootState) => state.tags.sortDirection, (state: RootState) => state.tags.sortDirection,
@@ -79,22 +39,7 @@ const Tags = () => {
); );
const dispatch = useDispatch(); const dispatch = useDispatch();
const [sortMenuVisible, setSortMenuVisible] = useState(false); const [flashListPadding, setFlashListPadding] = useState(0);
const handleSortModeChange = (newSort: TAG_SORT) => {
if (newSort === sort) {
dispatch(toggleTagsSortDirection());
} else {
dispatch(setTagsSort(newSort));
if (newSort === TAG_SORT.NAME) {
dispatch(setTagsSortDirection(SORT_DIRECTION.ASCENDING));
} else {
dispatch(setTagsSortDirection(SORT_DIRECTION.DESCENDING));
}
}
setSortMenuVisible(false);
};
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const tags = useQuery<Tag>( const tags = useQuery<Tag>(
@@ -158,63 +103,28 @@ const Tags = () => {
{ backgroundColor: colors.background }, { backgroundColor: colors.background },
]}> ]}>
<HideableHeader visible={navVisisble}> <HideableHeader visible={navVisisble}>
<Searchbar <TagsHeader
placeholder="Search Tags" search={search}
value={search} setSearch={setSearch}
onChangeText={(value: string) => { onLayout={event => {
setSearch(value); setFlashListPadding(event.nativeEvent.layout.height);
}} }}
/> />
<View
style={[
styles.flexRow,
styles.alignCenter,
tagsStyles.headerButtonView,
]}>
<Menu
visible={sortMenuVisible}
onDismiss={() => setSortMenuVisible(false)}
anchor={
<Button
onPress={() => setSortMenuVisible(true)}
icon={getSortIcon(sort, sortDirection)}
contentStyle={styles.flexRowReverse}
compact>
Sort By: {sort}
</Button>
}>
{Object.keys(TAG_SORT).map(key => {
return (
<Menu.Item
key={key}
onPress={() =>
handleSortModeChange(TAG_SORT[key as keyof typeof TAG_SORT])
}
title={TAG_SORT[key as keyof typeof TAG_SORT]}
/>
);
})}
</Menu>
</View>
<Divider />
</HideableHeader> </HideableHeader>
{flashListPadding > 0 && (
<FlashList <FlashList
ref={flashListRef} ref={flashListRef}
data={tags} data={tags}
estimatedItemSize={52} estimatedItemSize={52}
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
renderItem={({ item: tag }) => ( renderItem={({ item: tag }) => <TagRow tag={tag} />}
<TouchableRipple contentContainerStyle={{
onPress={() => paddingTop:
navigate(ROUTE.EDIT_TAG, { id: tag.id.toHexString() }) flashListPadding +
}> dimensions.height *
<View style={tagsStyles.tagRow}> (orientation === ORIENTATION.PORTRAIT ? 0.02 : 0.04),
<TagChip tag={tag} style={tagsStyles.tagChip} /> ...tagsStyles.flashList,
<Text>{tag.memesLength}</Text> }}
</View>
</TouchableRipple>
)}
contentContainerStyle={tagsStyles.flashList}
ItemSeparatorComponent={() => <Divider />} ItemSeparatorComponent={() => <Divider />}
ListEmptyComponent={() => ( ListEmptyComponent={() => (
<HelperText <HelperText
@@ -225,6 +135,7 @@ const Tags = () => {
)} )}
onScroll={handleScroll} onScroll={handleScroll}
/> />
)}
</View> </View>
); );
}; };

View File

@@ -1,16 +1,25 @@
import React from 'react'; import React from 'react';
import { View } from 'react-native'; import { StyleSheet, View } from 'react-native';
import { Button, Text, useTheme } from 'react-native-paper'; import { Button, Text, useTheme } from 'react-native-paper';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { openDocumentTree } from 'react-native-scoped-storage'; import { openDocumentTree } from 'react-native-scoped-storage';
import styles from '../styles'; import styles from '../styles';
import { noOp } from '../utilities'; import { noOp } from '../utilities';
import { setStorageUri } from '../state'; import { setStorageUri } from '../state';
import { useDimensions } from '../contexts'; import { ORIENTATION, useDimensions } from '../contexts';
const welcomeStyles = StyleSheet.create({
text: {
marginBottom: 30,
},
button: {
marginBottom: 100,
},
});
const Welcome = ({ onWelcomeComplete }: { onWelcomeComplete: () => void }) => { const Welcome = ({ onWelcomeComplete }: { onWelcomeComplete: () => void }) => {
const { colors } = useTheme(); const { colors } = useTheme();
const { orientation, responsive } = useDimensions(); const { orientation } = useDimensions();
const dispatch = useDispatch(); const dispatch = useDispatch();
const selectStorageLocation = async () => { const selectStorageLocation = async () => {
@@ -23,8 +32,9 @@ const Welcome = ({ onWelcomeComplete }: { onWelcomeComplete: () => void }) => {
return ( return (
<View <View
style={[ style={[
orientation == 'portrait' && styles.paddingTop, orientation === ORIENTATION.PORTRAIT
orientation == 'landscape' && styles.smallPaddingTop, ? styles.paddingTop
: styles.smallPaddingTop,
styles.paddingHorizontal, styles.paddingHorizontal,
styles.centered, styles.centered,
styles.flex, styles.flex,
@@ -33,20 +43,13 @@ const Welcome = ({ onWelcomeComplete }: { onWelcomeComplete: () => void }) => {
]}> ]}>
<Text <Text
variant="displayMedium" variant="displayMedium"
style={[ style={[welcomeStyles.text, styles.centerText]}>
{
marginBottom: responsive.verticalScale(30),
},
styles.centerText,
]}>
Welcome to Terminally Online! Welcome to Terminally Online!
</Text> </Text>
<Button <Button
mode="contained" mode="contained"
onPress={selectStorageLocation} onPress={selectStorageLocation}
style={{ style={welcomeStyles.button}>
marginBottom: responsive.verticalScale(100),
}}>
Select Storage Location Select Storage Location
</Button> </Button>
</View> </View>