From 803fc87fd92714e78685dfd2d4e473688562a5df Mon Sep 17 00:00:00 2001 From: zimplexing Date: Wed, 9 Jul 2025 17:23:46 +0800 Subject: [PATCH] Enhance responsive design and update dependencies - Added expo-screen-orientation package to manage screen orientation. - Updated react-native-gesture-handler to version 2.27.1 for improved gesture handling. - Implemented responsive design features across multiple screens using the new useResponsive hook. - Refactored DetailScreen, HomeScreen, SearchScreen, and PlayScreen to adapt layouts based on screen size. - Introduced PlayerControlsMobile for optimized playback controls on mobile devices. - Adjusted button styles in StyledButton for better responsiveness. --- app/detail.tsx | 52 ++++++-- app/index.tsx | 19 +-- app/play.tsx | 114 ++++++++++++----- app/search.tsx | 28 +++-- components/PlayerControls.mobile.tsx | 179 +++++++++++++++++++++++++++ components/StyledButton.tsx | 8 +- components/ThemedView.tsx | 18 +-- hooks/useResponsive.ts | 43 +++++++ package.json | 3 +- yarn.lock | 61 +++++---- 10 files changed, 428 insertions(+), 97 deletions(-) create mode 100644 components/PlayerControls.mobile.tsx create mode 100644 hooks/useResponsive.ts diff --git a/app/detail.tsx b/app/detail.tsx index 4ce1f08..221bba8 100644 --- a/app/detail.tsx +++ b/app/detail.tsx @@ -1,15 +1,17 @@ import React, { useEffect, useState, useRef } from "react"; -import { View, Text, StyleSheet, Image, ScrollView, ActivityIndicator } from "react-native"; +import { View, Text, StyleSheet, Image, ScrollView, ActivityIndicator, FlatList } from "react-native"; import { useLocalSearchParams, useRouter } from "expo-router"; import { ThemedView } from "@/components/ThemedView"; import { ThemedText } from "@/components/ThemedText"; import { api, SearchResult } from "@/services/api"; import { getResolutionFromM3U8 } from "@/services/m3u8"; import { StyledButton } from "@/components/StyledButton"; +import { useResponsive } from "@/hooks/useResponsive"; export default function DetailScreen() { const { source, q } = useLocalSearchParams(); const router = useRouter(); + const { isMobile, screenWidth, numColumns } = useResponsive(); const [searchResults, setSearchResults] = useState<(SearchResult & { resolution?: string | null })[]>([]); const [detail, setDetail] = useState<(SearchResult & { resolution?: string | null }) | null>(null); const [loading, setLoading] = useState(true); @@ -147,9 +149,12 @@ export default function DetailScreen() { return ( - - - + + + {detail.title} @@ -197,17 +202,26 @@ export default function DetailScreen() { 播放列表 - - {detail.episodes.map((episode, index) => ( + ( handlePlay(episode, index)} + onPress={() => handlePlay(item, index)} text={`第 ${index + 1} 集`} textStyle={styles.episodeButtonText} /> - ))} - + )} + keyExtractor={(_item, index) => index.toString()} + numColumns={numColumns(80, 10)} + key={numColumns(80, 10)} // Re-render on column change + contentContainerStyle={styles.episodeList} + // The FlatList should not be scrollable itself, the parent ScrollView handles it. + // This can be achieved by making it non-scrollable and letting it expand. + // However, for performance, if the list is very long, a fixed height would be better. + // For now, we let the parent ScrollView handle scrolling. + scrollEnabled={false} + /> @@ -222,16 +236,31 @@ const styles = StyleSheet.create({ flexDirection: "row", padding: 20, }, + topContainerMobile: { + flexDirection: "column", + alignItems: "center", + }, poster: { width: 200, height: 300, borderRadius: 8, }, + posterMobile: { + width: "80%", + height: undefined, + aspectRatio: 2 / 3, // Maintain aspect ratio + marginBottom: 20, + }, infoContainer: { flex: 1, marginLeft: 20, justifyContent: "flex-start", }, + infoContainerMobile: { + marginLeft: 0, + width: "100%", + alignItems: "center", + }, title: { fontSize: 28, fontWeight: "bold", @@ -302,8 +331,7 @@ const styles = StyleSheet.create({ marginBottom: 10, }, episodeList: { - flexDirection: "row", - flexWrap: "wrap", + // flexDirection is now handled by FlatList's numColumns }, episodeButton: { margin: 5, diff --git a/app/index.tsx b/app/index.tsx index 68b2e86..759f0c9 100644 --- a/app/index.tsx +++ b/app/index.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useCallback, useRef } from "react"; -import { View, StyleSheet, ActivityIndicator, FlatList, Pressable, Dimensions } from "react-native"; +import { View, StyleSheet, ActivityIndicator, FlatList, Pressable } from "react-native"; import { ThemedView } from "@/components/ThemedView"; import { ThemedText } from "@/components/ThemedText"; import { api } from "@/services/api"; @@ -10,16 +10,18 @@ import { SettingsModal } from "@/components/SettingsModal"; import { StyledButton } from "@/components/StyledButton"; import useHomeStore, { RowItem, Category } from "@/stores/homeStore"; import { useSettingsStore } from "@/stores/settingsStore"; - -const NUM_COLUMNS = 5; -const { width } = Dimensions.get("window"); -const ITEM_WIDTH = width / NUM_COLUMNS - 24; +import { useResponsive } from "@/hooks/useResponsive"; export default function HomeScreen() { const router = useRouter(); + const { isMobile, screenWidth, numColumns } = useResponsive(); const colorScheme = "dark"; const flatListRef = useRef(null); + // Calculate item width based on screen size and number of columns + const itemWidth = isMobile ? screenWidth / numColumns(150, 16) - 24 : screenWidth / 5 - 24; + const calculatedNumColumns = numColumns(150, 16); + const { categories, selectedCategory, @@ -64,7 +66,7 @@ export default function HomeScreen() { }; const renderContentItem = ({ item }: { item: RowItem }) => ( - + `${item.source}-${item.id}-${index}`} - numColumns={NUM_COLUMNS} + numColumns={calculatedNumColumns} + key={calculatedNumColumns} // Re-render FlatList when numColumns changes contentContainerStyle={styles.listContent} onEndReached={loadMoreData} onEndReachedThreshold={0.5} @@ -210,7 +213,7 @@ const styles = StyleSheet.create({ }, itemContainer: { margin: 8, - width: ITEM_WIDTH, + // width is now set dynamically in renderContentItem alignItems: "center", }, }); diff --git a/app/play.tsx b/app/play.tsx index 655068b..0a7814e 100644 --- a/app/play.tsx +++ b/app/play.tsx @@ -1,10 +1,13 @@ import React, { useEffect, useRef } from "react"; -import { View, StyleSheet, TouchableOpacity, ActivityIndicator, BackHandler } from "react-native"; +import { View, StyleSheet, ActivityIndicator, BackHandler, Platform, SafeAreaView, Dimensions } from "react-native"; import { useLocalSearchParams, useRouter } from "expo-router"; import { Video, ResizeMode } from "expo-av"; +import * as ScreenOrientation from "expo-screen-orientation"; import { useKeepAwake } from "expo-keep-awake"; +import { Gesture, GestureDetector } from "react-native-gesture-handler"; import { ThemedView } from "@/components/ThemedView"; import { PlayerControls } from "@/components/PlayerControls"; +import { PlayerControlsMobile } from "@/components/PlayerControls.mobile"; import { EpisodeSelectionModal } from "@/components/EpisodeSelectionModal"; import { SourceSelectionModal } from "@/components/SourceSelectionModal"; import { SeekingBar } from "@/components/SeekingBar"; @@ -12,11 +15,13 @@ import { NextEpisodeOverlay } from "@/components/NextEpisodeOverlay"; import { LoadingOverlay } from "@/components/LoadingOverlay"; import usePlayerStore from "@/stores/playerStore"; import { useTVRemoteHandler } from "@/hooks/useTVRemoteHandler"; +import { useResponsive } from "@/hooks/useResponsive"; export default function PlayScreen() { const videoRef = useRef + - - - + + + + ); } diff --git a/app/search.tsx b/app/search.tsx index 003fa03..6ba7dce 100644 --- a/app/search.tsx +++ b/app/search.tsx @@ -6,6 +6,7 @@ import VideoCard from "@/components/VideoCard.tv"; import { api, SearchResult } from "@/services/api"; import { Search } from "lucide-react-native"; import { StyledButton } from "@/components/StyledButton"; +import { useResponsive } from "@/hooks/useResponsive"; export default function SearchScreen() { const [keyword, setKeyword] = useState(""); @@ -15,6 +16,10 @@ export default function SearchScreen() { const textInputRef = useRef(null); const colorScheme = "dark"; // Replace with useColorScheme() if needed const [isInputFocused, setIsInputFocused] = useState(false); + const { isMobile, screenWidth, numColumns } = useResponsive(); + + const itemWidth = isMobile ? screenWidth / numColumns(150, 16) - 24 : screenWidth / 5 - 24; + const calculatedNumColumns = numColumns(150, 16); useEffect(() => { // Focus the text input when the screen loads @@ -48,15 +53,17 @@ export default function SearchScreen() { }; const renderItem = ({ item }: { item: SearchResult }) => ( - + + + ); return ( @@ -99,7 +106,8 @@ export default function SearchScreen() { data={results} renderItem={renderItem} keyExtractor={(item, index) => `${item.id}-${item.source}-${index}`} - numColumns={5} // Adjust based on your card size and desired layout + numColumns={calculatedNumColumns} + key={calculatedNumColumns} contentContainerStyle={styles.listContent} ListEmptyComponent={ diff --git a/components/PlayerControls.mobile.tsx b/components/PlayerControls.mobile.tsx new file mode 100644 index 0000000..11d28cc --- /dev/null +++ b/components/PlayerControls.mobile.tsx @@ -0,0 +1,179 @@ +import React from "react"; +import { View, Text, StyleSheet, Pressable } from "react-native"; +import { Pause, Play, SkipForward, List, Tv } from "lucide-react-native"; +import { ThemedText } from "@/components/ThemedText"; +import { MediaButton } from "@/components/MediaButton"; +import usePlayerStore from "@/stores/playerStore"; + +interface PlayerControlsProps { + showControls: boolean; +} + +export const PlayerControlsMobile: React.FC = ({ showControls }) => { + const { + detail, + currentEpisodeIndex, + currentSourceIndex, + status, + isSeeking, + seekPosition, + progressPosition, + togglePlayPause, + playEpisode, + setShowEpisodeModal, + setShowSourceModal, + } = usePlayerStore(); + + const videoTitle = detail?.videoInfo?.title || ""; + const currentEpisode = detail?.episodes[currentEpisodeIndex]; + const currentEpisodeTitle = currentEpisode?.title; + const hasNextEpisode = currentEpisodeIndex < (detail?.episodes.length || 0) - 1; + + const formatTime = (milliseconds: number) => { + if (!milliseconds) return "00:00"; + const totalSeconds = Math.floor(milliseconds / 1000); + const minutes = Math.floor(totalSeconds / 60); + const seconds = totalSeconds % 60; + return `${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`; + }; + + const onPlayNextEpisode = () => { + if (hasNextEpisode) { + playEpisode(currentEpisodeIndex + 1); + } + }; + + return ( + + + + {videoTitle} {currentEpisodeTitle ? `- ${currentEpisodeTitle}` : ""} + + + + + {/* This area can be used for gesture-based seeking indicators in the future */} + + + + + + {status?.isLoaded + ? `${formatTime(status.positionMillis)} / ${formatTime(status.durationMillis || 0)}` + : "00:00 / 00:00"} + + + setShowEpisodeModal(true)}> + + + setShowSourceModal(true)}> + + + + + + + + + + + + + + + {status?.isLoaded && status.isPlaying ? ( + + ) : ( + + )} + + + + + + + + ); +}; + +const styles = StyleSheet.create({ + controlsOverlay: { + ...StyleSheet.absoluteFillObject, + backgroundColor: "rgba(0, 0, 0, 0.4)", + justifyContent: "space-between", + paddingHorizontal: 15, + paddingVertical: 10, + }, + topControls: { + flexDirection: "row", + justifyContent: "center", + }, + controlTitle: { + color: "white", + fontSize: 14, + fontWeight: "bold", + textAlign: "center", + }, + middleControls: { + flex: 1, + justifyContent: "center", + alignItems: "center", + }, + bottomControlsContainer: { + width: "100%", + }, + timeAndActions: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + marginBottom: 5, + }, + timeText: { + color: "white", + fontSize: 12, + }, + actions: { + flexDirection: "row", + gap: 15, + }, + progressBarContainer: { + width: "100%", + height: 4, + position: "relative", + marginVertical: 10, + }, + progressBarBackground: { + ...StyleSheet.absoluteFillObject, + backgroundColor: "rgba(255, 255, 255, 0.3)", + borderRadius: 2, + }, + progressBarFilled: { + ...StyleSheet.absoluteFillObject, + backgroundColor: "#ff0000", + borderRadius: 2, + }, + progressBarTouchable: { + position: "absolute", + left: 0, + right: 0, + height: 20, + top: -8, + zIndex: 10, + }, + mainControls: { + flexDirection: "row", + justifyContent: "space-around", + alignItems: "center", + marginTop: 5, + }, + placeholder: { + width: 28, // to balance the skip forward button + }, +}); diff --git a/components/StyledButton.tsx b/components/StyledButton.tsx index 751fb8a..a070049 100644 --- a/components/StyledButton.tsx +++ b/components/StyledButton.tsx @@ -3,6 +3,7 @@ import { Animated, Pressable, StyleSheet, StyleProp, ViewStyle, PressableProps, import { ThemedText } from "./ThemedText"; import { Colors } from "@/constants/Colors"; import { useButtonAnimation } from "@/hooks/useButtonAnimation"; +import { useResponsive } from "@/hooks/useResponsive"; interface StyledButtonProps extends PressableProps { children?: React.ReactNode; @@ -26,6 +27,7 @@ export const StyledButton: React.FC = ({ const colors = Colors[colorScheme]; const [isFocused, setIsFocused] = React.useState(false); const animationStyle = useButtonAnimation(isFocused); + const { isMobile } = useResponsive(); const variantStyles = { default: StyleSheet.create({ @@ -81,8 +83,8 @@ export const StyledButton: React.FC = ({ const styles = StyleSheet.create({ button: { - paddingHorizontal: 16, - paddingVertical: 10, + paddingHorizontal: isMobile ? 12 : 16, + paddingVertical: isMobile ? 8 : 10, borderRadius: 8, borderWidth: 2, borderColor: "transparent", @@ -103,7 +105,7 @@ export const StyledButton: React.FC = ({ backgroundColor: colors.tint, }, text: { - fontSize: 16, + fontSize: isMobile ? 14 : 16, fontWeight: "500", color: colors.text, }, diff --git a/components/ThemedView.tsx b/components/ThemedView.tsx index e691379..97e4ae0 100644 --- a/components/ThemedView.tsx +++ b/components/ThemedView.tsx @@ -1,22 +1,14 @@ -import {View, type ViewProps} from 'react-native'; +import { View, type ViewProps } from "react-native"; -import {useThemeColor} from '@/hooks/useThemeColor'; +import { useThemeColor } from "@/hooks/useThemeColor"; export type ThemedViewProps = ViewProps & { lightColor?: string; darkColor?: string; }; -export function ThemedView({ - style, - lightColor, - darkColor, - ...otherProps -}: ThemedViewProps) { - const backgroundColor = useThemeColor( - {light: lightColor, dark: darkColor}, - 'background', - ); +export function ThemedView({ style, lightColor, darkColor, ...otherProps }: ThemedViewProps) { + const backgroundColor = useThemeColor({ light: lightColor, dark: darkColor }, "background"); - return ; + return ; } diff --git a/hooks/useResponsive.ts b/hooks/useResponsive.ts new file mode 100644 index 0000000..8aaac04 --- /dev/null +++ b/hooks/useResponsive.ts @@ -0,0 +1,43 @@ +import { useState, useEffect } from "react"; +import { Dimensions, Platform } from "react-native"; + +const isMobile = Platform.OS === "android" || Platform.OS === "ios"; + +interface ResponsiveInfo { + isMobile: boolean; + screenWidth: number; + numColumns: (itemWidth: number, gap?: number) => number; +} + +export function useResponsive(): ResponsiveInfo { + const [screenWidth, setScreenWidth] = useState(Dimensions.get("window").width); + + useEffect(() => { + const onChange = (result: { window: { width: number } }) => { + setScreenWidth(result.window.width); + }; + + const subscription = Dimensions.addEventListener("change", onChange); + + return () => { + subscription.remove(); + }; + }, []); + + const calculateNumColumns = (itemWidth: number, gap: number = 16) => { + if (!isMobile) { + // For TV, you might want a fixed number or a different logic + return 5; + } + const containerPadding = 16; // Horizontal padding of the container + const availableWidth = screenWidth - containerPadding * 2; + const num = Math.floor(availableWidth / (itemWidth + gap)); + return Math.max(1, num); // Ensure at least one column + }; + + return { + isMobile, + screenWidth, + numColumns: calculateNumColumns, + }; +} diff --git a/package.json b/package.json index c5210eb..03718de 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "expo-font": "~12.0.7", "expo-linking": "~6.3.1", "expo-router": "~3.5.16", + "expo-screen-orientation": "6", "expo-splash-screen": "~0.27.5", "expo-status-bar": "~1.12.1", "expo-system-ui": "~3.0.6", @@ -44,7 +45,7 @@ "react": "18.2.0", "react-dom": "18.2.0", "react-native": "npm:react-native-tvos@~0.74.2-0", - "react-native-gesture-handler": "~2.16.1", + "react-native-gesture-handler": "^2.27.1", "react-native-media-console": "*", "react-native-reanimated": "~3.10.1", "react-native-safe-area-context": "4.10.1", diff --git a/yarn.lock b/yarn.lock index 2d23161..f708aa3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4004,6 +4004,11 @@ expo-router@~3.5.16: react-native-helmet-async "2.0.4" schema-utils "^4.0.1" +expo-screen-orientation@6: + version "6.4.1" + resolved "https://registry.yarnpkg.com/expo-screen-orientation/-/expo-screen-orientation-6.4.1.tgz#1c4428a058921e48b5caa45367e626b8bec10e24" + integrity sha512-VM0C9ORNL1aT6Dr2OUeryzV519n0FjtXI2m+HlijOMi1QT2bPg4tBkCd7HLgywU4dZ1Esa46ewUudmk+fOqmMQ== + expo-splash-screen@0.27.7, expo-splash-screen@~0.27.5: version "0.27.7" resolved "https://registry.yarnpkg.com/expo-splash-screen/-/expo-splash-screen-0.27.7.tgz#52171be54d8c008880d928e802819d767fbd3c12" @@ -5850,7 +5855,7 @@ lodash.throttle@^4.1.1: resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4" integrity sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ== -lodash@^4.17.10, lodash@^4.17.13, lodash@^4.17.19, lodash@^4.17.21: +lodash@^4.17.10, lodash@^4.17.13, lodash@^4.17.19: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -5879,7 +5884,7 @@ logkitty@^0.7.1: dayjs "^1.8.15" yargs "^15.1.0" -loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: +loose-envify@^1.0.0, loose-envify@^1.1.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -6920,15 +6925,6 @@ prompts@^2.0.1, prompts@^2.2.1, prompts@^2.3.2, prompts@^2.4.2: kleur "^3.0.3" sisteransi "^1.0.5" -prop-types@^15.7.2: - version "15.8.1" - resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" - integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== - dependencies: - loose-envify "^1.4.0" - object-assign "^4.1.1" - react-is "^16.13.1" - psl@^1.1.33: version "1.15.0" resolved "https://registry.yarnpkg.com/psl/-/psl-1.15.0.tgz#bdace31896f1d97cec6a79e8224898ce93d974c6" @@ -7037,7 +7033,7 @@ react-freeze@^1.0.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e" integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== -react-is@^16.13.0, react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.4: +react-is@^16.13.0, react-is@^16.7.0, react-is@^16.8.4: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== @@ -7047,16 +7043,14 @@ react-is@^17.0.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== -react-native-gesture-handler@~2.16.1: - version "2.16.2" - resolved "https://registry.yarnpkg.com/react-native-gesture-handler/-/react-native-gesture-handler-2.16.2.tgz#032bd2a07334292d7f6cff1dc9d1ec928f72e26d" - integrity sha512-vGFlrDKlmyI+BT+FemqVxmvO7nqxU33cgXVsn6IKAFishvlG3oV2Ds67D5nPkHMea8T+s1IcuMm0bF8ntZtAyg== +react-native-gesture-handler@^2.27.1: + version "2.27.1" + resolved "https://registry.yarnpkg.com/react-native-gesture-handler/-/react-native-gesture-handler-2.27.1.tgz#8865b2345f4c65536517f20cedc420481fc72d49" + integrity sha512-57TUWerhdz589OcDD21e/YlL923Ma4OIpyWsP0hy7gItBCPm5d7qIUW7Yo/cS2wo1qDdOhJaNlvlBD1lDou1fA== dependencies: "@egjs/hammerjs" "^2.0.17" hoist-non-react-statics "^3.3.0" invariant "^2.2.4" - lodash "^4.17.21" - prop-types "^15.7.2" react-native-helmet-async@2.0.4: version "2.0.4" @@ -7914,7 +7908,16 @@ string-length@^5.0.1: char-regex "^2.0.0" strip-ansi "^7.0.1" -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -7978,7 +7981,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -7992,6 +7995,13 @@ strip-ansi@^5.0.0, strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -8739,7 +8749,7 @@ wonka@^6.3.2: resolved "https://registry.yarnpkg.com/wonka/-/wonka-6.3.5.tgz#33fa54ea700ff3e87b56fe32202112a9e8fea1a2" integrity sha512-SSil+ecw6B4/Dm7Pf2sAshKQ5hWFvfyGlfPbEd6A14dOH6VDjrmbY86u6nZvy9omGwwIPFR8V41+of1EezgoUw== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -8757,6 +8767,15 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"