diff --git a/src/components/Timeline/Shared/Card/Neodb.tsx b/src/components/Timeline/Shared/Card/Neodb.tsx index 98bffedd..460f891c 100644 --- a/src/components/Timeline/Shared/Card/Neodb.tsx +++ b/src/components/Timeline/Shared/Card/Neodb.tsx @@ -6,116 +6,137 @@ import { StyleConstants } from '@utils/styles/constants' import { useTheme } from '@utils/styles/ThemeManager' import * as Linking from 'expo-linking' import { Pressable, View } from 'react-native' +import { Rating } from './Rating' -export const CardNeodb = ({ card }: { card: Mastodon.Card }) => { +export type Props = { + card: Mastodon.Card +} + +export const CardNeodb: React.FC = ({ card }) => { const { colors } = useTheme() const segments = Linking.parse(card.url).path?.split('/') - if (!segments || !(segments[0] === 'movie' || segments[0] === 'book')) return null + if (!segments || !(segments[0] === 'movie' || segments[0] === 'book' || segments[0] === 'tv')) + return null const { data } = useNeodbQuery({ path: `${segments[0]}/${segments[1]}` }) if (!data) return null + const pressableDefaults = { + style: { + marginTop: StyleConstants.Spacing.S, + backgroundColor: colors.shimmerDefault, + borderRadius: StyleConstants.BorderRadius, + padding: StyleConstants.Spacing.S, + flexDirection: 'row' as 'row' + }, + onPress: () => openLink(card.url) + } + + const itemImage = data.cover_image_url ? ( + + ) : null + const itemHeading = (value: string) => ( + + ) + const itemDetails = (value: string) => ( + + ) + switch (segments[0]) { case 'movie': return ( - openLink(card.url)} - > - + + {itemImage} - - {[ - data.data.title, - data.data.orig_title, - data.data.year ? `(${data.data.year})` : null + {itemHeading( + [data.title, data.orig_title, data.year ? `(${data.year})` : null] + .filter(d => d) + .join(' ') + )} + + {itemDetails( + [ + data.duration ? `${data.duration}分钟` : null, + data.area?.join(' '), + data.genre?.join(' '), + data.director?.join(' ') ] .filter(d => d) - .join(' ')} - - - {[ - data.data.duration ? `${data.data.duration}分钟` : null, - data.data.area?.join(' '), - data.data.genre?.join(' '), - data.data.director?.join(' ') - ] - .filter(d => d) - .join(' / ')} - + .join(' / ') + )} ) case 'book': return ( - openLink(card.url)} - > - + + {itemImage} - - {[ - data.data.title, - data.data.pub_year && data.data.pub_month - ? `(${data.data.pub_year}年${data.data.pub_month}月)` - : null + {itemHeading( + [ + data.title, + data.pub_year && data.pub_month ? `(${data.pub_year}年${data.pub_month}月)` : null ] .filter(d => d) - .join(' ')} - - - {[ - data.data.author?.join(' '), - data.data.language, - data.data.pages ? `${data.data.pages}页` : null, - data.data.pub_house + .join(' ') + )} + + {itemDetails( + [ + data.author?.join(' '), + data.language, + data.pages ? `${data.pages}页` : null, + data.pub_house ] .filter(d => d) - .join(' / ')} - + .join(' / ') + )} + + + ) + case 'tv': + return ( + + {itemImage} + + {itemHeading( + [data.title, data.orig_title, data.year ? `(${data.year})` : null] + .filter(d => d) + .join(' ') + )} + + {itemDetails( + [ + data.season_count ? `共${data.season_count}季` : null, + data.area?.join(' '), + data.genre?.join(' '), + data.director?.join(' ') + ] + .filter(d => d) + .join(' / ') + )} ) diff --git a/src/components/Timeline/Shared/Card/Rating.tsx b/src/components/Timeline/Shared/Card/Rating.tsx new file mode 100644 index 00000000..8fa0901b --- /dev/null +++ b/src/components/Timeline/Shared/Card/Rating.tsx @@ -0,0 +1,57 @@ +import { StyleConstants } from '@utils/styles/constants' +import { View } from 'react-native' +import { Star } from './Star' + +interface StarRatingProps { + rating?: number + + unit?: 'full' | 'half' | 'float' + size?: number + count?: number + roundedCorner?: boolean +} + +const starUnitMap = { + full: 100, + half: 50, + float: 10 +} +export const Rating: React.FC = ({ + rating, + size = StyleConstants.Font.Size.M, + count = 5, + roundedCorner = true, + unit = 'float' +}) => { + if (!rating) return null + + const unitValue = starUnitMap[unit] + + const getSelectedOffsetPercent = (starIndex: number) => { + const roundedSelectedValue = Math.floor(rating) + if (starIndex < roundedSelectedValue) { + return 100 + } else if (starIndex > roundedSelectedValue) { + return 0 + } else { + const currentStarOffsetPercentage = (rating % 1) * 100 + return Math.ceil(currentStarOffsetPercentage / unitValue) * unitValue + } + } + + return ( + + {Array.from({ length: count }, (v, i) => { + return ( + + ) + })} + + ) +} diff --git a/src/components/Timeline/Shared/Card/Star.tsx b/src/components/Timeline/Shared/Card/Star.tsx new file mode 100644 index 00000000..210ba93e --- /dev/null +++ b/src/components/Timeline/Shared/Card/Star.tsx @@ -0,0 +1,52 @@ +import { useTheme } from '@utils/styles/ThemeManager' +import { uniqueId } from 'lodash' +import { useEffect, useState } from 'react' +import { Defs, LinearGradient, Path, Stop, Svg } from 'react-native-svg' + +interface StarProps { + size: number + strokeLinejoin: 'miter' | 'round' + strokeLinecap: 'butt' | 'round' + offset: number +} + +const NUM_POINT = 5 +export const Star: React.FC = ({ size, strokeLinejoin, strokeLinecap, offset }) => { + const { colors } = useTheme() + + const innerRadius = 25 + const outerRadius = 50 + + const [id, setId] = useState('') + useEffect(() => { + setId(uniqueId()) + }, []) + + const center = Math.max(innerRadius, outerRadius) + const angle = Math.PI / NUM_POINT + const points = [] + + for (let i = 0; i < NUM_POINT * 2; i++) { + let radius = i % 2 === 0 ? outerRadius : innerRadius + points.push(center + radius * Math.sin(i * angle)) + points.push(center - radius * Math.cos(i * angle)) + } + + return ( + + + + + + + + + + + ) +} diff --git a/src/components/Timeline/Shared/Card/index.tsx b/src/components/Timeline/Shared/Card/index.tsx index 7372c0a8..6d6ac8e5 100644 --- a/src/components/Timeline/Shared/Card/index.tsx +++ b/src/components/Timeline/Shared/Card/index.tsx @@ -22,7 +22,10 @@ const TimelineCard: React.FC = () => { if (!status || !status.card) return null const { i18n } = useTranslation() - if (status.card.url.includes('://neodb.social/') && i18n.language.toLowerCase() === 'zh-hans') { + if ( + status.card.url.includes('://neodb.social/') && + i18n.language.toLowerCase().startsWith('zh-hans') + ) { return } diff --git a/src/utils/queryHooks/neodb.ts b/src/utils/queryHooks/neodb.ts index fce9cfdf..5486ab20 100644 --- a/src/utils/queryHooks/neodb.ts +++ b/src/utils/queryHooks/neodb.ts @@ -4,27 +4,12 @@ import { AxiosError } from 'axios' export type QueryKeyNeodb = ['Neodb', { path: string }] -const queryFunction = async ({ queryKey }: QueryFunctionContext) => { - const data: any = {} - - await Promise.all([ - apiGeneral({ - method: 'get', - domain: 'neodb.social', - url: `/${queryKey[1].path}` - }).then(res => { - const matches = (res.body as string).match(/"(\/media\/.+autocrop.+?)"/) - data.image = matches?.[1] - }), - apiGeneral({ - method: 'get', - domain: 'neodb.social', - url: `/api/${queryKey[1].path}` - }).then(res => (data.data = res.body)) - ]) - - return data -} +const queryFunction = async ({ queryKey }: QueryFunctionContext) => + apiGeneral({ + method: 'get', + domain: 'neodb.social', + url: `/api/${queryKey[1].path}` + }).then(res => res.body) export const useNeodbQuery = ( params: QueryKeyNeodb[1] & {