diff options
| -rw-r--r-- | App.tsx | 31 | ||||
| -rw-r--r-- | android/app/build.gradle | 2 | ||||
| -rw-r--r-- | babel.config.js | 2 | ||||
| -rwxr-xr-x | bun.lockb | bin | 391227 -> 397425 bytes | |||
| -rw-r--r-- | flake.nix | 9 | ||||
| -rw-r--r-- | package.json | 5 | ||||
| -rw-r--r-- | src/components/Favorites.tsx | 65 | ||||
| -rw-r--r-- | src/hooks/useFavorites.ts | 52 | ||||
| -rw-r--r-- | src/screens/Home.tsx | 12 |
9 files changed, 156 insertions, 22 deletions
@@ -4,6 +4,7 @@ import {InstalledApps} from 'react-native-launcher-kit'; import Swiper from 'react-native-swiper'; import {AppList} from './src/screens/AppList'; import {Home} from './src/screens/Home'; +import {GestureHandlerRootView} from 'react-native-gesture-handler'; function App(): React.JSX.Element { const [apps, setApps] = useState(() => InstalledApps.getSortedApps()); @@ -11,21 +12,23 @@ function App(): React.JSX.Element { const [screenIndex, setScreenIndex] = useState(0); return ( - <SafeAreaView className="flex flex-1 bg-slate-100 dark:bg-black"> - <Swiper - autoplay={false} - horizontal - loop={false} - loadMinimal - dot={<></>} - activeDot={<></>} - index={screenIndex} - onIndexChanged={setScreenIndex}> - <Home /> + <GestureHandlerRootView> + <SafeAreaView className="flex flex-1 bg-slate-100 dark:bg-black"> + <Swiper + autoplay={false} + horizontal + loop={false} + loadMinimal + dot={<></>} + activeDot={<></>} + index={screenIndex} + onIndexChanged={setScreenIndex}> + <Home apps={apps} /> - <AppList apps={apps} screenIndex={screenIndex} /> - </Swiper> - </SafeAreaView> + <AppList apps={apps} screenIndex={screenIndex} /> + </Swiper> + </SafeAreaView> + </GestureHandlerRootView> ); } diff --git a/android/app/build.gradle b/android/app/build.gradle index ebda1a8..378d7b8 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -2,6 +2,8 @@ apply plugin: "com.android.application" apply plugin: "org.jetbrains.kotlin.android" apply plugin: "com.facebook.react" +apply from: file("../../node_modules/react-native-vector-icons/fonts.gradle") + /** * This is the configuration block to customize your React Native Android app. * By default you don't need to apply any configuration, just uncomment the lines you need. diff --git a/babel.config.js b/babel.config.js index 9b9a4fe..bbc15a6 100644 --- a/babel.config.js +++ b/babel.config.js @@ -1,4 +1,4 @@ module.exports = { presets: ['module:@react-native/babel-preset'], - plugins: ['nativewind/babel'], + plugins: ['nativewind/babel', 'react-native-reanimated/plugin'], }; Binary files differ@@ -23,7 +23,7 @@ useGoogleAPIs = true; buildToolsVersions = [ "30.0.3" buildToolVersion ]; includeNDK = true; - ndkVersion = "23.1.7779620"; + ndkVersion = "26.1.10909125"; cmakeVersions = [ "3.22.1" ]; }; in pkgs.mkShell rec { @@ -36,8 +36,12 @@ androidComposition.androidsdk androidComposition.platform-tools jdk + just + + libxml2 ]; + nativeBuildInputs = with pkgs; [ clang ]; JAVA_HOME = "${jdk.home}"; ANDROID_JAVA_HOME = JAVA_HOME; @@ -48,6 +52,9 @@ GRADLE_OPTS = "-Dorg.gradle.project.android.aapt2FromMavenOverride=${androidComposition.androidsdk}/libexec/android-sdk/build-tools/${buildToolVersion}/aapt2"; + + LIBCLANG_PATH = "${pkgs.libclang.lib}/lib"; + LD_LIBRARY_PATH = "${pkgs.lib.makeLibraryPath (buildInputs ++ nativeBuildInputs)}"; }; in flake-utils.lib.eachDefaultSystem diff --git a/package.json b/package.json index 13ababc..35f548c 100644 --- a/package.json +++ b/package.json @@ -10,12 +10,17 @@ "test": "jest" }, "dependencies": { + "@react-native-async-storage/async-storage": "^1.23.1", "match-sorter": "^6.3.4", "nativewind": "^2.0.11", "react": "18.2.0", "react-native": "0.74.1", + "react-native-draggable-flatlist": "^4.0.1", + "react-native-gesture-handler": "^2.16.2", "react-native-launcher-kit": "^1.0.4", + "react-native-reanimated": "^3.11.0", "react-native-swiper": "^1.6.0", + "react-native-vector-icons": "^10.1.0", "tailwindcss": "3.3.2" }, "devDependencies": { diff --git a/src/components/Favorites.tsx b/src/components/Favorites.tsx new file mode 100644 index 0000000..712a07f --- /dev/null +++ b/src/components/Favorites.tsx @@ -0,0 +1,65 @@ +import React, {useCallback, useMemo} from 'react'; +import {ScrollView, Text, TouchableOpacity, View} from 'react-native'; +import {AppDetail} from 'react-native-launcher-kit/typescript/Interfaces/InstalledApps'; +import {useFavorites} from '../hooks/useFavorites'; +import DraggableFlatList, { + OpacityDecorator, + ScaleDecorator, + ShadowDecorator, +} from 'react-native-draggable-flatlist'; +import Icon from 'react-native-vector-icons/MaterialIcons'; + +export const Favorites: React.FC<{apps: AppDetail[]}> = ({apps}) => { + const {favoriteAppNames, setFavorites} = useFavorites(); + + const appByName = useMemo( + () => Object.fromEntries(apps.map(app => [app.packageName, app])), + [apps], + ); + + const favoriteApps = useMemo( + () => favoriteAppNames.map(name => appByName[name]).filter(Boolean), + [appByName, favoriteAppNames], + ); + + const onDragEnd = useCallback( + ({data}: {data: AppDetail[]}) => { + setFavorites(data.map(app => app.packageName)); + }, + [setFavorites], + ); + + return ( + <View> + <DraggableFlatList + data={favoriteApps} + onDragEnd={onDragEnd} + keyExtractor={item => item.packageName} + renderItem={({item: app, drag, isActive}) => ( + <ShadowDecorator> + <ScaleDecorator> + <OpacityDecorator> + <View + className={`py-4 px-2 flex-row ${ + isActive ? 'bg-gray-700 opacity-60' : '' + }`}> + <TouchableOpacity + onLongPress={drag} + disabled={isActive} + delayLongPress={500}> + <View className={'py-1 pl-3 pr-3'}> + <Icon name="drag-indicator" size={21} color="#666" /> + </View> + </TouchableOpacity> + + <Text className="text-lg">{app.label}</Text> + </View> + </OpacityDecorator> + </ScaleDecorator> + </ShadowDecorator> + )} + renderPlaceholder={() => <View className="flex-1 bg-gray-700" />} + /> + </View> + ); +}; diff --git a/src/hooks/useFavorites.ts b/src/hooks/useFavorites.ts new file mode 100644 index 0000000..54d77fd --- /dev/null +++ b/src/hooks/useFavorites.ts @@ -0,0 +1,52 @@ +import {useCallback, useEffect, useState} from 'react'; +import {useAsyncStorage} from '@react-native-async-storage/async-storage'; + +export const useFavorites = () => { + const favoritesStorage = useAsyncStorage('favorites'); + const [favoriteAppNames, setFavoriteAppNames] = useState<string[]>([]); + + const getFavoriteAppNames = useCallback(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 []; + } + }, [favoritesStorage]); + + const refreshFavorites = useCallback(async () => { + getFavoriteAppNames().then(setFavoriteAppNames); + }, [getFavoriteAppNames]); + + const setFavorites = useCallback( + async (names: string[]) => { + const newNames = [...new Set(names)]; + setFavoriteAppNames(newNames); + await favoritesStorage.setItem(JSON.stringify(newNames)); + }, + [favoritesStorage], + ); + + const addToFavorites = useCallback( + async (packageName: string) => { + const names = await getFavoriteAppNames(); + setFavorites([...names, packageName]); + }, + [getFavoriteAppNames, setFavorites], + ); + + useEffect(() => { + refreshFavorites(); + }, [refreshFavorites]); + + return { + favoriteAppNames, + getFavoriteAppNames, + addToFavorites, + refreshFavorites, + setFavorites, + }; +}; diff --git a/src/screens/Home.tsx b/src/screens/Home.tsx index da24e13..00a4b1f 100644 --- a/src/screens/Home.tsx +++ b/src/screens/Home.tsx @@ -1,14 +1,14 @@ -import React, {useMemo} from 'react'; -import {Text, View} from 'react-native'; +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 = () => { +export const Home: React.FC<{apps: AppDetail[]}> = ({apps}) => { return ( <View> <Clock /> - <View className="p-6"> - <Text>Favorites list</Text> - </View> + <Favorites apps={apps} /> </View> ); }; |
