Simplify navigation, improve animations

This commit is contained in:
DH
2025-08-23 21:07:23 +03:00
parent 5910fda01d
commit 0beddaef68
6 changed files with 346 additions and 342 deletions

View File

@@ -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"
},

View File

@@ -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>
)
}

View File

@@ -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>
)
};

View File

@@ -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) => {

View File

@@ -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>
)
}
};

View File

@@ -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;