Fix layout bugs
Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
This commit is contained in:
@@ -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
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -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: [
|
||||||
|
@@ -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';
|
||||||
|
@@ -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';
|
||||||
|
@@ -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,
|
{
|
||||||
height: imageHeight,
|
width: imageWidth,
|
||||||
marginBottom: fixed.verticalScale(10),
|
height: imageHeight,
|
||||||
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}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@@ -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())}
|
||||||
/>
|
/>
|
||||||
|
@@ -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>
|
||||||
|
167
src/components/memes/memesHeader.tsx
Normal file
167
src/components/memes/memesHeader.tsx
Normal 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;
|
@@ -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';
|
||||||
|
@@ -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(() => {
|
||||||
|
@@ -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}>
|
||||||
|
37
src/components/tags/tagRow.tsx
Normal file
37
src/components/tags/tagRow.tsx
Normal 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;
|
96
src/components/tags/tagsHeader.tsx
Normal file
96
src/components/tags/tagsHeader.tsx
Normal 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;
|
@@ -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 };
|
||||||
|
@@ -1 +1 @@
|
|||||||
export { DimensionsProvider, useDimensions } from './dimensions';
|
export { ORIENTATION, DimensionsProvider, useDimensions } from './dimensions';
|
||||||
|
@@ -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',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -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 (
|
||||||
|
@@ -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;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
const memeType =
|
const memeType = getMemeType(route.params.uri[0].type!);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
uri.length > 1 ? MEME_TYPE.ALBUM : getMemeType(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}
|
||||||
|
@@ -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,
|
||||||
|
@@ -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,
|
||||||
|
@@ -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,
|
||||||
|
@@ -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 => {
|
||||||
<View
|
setFlashListPadding(event.nativeEvent.layout.height);
|
||||||
style={[
|
}}
|
||||||
styles.flexRowSpaceBetween,
|
/>
|
||||||
styles.alignCenter,
|
</HideableHeader>
|
||||||
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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -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 },
|
||||||
]}>
|
]}>
|
||||||
|
@@ -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,73 +103,39 @@ 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>
|
||||||
<FlashList
|
{flashListPadding > 0 && (
|
||||||
ref={flashListRef}
|
<FlashList
|
||||||
data={tags}
|
ref={flashListRef}
|
||||||
estimatedItemSize={52}
|
data={tags}
|
||||||
showsVerticalScrollIndicator={false}
|
estimatedItemSize={52}
|
||||||
renderItem={({ item: tag }) => (
|
showsVerticalScrollIndicator={false}
|
||||||
<TouchableRipple
|
renderItem={({ item: tag }) => <TagRow tag={tag} />}
|
||||||
onPress={() =>
|
contentContainerStyle={{
|
||||||
navigate(ROUTE.EDIT_TAG, { id: tag.id.toHexString() })
|
paddingTop:
|
||||||
}>
|
flashListPadding +
|
||||||
<View style={tagsStyles.tagRow}>
|
dimensions.height *
|
||||||
<TagChip tag={tag} style={tagsStyles.tagChip} />
|
(orientation === ORIENTATION.PORTRAIT ? 0.02 : 0.04),
|
||||||
<Text>{tag.memesLength}</Text>
|
...tagsStyles.flashList,
|
||||||
</View>
|
}}
|
||||||
</TouchableRipple>
|
ItemSeparatorComponent={() => <Divider />}
|
||||||
)}
|
ListEmptyComponent={() => (
|
||||||
contentContainerStyle={tagsStyles.flashList}
|
<HelperText
|
||||||
ItemSeparatorComponent={() => <Divider />}
|
type={'info'}
|
||||||
ListEmptyComponent={() => (
|
style={[tagsStyles.helperText, styles.centerText]}>
|
||||||
<HelperText
|
No tags found
|
||||||
type={'info'}
|
</HelperText>
|
||||||
style={[tagsStyles.helperText, styles.centerText]}>
|
)}
|
||||||
No tags found
|
onScroll={handleScroll}
|
||||||
</HelperText>
|
/>
|
||||||
)}
|
)}
|
||||||
onScroll={handleScroll}
|
|
||||||
/>
|
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -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>
|
||||||
|
Reference in New Issue
Block a user