Add new types #722

This commit is contained in:
xmflsct 2023-03-14 10:50:22 +01:00
parent f78693eee8
commit a0b3b38d8d
5 changed files with 225 additions and 107 deletions

View File

@ -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<Props> = ({ 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 ? (
<GracefullyImage
sources={{ default: { uri: data.cover_image_url } }}
dimension={{
width: StyleConstants.Font.LineHeight.M * 4,
height: StyleConstants.Font.LineHeight.M * 5
}}
style={{ marginRight: StyleConstants.Spacing.S }}
imageStyle={{ borderRadius: StyleConstants.BorderRadius / 2 }}
dim
/>
) : null
const itemHeading = (value: string) => (
<CustomText
fontStyle='S'
fontWeight='Bold'
style={{ color: colors.primaryDefault }}
numberOfLines={3}
children={value}
/>
)
const itemDetails = (value: string) => (
<CustomText
fontStyle='S'
style={{ color: colors.secondary }}
numberOfLines={1}
children={value}
/>
)
switch (segments[0]) {
case 'movie':
return (
<Pressable
style={{
marginTop: StyleConstants.Spacing.S,
backgroundColor: colors.shimmerDefault,
borderRadius: StyleConstants.BorderRadius,
padding: StyleConstants.Spacing.S,
flexDirection: 'row'
}}
onPress={() => openLink(card.url)}
>
<GracefullyImage
sources={{ default: { uri: `https://neodb.social${data.image}` } }}
dimension={{
width: StyleConstants.Font.LineHeight.M * 4,
height: StyleConstants.Font.LineHeight.M * 5
}}
style={{ marginRight: StyleConstants.Spacing.S }}
imageStyle={{ borderRadius: StyleConstants.BorderRadius / 2 }}
dim
/>
<Pressable {...pressableDefaults}>
{itemImage}
<View style={{ flex: 1, gap: StyleConstants.Spacing.S }}>
<CustomText
fontStyle='S'
fontWeight='Bold'
style={{ color: colors.primaryDefault }}
numberOfLines={3}
>
{[
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(' ')
)}
<Rating rating={data.rating / 2} />
{itemDetails(
[
data.duration ? `${data.duration}分钟` : null,
data.area?.join(' '),
data.genre?.join(' '),
data.director?.join(' ')
]
.filter(d => d)
.join(' ')}
</CustomText>
<CustomText fontStyle='S' style={{ color: colors.secondary }} numberOfLines={2}>
{[
data.data.duration ? `${data.data.duration}分钟` : null,
data.data.area?.join(' '),
data.data.genre?.join(' '),
data.data.director?.join(' ')
]
.filter(d => d)
.join(' / ')}
</CustomText>
.join(' / ')
)}
</View>
</Pressable>
)
case 'book':
return (
<Pressable
style={{
marginTop: StyleConstants.Spacing.S,
backgroundColor: colors.shimmerDefault,
borderRadius: StyleConstants.BorderRadius,
padding: StyleConstants.Spacing.S,
flexDirection: 'row'
}}
onPress={() => openLink(card.url)}
>
<GracefullyImage
sources={{ default: { uri: `https://neodb.social${data.image}` } }}
dimension={{
width: StyleConstants.Font.LineHeight.M * 4,
height: StyleConstants.Font.LineHeight.M * 5
}}
style={{ marginRight: StyleConstants.Spacing.S }}
imageStyle={{ borderRadius: StyleConstants.BorderRadius / 2 }}
dim
/>
<Pressable {...pressableDefaults}>
{itemImage}
<View style={{ flex: 1, gap: StyleConstants.Spacing.S }}>
<CustomText
fontStyle='S'
fontWeight='Bold'
style={{ color: colors.primaryDefault }}
numberOfLines={3}
>
{[
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(' ')}
</CustomText>
<CustomText fontStyle='S' style={{ color: colors.secondary }} numberOfLines={2}>
{[
data.data.author?.join(' '),
data.data.language,
data.data.pages ? `${data.data.pages}` : null,
data.data.pub_house
.join(' ')
)}
<Rating rating={data.rating / 2} />
{itemDetails(
[
data.author?.join(' '),
data.language,
data.pages ? `${data.pages}` : null,
data.pub_house
]
.filter(d => d)
.join(' / ')}
</CustomText>
.join(' / ')
)}
</View>
</Pressable>
)
case 'tv':
return (
<Pressable {...pressableDefaults}>
{itemImage}
<View style={{ flex: 1, gap: StyleConstants.Spacing.S }}>
{itemHeading(
[data.title, data.orig_title, data.year ? `(${data.year})` : null]
.filter(d => d)
.join(' ')
)}
<Rating rating={data.rating / 2} />
{itemDetails(
[
data.season_count ? `${data.season_count}` : null,
data.area?.join(' '),
data.genre?.join(' '),
data.director?.join(' ')
]
.filter(d => d)
.join(' / ')
)}
</View>
</Pressable>
)

View File

@ -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<StarRatingProps> = ({
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 (
<View style={{ flexDirection: 'row' }}>
{Array.from({ length: count }, (v, i) => {
return (
<Star
key={i}
size={size}
strokeLinejoin={roundedCorner ? 'round' : 'miter'}
strokeLinecap={roundedCorner ? 'round' : 'butt'}
offset={getSelectedOffsetPercent(i)}
/>
)
})}
</View>
)
}

View File

@ -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<StarProps> = ({ size, strokeLinejoin, strokeLinecap, offset }) => {
const { colors } = useTheme()
const innerRadius = 25
const outerRadius = 50
const [id, setId] = useState<string>('')
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 (
<Svg width={size} height={size} viewBox={`0 0 100 100`}>
<Defs>
<LinearGradient id={id} x1='0' x2='100%' y1='0' y2='0'>
<Stop offset={`0%`} stopColor={colors.yellow} />
<Stop offset={`${offset}%`} stopColor={colors.yellow} />
<Stop offset={`${offset}%`} stopColor={colors.secondary} />
</LinearGradient>
</Defs>
<Path
d={`M${points.toString()}Z`}
fill={`url(#${id})`}
strokeLinejoin={strokeLinejoin}
strokeLinecap={strokeLinecap}
/>
</Svg>
)
}

View File

@ -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 <CardNeodb card={status.card} />
}

View File

@ -4,27 +4,12 @@ import { AxiosError } from 'axios'
export type QueryKeyNeodb = ['Neodb', { path: string }]
const queryFunction = async ({ queryKey }: QueryFunctionContext<QueryKeyNeodb>) => {
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<QueryKeyNeodb>) =>
apiGeneral({
method: 'get',
domain: 'neodb.social',
url: `/api/${queryKey[1].path}`
}).then(res => res.body)
export const useNeodbQuery = (
params: QueryKeyNeodb[1] & {