diff --git a/package.json b/package.json
index d05d510..b7dbc01 100644
--- a/package.json
+++ b/package.json
@@ -19,7 +19,7 @@
"expo-haptics": "^8.4.0",
"expo-status-bar": "~1.0.3",
"fast-xml-parser": "^3.17.6",
- "lottie-react-native": "^3.5.0",
+ "lottie-react-native": "~2.6.1",
"react": "16.13.1",
"react-dom": "16.13.1",
"react-native": "https://github.com/expo/react-native/archive/sdk-40.0.1.tar.gz",
diff --git a/src/components/Book.jsx b/src/components/Book.jsx
index 51e7137..db32e4e 100644
--- a/src/components/Book.jsx
+++ b/src/components/Book.jsx
@@ -15,7 +15,7 @@ import Text from './Text';
function Book({ book, scrollX, index }) {
const navigation = useNavigation();
const { margin, normalize } = useTheme();
- const BOOKW = normalize(150, 180);
+ const BOOKW = normalize(130, 160);
const BOOKH = BOOKW * 1.5;
const position = useDerivedValue(() => (index + 0.00001) * (BOOKW + margin) - scrollX.value);
const inputRange = [-BOOKW, 0, BOOKW, BOOKW * 3];
diff --git a/src/components/BookHeader.jsx b/src/components/BookHeader.jsx
index 1ada542..3a870d3 100644
--- a/src/components/BookHeader.jsx
+++ b/src/components/BookHeader.jsx
@@ -1,73 +1,37 @@
/* eslint-disable no-param-reassign */
import React from 'react';
import { Image, StyleSheet, View } from 'react-native';
-import Animated, {
- withTiming, interpolate, Extrapolate, runOnJS,
- useAnimatedStyle, useSharedValue, useAnimatedGestureHandler,
-} from 'react-native-reanimated';
-import { useSafeAreaInsets } from 'react-native-safe-area-context';
-import { PanGestureHandler } from 'react-native-gesture-handler';
+import Animated, { interpolate, useAnimatedStyle } from 'react-native-reanimated';
import { SharedElement } from 'react-navigation-shared-element';
import { useTheme } from '@react-navigation/native';
-import * as Haptics from 'expo-haptics';
import Text from './Text';
// Load a single book
-function BookHeader({
- scrollY, book, x, y, navigation,
-}) {
+function BookHeader({ scrollY, book }) {
const {
- width, margin, dark, normalize,
+ width, margin, colors, normalize, navbar, status,
} = useTheme();
- const insets = useSafeAreaInsets();
- const moving = useSharedValue(0);
- const closing = useSharedValue(0);
- const BOOKW = normalize(150, 180);
+ const BOOKW = normalize(140, 180);
const BOOKH = BOOKW * 1.5;
-
- // Pan gesture handler
- const gestureHandler = useAnimatedGestureHandler({
- onStart: (_, ctx) => {
- ctx.startX = x.value;
- ctx.startY = y.value;
- moving.value = 1;
- },
- onActive: (e, ctx) => {
- x.value = ctx.startX + e.translationX;
- y.value = ctx.startY + e.translationY;
-
- // See if closing screen
- const flung = Math.abs(e.velocityX) > 250 || Math.abs(y.value) >= 50;
- if (flung && !closing.value) {
- closing.value = 1;
- runOnJS(navigation.goBack)();
- runOnJS(Haptics.selectionAsync)();
- }
- },
- onEnd: () => {
- if (y.value < 50) {
- x.value = withTiming(0);
- y.value = withTiming(0);
- }
- moving.value = 0;
- },
- });
+ const HEADER = normalize(width + status, 500);
// Animated styles
const anims = {
header: useAnimatedStyle(() => ({
width,
zIndex: 10,
+ height: HEADER,
+ paddingTop: status,
position: 'absolute',
justifyContent: 'center',
- paddingTop: insets.top,
- height: width + insets.top,
+ borderTopLeftRadius: 20,
+ borderTopRightRadius: 20,
shadowOffset: { height: 2 },
- backgroundColor: dark ? `rgba(0,0,0,${interpolate(y.value, [0, 50], [1, 0])})` : `rgba(255,255,255,${interpolate(y.value, [0, 50], [1, 0])})`,
- shadowOpacity: interpolate(scrollY.value, [width - 44, width - 20], [0, 0.5], 'clamp'),
+ backgroundColor: colors.card,
+ shadowOpacity: interpolate(scrollY.value, [HEADER - navbar - 20, HEADER - navbar], [0, 0.25], 'clamp'),
transform: [
- { translateY: interpolate(scrollY.value, [0, width - 44], [0, -width + 44], 'clamp') },
+ { translateY: interpolate(scrollY.value, [0, HEADER - navbar], [0, -HEADER + navbar], 'clamp') },
],
})),
bg: useAnimatedStyle(() => ({
@@ -75,29 +39,26 @@ function BookHeader({
left: 0,
right: 0,
bottom: 0,
+ opacity: 0.5,
position: 'absolute',
- opacity: interpolate(y.value, [0, 50], [0.5, 0], Extrapolate.CLAMP),
+ borderTopLeftRadius: 20,
+ borderTopRightRadius: 20,
})),
cover: useAnimatedStyle(() => ({
- zIndex: 10,
alignItems: 'center',
- opacity: interpolate(scrollY.value, [0, width - 44], [1, 0], Extrapolate.CLAMP),
+ opacity: interpolate(scrollY.value, [HEADER - navbar - 20, HEADER - navbar], [1, 0], 'clamp'),
transform: [
- { scale: interpolate(scrollY.value, [-50, 0], [1.1, 1], Extrapolate.CLAMP) },
- { translateX: x.value },
- { translateY: y.value },
+ { translateY: interpolate(scrollY.value, [0, HEADER / 6], [0, HEADER / 6], 'clamp') },
],
})),
title: useAnimatedStyle(() => ({
- alignItems: 'center',
paddingTop: margin,
+ alignItems: 'center',
paddingHorizontal: margin * 3,
transform: [
- { translateY: interpolate(scrollY.value, [-50, 0], [20, 0], Extrapolate.CLAMP) },
+ { translateY: interpolate(scrollY.value, [-50, 0], [20, 0], 'clamp') },
],
- opacity: moving.value
- ? interpolate(y.value, [0, 50], [1, 0], Extrapolate.CLAMP)
- : interpolate(scrollY.value, [0, width - 44], [1, 0], Extrapolate.CLAMP),
+ opacity: interpolate(scrollY.value, [0, 30], [1, 0], 'clamp'),
})),
title2: useAnimatedStyle(() => ({
left: 0,
@@ -108,7 +69,7 @@ function BookHeader({
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: margin,
- opacity: interpolate(scrollY.value, [width - 44, width], [0, 1], Extrapolate.CLAMP),
+ opacity: interpolate(scrollY.value, [HEADER - navbar - 20, HEADER - navbar], [0, 1], 'clamp'),
})),
};
@@ -131,30 +92,28 @@ function BookHeader({
});
return (
-
-
-
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
- {book.bookTitleBare}
- {`by ${book.author.name}`}
-
+
+ {book.bookTitleBare}
+ {`by ${book.author.name}`}
+
-
-
- {book.bookTitleBare}
-
-
+
+
+ {book.bookTitleBare}
+
-
+
);
}
diff --git a/src/components/Button.jsx b/src/components/Button.jsx
index a143de9..75dd1b7 100644
--- a/src/components/Button.jsx
+++ b/src/components/Button.jsx
@@ -19,7 +19,7 @@ function ThemedButton({
justifyContent: 'center',
shadowRadius: 0,
shadowOpacity: 1,
- shadowColor: colors.shadow,
+ shadowColor: colors.primary,
shadowOffset: { width: 3, height: 3 },
backgroundColor: colors.button,
},
diff --git a/src/screens/BookDetailsScreen.jsx b/src/screens/BookDetailsScreen.jsx
index 9e9f4cb..f6429b0 100644
--- a/src/screens/BookDetailsScreen.jsx
+++ b/src/screens/BookDetailsScreen.jsx
@@ -1,12 +1,14 @@
/* eslint-disable no-param-reassign */
-import React, { useState, useEffect } from 'react';
+import React, { useState, useEffect, useRef } from 'react';
import {
View, Image, StyleSheet, Alert, StatusBar,
} from 'react-native';
import Animated, {
- interpolate, Extrapolate, useAnimatedStyle, useSharedValue, useAnimatedScrollHandler, withTiming,
+ interpolate, withTiming, runOnJS,
+ useAnimatedGestureHandler, useAnimatedStyle, useSharedValue, useAnimatedScrollHandler,
} from 'react-native-reanimated';
-import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
+import { PanGestureHandler, ScrollView } from 'react-native-gesture-handler';
+import { SafeAreaView } from 'react-native-safe-area-context';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { useIsFocused, useTheme } from '@react-navigation/native';
import { AntDesign } from '@expo/vector-icons';
@@ -18,20 +20,24 @@ import Text from '../components/Text';
import Button from '../components/Button';
import BookHeader from '../components/BookHeader';
+const AnimatedScrollView = Animated.createAnimatedComponent(ScrollView);
+
// Default screen
function BookDetails({ navigation, route }) {
const book = route.params?.book;
- const insets = useSafeAreaInsets();
const [bookList, setBookList] = useState([]);
const [fullBook, setFullBook] = useState(null);
const [author, setAuthor] = useState(null);
+ const [enabled, setEnabled] = useState(true);
+ const ref = useRef();
const loaded = useSharedValue(0);
- const scrollY = useSharedValue(0);
- const x = useSharedValue(0);
const y = useSharedValue(0);
+ const closing = useSharedValue(0);
+ const scrollY = useSharedValue(0);
const {
- margin, width, dark, colors,
+ margin, width, dark, colors, normalize, status,
} = useTheme();
+ const HEADER = normalize(width + status, 500) + margin;
// Save data to async storage
const saveData = async () => {
@@ -91,11 +97,42 @@ function BookDetails({ navigation, route }) {
// Scroll handler
const scrollHandler = useAnimatedScrollHandler((event) => {
scrollY.value = event.contentOffset.y;
+ if (event.contentOffset.y <= 0 && !enabled) {
+ runOnJS(setEnabled)(true);
+ }
+ if (event.contentOffset.y > 0 && enabled) {
+ runOnJS(setEnabled)(false);
+ }
+ });
+
+ // Pan gesture handler
+ const gestureHandler = useAnimatedGestureHandler({
+ onStart: (_, ctx) => {
+ ctx.startY = y.value;
+ },
+ onActive: (e, ctx) => {
+ y.value = ctx.startY + e.translationY;
+
+ // See if closing screen
+ const flung = e.velocityY >= 250 || y.value >= 75;
+ if (flung && !closing.value) {
+ closing.value = 1;
+ runOnJS(navigation.goBack)();
+ runOnJS(Haptics.selectionAsync)();
+ }
+ },
+ onEnd: (e) => {
+ if (y.value < 75 && e.velocityY < 250) {
+ y.value = withTiming(0);
+ }
+ },
});
// Load book details
useEffect(() => {
loadData();
+
+ // Book details
axios.get(`https://www.goodreads.com/book/show/${book.bookId}.xml?key=Bi8vh08utrMY3HAqM9rkWA`)
.then((resp) => {
const data = parse(resp.data);
@@ -121,33 +158,41 @@ function BookDetails({ navigation, route }) {
const anims = {
screen: useAnimatedStyle(() => ({
flex: 1,
+ shadowRadius: 10,
+ shadowOpacity: 0.5,
+ shadowOffset: { height: 5 },
+ transform: [
+ { translateY: y.value },
+ { scale: interpolate(y.value, [0, 75], [1, 0.90], 'clamp') },
+ ],
+ })),
+ scrollView: useAnimatedStyle(() => ({
+ flex: 1,
+ borderRadius: 20,
backgroundColor: colors.background,
- opacity: interpolate(y.value, [0, 50], [1, 0], Extrapolate.CLAMP),
})),
details: useAnimatedStyle(() => ({
opacity: loaded.value,
transform: [
- { translateY: interpolate(loaded.value, [0, 1], [20, 0], Extrapolate.CLAMP) },
+ { translateY: interpolate(loaded.value, [0, 1], [20, 0], 'clamp') },
],
})),
};
// Styles
const styles = StyleSheet.create({
- screen: {
- flex: 1,
- },
closeIcon: {
zIndex: 10,
top: margin,
right: margin,
+ opacity: 0.75,
color: colors.text,
position: 'absolute',
},
scrollContainer: {
padding: margin,
- paddingTop: width + insets.top + margin,
- paddingBottom: insets.bottom + margin + 50,
+ paddingTop: HEADER,
+ paddingBottom: status + margin + 50,
},
detailsBox: {
borderRadius: 10,
@@ -187,7 +232,6 @@ function BookDetails({ navigation, route }) {
aboutBook: {
lineHeight: 25,
marginTop: margin,
- textAlign: 'justify',
},
footer: {
left: 0,
@@ -203,55 +247,64 @@ function BookDetails({ navigation, route }) {
// Render book details
return (
-
-
-
- navigation.goBack()} style={styles.closeIcon} />
-
+
-
-
-
- RATING
- {book.avgRating}
-
-
- PAGES
- {book.numPages}
-
-
- STATUS
- {item ? item.status : '-'}
-
-
+
+
+ navigation.goBack()} style={styles.closeIcon} />
-
-
-
-
- {author?.name || '...'}
-
- {author?.about.replace(/(<([^>]+)>)/ig, '')}
-
+
+
+
+
+ RATING
+ {book.avgRating}
+
+
+ PAGES
+ {book.numPages}
+
+
+ STATUS
+ {item ? item.status : '-'}
-
- {fullBook?.description.replace(/(<([^>]+)>)/ig, '')}
-
-
-
-
-
-
+
+
+
+
+ {author?.name || '...'}
+
+ {author?.about.replace(/(<([^>]+)>)/ig, '')}
+
+
+
+
+ {fullBook?.description.replace(/(<([^>]+)>)/ig, '')}
+
+
+
+
+
+
+
+
-
+
);
}
diff --git a/src/screens/BookSearchScreen.jsx b/src/screens/BookSearchScreen.jsx
index 2508658..b749057 100644
--- a/src/screens/BookSearchScreen.jsx
+++ b/src/screens/BookSearchScreen.jsx
@@ -18,45 +18,22 @@ const bookImg = require('../images/books.png');
// Star rating
const Rating = React.memo(({ rating }) => (
-
-
-
-
-
-
+
+
+
+
+
+
));
-// What the button text should be
-const ListText = React.memo(({ list, book }) => {
- const move = useSharedValue(0);
- const item = list.find((b) => b.bookId === book.bookId);
- move.value = item ? 1 : 0;
-
- // animate text up
- const anims = useAnimatedStyle(() => ({
- opacity: withTiming(move.value ? 1 : 0),
- height: withTiming(move.value ? 25 : 0),
- }));
-
- const styles = StyleSheet.compose({
- color: '#27ae60',
- });
-
- return (
-
-
- {item?.status}
-
-
- );
-});
-
// Render book
-const Book = React.memo(({ book, list }) => {
- const { margin } = useTheme();
+const Book = React.memo(({ book, bookList }) => {
+ const { margin, colors, normalize } = useTheme();
const navigation = useNavigation();
- const move = useSharedValue(0);
+ const BOOKW = normalize(120, 150);
+ const BOOKH = BOOKW * 1.5;
+ const item = bookList.find((b) => b.bookId === book.bookId);
// View book details
const bookDetails = () => {
@@ -64,51 +41,52 @@ const Book = React.memo(({ book, list }) => {
navigation.navigate('BookDetails', { book });
};
- // Animated styles
- const imageStyles = useAnimatedStyle(() => ({
- width: 120,
- height: 180,
- borderRadius: 10,
- transform: [
- { translateX: interpolate(move.value, [0, 1], [0, margin / 2], Extrapolate.CLAMP) },
- ],
- }));
-
// Styles
const styles = StyleSheet.create({
bookBox: {
flexDirection: 'row',
marginBottom: margin * 1.5,
},
- bookCover: {
+ imgBox: {
+ borderRadius: 10,
shadowRadius: 5,
shadowOpacity: 0.5,
shadowOffset: { width: 5, height: 5 },
},
+ bookImg: {
+ width: BOOKW,
+ height: BOOKH,
+ borderRadius: 10,
+ },
bookDetails: {
flex: 1,
justifyContent: 'center',
paddingLeft: margin * 1.5,
},
bookAuthor: {
- marginVertical: margin / 2,
+ marginVertical: margin / 4,
},
});
// Render Book
return (
-
-
-
-
-
+
+
+
+
+
+
-
+ {item?.status && (
+
+ {item.status}
+
+ )}
{book.bookTitleBare}
-
+
{book.author.name}
@@ -119,7 +97,7 @@ const Book = React.memo(({ book, list }) => {
// Default screen
function SearchScreen({ navigation, route }) {
- const list = route.params?.bookList || [];
+ const bookList = route.params?.bookList;
const [query, setQuery] = useState('');
const [books, setBooks] = useState([]);
const scrollY = useSharedValue(0);
@@ -137,7 +115,7 @@ function SearchScreen({ navigation, route }) {
const goBack = () => {
loaded.value = withTiming(0);
Haptics.selectionAsync();
- navigation.navigate('BookList', { bookList: list });
+ navigation.navigate('BookList');
};
// Search
@@ -269,7 +247,7 @@ function SearchScreen({ navigation, route }) {
style={anims.scrollView}
>
{!books.length && }
- {books.map((book) => )}
+ {books.map((book) => )}
);
diff --git a/src/theme.js b/src/theme.js
index c3b54fb..2a60f37 100644
--- a/src/theme.js
+++ b/src/theme.js
@@ -12,14 +12,12 @@ export default function getTheme(scheme) {
height,
margin: 20,
colors: {
- primary: '#2ecc71',
+ primary: '#16a085',
text: dark ? '#f6f5f0' : '#100f0a',
card: dark ? '#000000' : '#ffffff',
background: dark ? '#100f0a' : '#f6f5f0',
- border: dark ? '#f6f5f033' : '#100f0add',
+ border: dark ? '#f6f5f0dd' : '#100f0add',
button: dark ? '#100f0add' : '#f6f5f0dd',
- // shadow: dark ? '#739ff2' : '#1660e9',
- shadow: '#2ecc71',
},
status: Constants.statusBarHeight,
navbar: Constants.statusBarHeight + 44,
diff --git a/yarn.lock b/yarn.lock
index 76eff9d..8fdf736 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4238,20 +4238,20 @@ loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.3.1, loose-envify@^1.4
dependencies:
js-tokens "^3.0.0 || ^4.0.0"
-lottie-ios@^3.1.8:
- version "3.1.9"
- resolved "https://registry.yarnpkg.com/lottie-ios/-/lottie-ios-3.1.9.tgz#592a8978cd679945c66a2b5fc087146c33e9d475"
- integrity sha512-iB/jtMQiZnhNvPiQOkyRU3h8y+SKsIL8bqo8aAC9wTv2YStXSInmCPLw3JbIFy0HqW6YPwEFbqgFjtgo9vAHUg==
+lottie-ios@2.5.0:
+ version "2.5.0"
+ resolved "https://registry.yarnpkg.com/lottie-ios/-/lottie-ios-2.5.0.tgz#55c808e785d4a6933b0c10b395530b17098b05de"
+ integrity sha1-VcgI54XUppM7DBCzlVMLFwmLBd4=
-lottie-react-native@^3.5.0:
- version "3.5.0"
- resolved "https://registry.yarnpkg.com/lottie-react-native/-/lottie-react-native-3.5.0.tgz#749fed964bdc9fabfae24ef81696f90f4d758f59"
- integrity sha512-yKYj58xynDAG/BqJUhg9LTATBqwD4ATGXJKL2Ho6g4BTmPexOjDBNnPSeRBwjlpBxQ1nP4Qw//0zbuFsTFD1TA==
+lottie-react-native@~2.6.1:
+ version "2.6.1"
+ resolved "https://registry.yarnpkg.com/lottie-react-native/-/lottie-react-native-2.6.1.tgz#330d24fa6aac5928ea63f8e181b9b7d930a1a119"
+ integrity sha512-Z+6lARvWWhB8n8OSmW7/aHkV71ftsmO7hYXFt0D+REy/G40mpkQt1H7Cdy1HqY4cKAp7EYDWVxhu5+fkdD6o4g==
dependencies:
invariant "^2.2.2"
- lottie-ios "^3.1.8"
+ lottie-ios "2.5.0"
prop-types "^15.5.10"
- react-native-safe-modules "^1.0.0"
+ react-native-safe-module "^1.1.0"
lru-cache@^4.0.1:
version "4.1.5"
@@ -5402,10 +5402,10 @@ react-native-safe-area-context@3.1.9:
resolved "https://registry.yarnpkg.com/react-native-safe-area-context/-/react-native-safe-area-context-3.1.9.tgz#48864ea976b0fa57142a2cc523e1fd3314e7247e"
integrity sha512-wmcGbdyE/vBSL5IjDPReoJUEqxkZsywZw5gPwsVUV1NBpw5eTIdnL6Y0uNKHE25Z661moxPHQz6kwAkYQyorxA==
-react-native-safe-modules@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/react-native-safe-modules/-/react-native-safe-modules-1.0.0.tgz#10a918adf97da920adb1e33e0c852b1e80123b65"
- integrity sha512-ShT8duWBT30W4OFcltZl+UvpPDikZFURvLDQqAsrvbyy6HzWPGJDCpdqM+6GqzPPs4DPEW31YfMNmdJcZ6zI2w==
+react-native-safe-module@^1.1.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/react-native-safe-module/-/react-native-safe-module-1.2.0.tgz#a23824ca24edc2901913694a76646475113d570d"
+ integrity sha1-ojgkyiTtwpAZE2lKdmRkdRE9Vw0=
dependencies:
dedent "^0.6.0"