aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAkshay Nair <phenax5@gmail.com>2024-09-28 22:19:31 +0530
committerAkshay Nair <phenax5@gmail.com>2024-09-29 16:16:39 +0530
commit34b8c1f962a5c835351dc6f4369757259998af16 (patch)
tree6f836fdaaedfec125b1202f3b5f9ce24e6277f11
parenta1143f3696a0b272018f7eea43ee23a674c618e1 (diff)
downloaddaft-launcher-34b8c1f962a5c835351dc6f4369757259998af16.tar.gz
daft-launcher-34b8c1f962a5c835351dc6f4369757259998af16.zip
Finish up app menu + fix drag interaction
-rw-r--r--App.tsx13
-rwxr-xr-xbun.lockbbin398665 -> 399052 bytes
-rw-r--r--package.json1
-rw-r--r--src/View.tsx25
-rw-r--r--src/components/AppMenu.tsx39
-rw-r--r--src/components/Clock.tsx4
-rw-r--r--src/components/Favorites.tsx62
-rw-r--r--src/hooks/useFavorites.ts53
-rw-r--r--src/hooks/useInstalledApps.ts19
-rw-r--r--src/screens/AppList.tsx31
-rw-r--r--src/screens/Home.tsx8
11 files changed, 153 insertions, 102 deletions
diff --git a/App.tsx b/App.tsx
index 1852a04..dd4cdd8 100644
--- a/App.tsx
+++ b/App.tsx
@@ -2,14 +2,17 @@ import React from 'react';
import {SafeAreaView} from 'react-native';
import {GestureHandlerRootView} from 'react-native-gesture-handler';
import {View} from './src/View';
+import {Provider as JotaiProvider} from 'jotai';
const App: React.FC = () => {
return (
- <GestureHandlerRootView>
- <SafeAreaView className="flex flex-1 bg-black">
- <View />
- </SafeAreaView>
- </GestureHandlerRootView>
+ <JotaiProvider>
+ <GestureHandlerRootView>
+ <SafeAreaView className="flex flex-1 bg-black text-slate-200">
+ <View />
+ </SafeAreaView>
+ </GestureHandlerRootView>
+ </JotaiProvider>
);
};
diff --git a/bun.lockb b/bun.lockb
index 1506e50..33efa1e 100755
--- a/bun.lockb
+++ b/bun.lockb
Binary files differ
diff --git a/package.json b/package.json
index ba53403..35311b7 100644
--- a/package.json
+++ b/package.json
@@ -12,6 +12,7 @@
"dependencies": {
"@angelkrak/react-native-intent-launcher": "^1.0.2",
"@react-native-async-storage/async-storage": "^1.23.1",
+ "jotai": "^2.10.0",
"match-sorter": "^6.3.4",
"nativewind": "^2.0.11",
"react": "18.2.0",
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>
);
};