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 ( - - + ); } 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"