vendor progress circle

This commit is contained in:
Samuel Newman
2026-05-21 14:31:01 +03:00
parent 1b546c54db
commit 16699d98a5
9 changed files with 173 additions and 30 deletions
-1
View File
@@ -222,7 +222,6 @@
"react-native-gesture-handler": "~2.28.0",
"react-native-keyboard-controller": "^1.21.7",
"react-native-pager-view": "6.8.0",
"react-native-progress": "bluesky-social/react-native-progress",
"react-native-qrcode-styled": "^0.3.3",
"react-native-reanimated": "3.19.1",
"react-native-safe-area-context": "~5.6.0",
-14
View File
@@ -454,9 +454,6 @@ importers:
react-native-pager-view:
specifier: 6.8.0
version: 6.8.0(patch_hash=0979d6fe1e2fa43b2784cc0b5e0ff0117d53b53423e85ee5855eefdc41a00108)(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
react-native-progress:
specifier: bluesky-social/react-native-progress
version: https://codeload.github.com/bluesky-social/react-native-progress/tar.gz/5a372f4f2ce5feb26f4f47b6a4d187ab9b923ab4(react-native-svg@15.12.1(patch_hash=31401fa6ff01498773afb51a7b066821c471eb3e71b0764a10017938beed49e9)(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))
react-native-qrcode-styled:
specifier: ^0.3.3
version: 0.3.3(react-native-svg@15.12.1(patch_hash=31401fa6ff01498773afb51a7b066821c471eb3e71b0764a10017938beed49e9)(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
@@ -8686,12 +8683,6 @@ packages:
react: '*'
react-native: '*'
react-native-progress@https://codeload.github.com/bluesky-social/react-native-progress/tar.gz/5a372f4f2ce5feb26f4f47b6a4d187ab9b923ab4:
resolution: {gitHosted: true, integrity: sha512-gDJJirvdhK5uUuI2OkiuleTbmeCyjkf1G6kPd4HAMc1pW8pUUeA0wKyPxZaI+s9htw8IDH6mFlmBB1ob8QMB4g==, tarball: https://codeload.github.com/bluesky-social/react-native-progress/tar.gz/5a372f4f2ce5feb26f4f47b6a4d187ab9b923ab4}
version: 5.0.0
peerDependencies:
react-native-svg: '*'
react-native-qrcode-styled@0.3.3:
resolution: {integrity: sha512-DjxFhFAJBY3y2FEBbWSpwtPaSjsEc5RutTHejxzfRNuyTzg1f5thplGyON8yx0kbi6h0h+57s8cH4aSFGj1mQg==}
engines: {node: '>= 16.0.0'}
@@ -19954,11 +19945,6 @@ snapshots:
react: 19.1.0
react-native: 0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0)
react-native-progress@https://codeload.github.com/bluesky-social/react-native-progress/tar.gz/5a372f4f2ce5feb26f4f47b6a4d187ab9b923ab4(react-native-svg@15.12.1(patch_hash=31401fa6ff01498773afb51a7b066821c471eb3e71b0764a10017938beed49e9)(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)):
dependencies:
prop-types: 15.8.1
react-native-svg: 15.12.1(patch_hash=31401fa6ff01498773afb51a7b066821c471eb3e71b0764a10017938beed49e9)(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
react-native-qrcode-styled@0.3.3(react-native-svg@15.12.1(patch_hash=31401fa6ff01498773afb51a7b066821c471eb3e71b0764a10017938beed49e9)(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0):
dependencies:
qrcode: 1.5.4
+85
View File
@@ -0,0 +1,85 @@
import {useEffect} from 'react'
import Animated, {
Easing,
useAnimatedProps,
useSharedValue,
withTiming,
} from 'react-native-reanimated'
import Svg, {Circle as SvgCircle} from 'react-native-svg'
import {clamp01} from './shared'
const AnimatedCircle = Animated.createAnimatedComponent(SvgCircle)
export function ProgressCircle({
progress,
size,
color,
thickness = 3,
borderWidth = 1,
borderColor,
unfilledColor,
}: {
progress: number
size: number
color: string
thickness?: number
borderWidth?: number
borderColor?: string
unfilledColor?: string
}) {
const stroke = borderColor ?? color
const r = (size - Math.max(borderWidth, thickness)) / 2
const cx = size / 2
const circumference = 2 * Math.PI * r
const sv = useSharedValue(clamp01(progress))
useEffect(() => {
sv.set(
withTiming(clamp01(progress), {
duration: 200,
easing: Easing.out(Easing.quad),
}),
)
}, [progress, sv])
const animatedProps = useAnimatedProps(() => ({
strokeDashoffset: circumference * (1 - sv.get()),
}))
return (
<Svg width={size} height={size}>
{borderWidth > 0 && (
<SvgCircle
cx={cx}
cy={cx}
r={r}
stroke={stroke}
strokeWidth={borderWidth}
fill="none"
/>
)}
{unfilledColor && (
<SvgCircle
cx={cx}
cy={cx}
r={r}
stroke={unfilledColor}
strokeWidth={thickness}
fill="none"
/>
)}
<AnimatedCircle
cx={cx}
cy={cx}
r={r}
stroke={color}
strokeWidth={thickness}
fill="none"
strokeDasharray={circumference}
animatedProps={animatedProps}
transform={`rotate(-90 ${cx} ${cx})`}
/>
</Svg>
)
}
+70
View File
@@ -0,0 +1,70 @@
import {useEffect} from 'react'
import Animated, {
Easing,
useAnimatedProps,
useSharedValue,
withTiming,
} from 'react-native-reanimated'
import Svg, {Circle as SvgCircle, Path} from 'react-native-svg'
import {clamp01} from './shared'
const AnimatedPath = Animated.createAnimatedComponent(Path)
function describeWedge(cx: number, cy: number, r: number, progress: number) {
'worklet'
// avoid degenerate path at 360deg where start and end points collide
const p = progress >= 1 ? 0.9999 : progress
if (p <= 0) return ''
const angle = p * 2 * Math.PI - Math.PI / 2
const x = cx + r * Math.cos(angle)
const y = cy + r * Math.sin(angle)
const largeArc = p > 0.5 ? 1 : 0
return `M ${cx} ${cy} L ${cx} ${cy - r} A ${r} ${r} 0 ${largeArc} 1 ${x} ${y} Z`
}
export function ProgressPie({
progress,
size,
color,
borderWidth = 1,
borderColor,
}: {
progress: number
size: number
color: string
borderWidth?: number
borderColor?: string
}) {
const stroke = borderColor ?? color
const r = (size - borderWidth) / 2
const cx = size / 2
const sv = useSharedValue(clamp01(progress))
useEffect(() => {
sv.set(
withTiming(clamp01(progress), {
duration: 200,
easing: Easing.out(Easing.quad),
}),
)
}, [progress, sv])
const animatedProps = useAnimatedProps(() => ({
d: describeWedge(cx, cx, r, sv.get()),
}))
return (
<Svg width={size} height={size}>
<SvgCircle
cx={cx}
cy={cx}
r={r}
stroke={stroke}
strokeWidth={borderWidth}
fill="none"
/>
<AnimatedPath fill={color} animatedProps={animatedProps} />
</Svg>
)
}
+2
View File
@@ -0,0 +1,2 @@
export {ProgressCircle} from './Circle'
export {ProgressPie} from './Pie'
+6
View File
@@ -0,0 +1,6 @@
export function clamp01(n: number) {
'worklet'
if (n < 0) return 0
if (n > 1) return 1
return n
}
+2 -2
View File
@@ -1,8 +1,8 @@
import {View} from 'react-native'
import * as Progress from 'react-native-progress'
import {atoms as a, useTheme} from '#/alf'
import {AnimatedCheck} from '../anim/AnimatedCheck'
import {ProgressCircle} from '../Progress'
import {Text} from '../Typography'
export function ProgressGuideTask({
@@ -25,7 +25,7 @@ export function ProgressGuideTask({
{current === total ? (
<AnimatedCheck playOnMount fill={t.palette.primary_500} width={20} />
) : (
<Progress.Circle
<ProgressCircle
progress={current / total}
color={t.palette.primary_400}
size={20}
+7 -11
View File
@@ -21,8 +21,6 @@ import {
View,
type ViewStyle,
} from 'react-native'
// @ts-expect-error no type definition
import ProgressCircle from 'react-native-progress/Circle'
import Animated, {
type AnimatedRef,
Easing,
@@ -129,6 +127,7 @@ import {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmileIcon} from '#/components/i
import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus'
import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times'
import {LazyQuoteEmbed} from '#/components/Post/Embed/LazyQuoteEmbed'
import {ProgressCircle} from '#/components/Progress'
import * as Prompt from '#/components/Prompt'
import * as Toast from '#/components/Toast'
import {Text} from '#/components/Typography'
@@ -2072,10 +2071,7 @@ function ComposerFooter({
onSelectLanguage={onSelectLanguage}
nudgeAt={languageNudgeAt}
/>
<CharProgress
count={post.shortenedGraphemeLength}
style={{width: 65}}
/>
<CharProgress count={post.shortenedGraphemeLength} />
</View>
</View>
)
@@ -2413,7 +2409,7 @@ function ToolbarWrapper({
if (IS_WEB) return children
return (
<Animated.View
style={style}
style={[style, {minHeight: 40}]}
entering={FadeIn.duration(400)}
exiting={FadeOut.duration(400)}>
{children}
@@ -2488,12 +2484,12 @@ function VideoUploadToolbar({state}: {state: VideoState}) {
}
return (
<ToolbarWrapper style={[a.flex_row, a.align_center, {paddingVertical: 5}]}>
<ToolbarWrapper style={[a.flex_row, a.align_center]}>
<Animated.View style={[animatedStyle]}>
<ProgressCircle
size={30}
borderWidth={1}
borderColor={t.atoms.border_contrast_low.borderColor}
size={32}
borderWidth={3}
borderColor={t.palette.contrast_50}
color={
state.status === 'error'
? t.palette.negative_500
@@ -1,10 +1,9 @@
import {View} from 'react-native'
// @ts-expect-error no type definition
import ProgressPie from 'react-native-progress/Pie'
import {type ImagePickerAsset} from 'expo-image-picker'
import {atoms as a, useTheme} from '#/alf'
import {ConstrainedImage} from '#/components/images/AutoSizedImage'
import {ProgressPie} from '#/components/Progress'
import {IS_WEB} from '#/env'
import {ExternalEmbedRemoveBtn} from '../ExternalEmbedRemoveBtn'
import {VideoTranscodeBackdrop} from './VideoTranscodeBackdrop'