mirror of
https://github.com/RPCSX/rpcsx-ui.git
synced 2026-01-31 01:05:23 +01:00
Simplify navigation, improve animations
This commit is contained in:
@@ -19,7 +19,7 @@
|
||||
"build:all": "npm run build && npm run build:web && npm run build:android",
|
||||
"validate": "",
|
||||
"dev:ui": "npx expo start --dev-client",
|
||||
"dev:web:server": "electron build/main.js --dev",
|
||||
"dev:web:server": "electron electron/build/main.js --dev",
|
||||
"install:android:release": "adb install android/app/build/outputs/apk/release/app-release.apk",
|
||||
"install:android": "adb install android/app/build/outputs/apk/debug/app-debug.apk"
|
||||
},
|
||||
|
||||
@@ -1908,40 +1908,46 @@ declare global {
|
||||
navigationFile.content = `${generatedHeader}
|
||||
${Object.keys(views).map(x => `import { ${x} } from '${pathWithoutExt(views[x])}'`).join(';\n')};
|
||||
import * as bridge from '$core/bridge';
|
||||
import { TopViewSelector } from '$core/ViewSelector';
|
||||
import * as SplashScreen from 'expo-splash-screen';
|
||||
import { BackHandler } from 'react-native';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
SplashScreen.preventAutoHideAsync();
|
||||
|
||||
const views: Record<string, (...props: any[]) => React.JSX.Element> = {
|
||||
${Object.keys(views).map(x => ` "${x}": ${x}`).join(',\n')}
|
||||
};
|
||||
|
||||
let onViewChangeCb: ((view: React.JSX.Element) => void) | undefined;
|
||||
let onViewChangeCb: (() => void) | undefined;
|
||||
let viewStack: React.JSX.Element[] = [];
|
||||
|
||||
export function onViewChange(cb?: (view: React.JSX.Element | undefined) => void) {
|
||||
onViewChangeCb = cb;
|
||||
if (cb && viewStack.length != 0) {
|
||||
cb(viewStack[viewStack.length - 1]);
|
||||
}
|
||||
}
|
||||
|
||||
function update() {
|
||||
if (onViewChangeCb) {
|
||||
onViewChangeCb(viewStack[viewStack.length - 1]);
|
||||
onViewChangeCb();
|
||||
}
|
||||
}
|
||||
|
||||
export function viewPush(name: string, props: any) {
|
||||
function renderView(name: string, props: any) {
|
||||
const View = views[name];
|
||||
viewStack.push(<View {...props} />);
|
||||
return <View key={name} {...props} />;
|
||||
}
|
||||
|
||||
function viewPush(name: string, props: any) {
|
||||
if (viewStack.length == 0) {
|
||||
SplashScreen.hideAsync();
|
||||
}
|
||||
|
||||
viewStack.push(renderView(name, props));
|
||||
update();
|
||||
}
|
||||
|
||||
export function viewSet(name: string, props: any) {
|
||||
const View = views[name];
|
||||
viewStack = [<View {...props} />];
|
||||
function viewSet(name: string, props: any) {
|
||||
viewStack = [renderView(name, props)];
|
||||
update();
|
||||
}
|
||||
|
||||
export function viewPop() {
|
||||
function viewPop() {
|
||||
if (viewStack.length < 2) {
|
||||
return false;
|
||||
}
|
||||
@@ -1951,6 +1957,36 @@ export function viewPop() {
|
||||
return true;
|
||||
}
|
||||
|
||||
export function Navigation() {
|
||||
const [renderItem, setRenderItem] = useState<number>(viewStack.length - 1);
|
||||
|
||||
useEffect(() => {
|
||||
onViewChangeCb = () => {
|
||||
if (viewStack.length > 0) {
|
||||
const item = viewStack.length - 1;
|
||||
if (item != renderItem) {
|
||||
setRenderItem(item);
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const backAction = () => {
|
||||
return viewPop();
|
||||
};
|
||||
|
||||
const backHandler = BackHandler.addEventListener(
|
||||
'hardwareBackPress',
|
||||
backAction
|
||||
);
|
||||
|
||||
return () => backHandler.remove();
|
||||
}, []);
|
||||
|
||||
return <TopViewSelector stack={viewStack} index={viewStack.length - 1} />;
|
||||
}
|
||||
|
||||
bridge.onViewPush(viewPush);
|
||||
bridge.onViewSet(viewSet);
|
||||
bridge.onViewPop(viewPop);
|
||||
@@ -1985,63 +2021,28 @@ export async function startup() {
|
||||
if (indexFile) {
|
||||
indexFile.content = `${generatedHeader}
|
||||
import '@expo/metro-runtime';
|
||||
// import { Asset } from 'expo-asset';
|
||||
import * as SplashScreen from 'expo-splash-screen';
|
||||
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
||||
import { StrictMode } from 'react';
|
||||
import { registerRootComponent } from 'expo';
|
||||
import * as navigation from './navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import * as bridge from '$core/bridge';
|
||||
import { BackHandler } from 'react-native';
|
||||
import { TopViewSelector } from '$core/ViewSelector';
|
||||
import { startup } from './startup';
|
||||
|
||||
SplashScreen.preventAutoHideAsync();
|
||||
// import { Asset } from 'expo-asset';
|
||||
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
||||
import { registerRootComponent } from 'expo';
|
||||
import { Navigation } from './navigation';
|
||||
import { useEffect } from 'react';
|
||||
import * as bridge from '$core/bridge';
|
||||
import { startup } from './startup';
|
||||
|
||||
const startupPromise = startup();
|
||||
|
||||
function App() {
|
||||
const [renderElement, setRenderElement] = useState<React.JSX.Element | undefined>(undefined);
|
||||
const [initialized, setInitialized] = useState<boolean>(false);
|
||||
const [viewInitializationSent, setViewInitializationSent] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
navigation.onViewChange(view => {
|
||||
setRenderElement(view);
|
||||
|
||||
if (!initialized) {
|
||||
SplashScreen.hideAsync();
|
||||
setInitialized(true);
|
||||
}
|
||||
startupPromise.then(() => {
|
||||
bridge.sendViewInitializationComplete();
|
||||
});
|
||||
|
||||
if (!viewInitializationSent) {
|
||||
startupPromise.then(() => {
|
||||
bridge.sendViewInitializationComplete();
|
||||
});
|
||||
|
||||
setViewInitializationSent(true);
|
||||
}
|
||||
|
||||
const backAction = () => {
|
||||
return navigation.viewPop();
|
||||
};
|
||||
|
||||
const backHandler = BackHandler.addEventListener(
|
||||
'hardwareBackPress',
|
||||
backAction
|
||||
);
|
||||
|
||||
return () => backHandler.remove();
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<StrictMode>
|
||||
<SafeAreaProvider>
|
||||
<TopViewSelector view={renderElement}/>
|
||||
</SafeAreaProvider>
|
||||
</StrictMode>
|
||||
<SafeAreaProvider>
|
||||
<Navigation />
|
||||
</SafeAreaProvider>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { scheduleOnMainThread } from './scheduleOnMainThread';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { StyleProp, StyleSheet, View, ViewStyle, useWindowDimensions } from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { StyleProp, View, ViewStyle, useWindowDimensions } from 'react-native';
|
||||
|
||||
import Animated, {
|
||||
Easing,
|
||||
@@ -9,250 +7,244 @@ import Animated, {
|
||||
useSharedValue,
|
||||
interpolate,
|
||||
withTiming,
|
||||
cancelAnimation,
|
||||
withSequence
|
||||
withSequence,
|
||||
FadeIn,
|
||||
FadeOut,
|
||||
} from 'react-native-reanimated';
|
||||
import { useThemeColor } from './useThemeColor';
|
||||
|
||||
export function LeftRightViewSelector<T extends any>(props: { selectedItem: number, list: T[], renderItem: (item: T) => React.JSX.Element, style?: StyleProp<ViewStyle> }) {
|
||||
const [selectedItem, setSelectedItem] = useState(props.selectedItem);
|
||||
const [prevSelectedItem, setPrevSelectedItem] = useState(-1);
|
||||
export function ViewSelector<T extends any, ShowStyle extends ViewStyle, HideStyle extends ViewStyle>(props: {
|
||||
selectedItem: number,
|
||||
list: T[],
|
||||
renderItem: (item: T) => React.JSX.Element,
|
||||
style?: StyleProp<ViewStyle>,
|
||||
showStyle: (indexIncreased: boolean, value: number) => ShowStyle,
|
||||
hideStyle: (indexIncreased: boolean, value: number) => HideStyle,
|
||||
activate: () => number
|
||||
}) {
|
||||
const animation = useSharedValue(100);
|
||||
const layout = useWindowDimensions();
|
||||
|
||||
const directionRight = selectedItem > prevSelectedItem;
|
||||
const hideStyle = useAnimatedStyle(() => ({
|
||||
flexWrap: 'nowrap',
|
||||
transform: [{
|
||||
translateX: directionRight ? interpolate(animation.value, [0, 100], [0, -layout.width]) : interpolate(animation.value, [0, 100], [0, layout.width])
|
||||
}],
|
||||
opacity: interpolate(animation.value, [0, 100], [1, 0])
|
||||
}));
|
||||
type SelectorState = {
|
||||
views: (React.JSX.Element | undefined)[];
|
||||
index: number;
|
||||
itemIndex: number[];
|
||||
};
|
||||
|
||||
const showStyle = useAnimatedStyle(() => ({
|
||||
flexWrap: 'nowrap',
|
||||
transform: [{
|
||||
translateX: directionRight ? interpolate(animation.value, [0, 100], [layout.width, 0]) : interpolate(animation.value, [0, 100], [-layout.width, 0])
|
||||
}],
|
||||
opacity: interpolate(animation.value, [0, 100], [0, 1])
|
||||
}));
|
||||
const currentView = props.list[props.selectedItem];
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedItem != props.selectedItem) {
|
||||
setPrevSelectedItem(selectedItem);
|
||||
setSelectedItem(props.selectedItem);
|
||||
animation.value = 0;
|
||||
}
|
||||
|
||||
if (prevSelectedItem != -1) {
|
||||
animation.value = withTiming(100, {
|
||||
duration: 400,
|
||||
easing: Easing.inOut(Easing.ease)
|
||||
}, (finished) => {
|
||||
if (finished) {
|
||||
scheduleOnMainThread(setPrevSelectedItem, -1);
|
||||
}
|
||||
});
|
||||
}
|
||||
const [state, setState] = useState<SelectorState>({
|
||||
views: [currentView ? props.renderItem(currentView) : undefined, undefined],
|
||||
index: 0,
|
||||
itemIndex: [currentView ? props.selectedItem : -1, -1]
|
||||
});
|
||||
|
||||
const item = props.renderItem(props.list[selectedItem]);
|
||||
const [indexIncreased, setIndexIncreased] = useState<boolean>(false);
|
||||
const prevItemIndex = state.itemIndex[state.index];
|
||||
|
||||
if (prevSelectedItem == -1) {
|
||||
return (<View style={props.style}>{item}</View>)
|
||||
}
|
||||
useEffect(() => {
|
||||
if (props.selectedItem != prevItemIndex) {
|
||||
if (state.itemIndex[0] >= 0) {
|
||||
setIndexIncreased(props.selectedItem > prevItemIndex);
|
||||
animation.value = 0;
|
||||
animation.value = props.activate();
|
||||
}
|
||||
|
||||
const nextRenderIndex = (state.index + 1) & 1;
|
||||
state.views[nextRenderIndex] = props.renderItem(currentView);
|
||||
state.itemIndex[nextRenderIndex] = props.selectedItem;
|
||||
|
||||
setState({
|
||||
views: state.views,
|
||||
index: nextRenderIndex,
|
||||
itemIndex: state.itemIndex,
|
||||
});
|
||||
}
|
||||
|
||||
}, [props.selectedItem]);
|
||||
|
||||
const waitAnimation = props.selectedItem != state.itemIndex[state.index];
|
||||
|
||||
const hideStyle = useAnimatedStyle(() => props.hideStyle(indexIncreased, waitAnimation ? 0 : animation.value));
|
||||
const showStyle = useAnimatedStyle(() => props.showStyle(indexIncreased, waitAnimation ? 0 : animation.value));
|
||||
|
||||
const drawIndex = waitAnimation ? 1 - state.index : state.index;
|
||||
|
||||
const styles = [
|
||||
drawIndex == 0 ? showStyle : hideStyle,
|
||||
drawIndex == 1 ? showStyle : hideStyle,
|
||||
];
|
||||
|
||||
const staticStyle: StyleProp<ViewStyle> = { position: 'absolute', height: "100%", width: "100%" };
|
||||
|
||||
return (
|
||||
<View style={props.style}>
|
||||
{prevSelectedItem != -1 && <Animated.View style={[props.style, hideStyle, { position: 'absolute', height: "100%", width: "100%" }]}>
|
||||
{props.renderItem(props.list[prevSelectedItem])}
|
||||
</Animated.View>}
|
||||
{<Animated.View style={[props.style, showStyle, { position: 'absolute', height: "100%", width: "100%" }]}>
|
||||
{item}
|
||||
</Animated.View>}
|
||||
<Animated.View style={[staticStyle, styles[0]]}>
|
||||
{state.views[0]}
|
||||
</Animated.View>
|
||||
<Animated.View style={[staticStyle, styles[1]]}>
|
||||
{state.views[1]}
|
||||
</Animated.View>
|
||||
</View >
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export function LeftRightViewSelector<T extends any>(props: { selectedItem: number, list: T[], renderItem: (item: T) => React.JSX.Element, style?: StyleProp<ViewStyle> }) {
|
||||
const layout = useWindowDimensions();
|
||||
|
||||
function activate() {
|
||||
return withTiming(100, {
|
||||
duration: 400,
|
||||
easing: Easing.inOut(Easing.ease)
|
||||
});
|
||||
}
|
||||
|
||||
function hideStyle(directionRight: boolean, animationValue: number): ViewStyle {
|
||||
'worklet';
|
||||
|
||||
return {
|
||||
transform: [{
|
||||
translateX: directionRight ? interpolate(animationValue, [0, 100], [0, -layout.width]) : interpolate(animationValue, [0, 100], [0, layout.width])
|
||||
}],
|
||||
opacity: interpolate(animationValue, [0, 100], [1, 0])
|
||||
};
|
||||
}
|
||||
|
||||
function showStyle(directionRight: boolean, animationValue: number): ViewStyle {
|
||||
'worklet';
|
||||
return {
|
||||
transform: [{
|
||||
translateX: directionRight ? interpolate(animationValue, [0, 100], [layout.width, 0]) : interpolate(animationValue, [0, 100], [-layout.width, 0])
|
||||
}],
|
||||
opacity: interpolate(animationValue, [0, 100], [0, 1])
|
||||
};
|
||||
}
|
||||
|
||||
return <ViewSelector {...props}
|
||||
activate={activate}
|
||||
showStyle={showStyle}
|
||||
hideStyle={hideStyle}
|
||||
/>;
|
||||
}
|
||||
|
||||
export function UpDownViewSelector<T extends any>(props: { selectedItem: number, list: T[], renderItem: (item: T) => React.JSX.Element, style?: StyleProp<ViewStyle> }) {
|
||||
const [selectedItem, setSelectedItem] = useState(props.selectedItem);
|
||||
const [prevSelectedItem, setPrevSelectedItem] = useState(-1);
|
||||
const animation = useSharedValue(100);
|
||||
const layout = useWindowDimensions();
|
||||
|
||||
const directionDown = selectedItem > prevSelectedItem;
|
||||
const hideStyle = useAnimatedStyle(() => ({
|
||||
flexWrap: 'nowrap',
|
||||
transform: [{
|
||||
translateY: directionDown ? interpolate(animation.value, [0, 100], [0, -layout.height]) : interpolate(animation.value, [0, 100], [0, layout.height])
|
||||
}],
|
||||
opacity: interpolate(animation.value, [0, 100], [1, 0])
|
||||
}));
|
||||
|
||||
const showStyle = useAnimatedStyle(() => ({
|
||||
flexWrap: 'nowrap',
|
||||
transform: [{
|
||||
translateY: directionDown ? interpolate(animation.value, [0, 100], [layout.height, 0]) : interpolate(animation.value, [0, 100], [-layout.height, 0])
|
||||
}],
|
||||
opacity: interpolate(animation.value, [0, 100], [0, 1])
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedItem != props.selectedItem) {
|
||||
setPrevSelectedItem(selectedItem);
|
||||
setSelectedItem(props.selectedItem);
|
||||
animation.value = 0;
|
||||
}
|
||||
|
||||
if (prevSelectedItem != -1) {
|
||||
animation.value = withTiming(100, {
|
||||
duration: 400,
|
||||
easing: Easing.inOut(Easing.ease)
|
||||
}, (finished) => {
|
||||
if (finished) {
|
||||
scheduleOnMainThread(setPrevSelectedItem, -1);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const item = props.renderItem(props.list[selectedItem]);
|
||||
|
||||
if (prevSelectedItem == -1) {
|
||||
return (<View style={props.style}>{item}</View>)
|
||||
function activate() {
|
||||
return withTiming(100, {
|
||||
duration: 400,
|
||||
easing: Easing.inOut(Easing.ease)
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={props.style}>
|
||||
{prevSelectedItem != -1 && <Animated.View style={[props.style, hideStyle, { position: 'absolute', height: "100%", width: "100%" }]}>
|
||||
{props.renderItem(props.list[prevSelectedItem])}
|
||||
</Animated.View>}
|
||||
{<Animated.View style={[props.style, showStyle, { position: 'absolute', height: "100%", width: "100%" }]}>
|
||||
{item}
|
||||
</Animated.View>}
|
||||
</View >
|
||||
);
|
||||
function hideStyle(directionDown: boolean, animationValue: number): ViewStyle {
|
||||
'worklet';
|
||||
const y = directionDown ? interpolate(animationValue, [0, 100], [0, -layout.height]) : interpolate(animationValue, [0, 100], [0, layout.height]);
|
||||
|
||||
return {
|
||||
transform: [{
|
||||
translateY: y
|
||||
}],
|
||||
height: Math.max(layout.height, layout.height - y),
|
||||
opacity: interpolate(animationValue, [0, 100], [1, 0])
|
||||
};
|
||||
}
|
||||
|
||||
function showStyle(directionDown: boolean, animationValue: number): ViewStyle {
|
||||
'worklet';
|
||||
const y = directionDown ? interpolate(animationValue, [0, 100], [layout.height, 0]) : interpolate(animationValue, [0, 100], [-layout.height, 0]);
|
||||
return {
|
||||
transform: [{
|
||||
translateY: y
|
||||
}],
|
||||
height: Math.max(layout.height, y + layout.height),
|
||||
opacity: interpolate(animationValue, [0, 100], [0, 1])
|
||||
};
|
||||
}
|
||||
|
||||
return <ViewSelector {...props}
|
||||
activate={activate}
|
||||
showStyle={showStyle}
|
||||
hideStyle={hideStyle}
|
||||
/>;
|
||||
}
|
||||
|
||||
export function DownShowViewSelector<T extends any>(props: { selectedItem: number, list: T[], renderItem: (item: T) => React.JSX.Element, style?: StyleProp<ViewStyle> }) {
|
||||
const [selectedItem, setSelectedItem] = useState(props.selectedItem);
|
||||
const [prevSelectedItem, setPrevSelectedItem] = useState(-1);
|
||||
const animation = useSharedValue(200);
|
||||
const layout = useWindowDimensions();
|
||||
|
||||
const hideStyle = useAnimatedStyle(() => ({
|
||||
flexWrap: 'nowrap',
|
||||
transform: [{
|
||||
translateY: animation.value < 100 ? interpolate(animation.value, [0, 100], [0, layout.height], 'clamp') : layout.height
|
||||
}],
|
||||
opacity: animation.value < 100 ? interpolate(animation.value * 10, [0, 100], [1, 0], 'clamp') : 0,
|
||||
display: animation.value < 100 ? undefined : 'none'
|
||||
}));
|
||||
|
||||
const showStyle = useAnimatedStyle(() => ({
|
||||
flexWrap: 'nowrap',
|
||||
opacity: interpolate(animation.value, [100, 200], [0, 1], 'clamp')
|
||||
}));
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedItem != props.selectedItem) {
|
||||
cancelAnimation(animation);
|
||||
|
||||
setPrevSelectedItem(selectedItem);
|
||||
setSelectedItem(props.selectedItem);
|
||||
animation.value = 0;
|
||||
}
|
||||
|
||||
if (prevSelectedItem != -1) {
|
||||
animation.value = withSequence(
|
||||
withTiming(100, {
|
||||
duration: 900,
|
||||
easing: Easing.inOut(Easing.ease)
|
||||
}),
|
||||
withTiming(200, {
|
||||
duration: 600,
|
||||
easing: Easing.inOut(Easing.ease)
|
||||
}, (finished) => {
|
||||
if (finished) {
|
||||
scheduleOnMainThread(setPrevSelectedItem, -1);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const showItem = props.renderItem(props.list[selectedItem]);
|
||||
|
||||
if (prevSelectedItem == -1) {
|
||||
return (<View style={props.style}>{showItem}</View>)
|
||||
}
|
||||
|
||||
const hideItem = props.renderItem(props.list[prevSelectedItem]);
|
||||
|
||||
return (
|
||||
<View style={props.style}>
|
||||
<Animated.View style={[props.style, hideStyle, { position: 'absolute', height: "100%", width: "100%" }]}>
|
||||
{hideItem}
|
||||
</Animated.View>
|
||||
<Animated.View style={[props.style, showStyle]}>
|
||||
{showItem}
|
||||
</Animated.View>
|
||||
</View >
|
||||
);
|
||||
}
|
||||
|
||||
export function TopViewSelector(props: { view?: React.JSX.Element }) {
|
||||
const animation = useSharedValue(0);
|
||||
const [prevView, setPrevView] = useState<React.JSX.Element | undefined>(undefined);
|
||||
const [currentView, setCurrentView] = useState<React.JSX.Element | undefined>(undefined);
|
||||
|
||||
const hideStyle = useAnimatedStyle(() => ({
|
||||
opacity: interpolate(animation.value * 5, [0, 100], [1, 0], 'clamp'),
|
||||
}));
|
||||
|
||||
const showStyle = useAnimatedStyle(() => ({
|
||||
opacity: interpolate(animation.value, [0, 100], [0, 1], 'clamp'),
|
||||
}));
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (props.view != currentView) {
|
||||
setCurrentView(props.view);
|
||||
if (currentView) {
|
||||
setPrevView(currentView);
|
||||
cancelAnimation(animation);
|
||||
animation.value = 0;
|
||||
}
|
||||
}
|
||||
|
||||
animation.value = withSequence(
|
||||
function activate() {
|
||||
return withSequence(
|
||||
withTiming(50, {
|
||||
duration: 900,
|
||||
easing: Easing.inOut(Easing.ease)
|
||||
}),
|
||||
withTiming(100, {
|
||||
duration: 500,
|
||||
easing: Easing.ease
|
||||
}, (finished) => {
|
||||
if (finished) {
|
||||
animation.value = 0;
|
||||
scheduleOnMainThread(setPrevView, undefined);
|
||||
}
|
||||
duration: 600,
|
||||
easing: Easing.inOut(Easing.ease)
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
const backgroundColor = useThemeColor("background");
|
||||
|
||||
if (!prevView) {
|
||||
return currentView;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[{ height: "100%", width: "100%", backgroundColor: backgroundColor }]}>
|
||||
<Animated.View style={[{ position: 'absolute', height: "100%", width: "100%" }, hideStyle]}>
|
||||
{prevView}
|
||||
</Animated.View>
|
||||
<Animated.View style={[{ opacity: 0, position: 'absolute', height: "100%", width: "100%" }, showStyle]}>
|
||||
{currentView}
|
||||
</Animated.View>
|
||||
</View >
|
||||
);
|
||||
function hideStyle(_indexIncreased: boolean, animationValue: number): ViewStyle {
|
||||
'worklet';
|
||||
|
||||
return {
|
||||
transform: [{
|
||||
translateY: interpolate(animationValue, [0, 50], [0, layout.height], 'clamp')
|
||||
}],
|
||||
opacity: interpolate(animationValue * 10, [0, 50], [1, 0], 'clamp')
|
||||
};
|
||||
}
|
||||
|
||||
function showStyle(_indexIncreased: boolean, animationValue: number): ViewStyle {
|
||||
'worklet';
|
||||
|
||||
return {
|
||||
transform: [{
|
||||
translateY: 0
|
||||
}],
|
||||
opacity: interpolate(animationValue, [50, 100], [0, 1], 'clamp')
|
||||
};
|
||||
}
|
||||
|
||||
return <ViewSelector {...props}
|
||||
activate={activate}
|
||||
showStyle={showStyle}
|
||||
hideStyle={hideStyle}
|
||||
/>;
|
||||
}
|
||||
|
||||
export function TopViewSelector(props: { stack: React.JSX.Element[], index: number }) {
|
||||
const currentView = props.stack[props.index];
|
||||
|
||||
type SelectorState = {
|
||||
views: (React.JSX.Element | undefined)[];
|
||||
index: number;
|
||||
};
|
||||
|
||||
const [state, setState] = useState<SelectorState>({ views: [currentView, undefined], index: currentView == undefined ? -1 : 0 });
|
||||
|
||||
useEffect(() => {
|
||||
const nextRenderIndex = (state.index + 1) & 1;
|
||||
state.views[nextRenderIndex] = currentView;
|
||||
|
||||
setState({
|
||||
views: state.views,
|
||||
index: nextRenderIndex
|
||||
});
|
||||
}, [props.index]);
|
||||
|
||||
const backgroundColor = useThemeColor("background");
|
||||
const style: StyleProp<ViewStyle> = { height: "100%", width: "100%" };
|
||||
|
||||
return (
|
||||
<View style={{ backgroundColor, flex: 1 }}>
|
||||
{state.index == 0 && <Animated.View entering={FadeIn} exiting={FadeOut} style={style}>
|
||||
{state.views[0]}
|
||||
</Animated.View>}
|
||||
{state.index == 1 && <Animated.View entering={FadeIn} exiting={FadeOut} style={style}>
|
||||
{state.views[1]}
|
||||
</Animated.View>}
|
||||
</View>
|
||||
)
|
||||
};
|
||||
|
||||
@@ -2,7 +2,8 @@ import { dirname } from "path";
|
||||
import { addLauncher, Launcher, LaunchParams, Process } from "./Launcher";
|
||||
import { Target } from "./Target";
|
||||
import { fork, spawn } from "child_process";
|
||||
import { Duplex, EventEmitter } from "stream";
|
||||
import { Duplex } from "stream";
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
const nativeLauncher: Launcher = {
|
||||
launch: async (path: string, args: string[], params: LaunchParams) => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ComponentProps, ReactElement, useEffect, useState } from 'react';
|
||||
import { ComponentProps, memo, ReactElement, useEffect, useState } from 'react';
|
||||
import { Image, Pressable, ScrollView, StyleSheet, View, FlatList } from 'react-native';
|
||||
import { useThemeColor } from '$core/useThemeColor'
|
||||
import { } from '@react-navigation/elements';
|
||||
@@ -19,7 +19,6 @@ import Animated, {
|
||||
cancelAnimation,
|
||||
interpolateColor,
|
||||
} from 'react-native-reanimated';
|
||||
import { useColorScheme } from '$core/useColorScheme';
|
||||
|
||||
|
||||
const games: (ExecutableInfo & ExplorerItem)[] = [
|
||||
@@ -223,7 +222,7 @@ type ScreenTabProps = {
|
||||
|
||||
const AnimatedThemedText = Animated.createAnimatedComponent(ThemedText);
|
||||
|
||||
function ScreenTab(props: Omit<ComponentProps<typeof HapticPressable>, "children"> & ScreenTabProps) {
|
||||
const ScreenTab = memo(function (props: Omit<ComponentProps<typeof HapticPressable>, "children"> & ScreenTabProps) {
|
||||
const [activeState, setActiveState] = useState(props.active);
|
||||
const animation = useSharedValue(props.active ? 100 : 0);
|
||||
|
||||
@@ -258,11 +257,11 @@ function ScreenTab(props: Omit<ComponentProps<typeof HapticPressable>, "children
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
|
||||
|
||||
function ExplorerItemHeader({ item, active, ...rest }: { item: ExplorerItem, active: boolean, } & ComponentProps<typeof Pressable>) {
|
||||
const ExplorerItemHeader = memo(function ({ item, active, ...rest }: { item: ExplorerItem, active: boolean, } & ComponentProps<typeof Pressable>) {
|
||||
const [activeState, setActiveState] = useState(active);
|
||||
const animation = useSharedValue(active ? 100 : 0);
|
||||
|
||||
@@ -311,30 +310,32 @@ function ExplorerItemHeader({ item, active, ...rest }: { item: ExplorerItem, act
|
||||
<View style={styles.image}><ThemedIcon iconSet="Ionicons" name="extension-puzzle-sharp" size={100} /><ThemedText>{getName(item)}</ThemedText></View>
|
||||
}
|
||||
</AnimatedPressable>
|
||||
}
|
||||
});
|
||||
|
||||
function ExplorerItemBody({ item }: { item: ExplorerItem }) {
|
||||
return (<Animated.ScrollView style={{ margin: 80, flex: 1, flexGrow: 5 }}>
|
||||
{item.description && <ThemedText type='subtitle'>{item.description && getLocalizedString(item.description)}</ThemedText>}
|
||||
</Animated.ScrollView>)
|
||||
}
|
||||
const ExplorerItemBody = memo(function ({ item }: { item: ExplorerItem }) {
|
||||
return (
|
||||
<View>
|
||||
{item.description && <ThemedText type='subtitle'>{item.description && getLocalizedString(item.description)}</ThemedText>}
|
||||
</View>
|
||||
)
|
||||
});
|
||||
|
||||
function ExplorerView({ items }: { items: ExplorerItem[] }) {
|
||||
const ExplorerView = function ({ items }: { items: ExplorerItem[] }) {
|
||||
const styles = StyleSheet.create({
|
||||
topContainer: {
|
||||
flex: 1,
|
||||
flexGrow: 1,
|
||||
width: "100%",
|
||||
height: "100%"
|
||||
},
|
||||
scrollContainer: {
|
||||
minHeight: 150,
|
||||
maxHeight: 250,
|
||||
flex: 1,
|
||||
flexGrow: 1,
|
||||
marginHorizontal: 60,
|
||||
marginTop: 30,
|
||||
marginBottom: 60,
|
||||
},
|
||||
scrollContentContainer: {
|
||||
},
|
||||
descriptionContainer: {
|
||||
flex: 1,
|
||||
flexGrow: 1,
|
||||
flexDirection: 'column',
|
||||
}
|
||||
});
|
||||
@@ -342,21 +343,25 @@ function ExplorerView({ items }: { items: ExplorerItem[] }) {
|
||||
const [selectedItem, selectItem] = useState(0);
|
||||
|
||||
return (
|
||||
<View style={styles.topContainer}>
|
||||
<ScrollView style={styles.descriptionContainer} showsVerticalScrollIndicator={false}>
|
||||
<FlatList data={items} style={styles.scrollContainer} contentContainerStyle={styles.scrollContentContainer} horizontal={true} showsHorizontalScrollIndicator={false} renderItem={
|
||||
({ item, index }) => {
|
||||
const name = getName(item);
|
||||
return <ExplorerItemHeader key={index + name + item.type + item.version} item={item} style={{ margin: 30 }} onPress={() => selectItem(index)} active={index == selectedItem}></ExplorerItemHeader>
|
||||
}
|
||||
}>
|
||||
</FlatList>
|
||||
|
||||
<DownShowViewSelector list={items} renderItem={item => <ExplorerItemBody item={item} />} selectedItem={selectedItem} />
|
||||
<View style={[styles.topContainer, {}]}>
|
||||
<ScrollView style={styles.descriptionContainer} contentContainerStyle={{}} showsVerticalScrollIndicator={false}>
|
||||
<View style={{ marginHorizontal: 60 }}>
|
||||
<FlatList data={items} style={styles.scrollContainer} contentContainerStyle={styles.scrollContentContainer} horizontal={true} showsHorizontalScrollIndicator={false} renderItem={
|
||||
({ item, index }) => {
|
||||
const name = getName(item);
|
||||
return <ExplorerItemHeader key={index + name + item.type + item.version} item={item} style={{ margin: 30 }} onPress={() => selectItem(index)} active={index == selectedItem}></ExplorerItemHeader>
|
||||
}
|
||||
}>
|
||||
</FlatList>
|
||||
<DownShowViewSelector
|
||||
list={items}
|
||||
renderItem={item => <ExplorerItemBody item={item} />}
|
||||
selectedItem={selectedItem} />
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
type Screen = {
|
||||
title: string;
|
||||
@@ -371,10 +376,16 @@ type Props = {
|
||||
};
|
||||
|
||||
const ExplorerStyles = StyleSheet.create({
|
||||
rootContainer: { height: "100%", width: "100%" },
|
||||
rootContainer: {
|
||||
width: "100%",
|
||||
height: "100%"
|
||||
},
|
||||
headerContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
menuContainer: {
|
||||
height: 40,
|
||||
flexDirection: 'row',
|
||||
minHeight: 50,
|
||||
marginLeft: 60,
|
||||
marginRight: 40,
|
||||
marginTop: 60
|
||||
@@ -422,12 +433,11 @@ const screens: Screen[] = [
|
||||
|
||||
export function Explorer(props?: Props) {
|
||||
const [background, setBackground] = useState<string | undefined>(undefined);
|
||||
const [activeTab, setActiveTab] = useState(1);
|
||||
const theme = useColorScheme();
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
|
||||
return (
|
||||
<View style={[ExplorerStyles.rootContainer, { backgroundColor: theme == 'dark' ? "black" : "white", backgroundImage: background }]}>
|
||||
<View style={ExplorerStyles.menuContainer}>
|
||||
<View style={[ExplorerStyles.rootContainer, { backgroundImage: background }]}>
|
||||
<View style={[ExplorerStyles.menuContainer, {}]}>
|
||||
<View style={ExplorerStyles.containerTabs}>
|
||||
<View style={ExplorerStyles.containerMenuItems}>
|
||||
{
|
||||
@@ -437,7 +447,7 @@ export function Explorer(props?: Props) {
|
||||
}
|
||||
</View>
|
||||
</View>
|
||||
<View style={ExplorerStyles.containerButtons}>
|
||||
<View style={[ExplorerStyles.containerButtons]}>
|
||||
<View style={ExplorerStyles.containerMenuItems}>
|
||||
<HapticPressable><ThemedIcon iconSet="Ionicons" name="search" size={40} /></HapticPressable>
|
||||
<HapticPressable onPress={() => settings.pushSettingsView({})}><ThemedIcon iconSet="Ionicons" name="settings-outline" size={40} /></HapticPressable>
|
||||
@@ -446,9 +456,9 @@ export function Explorer(props?: Props) {
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<LeftRightViewSelector list={screens} style={{ flex: 1, flexGrow: 1 }} renderItem={item => item.view((image) => setBackground(image))} selectedItem={activeTab} />
|
||||
<LeftRightViewSelector list={screens} style={{ flex: 1 }} renderItem={item => item.view((image) => setBackground(image))} selectedItem={activeTab} />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ComponentProps, useEffect, useState } from 'react';
|
||||
import { ComponentProps, memo, useEffect, useState } from 'react';
|
||||
import { ScrollView, StyleSheet, View, Switch, Alert, TextInput, Modal, useWindowDimensions, BackHandler } from 'react-native';
|
||||
import { useThemeColor } from '$core/useThemeColor'
|
||||
import ThemedIcon from '$core/ThemedIcon';
|
||||
@@ -25,7 +25,7 @@ type SettingsTabProps = {
|
||||
iconSet: 'Ionicons' | 'FontAwesome6' | 'MaterialIcons';
|
||||
};
|
||||
|
||||
function SettingsTab(props: Omit<ComponentProps<typeof HapticPressable>, "children"> & SettingsTabProps) {
|
||||
const SettingsTab = memo(function (props: Omit<ComponentProps<typeof HapticPressable>, "children"> & SettingsTabProps) {
|
||||
const [activeState, setActiveState] = useState(props.active);
|
||||
const animation = useSharedValue(props.active ? 100 : 0);
|
||||
|
||||
@@ -60,7 +60,7 @@ function SettingsTab(props: Omit<ComponentProps<typeof HapticPressable>, "childr
|
||||
</Animated.View>
|
||||
</HapticPressable>
|
||||
);
|
||||
}
|
||||
})
|
||||
|
||||
enum SettingItemType {
|
||||
Text,
|
||||
@@ -159,7 +159,7 @@ type SettingsItemProps =
|
||||
SettingItemSingleSelectionProps |
|
||||
SettingItemMultipleSelectionProps;
|
||||
|
||||
function TextInputModal({ visible, onClose, value, onSave, title, placeholder, mode = SettingTextMode.Plain }: {
|
||||
const TextInputModal = memo(function ({ visible, onClose, value, onSave, title, placeholder, mode = SettingTextMode.Plain }: {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
value: string;
|
||||
@@ -187,7 +187,7 @@ function TextInputModal({ visible, onClose, value, onSave, title, placeholder, m
|
||||
try {
|
||||
const result = await pickDirectory({ requestLongTermAccess: true });
|
||||
setInputValue(result.uri);
|
||||
} catch {}
|
||||
} catch { }
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -231,9 +231,9 @@ function TextInputModal({ visible, onClose, value, onSave, title, placeholder, m
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
function NumberInputModal({ visible, onClose, value, onSave, title, placeholder }: {
|
||||
const NumberInputModal = memo(function ({ visible, onClose, value, onSave, title, placeholder }: {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
value: number;
|
||||
@@ -289,9 +289,9 @@ function NumberInputModal({ visible, onClose, value, onSave, title, placeholder
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
function DateTimeModal({ visible, onClose, value, onSave, title, mode }: {
|
||||
const DateTimeModal = memo(function ({ visible, onClose, value, onSave, title, mode }: {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
value: Date;
|
||||
@@ -380,9 +380,9 @@ function DateTimeModal({ visible, onClose, value, onSave, title, mode }: {
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
function ListManagerModal({ visible, onClose, items, onSave, title, placeholder }: {
|
||||
const ListManagerModal = memo(function ({ visible, onClose, items, onSave, title, placeholder }: {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
items: string[];
|
||||
@@ -472,9 +472,9 @@ function ListManagerModal({ visible, onClose, items, onSave, title, placeholder
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
function SingleSelectionModal({ visible, onClose, options, selectedIndex, onSave, title }: {
|
||||
const SingleSelectionModal = memo(function ({ visible, onClose, options, selectedIndex, onSave, title }: {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
options: string[];
|
||||
@@ -542,9 +542,9 @@ function SingleSelectionModal({ visible, onClose, options, selectedIndex, onSave
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
function MultipleSelectionModal({ visible, onClose, options, selectedIndices, onSave, title }: {
|
||||
const MultipleSelectionModal = memo(function ({ visible, onClose, options, selectedIndices, onSave, title }: {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
options: string[];
|
||||
@@ -617,9 +617,9 @@ function MultipleSelectionModal({ visible, onClose, options, selectedIndices, on
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
function SettingsItem(props: SettingsItemProps & { isLast: boolean }) {
|
||||
const SettingsItem = memo(function (props: SettingsItemProps & { isLast: boolean }) {
|
||||
const borderColor = useThemeColor("outline");
|
||||
const backgroundColor = useThemeColor("surface");
|
||||
const primaryColor = useThemeColor('primary');
|
||||
@@ -779,7 +779,7 @@ function SettingsItem(props: SettingsItemProps & { isLast: boolean }) {
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
type SettingsSection = {
|
||||
title: string;
|
||||
@@ -1032,11 +1032,11 @@ const settingsCategories: SettingsCategory[] = [
|
||||
}
|
||||
];
|
||||
|
||||
function SettingsContent({ category }: { category: SettingsCategory }) {
|
||||
const SettingsContent = memo(function ({ category }: { category: SettingsCategory }) {
|
||||
return (
|
||||
<ScrollView style={styles.settingsContent} showsVerticalScrollIndicator={false}>
|
||||
{category.sections.map((section, sectionIndex) => (
|
||||
<View key={sectionIndex} style={styles.settingsSection}>
|
||||
<View key={category.title + sectionIndex} style={styles.settingsSection}>
|
||||
<ThemedText type='subtitle' style={styles.sectionTitle}>
|
||||
{section.title}
|
||||
</ThemedText>
|
||||
@@ -1049,7 +1049,7 @@ function SettingsContent({ category }: { category: SettingsCategory }) {
|
||||
))}
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
type Props = {
|
||||
category?: string;
|
||||
|
||||
Reference in New Issue
Block a user