动态和主播推荐改为服务器逻辑;优化全屏看图功能,添加会员保存图片功能;更新icon;修复bug
This commit is contained in:
parent
46b93373ce
commit
789842a95d
BIN
assets/icon.png
BIN
assets/icon.png
Binary file not shown.
Before Width: | Height: | Size: 123 KiB After Width: | Height: | Size: 102 KiB |
|
@ -0,0 +1,191 @@
|
|||
import {
|
||||
View,
|
||||
Text,
|
||||
Modal,
|
||||
TouchableOpacity,
|
||||
ActivityIndicator,
|
||||
} from "react-native";
|
||||
import React, { useState, useCallback, useEffect } from "react";
|
||||
import { ImageViewer as OriginImageViewer } from "react-native-image-zoom-viewer";
|
||||
import { Icon } from "@rneui/themed";
|
||||
import { useTailwind } from "tailwind-rn";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import saveImage from "../../utils/saveImage";
|
||||
import Toast from "react-native-toast-message";
|
||||
import { get } from "../../utils/storeInfo";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import MyModal from "../MyModal";
|
||||
|
||||
//isVisible boolean
|
||||
//setIsVisible function
|
||||
//imageUrls [{ url: string }]
|
||||
//index int
|
||||
|
||||
export default function ImageViewer({
|
||||
isVisible,
|
||||
setIsVisible,
|
||||
imageUrls,
|
||||
index,
|
||||
}) {
|
||||
const tailwind = useTailwind();
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
const navigation = useNavigation();
|
||||
|
||||
const [isSaved, setIsSaved] = useState(false);
|
||||
const [isVip, setIsVip] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const checkRole = async () => {
|
||||
const account = await get("account");
|
||||
const role = account.role;
|
||||
const isVip = account.is_a_member;
|
||||
if (role !== 0 || isVip === 1) {
|
||||
setIsVip(true);
|
||||
}
|
||||
};
|
||||
checkRole();
|
||||
}, []);
|
||||
|
||||
const MenusComponent = useCallback(
|
||||
({ cancel, saveToLocal }) => (
|
||||
<Modal
|
||||
visible={true}
|
||||
transparent={true}
|
||||
statusBarTranslucent
|
||||
animationType="fade"
|
||||
>
|
||||
<TouchableOpacity
|
||||
onPress={cancel}
|
||||
activeOpacity={1}
|
||||
style={tailwind("flex flex-1 bg-[#00000080]")}
|
||||
>
|
||||
<TouchableOpacity
|
||||
activeOpacity={1}
|
||||
style={{
|
||||
paddingBottom: insets.bottom,
|
||||
paddingLeft: insets.left,
|
||||
paddingRight: insets.right,
|
||||
...tailwind(
|
||||
"absolute bottom-0 left-0 h-32 w-full bg-[#13121F] rounded-t-2xl"
|
||||
),
|
||||
}}
|
||||
>
|
||||
<View style={tailwind("flex flex-1 flex-row items-center px-4")}>
|
||||
<TouchableOpacity
|
||||
onPress={
|
||||
isVip
|
||||
? saveToLocal
|
||||
: () => {
|
||||
setIsVisible(false);
|
||||
navigation.navigate("WebWithoutHeader", {
|
||||
uri: process.env.EXPO_PUBLIC_WEB_URL + "/vip",
|
||||
});
|
||||
}
|
||||
}
|
||||
style={tailwind("flex flex-col items-center px-4")}
|
||||
>
|
||||
<Icon type="ionicon" name="image" size={30} color="white" />
|
||||
<Text
|
||||
style={tailwind("text-sm text-[#FFFFFF80] font-medium mt-1")}
|
||||
>
|
||||
{isSaved ? "已保存" : "保存(会员特权)"}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
onPress={cancel}
|
||||
style={tailwind("flex flex-col items-center px-4")}
|
||||
>
|
||||
<Icon
|
||||
type="ionicon"
|
||||
name="close-circle"
|
||||
size={30}
|
||||
color="white"
|
||||
/>
|
||||
<Text
|
||||
style={tailwind("text-sm text-[#FFFFFF80] font-medium mt-1")}
|
||||
>
|
||||
取消
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</TouchableOpacity>
|
||||
</Modal>
|
||||
),
|
||||
[isSaved, isVip]
|
||||
);
|
||||
|
||||
const [isVipModalVisible, setIsVipModalVisible] = useState(false);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const hanldSaveImage = async (index) => {
|
||||
if (!isVip) {
|
||||
setIsVipModalVisible(true);
|
||||
return;
|
||||
}
|
||||
if (isSaving) return;
|
||||
setIsSaving(true);
|
||||
const isSuccess = await saveImage(imageUrls[index].url);
|
||||
setIsSaved(isSuccess);
|
||||
setIsSaving(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal visible={isVisible} statusBarTranslucent transparent={true}>
|
||||
<OriginImageViewer
|
||||
imageUrls={imageUrls}
|
||||
index={index}
|
||||
onClick={() => setIsVisible(false)}
|
||||
onSwipeDown={() => setIsVisible(false)}
|
||||
enableSwipeDown
|
||||
backgroundColor="#07050A"
|
||||
renderFooter={(index) => (
|
||||
<TouchableOpacity
|
||||
onPress={() => hanldSaveImage(index)}
|
||||
style={{
|
||||
marginLeft: 20,
|
||||
marginBottom: insets.bottom,
|
||||
...tailwind(
|
||||
"flex justify-center items-center w-12 h-12 bg-[#FFFFFF1A] rounded-full"
|
||||
),
|
||||
}}
|
||||
>
|
||||
{isSaving && <ActivityIndicator size="small" />}
|
||||
{!isSaving && (
|
||||
<Icon
|
||||
type="ionicon"
|
||||
name="save-outline"
|
||||
size={20}
|
||||
color="white"
|
||||
/>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
onSave={async (url) => {
|
||||
const isSuccess = await saveImage(url);
|
||||
setIsSaved(isSuccess);
|
||||
}}
|
||||
saveToLocalByLongPress={true}
|
||||
menus={MenusComponent}
|
||||
loadingRender={() => <ActivityIndicator size="large" />}
|
||||
/>
|
||||
<Toast />
|
||||
<MyModal
|
||||
visible={isVipModalVisible}
|
||||
setVisible={setIsVipModalVisible}
|
||||
title="是否开通会员?"
|
||||
content="会员可无限制保存图片,一次开通永久有效。"
|
||||
cancel={() => {
|
||||
setIsVipModalVisible(false);
|
||||
}}
|
||||
confirm={() => {
|
||||
setIsVipModalVisible(false);
|
||||
setIsVisible(false);
|
||||
navigation.navigate("WebWithoutHeader", {
|
||||
uri: process.env.EXPO_PUBLIC_WEB_URL + "/vip",
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
|
@ -4,14 +4,13 @@ import {
|
|||
TouchableOpacity,
|
||||
Modal,
|
||||
TouchableWithoutFeedback,
|
||||
ActivityIndicator,
|
||||
Platform,
|
||||
Image as NativeImage,
|
||||
} from "react-native";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTailwind } from "tailwind-rn";
|
||||
import { Image } from "expo-image";
|
||||
import ImageViewer from "react-native-image-zoom-viewer";
|
||||
import ImageViewer from "../ImageViewer";
|
||||
import VideoModal from "../VideoModal";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import formatTimestamp from "../../utils/formatTimestamp";
|
||||
|
@ -327,18 +326,12 @@ function ImageDisplay({ blur, media }) {
|
|||
/>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
<Modal visible={isModalVisible} statusBarTranslucent transparent={true}>
|
||||
<TouchableWithoutFeedback onPress={() => setIsModalVisible(false)}>
|
||||
<ImageViewer
|
||||
imageUrls={images}
|
||||
enableSwipeDown
|
||||
saveToLocalByLongPress={false}
|
||||
onSwipeDown={() => setIsModalVisible(false)}
|
||||
index={imageIndex}
|
||||
loadingRender={() => <ActivityIndicator size="large" />}
|
||||
/>
|
||||
</TouchableWithoutFeedback>
|
||||
</Modal>
|
||||
<ImageViewer
|
||||
isVisible={isModalVisible}
|
||||
setIsVisible={setIsModalVisible}
|
||||
imageUrls={images}
|
||||
index={imageIndex}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -10,11 +10,14 @@ import React, { useState, useRef } from "react";
|
|||
import { useTailwind } from "tailwind-rn";
|
||||
import { Icon } from "@rneui/themed";
|
||||
import { Video, ResizeMode } from "expo-av";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
|
||||
const screenWidth = Dimensions.get("window").width;
|
||||
|
||||
export default function VideoModal({ visible, setVisible, url }) {
|
||||
const tailwind = useTailwind();
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
const [isReady, setIsReady] = useState(false);
|
||||
const videoRef = useRef(null);
|
||||
const [videoSize, setVideoSize] = useState({ width: 720, height: 1280 });
|
||||
|
@ -32,13 +35,16 @@ export default function VideoModal({ visible, setVisible, url }) {
|
|||
...tailwind("flex-1 justify-center items-center"),
|
||||
}}
|
||||
>
|
||||
<View style={tailwind("absolute top-10 right-4 z-10")}>
|
||||
<Icon
|
||||
type="ionicon"
|
||||
name="close-circle-outline"
|
||||
size={40}
|
||||
color="#f9fafb"
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
top: insets.top,
|
||||
right: 20,
|
||||
...tailwind(
|
||||
"absolute z-10 flex justify-center items-center w-12 h-12 bg-[#FFFFFF1A] rounded-full"
|
||||
),
|
||||
}}
|
||||
>
|
||||
<Icon type="ionicon" name="close" size={24} color="#f9fafb" />
|
||||
</View>
|
||||
<TouchableOpacity activeOpacity={1}>
|
||||
{!isReady && <ActivityIndicator size="large" />}
|
||||
|
|
|
@ -98,7 +98,6 @@ export default function SetPassword({ navigation, route }) {
|
|||
}
|
||||
);
|
||||
const newData = await response.json();
|
||||
console.log("base", base);
|
||||
if (newData.ret === -1) {
|
||||
Toast.show({
|
||||
type: "error",
|
||||
|
|
|
@ -5,6 +5,7 @@ import {
|
|||
Image as NativeImage,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
FlatList,
|
||||
} from "react-native";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import Empty from "../../../components/Empty";
|
||||
|
@ -12,7 +13,6 @@ import { useTailwind } from "tailwind-rn";
|
|||
import baseRequest from "../../../utils/baseRequest";
|
||||
import Toast from "react-native-toast-message";
|
||||
import { generateSignature } from "../../../utils/crypto";
|
||||
import { FlashList } from "@shopify/flash-list";
|
||||
import Post from "../../../components/Post";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import { get } from "../../../utils/storeInfo";
|
||||
|
@ -136,45 +136,30 @@ export default function FeedPosts({ blur }) {
|
|||
}
|
||||
};
|
||||
|
||||
//获取当前时间
|
||||
const [currentTime, setCurrentTime] = useState();
|
||||
const getCurrentTime = async () => {
|
||||
setCurrentTime(Math.floor(new Date().getTime() / 1000));
|
||||
};
|
||||
|
||||
//获取截止当前时间的动态数据
|
||||
//获取新数据
|
||||
const [data, setData] = useState([]);
|
||||
const [offset, setOffset] = useState(0);
|
||||
const [more, setMore] = useState(1);
|
||||
const [isActivityIndicatorShow, setIsActivityIndicatorShow] = useState(true);
|
||||
const getData = async (type) => {
|
||||
if (!currentTime) return;
|
||||
if (data.length === 0 && type === "bottom") return;
|
||||
if (!more && type === "bottom") {
|
||||
setIsActivityIndicatorShow(false);
|
||||
return;
|
||||
}
|
||||
setIsActivityIndicatorShow(true);
|
||||
const [isFetching, setIsFetching] = useState(false);
|
||||
const getData = async (type = 0) => {
|
||||
//type 0-上滑 1-下拉 2-初始化
|
||||
if (isFetching) return;
|
||||
const apiUrl = process.env.EXPO_PUBLIC_API_URL;
|
||||
setIsFetching(true);
|
||||
try {
|
||||
const apiUrl = process.env.EXPO_PUBLIC_API_URL;
|
||||
const base = await baseRequest();
|
||||
const signature = await generateSignature({
|
||||
ct_upper_bound: currentTime,
|
||||
offset: offset,
|
||||
limit: 4,
|
||||
op_type: type,
|
||||
...base,
|
||||
});
|
||||
const _response = await fetch(
|
||||
`${apiUrl}/api/moment/list?signature=${signature}`,
|
||||
`${apiUrl}/api/moment/recomm_list?signature=${signature}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
ct_upper_bound: currentTime,
|
||||
offset: offset,
|
||||
limit: 4,
|
||||
op_type: type,
|
||||
...base,
|
||||
}),
|
||||
}
|
||||
|
@ -188,48 +173,44 @@ export default function FeedPosts({ blur }) {
|
|||
});
|
||||
return;
|
||||
}
|
||||
if (type === "top") {
|
||||
if (type === 1 || type === 2) {
|
||||
const topPosts = await getTopPostsData();
|
||||
setData((prev) => [...topPosts, ..._data.data.list]);
|
||||
setData((prev) => [...topPosts, ..._data.data.recomm_list]);
|
||||
setIsActivityIndicatorShow(true);
|
||||
} else {
|
||||
setData((prev) => [...prev, ..._data.data.list]);
|
||||
setData((prev) => [...prev, ..._data.data.recomm_list]);
|
||||
if (_data.data.recomm_list.length === 0)
|
||||
setIsActivityIndicatorShow(false);
|
||||
}
|
||||
setOffset(_data.data.offset);
|
||||
setMore(_data.data.more);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
setIsFetching(false);
|
||||
}
|
||||
};
|
||||
|
||||
//获取当前时间
|
||||
//初始化加载
|
||||
useEffect(() => {
|
||||
getVipPrice();
|
||||
getCurrentTime();
|
||||
getData(2);
|
||||
}, []);
|
||||
|
||||
//当时间改变,获取新数据
|
||||
useEffect(() => {
|
||||
getData("top");
|
||||
}, [currentTime]);
|
||||
|
||||
const tailwind = useTailwind();
|
||||
const renderItem = ({ item }) => <Post isBlur={blur} data={item} />;
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
//下拉刷新
|
||||
const handleRefresh = async () => {
|
||||
setRefreshing(true);
|
||||
setOffset(0);
|
||||
setMore(1);
|
||||
await getCurrentTime();
|
||||
await getData(1);
|
||||
setRefreshing(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={tailwind("flex-1")}>
|
||||
<FlashList
|
||||
<FlatList
|
||||
data={data}
|
||||
extraData={blur}
|
||||
renderItem={renderItem}
|
||||
estimatedItemSize={310}
|
||||
initialNumToRender={4}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
|
@ -239,7 +220,7 @@ export default function FeedPosts({ blur }) {
|
|||
onRefresh={() => handleRefresh()}
|
||||
/>
|
||||
}
|
||||
onEndReached={() => getData("bottom")}
|
||||
onEndReached={() => getData(0)}
|
||||
ListEmptyComponent={<Empty type="nodata" />}
|
||||
ListFooterComponent={
|
||||
<View>
|
||||
|
|
|
@ -4,6 +4,7 @@ import {
|
|||
Image as NativeImage,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
FlatList,
|
||||
} from "react-native";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import Empty from "../../../components/Empty";
|
||||
|
@ -12,7 +13,6 @@ import baseRequest from "../../../utils/baseRequest";
|
|||
import Toast from "react-native-toast-message";
|
||||
import { get } from "../../../utils/storeInfo";
|
||||
import { generateSignature } from "../../../utils/crypto";
|
||||
import { FlashList } from "@shopify/flash-list";
|
||||
import Post from "../../../components/Post";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
|
||||
|
@ -190,10 +190,9 @@ export default function FollowPosts({ blur }) {
|
|||
|
||||
return (
|
||||
<View style={tailwind("flex-1")}>
|
||||
<FlashList
|
||||
<FlatList
|
||||
data={data}
|
||||
renderItem={renderItem}
|
||||
estimatedItemSize={287}
|
||||
initialNumToRender={4}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
|
|
|
@ -14,6 +14,7 @@ import { get, save } from "../../utils/storeInfo";
|
|||
import { useFocusEffect } from "@react-navigation/native";
|
||||
import baseRequest from "../../utils/baseRequest";
|
||||
import { generateSignature } from "../../utils/crypto";
|
||||
import Toast from "react-native-toast-message";
|
||||
|
||||
export default function Posts({ navigation }) {
|
||||
const tailwind = useTailwind();
|
||||
|
|
|
@ -17,17 +17,22 @@ import { FlashList } from "@shopify/flash-list";
|
|||
import { Svg, Path } from "react-native-svg";
|
||||
|
||||
export default function FeedStream() {
|
||||
//获取推荐的主播mid
|
||||
const [recommMids, setRecommMids] = useState([]);
|
||||
const getRecommMids = async () => {
|
||||
if (!isEnd) return;
|
||||
//获取新数据
|
||||
const [data, setData] = useState([]);
|
||||
const [isActivityIndicatorShow, setIsActivityIndicatorShow] = useState(true);
|
||||
const [isFetching, setIsFetching] = useState(false);
|
||||
const getData = async (type = 0) => {
|
||||
//type 0-上滑 1-下拉 2-初始化
|
||||
if (isFetching) return;
|
||||
const apiUrl = process.env.EXPO_PUBLIC_API_URL;
|
||||
setIsFetching(true);
|
||||
try {
|
||||
const base = await baseRequest();
|
||||
const signature = await generateSignature({
|
||||
op_type: type,
|
||||
...base,
|
||||
});
|
||||
const response = await fetch(
|
||||
const _response = await fetch(
|
||||
`${apiUrl}/api/streamer/recomm_list?signature=${signature}`,
|
||||
{
|
||||
method: "POST",
|
||||
|
@ -35,110 +40,39 @@ export default function FeedStream() {
|
|||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
op_type: type,
|
||||
...base,
|
||||
}),
|
||||
}
|
||||
);
|
||||
const recommData = await response.json();
|
||||
if (recommData.ret === -1) {
|
||||
const _data = await _response.json();
|
||||
if (_data.ret === -1) {
|
||||
Toast.show({
|
||||
type: "error",
|
||||
text1: recommData.msg,
|
||||
text1: _data.msg,
|
||||
topOffset: 60,
|
||||
});
|
||||
return;
|
||||
}
|
||||
setIsEnd(false);
|
||||
setRecommMids(recommData.data.recomm_list);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
//查询的起始索引
|
||||
const [startIndex, setStartIndex] = useState(0);
|
||||
// 定义每次取多少个用户ID
|
||||
const batchSize = 4;
|
||||
//获取新数据
|
||||
const [data, setData] = useState([]);
|
||||
const [isEnd, setIsEnd] = useState(true);
|
||||
const [isActivityIndicatorShow, setIsActivityIndicatorShow] = useState(true);
|
||||
const [isFetching, setIsFetching] = useState(false);
|
||||
const getData = async (type) => {
|
||||
if (isFetching) return;
|
||||
if (isEnd) return;
|
||||
if (recommMids.length === 0) return;
|
||||
const apiUrl = process.env.EXPO_PUBLIC_API_URL;
|
||||
const batchStreamerMids = recommMids.slice(
|
||||
startIndex,
|
||||
startIndex + batchSize
|
||||
);
|
||||
if (batchStreamerMids.length === 0) {
|
||||
if (type !== "top") {
|
||||
setIsActivityIndicatorShow(false);
|
||||
return;
|
||||
}
|
||||
setIsActivityIndicatorShow(true);
|
||||
setIsEnd(true);
|
||||
setStartIndex(0);
|
||||
return;
|
||||
}
|
||||
setIsFetching((prev) => true);
|
||||
try {
|
||||
const base = await baseRequest();
|
||||
const signature = await generateSignature({
|
||||
mids: batchStreamerMids,
|
||||
...base,
|
||||
});
|
||||
const detailResponse = await fetch(
|
||||
`${apiUrl}/api/streamer/list_ext_by_mids?signature=${signature}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
mids: batchStreamerMids,
|
||||
...base,
|
||||
}),
|
||||
}
|
||||
);
|
||||
const detailData = await detailResponse.json();
|
||||
if (detailData.ret === -1) {
|
||||
Toast.show({
|
||||
type: "error",
|
||||
text1: detailData.msg,
|
||||
topOffset: 60,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const sortedData = detailData.data.list.sort((a, b) => b.fans - a.fans);
|
||||
if (type === "top") {
|
||||
setData((prev) => {
|
||||
if (startIndex === 0) {
|
||||
return sortedData;
|
||||
} else return [...sortedData, ...prev];
|
||||
});
|
||||
if (type === 1) {
|
||||
setData((prev) => _data.data.recomm_list);
|
||||
setIsActivityIndicatorShow(true);
|
||||
} else {
|
||||
setData((prev) => [...prev, ...sortedData]);
|
||||
setData((prev) => [...prev, ..._data.data.recomm_list]);
|
||||
if (_data.data.recomm_list.length === 0)
|
||||
setIsActivityIndicatorShow(false);
|
||||
}
|
||||
// 更新下一次查询的起始索引
|
||||
setStartIndex(startIndex + batchSize);
|
||||
setIsFetching((prev) => false);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
setIsFetching(false);
|
||||
}
|
||||
};
|
||||
|
||||
//获取推荐mid
|
||||
//初始化加载
|
||||
useEffect(() => {
|
||||
getRecommMids();
|
||||
}, [isEnd]);
|
||||
|
||||
//当推荐的mid改变,获取新数据
|
||||
useEffect(() => {
|
||||
getData("top");
|
||||
}, [recommMids]);
|
||||
getData(2);
|
||||
}, []);
|
||||
|
||||
const tailwind = useTailwind();
|
||||
const renderItem = ({ item }) => <StreamerCard data={item} />;
|
||||
|
@ -166,7 +100,7 @@ export default function FeedStream() {
|
|||
startRotation();
|
||||
flatListRef.current.scrollToOffset({ animated: false, offset: 0 });
|
||||
setRefreshing(true);
|
||||
await getData("top");
|
||||
await getData(1);
|
||||
setRefreshing(false);
|
||||
};
|
||||
|
||||
|
@ -175,6 +109,7 @@ export default function FeedStream() {
|
|||
<FlashList
|
||||
ref={flatListRef}
|
||||
data={data}
|
||||
keyExtractor={(item) => item.mid}
|
||||
renderItem={renderItem}
|
||||
estimatedItemSize={287}
|
||||
initialNumToRender={4}
|
||||
|
@ -186,7 +121,7 @@ export default function FeedStream() {
|
|||
onRefresh={() => handleRefresh()}
|
||||
/>
|
||||
}
|
||||
onEndReached={() => getData("bottom")}
|
||||
onEndReached={() => getData(0)}
|
||||
ListEmptyComponent={<Empty type="nodata" />}
|
||||
ListFooterComponent={
|
||||
<View>
|
||||
|
@ -195,7 +130,6 @@ export default function FeedStream() {
|
|||
)}
|
||||
</View>
|
||||
}
|
||||
keyExtractor={(item) => item.mid}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
style={tailwind(
|
||||
|
|
|
@ -4,7 +4,6 @@ import {
|
|||
TouchableOpacity,
|
||||
TouchableWithoutFeedback,
|
||||
Modal,
|
||||
ActivityIndicator,
|
||||
Text,
|
||||
Alert,
|
||||
ScrollView,
|
||||
|
@ -15,7 +14,7 @@ import React, { useState, useEffect } from "react";
|
|||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { useTailwind } from "tailwind-rn";
|
||||
import { Image } from "expo-image";
|
||||
import ImageViewer from "react-native-image-zoom-viewer";
|
||||
import ImageViewer from "../../components/ImageViewer";
|
||||
import Swiper from "react-native-swiper";
|
||||
import { Divider, Icon } from "@rneui/themed";
|
||||
import VideoModal from "../../components/VideoModal";
|
||||
|
@ -300,18 +299,12 @@ export default function StreamerProfile({ navigation, route }) {
|
|||
url={data?.shorts?.videos[0]?.urls[0]}
|
||||
/>
|
||||
{/* 展示图片的modal */}
|
||||
<Modal visible={imagesVisible} statusBarTranslucent transparent={true}>
|
||||
<TouchableWithoutFeedback onPress={() => setImagesVisible(false)}>
|
||||
<ImageViewer
|
||||
imageUrls={imagesForImageViewer}
|
||||
enableSwipeDown
|
||||
saveToLocalByLongPress={false}
|
||||
onSwipeDown={() => setImagesVisible(false)}
|
||||
index={imageIndex}
|
||||
loadingRender={() => <ActivityIndicator size="large" />}
|
||||
/>
|
||||
</TouchableWithoutFeedback>
|
||||
</Modal>
|
||||
<ImageViewer
|
||||
isVisible={imagesVisible}
|
||||
setIsVisible={setImagesVisible}
|
||||
imageUrls={imagesForImageViewer}
|
||||
index={imageIndex}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
26
tailwind.css
26
tailwind.css
|
@ -14,6 +14,10 @@
|
|||
top: -6rem
|
||||
}
|
||||
|
||||
.bottom-0 {
|
||||
bottom: 0px
|
||||
}
|
||||
|
||||
.bottom-12 {
|
||||
bottom: 3rem
|
||||
}
|
||||
|
@ -277,6 +281,10 @@
|
|||
height: 2.25rem
|
||||
}
|
||||
|
||||
.h-\[1px\] {
|
||||
height: 1px
|
||||
}
|
||||
|
||||
.h-\[3px\] {
|
||||
height: 3px
|
||||
}
|
||||
|
@ -293,8 +301,8 @@
|
|||
height: 100%
|
||||
}
|
||||
|
||||
.h-\[1px\] {
|
||||
height: 1px
|
||||
.h-48 {
|
||||
height: 12rem
|
||||
}
|
||||
|
||||
.w-1\/2 {
|
||||
|
@ -475,6 +483,16 @@
|
|||
border-bottom-right-radius: 0.25rem
|
||||
}
|
||||
|
||||
.rounded-t-xl {
|
||||
border-top-left-radius: 0.75rem;
|
||||
border-top-right-radius: 0.75rem
|
||||
}
|
||||
|
||||
.rounded-t-2xl {
|
||||
border-top-left-radius: 1rem;
|
||||
border-top-right-radius: 1rem
|
||||
}
|
||||
|
||||
.border {
|
||||
border-width: 1px
|
||||
}
|
||||
|
@ -618,6 +636,10 @@
|
|||
background-color: rgb(250 204 21 / var(--tw-bg-opacity))
|
||||
}
|
||||
|
||||
.bg-\[\#00000080\] {
|
||||
background-color: #00000080
|
||||
}
|
||||
|
||||
.p-0 {
|
||||
padding: 0px
|
||||
}
|
||||
|
|
|
@ -19,6 +19,11 @@
|
|||
"top": -96
|
||||
}
|
||||
},
|
||||
"bottom-0": {
|
||||
"style": {
|
||||
"bottom": 0
|
||||
}
|
||||
},
|
||||
"bottom-12": {
|
||||
"style": {
|
||||
"bottom": 48
|
||||
|
@ -352,6 +357,11 @@
|
|||
"height": 36
|
||||
}
|
||||
},
|
||||
"h-[1px]": {
|
||||
"style": {
|
||||
"height": 1
|
||||
}
|
||||
},
|
||||
"h-[3px]": {
|
||||
"style": {
|
||||
"height": 3
|
||||
|
@ -372,9 +382,9 @@
|
|||
"height": "100%"
|
||||
}
|
||||
},
|
||||
"h-[1px]": {
|
||||
"h-48": {
|
||||
"style": {
|
||||
"height": 1
|
||||
"height": 192
|
||||
}
|
||||
},
|
||||
"w-1/2": {
|
||||
|
@ -621,6 +631,18 @@
|
|||
"borderBottomRightRadius": 4
|
||||
}
|
||||
},
|
||||
"rounded-t-xl": {
|
||||
"style": {
|
||||
"borderTopLeftRadius": 12,
|
||||
"borderTopRightRadius": 12
|
||||
}
|
||||
},
|
||||
"rounded-t-2xl": {
|
||||
"style": {
|
||||
"borderTopLeftRadius": 16,
|
||||
"borderTopRightRadius": 16
|
||||
}
|
||||
},
|
||||
"border": {
|
||||
"style": {
|
||||
"borderTopWidth": 1,
|
||||
|
@ -816,6 +838,11 @@
|
|||
"backgroundColor": "rgb(250 204 21 / var(--tw-bg-opacity))"
|
||||
}
|
||||
},
|
||||
"bg-[#00000080]": {
|
||||
"style": {
|
||||
"backgroundColor": "#00000080"
|
||||
}
|
||||
},
|
||||
"p-0": {
|
||||
"style": {
|
||||
"paddingTop": 0,
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
import * as MediaLibrary from "expo-media-library";
|
||||
import * as FileSystem from "expo-file-system";
|
||||
import Toast from "react-native-toast-message";
|
||||
|
||||
export default async function saveImage(uri) {
|
||||
const permission = await MediaLibrary.requestPermissionsAsync();
|
||||
if (permission.granted) {
|
||||
const timestamp = new Date().getTime();
|
||||
const fileUri = FileSystem.cacheDirectory + `${timestamp}.png`;
|
||||
try {
|
||||
const res = await FileSystem.downloadAsync(uri, fileUri);
|
||||
await MediaLibrary.saveToLibraryAsync(res.uri);
|
||||
Toast.show({
|
||||
type: "success",
|
||||
text1: "已保存到相册",
|
||||
topOffset: 60,
|
||||
});
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error("FS Err: ", err);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
Toast.show({
|
||||
type: "error",
|
||||
text1: "保存失败,请检查APP媒体权限",
|
||||
topOffset: 60,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -274,16 +274,18 @@ export async function uploadVideo(asset) {
|
|||
//上传多个图片或者视频
|
||||
export async function multiUpload(assets) {
|
||||
let ids = { image_ids: [], video_ids: [] };
|
||||
await Promise.all(
|
||||
assets.map(async (asset) => {
|
||||
if (asset.duration === 0) {
|
||||
const id = await uploadImage(asset);
|
||||
ids.image_ids.push(id);
|
||||
} else {
|
||||
const id = await uploadVideo(asset);
|
||||
ids.video_ids.push(id);
|
||||
}
|
||||
})
|
||||
);
|
||||
return ids;
|
||||
const tasks = assets.map(async (asset, index) => {
|
||||
if (asset.duration === 0) {
|
||||
const id = await uploadImage(asset);
|
||||
ids.image_ids[index] = id;
|
||||
} else {
|
||||
const id = await uploadVideo(asset);
|
||||
ids.video_ids[index] = id;
|
||||
}
|
||||
});
|
||||
await Promise.all(tasks);
|
||||
return {
|
||||
image_ids: ids.image_ids.filter(Boolean),
|
||||
video_ids: ids.video_ids.filter(Boolean),
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue