diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/View.tsx | 25 | ||||
| -rw-r--r-- | src/components/AppMenu.tsx | 39 | ||||
| -rw-r--r-- | src/components/Clock.tsx | 4 | ||||
| -rw-r--r-- | src/components/Favorites.tsx | 62 | ||||
| -rw-r--r-- | src/hooks/useFavorites.ts | 53 | ||||
| -rw-r--r-- | src/hooks/useInstalledApps.ts | 19 | ||||
| -rw-r--r-- | src/screens/AppList.tsx | 31 | ||||
| -rw-r--r-- | src/screens/Home.tsx | 8 |
8 files changed, 144 insertions, 97 deletions
diff --git a/src/View.tsx b/src/View.tsx index c5084f2..110e3fe 100644 --- a/src/View.tsx +++ b/src/View.tsx @@ -1,18 +1,19 @@ -import React, {useState} from 'react'; -import {InstalledApps} from 'react-native-launcher-kit'; +import React, {useEffect, useState} from 'react'; import Swiper from 'react-native-swiper'; import {AppList} from './screens/AppList'; import {Home} from './screens/Home'; +import {useInstalledApps} from './hooks/useInstalledApps'; +import {useFavorites} from './hooks/useFavorites'; -export const View: React.FC = () => { - const [apps, _setApps] = useState(() => InstalledApps.getSortedApps()); +export const View: React.FC = React.memo(() => { + const {refreshApps} = useInstalledApps(); + const {refreshFavorites} = useFavorites(); const [screenIndex, setScreenIndex] = useState(0); - // const refreshApps = () => setApps(InstalledApps.getSortedApps()); - - // useEffect(() => { - // refreshApps(); - // }, [screenIndex]); + useEffect(() => { + refreshApps(); + refreshFavorites(); + }, [screenIndex, refreshApps, refreshFavorites]); return ( <Swiper @@ -22,8 +23,8 @@ export const View: React.FC = () => { dot={<></>} activeDot={<></>} onIndexChanged={setScreenIndex}> - <Home apps={apps} active={screenIndex === 0} /> - <AppList apps={apps} active={screenIndex === 1} /> + <Home active={screenIndex === 0} /> + <AppList active={screenIndex === 1} /> </Swiper> ); -}; +}); diff --git a/src/components/AppMenu.tsx b/src/components/AppMenu.tsx index ec0d684..2319e73 100644 --- a/src/components/AppMenu.tsx +++ b/src/components/AppMenu.tsx @@ -2,7 +2,10 @@ import {Text, View, Modal, TouchableOpacity, Pressable} from 'react-native'; import {AppDetail} from 'react-native-launcher-kit/typescript/Interfaces/InstalledApps'; import {useFavorites} from '../hooks/useFavorites'; import React, {useState} from 'react'; +// @ts-expect-error No declaration file import IntentLauncher from '@angelkrak/react-native-intent-launcher'; +import {TouchableHighlight} from 'react-native-gesture-handler'; +import {useStableCallback} from '../hooks/useStableCallback'; export const AppMenu: React.FC<React.PropsWithChildren<{app: AppDetail}>> = ({ app, @@ -11,24 +14,26 @@ export const AppMenu: React.FC<React.PropsWithChildren<{app: AppDetail}>> = ({ const [isOpen, setIsOpen] = useState(false); const {addToFavorites, isFavorite, removeFromFavorites} = useFavorites(); - const menuPress = (fn: () => Promise<void> | void) => async () => { + const openContextMenu = useStableCallback(() => setIsOpen(true)); + const closeContextMenu = useStableCallback(() => setIsOpen(false)); + + const onMenuItemPress = (fn: () => Promise<void> | void) => async () => { + closeContextMenu(); await fn(); - setIsOpen(false); }; - const menuItems = [ isFavorite(app.packageName) ? { label: 'Remove from favorites', - onPress: menuPress(() => removeFromFavorites(app.packageName)), + onPress: onMenuItemPress(() => removeFromFavorites(app.packageName)), } : { label: 'Add to favorites', - onPress: menuPress(() => addToFavorites(app.packageName)), + onPress: onMenuItemPress(() => addToFavorites(app.packageName)), }, { label: 'App info', - onPress: menuPress(() => + onPress: onMenuItemPress(() => IntentLauncher.startActivity({ action: 'android.settings.APPLICATION_DETAILS_SETTINGS', data: 'package:' + app.packageName, @@ -37,7 +42,7 @@ export const AppMenu: React.FC<React.PropsWithChildren<{app: AppDetail}>> = ({ }, { label: 'Uninstall', - onPress: menuPress(() => + onPress: onMenuItemPress(() => IntentLauncher.startActivity({ action: 'android.intent.action.DELETE', data: 'package:' + app.packageName, @@ -46,25 +51,33 @@ export const AppMenu: React.FC<React.PropsWithChildren<{app: AppDetail}>> = ({ }, ]; + const openApp = useStableCallback(() => + IntentLauncher.startAppByPackageName(app.packageName), + ); + return ( <> - <TouchableOpacity onLongPress={() => setIsOpen(true)}> + <TouchableHighlight + activeOpacity={0.7} + underlayColor="#101010" + onPress={openApp} + onLongPress={openContextMenu}> {children} - </TouchableOpacity> + </TouchableHighlight> <Modal visible={isOpen} transparent={true} animationType="none" - onRequestClose={() => setIsOpen(false)}> - <TouchableOpacity className="flex-1" onPress={() => setIsOpen(false)}> + onRequestClose={closeContextMenu}> + <TouchableOpacity className="flex-1" onPress={closeContextMenu}> <View className="flex justify-center items-center h-full"> - <View className="shadow-lg bg-gray-900 w-2/3"> + <View className="bg-[#181818] border border-[#222] w-2/3"> {menuItems.map((menuItem) => ( <Pressable key={menuItem.label} onPress={menuItem.onPress} - className="py-3 px-4 border-b border-gray-800 last:border-b-0"> + className="py-3 px-4 border-b border-[#222] last:border-b-0 last:border-transparent"> <Text className="text-lg text-gray-300"> {menuItem.label} </Text> diff --git a/src/components/Clock.tsx b/src/components/Clock.tsx index 619a31b..f41a298 100644 --- a/src/components/Clock.tsx +++ b/src/components/Clock.tsx @@ -1,7 +1,7 @@ import React, {useEffect, useMemo, useState} from 'react'; import {Text, View} from 'react-native'; -export const Clock: React.FC = () => { +export const Clock: React.FC = React.memo(() => { const [date, setDate] = useState(() => new Date()); useEffect(() => { const interval = setInterval(() => { @@ -34,4 +34,4 @@ export const Clock: React.FC = () => { <Text className="text-lg">{dateText}</Text> </View> ); -}; +}); diff --git a/src/components/Favorites.tsx b/src/components/Favorites.tsx index c2adae7..6ee99cb 100644 --- a/src/components/Favorites.tsx +++ b/src/components/Favorites.tsx @@ -9,12 +9,23 @@ import DraggableFlatList, { } from 'react-native-draggable-flatlist'; import Icon from 'react-native-vector-icons/MaterialIcons'; import {AppMenu} from './AppMenu'; +import {useInstalledApps} from '../hooks/useInstalledApps'; -export const Favorites: React.FC<{apps: AppDetail[]; active: boolean}> = ({ - apps, - active, -}) => { - const {favoriteAppNames, setFavorites, refreshFavorites} = useFavorites(); +const AppLabel: React.FC<{app: AppDetail}> = React.memo(({app}) => { + return ( + <View className="flex-1"> + <AppMenu app={app}> + <View className="py-3 px-2"> + <Text className="text-lg">{app.label}</Text> + </View> + </AppMenu> + </View> + ); +}); + +export const Favorites: React.FC<{active: boolean}> = (_) => { + const {apps} = useInstalledApps(); + const {favoriteAppNames, setFavorites} = useFavorites(); const appByName = useMemo( () => Object.fromEntries(apps.map((app) => [app.packageName, app])), @@ -33,40 +44,33 @@ export const Favorites: React.FC<{apps: AppDetail[]; active: boolean}> = ({ [setFavorites], ); - useEffect(() => { - if (!active) return; - refreshFavorites(); - }, [active, refreshFavorites]); - return ( <View className="px-4 py-1"> <DraggableFlatList data={favoriteApps} onDragEnd={onDragEnd} - keyExtractor={(item) => item.packageName} + keyExtractor={(app) => app.packageName} renderItem={({item: app, drag, isActive}) => ( - <ShadowDecorator> + <ShadowDecorator key={app.packageName}> <OpacityDecorator> - <AppMenu app={app}> - <View - className={`py-3 px-2 flex-row w-full ${ - isActive ? 'bg-gray-950 opacity-60' : '' - }`}> - <Text className="text-lg flex-1">{app.label}</Text> - <TouchableOpacity - onLongPress={drag} - disabled={isActive} - delayLongPress={500}> - <View className="py-1"> - <Icon name="drag-indicator" size={21} color="#444" /> - </View> - </TouchableOpacity> - </View> - </AppMenu> + <View + className={`flex-row justify-stretch items-center ${ + isActive ? 'bg-[#181818] opacity-60' : '' + }`}> + <AppLabel app={app} /> + + <TouchableOpacity + onLongPress={drag} + disabled={isActive} + hitSlop={10} + className="p-3"> + <Icon name="drag-indicator" size={21} color="#444" /> + </TouchableOpacity> + </View> </OpacityDecorator> </ShadowDecorator> )} - renderPlaceholder={() => <View className="flex-1 bg-gray-950" />} + renderPlaceholder={() => <View className="flex-1 bg-[#181818]" />} /> </View> ); diff --git a/src/hooks/useFavorites.ts b/src/hooks/useFavorites.ts index 46b6e77..24684aa 100644 --- a/src/hooks/useFavorites.ts +++ b/src/hooks/useFavorites.ts @@ -1,31 +1,46 @@ -import {useEffect, useState} from 'react'; import {useAsyncStorage} from '@react-native-async-storage/async-storage'; import {useStableCallback} from './useStableCallback'; +import {atom, useAtom} from 'jotai'; + +const favoriteAppNamesAtom = atom<string[]>([]); + +const serialize = (packageNames: string[]) => JSON.stringify(packageNames); + +const deserialize = (packageNames: string): string[] => { + try { + const apps = JSON.parse(packageNames); + if (!Array.isArray(apps)) return []; + return apps; + } catch (err) { + console.error(err); + return []; + } +}; export const useFavorites = () => { const favoritesStorage = useAsyncStorage('favorites'); - const [favoriteAppNames, setFavoriteAppNames] = useState<string[]>([]); + const [favoriteAppNames, setFavoriteAppNames] = useAtom(favoriteAppNamesAtom); const getFavoriteAppNames = useStableCallback(async (): Promise<string[]> => { - try { - const result = await favoritesStorage.getItem(); - const apps = result ? JSON.parse(result) : []; - if (!Array.isArray(apps)) return []; - return apps; - } catch (err) { - console.error(err); - return []; - } + const result = await favoritesStorage.getItem(); + return deserialize(result ?? 'null'); }); - const refreshFavorites = useStableCallback(async () => { - getFavoriteAppNames().then(setFavoriteAppNames); + const setFavoritesState = useStableCallback((packageNames: string[]) => { + const newNames = [...new Set(packageNames)]; + if (serialize(newNames) === serialize(favoriteAppNames)) return newNames; + + setFavoriteAppNames(newNames); + return newNames; }); - const setFavorites = useStableCallback(async (names: string[]) => { - const newNames = [...new Set(names)]; - await favoritesStorage.setItem(JSON.stringify(newNames)); - refreshFavorites(); + const setFavorites = useStableCallback(async (packageNames: string[]) => { + const newNames = setFavoritesState(packageNames); + await favoritesStorage.setItem(serialize(newNames)); + }); + + const refreshFavorites = useStableCallback(async () => { + getFavoriteAppNames().then(setFavoritesState); }); const addToFavorites = useStableCallback(async (packageName: string) => { @@ -41,10 +56,6 @@ export const useFavorites = () => { favoriteAppNames.includes(packageName), ); - useEffect(() => { - refreshFavorites(); - }, [refreshFavorites]); - return { addToFavorites, favoriteAppNames, diff --git a/src/hooks/useInstalledApps.ts b/src/hooks/useInstalledApps.ts new file mode 100644 index 0000000..32290e5 --- /dev/null +++ b/src/hooks/useInstalledApps.ts @@ -0,0 +1,19 @@ +import {atom, useAtom} from 'jotai'; +import {InstalledApps} from 'react-native-launcher-kit'; +import {AppDetail} from 'react-native-launcher-kit/typescript/Interfaces/InstalledApps'; +import {useStableCallback} from './useStableCallback'; + +const installedAppsAtom = atom<AppDetail[]>(InstalledApps.getSortedApps()); + +export const useInstalledApps = () => { + const [apps, setApps] = useAtom(installedAppsAtom); + + const refreshApps = useStableCallback(() => + setApps(InstalledApps.getSortedApps()), + ); + + return { + apps, + refreshApps, + }; +}; diff --git a/src/screens/AppList.tsx b/src/screens/AppList.tsx index e6b3e6e..adb51b5 100644 --- a/src/screens/AppList.tsx +++ b/src/screens/AppList.tsx @@ -1,14 +1,24 @@ import {matchSorter} from 'match-sorter'; import React, {useEffect, useMemo, useRef, useState} from 'react'; import {ScrollView, Text, TextInput, View} from 'react-native'; -import {AppDetail} from 'react-native-launcher-kit/typescript/Interfaces/InstalledApps'; import Icon from 'react-native-vector-icons/MaterialIcons'; import {AppMenu} from '../components/AppMenu'; +import {useInstalledApps} from '../hooks/useInstalledApps'; +import {AppDetail} from 'react-native-launcher-kit/typescript/Interfaces/InstalledApps'; + +const AppListItem: React.FC<{app: AppDetail}> = React.memo(({app}) => { + return ( + <AppMenu app={app}> + <View className="px-4 py-2"> + <Text className="text-md font-bold">{app.label}</Text> + <Text className="text-xs text-gray-600">{app.packageName}</Text> + </View> + </AppMenu> + ); +}); -export const AppList: React.FC<{apps: AppDetail[]; active: boolean}> = ({ - apps, - active, -}) => { +export const AppList: React.FC<{active: boolean}> = React.memo(({active}) => { + const {apps} = useInstalledApps(); const textInputRef = useRef<TextInput>(null); const [searchText, setSearchText] = useState(''); @@ -41,17 +51,10 @@ export const AppList: React.FC<{apps: AppDetail[]; active: boolean}> = ({ <ScrollView contentInsetAdjustmentBehavior="automatic"> <View className="flex-1"> {filteredApps.map((app, index) => ( - <AppMenu app={app} key={app.packageName + index}> - <View className="mt-4 px-4"> - <Text className="text-md font-bold dark:text-white"> - {app.label} - </Text> - <Text className="text-xs">{app.packageName}</Text> - </View> - </AppMenu> + <AppListItem app={app} key={app.packageName + index} /> ))} </View> </ScrollView> </View> ); -}; +}); diff --git a/src/screens/Home.tsx b/src/screens/Home.tsx index 7bd1a3f..0d694c0 100644 --- a/src/screens/Home.tsx +++ b/src/screens/Home.tsx @@ -1,17 +1,13 @@ import React from 'react'; import {View} from 'react-native'; import {Clock} from '../components/Clock'; -import {AppDetail} from 'react-native-launcher-kit/typescript/Interfaces/InstalledApps'; import {Favorites} from '../components/Favorites'; -export const Home: React.FC<{apps: AppDetail[]; active: boolean}> = ({ - apps, - active, -}) => { +export const Home: React.FC<{active: boolean}> = ({active}) => { return ( <View> <Clock /> - <Favorites apps={apps} active={active} /> + <Favorites active={active} /> </View> ); }; |
