mirror of
https://github.com/tooot-app/app
synced 2025-04-25 07:28:41 +02:00
Add new types #722
This commit is contained in:
parent
f78693eee8
commit
a0b3b38d8d
@ -6,116 +6,137 @@ import { StyleConstants } from '@utils/styles/constants'
|
|||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import * as Linking from 'expo-linking'
|
import * as Linking from 'expo-linking'
|
||||||
import { Pressable, View } from 'react-native'
|
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 { colors } = useTheme()
|
||||||
|
|
||||||
const segments = Linking.parse(card.url).path?.split('/')
|
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]}` })
|
const { data } = useNeodbQuery({ path: `${segments[0]}/${segments[1]}` })
|
||||||
|
|
||||||
if (!data) return null
|
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]) {
|
switch (segments[0]) {
|
||||||
case 'movie':
|
case 'movie':
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable {...pressableDefaults}>
|
||||||
style={{
|
{itemImage}
|
||||||
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
|
|
||||||
/>
|
|
||||||
<View style={{ flex: 1, gap: StyleConstants.Spacing.S }}>
|
<View style={{ flex: 1, gap: StyleConstants.Spacing.S }}>
|
||||||
<CustomText
|
{itemHeading(
|
||||||
fontStyle='S'
|
[data.title, data.orig_title, data.year ? `(${data.year})` : null]
|
||||||
fontWeight='Bold'
|
.filter(d => d)
|
||||||
style={{ color: colors.primaryDefault }}
|
.join(' ')
|
||||||
numberOfLines={3}
|
)}
|
||||||
>
|
<Rating rating={data.rating / 2} />
|
||||||
{[
|
{itemDetails(
|
||||||
data.data.title,
|
[
|
||||||
data.data.orig_title,
|
data.duration ? `${data.duration}分钟` : null,
|
||||||
data.data.year ? `(${data.data.year})` : null
|
data.area?.join(' '),
|
||||||
|
data.genre?.join(' '),
|
||||||
|
data.director?.join(' ')
|
||||||
]
|
]
|
||||||
.filter(d => d)
|
.filter(d => d)
|
||||||
.join(' ')}
|
.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>
|
|
||||||
</View>
|
</View>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
)
|
)
|
||||||
case 'book':
|
case 'book':
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable {...pressableDefaults}>
|
||||||
style={{
|
{itemImage}
|
||||||
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
|
|
||||||
/>
|
|
||||||
<View style={{ flex: 1, gap: StyleConstants.Spacing.S }}>
|
<View style={{ flex: 1, gap: StyleConstants.Spacing.S }}>
|
||||||
<CustomText
|
{itemHeading(
|
||||||
fontStyle='S'
|
[
|
||||||
fontWeight='Bold'
|
data.title,
|
||||||
style={{ color: colors.primaryDefault }}
|
data.pub_year && data.pub_month ? `(${data.pub_year}年${data.pub_month}月)` : null
|
||||||
numberOfLines={3}
|
|
||||||
>
|
|
||||||
{[
|
|
||||||
data.data.title,
|
|
||||||
data.data.pub_year && data.data.pub_month
|
|
||||||
? `(${data.data.pub_year}年${data.data.pub_month}月)`
|
|
||||||
: null
|
|
||||||
]
|
]
|
||||||
.filter(d => d)
|
.filter(d => d)
|
||||||
.join(' ')}
|
.join(' ')
|
||||||
</CustomText>
|
)}
|
||||||
<CustomText fontStyle='S' style={{ color: colors.secondary }} numberOfLines={2}>
|
<Rating rating={data.rating / 2} />
|
||||||
{[
|
{itemDetails(
|
||||||
data.data.author?.join(' '),
|
[
|
||||||
data.data.language,
|
data.author?.join(' '),
|
||||||
data.data.pages ? `${data.data.pages}页` : null,
|
data.language,
|
||||||
data.data.pub_house
|
data.pages ? `${data.pages}页` : null,
|
||||||
|
data.pub_house
|
||||||
]
|
]
|
||||||
.filter(d => d)
|
.filter(d => d)
|
||||||
.join(' / ')}
|
.join(' / ')
|
||||||
</CustomText>
|
)}
|
||||||
|
</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>
|
</View>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
)
|
)
|
||||||
|
57
src/components/Timeline/Shared/Card/Rating.tsx
Normal file
57
src/components/Timeline/Shared/Card/Rating.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
52
src/components/Timeline/Shared/Card/Star.tsx
Normal file
52
src/components/Timeline/Shared/Card/Star.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
@ -22,7 +22,10 @@ const TimelineCard: React.FC = () => {
|
|||||||
if (!status || !status.card) return null
|
if (!status || !status.card) return null
|
||||||
|
|
||||||
const { i18n } = useTranslation()
|
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} />
|
return <CardNeodb card={status.card} />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,27 +4,12 @@ import { AxiosError } from 'axios'
|
|||||||
|
|
||||||
export type QueryKeyNeodb = ['Neodb', { path: string }]
|
export type QueryKeyNeodb = ['Neodb', { path: string }]
|
||||||
|
|
||||||
const queryFunction = async ({ queryKey }: QueryFunctionContext<QueryKeyNeodb>) => {
|
const queryFunction = async ({ queryKey }: QueryFunctionContext<QueryKeyNeodb>) =>
|
||||||
const data: any = {}
|
apiGeneral({
|
||||||
|
method: 'get',
|
||||||
await Promise.all([
|
domain: 'neodb.social',
|
||||||
apiGeneral({
|
url: `/api/${queryKey[1].path}`
|
||||||
method: 'get',
|
}).then(res => res.body)
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useNeodbQuery = (
|
export const useNeodbQuery = (
|
||||||
params: QueryKeyNeodb[1] & {
|
params: QueryKeyNeodb[1] & {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user