├── .env.example ├── assets ├── icon.png ├── favicon.png ├── splash.png └── adaptive-icon.png ├── tsconfig.json ├── babel.config.js ├── .gitignore ├── src ├── components │ ├── Loading │ │ ├── styles.ts │ │ └── index.tsx │ ├── Input │ │ ├── index.tsx │ │ └── styles.ts │ ├── Tag │ │ ├── styles.ts │ │ └── index.tsx │ ├── Toast │ │ ├── index.tsx │ │ └── styles.ts │ ├── ButtonIcon │ │ ├── styles.ts │ │ └── index.tsx │ ├── Button │ │ ├── styles.ts │ │ └── index.tsx │ ├── Header │ │ ├── styles.ts │ │ └── index.tsx │ ├── Modal │ │ ├── styles.ts │ │ └── index.tsx │ ├── Tags │ │ ├── styles.ts │ │ └── index.tsx │ └── TextArea │ │ ├── styles.ts │ │ └── index.tsx ├── screens │ └── Details │ │ ├── styles.ts │ │ └── index.tsx └── theme │ └── index.tsx ├── app.json ├── App.tsx └── package.json /.env.example: -------------------------------------------------------------------------------- 1 | CHAT_GPD_API_KEY= 2 | 3 | GCP_SPEECH_TO_TEXT_KEY= -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orodrigogo/powertags/HEAD/assets/icon.png -------------------------------------------------------------------------------- /assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orodrigogo/powertags/HEAD/assets/favicon.png -------------------------------------------------------------------------------- /assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orodrigogo/powertags/HEAD/assets/splash.png -------------------------------------------------------------------------------- /assets/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orodrigogo/powertags/HEAD/assets/adaptive-icon.png -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo/tsconfig.base", 3 | "compilerOptions": { 4 | "strict": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | plugins: [ 6 | 'inline-dotenv', 7 | 'react-native-reanimated/plugin', 8 | ], 9 | }; 10 | }; 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .expo/ 3 | dist/ 4 | npm-debug.* 5 | *.jks 6 | *.p8 7 | *.p12 8 | *.key 9 | *.mobileprovision 10 | *.orig.* 11 | web-build/ 12 | 13 | # macOS 14 | .DS_Store 15 | 16 | # Temporary files created by Metro to check the health of the file watcher 17 | .metro-health-check* 18 | 19 | .env -------------------------------------------------------------------------------- /src/components/Loading/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from "react-native"; 2 | import { THEME } from "../../theme"; 3 | 4 | export const styles = StyleSheet.create({ 5 | container: { 6 | flex: 1, 7 | justifyContent: "center", 8 | alignItems: "center", 9 | backgroundColor: THEME.COLORS.GRAY_600 10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /src/components/Loading/index.tsx: -------------------------------------------------------------------------------- 1 | import { View, ActivityIndicator } from "react-native"; 2 | import { THEME } from "../../theme"; 3 | 4 | import { styles } from "./styles"; 5 | 6 | export function Loading() { 7 | return ( 8 | 9 | 10 | 11 | ); 12 | } -------------------------------------------------------------------------------- /src/components/Input/index.tsx: -------------------------------------------------------------------------------- 1 | import { TextInput, TextInputProps } from "react-native"; 2 | 3 | import { THEME } from "../../theme"; 4 | import { styles } from "./styles"; 5 | 6 | export function Input({ ...rest }: TextInputProps) { 7 | return ( 8 | 13 | ); 14 | } -------------------------------------------------------------------------------- /src/screens/Details/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from "react-native"; 2 | 3 | import { THEME } from "../../theme"; 4 | 5 | export const styles = StyleSheet.create({ 6 | container: { 7 | flex: 1, 8 | backgroundColor: THEME.COLORS.GRAY_500 9 | }, 10 | content: { 11 | padding: 32, 12 | gap: 16, 13 | 14 | backgroundColor: THEME.COLORS.GRAY_600, 15 | }, 16 | options: { 17 | flexDirection: 'row', 18 | gap: 7 19 | } 20 | }); -------------------------------------------------------------------------------- /src/components/Input/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from "react-native"; 2 | import { THEME } from "../../theme"; 3 | 4 | export const styles = StyleSheet.create({ 5 | container: { 6 | width: "100%", 7 | height: 64, 8 | borderRadius: 7, 9 | padding: 16, 10 | 11 | backgroundColor: THEME.COLORS.GRAY_500, 12 | 13 | color: THEME.COLORS.GRAY_200, 14 | fontFamily: THEME.FONTS.FAMILY.PRIMARY.REGULAR, 15 | fontSize: THEME.FONTS.SIZE.MD 16 | } 17 | }); -------------------------------------------------------------------------------- /src/components/Tag/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | import { THEME } from '../../theme'; 3 | 4 | export const styles = StyleSheet.create({ 5 | container: { 6 | backgroundColor: THEME.COLORS.GRAY_600, 7 | paddingHorizontal: 12, 8 | paddingVertical: 4, 9 | borderRadius: 5, 10 | }, 11 | title: { 12 | color: THEME.COLORS.GRAY_300, 13 | fontFamily: THEME.FONTS.FAMILY.PRIMARY.REGULAR, 14 | fontSize: THEME.FONTS.SIZE.SM, 15 | } 16 | }); -------------------------------------------------------------------------------- /src/components/Toast/index.tsx: -------------------------------------------------------------------------------- 1 | import { Text } from 'react-native'; 2 | import Animated, { SlideInUp, SlideOutUp } from 'react-native-reanimated'; 3 | 4 | import { styles } from './styles'; 5 | 6 | type Props = { 7 | message: string; 8 | } 9 | 10 | export function Toast({ message }: Props) { 11 | return ( 12 | 17 | 18 | {message} 19 | 20 | 21 | ); 22 | } -------------------------------------------------------------------------------- /src/components/ButtonIcon/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | import { THEME } from '../../theme'; 3 | 4 | export const styles = StyleSheet.create({ 5 | container: { 6 | borderRadius: 7, 7 | 8 | justifyContent: 'center', 9 | alignItems: 'center', 10 | }, 11 | primary_size: { 12 | height: 42, 13 | width: 42, 14 | }, 15 | secondary_size: { 16 | height: 64, 17 | width: 64, 18 | }, 19 | active: { 20 | backgroundColor: THEME.COLORS.PRIMARY 21 | }, 22 | inative: { 23 | backgroundColor: THEME.COLORS.GRAY_400 24 | } 25 | }); -------------------------------------------------------------------------------- /src/components/Button/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | import { THEME } from '../../theme'; 3 | 4 | export const styles = StyleSheet.create({ 5 | container: { 6 | flex: 1, 7 | miHeight: 64, 8 | maxHeight: 64, 9 | borderRadius: 7, 10 | 11 | alignItems: 'center', 12 | justifyContent: 'center', 13 | 14 | backgroundColor: THEME.COLORS.PRIMARY 15 | }, 16 | title: { 17 | color: THEME.COLORS.GRAY_200, 18 | fontFamily: THEME.FONTS.FAMILY.PRIMARY.BOLD, 19 | fontSize: THEME.FONTS.SIZE.MD, 20 | }, 21 | disabled: { 22 | opacity: 0.5 23 | } 24 | }); -------------------------------------------------------------------------------- /src/components/Header/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from "react-native"; 2 | import { THEME } from "../../theme"; 3 | 4 | export const styles = StyleSheet.create({ 5 | container: { 6 | width: "100%", 7 | height: 120, 8 | padding: 32, 9 | 10 | flexDirection: "row", 11 | alignItems: "center", 12 | justifyContent: "space-between", 13 | 14 | backgroundColor: THEME.COLORS.GRAY_500 15 | }, 16 | title: { 17 | fontFamily: THEME.FONTS.FAMILY.PRIMARY.BOLD, 18 | fontSize: THEME.FONTS.SIZE.LG, 19 | color: THEME.COLORS.GRAY_200, 20 | 21 | textAlign: "center", 22 | paddingHorizontal: 16 23 | } 24 | }); -------------------------------------------------------------------------------- /src/theme/index.tsx: -------------------------------------------------------------------------------- 1 | export const THEME = { 2 | COLORS: { 3 | PRIMARY: "#7D12FE", 4 | 5 | GRAY_900: "#161819", 6 | GRAY_600: "#1C1D21", 7 | GRAY_500: "#29282D", 8 | GRAY_400: "#313135", 9 | GRAY_300: "#A0A0A7", 10 | GRAY_200: "#D5D4D9", 11 | 12 | OVERLAY: "rgba(0, 0, 0, 0.5)", 13 | }, 14 | 15 | FONTS: { 16 | FAMILY: { 17 | PRIMARY: { 18 | REGULAR: "NotoSans_400Regular", 19 | BOLD: "NotoSans_700Bold", 20 | EXTRA_BOLD: "NotoSans_800ExtraBold", 21 | } 22 | }, 23 | 24 | SIZE: { 25 | XS: 12, 26 | SM: 14, 27 | MD: 16, 28 | LG: 18, 29 | XL: 20, 30 | XXL: 24, 31 | XXXL: 32, 32 | } 33 | }, 34 | }; -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "powertags", 4 | "slug": "powertags", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./assets/icon.png", 8 | "userInterfaceStyle": "light", 9 | "splash": { 10 | "image": "./assets/splash.png", 11 | "resizeMode": "contain", 12 | "backgroundColor": "#ffffff" 13 | }, 14 | "assetBundlePatterns": [ 15 | "**/*" 16 | ], 17 | "ios": { 18 | "supportsTablet": true 19 | }, 20 | "android": { 21 | "adaptiveIcon": { 22 | "foregroundImage": "./assets/adaptive-icon.png", 23 | "backgroundColor": "#ffffff" 24 | } 25 | }, 26 | "web": { 27 | "favicon": "./assets/favicon.png" 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /src/components/Toast/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | import { THEME } from '../../theme'; 3 | import { initialWindowMetrics } from 'react-native-safe-area-context'; 4 | 5 | console.log(initialWindowMetrics) 6 | export const styles = StyleSheet.create({ 7 | container: { 8 | position: 'absolute', 9 | top: 0, 10 | left: 0, 11 | right: 0, 12 | zIndex: 1, 13 | paddingTop: (initialWindowMetrics?.insets.top || 0) + 44, 14 | backgroundColor: THEME.COLORS.PRIMARY, 15 | padding: 16, 16 | borderRadius: 4, 17 | }, 18 | text: { 19 | color: THEME.COLORS.GRAY_200, 20 | fontSize: THEME.FONTS.SIZE.SM, 21 | fontFamily: THEME.FONTS.FAMILY.PRIMARY.REGULAR, 22 | textAlign: 'center', 23 | } 24 | }); -------------------------------------------------------------------------------- /src/components/Modal/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | import { THEME } from '../../theme'; 3 | 4 | export const styles = StyleSheet.create({ 5 | overlay: { 6 | flex: 1, 7 | backgroundColor: THEME.COLORS.OVERLAY, 8 | justifyContent: 'center', 9 | }, 10 | header: { 11 | flexDirection: 'row', 12 | justifyContent: 'space-between', 13 | }, 14 | title: { 15 | color: THEME.COLORS.GRAY_900, 16 | fontFamily: THEME.FONTS.FAMILY.PRIMARY.BOLD, 17 | fontSize: THEME.FONTS.SIZE.XL, 18 | 19 | textAlign: "center", 20 | marginBottom: 16 21 | }, 22 | content: { 23 | margin: 32, 24 | padding: 32, 25 | backgroundColor: THEME.COLORS.GRAY_200, 26 | borderRadius: 12, 27 | gap: 12, 28 | } 29 | }); -------------------------------------------------------------------------------- /src/components/Tag/index.tsx: -------------------------------------------------------------------------------- 1 | import { Text, TouchableOpacity, TouchableOpacityProps } from 'react-native'; 2 | import Animated, { FadeIn, FadeOut, Layout } from 'react-native-reanimated'; 3 | 4 | const TouchableOpacityAnimated = Animated.createAnimatedComponent(TouchableOpacity); 5 | 6 | import { styles } from './styles'; 7 | 8 | type Props = TouchableOpacityProps & { 9 | title: string; 10 | } 11 | export function Tag({ title, ...rest }: Props) { 12 | return ( 13 | 20 | 21 | {title} 22 | 23 | 24 | ); 25 | } -------------------------------------------------------------------------------- /App.tsx: -------------------------------------------------------------------------------- 1 | import { StatusBar } from "react-native"; 2 | import { SafeAreaProvider } from 'react-native-safe-area-context'; 3 | import { useFonts, NotoSans_400Regular, NotoSans_700Bold, NotoSans_800ExtraBold } from "@expo-google-fonts/noto-sans"; 4 | 5 | import { Details } from "./src/screens/Details"; 6 | import { Loading } from "./src/components/Loading"; 7 | 8 | export default function App() { 9 | const [fontsLoaded] = useFonts({ 10 | NotoSans_400Regular, 11 | NotoSans_700Bold, 12 | NotoSans_800ExtraBold 13 | }); 14 | 15 | if (!fontsLoaded) { 16 | return ( 17 | 18 | ); 19 | } 20 | 21 | return ( 22 | 23 | 28 | 29 |
30 | 31 | ); 32 | } -------------------------------------------------------------------------------- /src/components/Tags/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | import { THEME } from '../../theme'; 3 | 4 | export const styles = StyleSheet.create({ 5 | container: { 6 | width: '100%', 7 | backgroundColor: THEME.COLORS.GRAY_500, 8 | 9 | paddingBottom: 100 10 | }, 11 | header: { 12 | paddingHorizontal: 32, 13 | paddingVertical: 16, 14 | 15 | borderBottomColor: THEME.COLORS.GRAY_400, 16 | borderBottomWidth: 1, 17 | 18 | flexDirection: 'row', 19 | justifyContent: 'space-between', 20 | alignItems: 'center' 21 | }, 22 | title: { 23 | color: THEME.COLORS.GRAY_300, 24 | fontFamily: THEME.FONTS.FAMILY.PRIMARY.REGULAR, 25 | fontSize: THEME.FONTS.SIZE.MD, 26 | }, 27 | content: { 28 | flex: 1, 29 | flexDirection: 'row', 30 | flexWrap: 'wrap', 31 | padding: 32, 32 | gap: 12, 33 | } 34 | }); -------------------------------------------------------------------------------- /src/components/Header/index.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | import { Text, TouchableOpacity, View } from "react-native"; 3 | import { MaterialIcons } from "@expo/vector-icons"; 4 | 5 | import { styles } from "./styles"; 6 | import { THEME } from "../../theme"; 7 | 8 | type Props = { 9 | title: string; 10 | children: ReactNode; 11 | } 12 | 13 | export function Header({ title, children }: Props) { 14 | return ( 15 | 16 | 17 | 22 | 23 | 24 | 28 | {title} 29 | 30 | 31 | {children} 32 | 33 | ); 34 | } -------------------------------------------------------------------------------- /src/components/TextArea/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from "react-native"; 2 | import { THEME } from "../../theme"; 3 | 4 | export const styles = StyleSheet.create({ 5 | container: { 6 | width: "100%", 7 | height: 128, 8 | borderRadius: 7, 9 | padding: 16, 10 | 11 | backgroundColor: THEME.COLORS.GRAY_500, 12 | }, 13 | input: { 14 | flex: 1, 15 | color: THEME.COLORS.GRAY_200, 16 | fontFamily: THEME.FONTS.FAMILY.PRIMARY.REGULAR, 17 | fontSize: THEME.FONTS.SIZE.MD, 18 | 19 | textAlignVertical: 'top', 20 | }, 21 | clear: { 22 | height: 22, 23 | width: 22, 24 | borderRadius: 11, 25 | 26 | justifyContent: 'center', 27 | alignItems: 'center', 28 | 29 | position: 'absolute', 30 | top: 10, 31 | right: 10, 32 | 33 | backgroundColor: THEME.COLORS.GRAY_400, 34 | }, 35 | disabled: { 36 | opacity: 0.5 37 | } 38 | }); -------------------------------------------------------------------------------- /src/components/Button/index.tsx: -------------------------------------------------------------------------------- 1 | import { TouchableOpacity, TouchableOpacityProps, Text, ActivityIndicator } from 'react-native'; 2 | 3 | import { THEME } from '../../theme'; 4 | import { styles } from './styles'; 5 | 6 | type Props = TouchableOpacityProps & { 7 | title: string; 8 | isLoading?: boolean; 9 | } 10 | 11 | export function Button({ isLoading = false, title, ...rest }: Props) { 12 | return ( 13 | 19 | { 20 | isLoading 21 | ? 22 | 26 | : 27 | 28 | {title} 29 | 30 | } 31 | 32 | ); 33 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "powertags", 3 | "version": "1.0.0", 4 | "main": "node_modules/expo/AppEntry.js", 5 | "scripts": { 6 | "start": "expo start", 7 | "android": "expo start --android", 8 | "ios": "expo start --ios", 9 | "web": "expo start --web" 10 | }, 11 | "dependencies": { 12 | "@expo-google-fonts/noto-sans": "^0.2.3", 13 | "expo": "~48.0.6", 14 | "expo-clipboard": "~4.1.2", 15 | "expo-font": "~11.1.1", 16 | "react": "18.2.0", 17 | "react-native": "0.71.3", 18 | "react-native-reanimated": "~2.14.4", 19 | "react-native-safe-area-context": "4.5.0", 20 | "expo-blur": "~12.2.2", 21 | "expo-av": "~13.2.1", 22 | "expo-file-system": "~15.2.2" 23 | }, 24 | "devDependencies": { 25 | "@babel/core": "^7.20.0", 26 | "@types/react": "~18.0.14", 27 | "babel-plugin-inline-dotenv": "^1.7.0", 28 | "dotenv": "^16.0.3", 29 | "typescript": "^4.9.4" 30 | }, 31 | "private": true 32 | } 33 | -------------------------------------------------------------------------------- /src/components/TextArea/index.tsx: -------------------------------------------------------------------------------- 1 | import { View, TextInput, TextInputProps, TouchableOpacity } from "react-native"; 2 | import { MaterialIcons } from "@expo/vector-icons"; 3 | 4 | import { THEME } from "../../theme"; 5 | import { styles } from "./styles"; 6 | 7 | type Props = TextInputProps & { 8 | onClear?: () => void; 9 | } 10 | 11 | export function TextArea({ value, editable, onClear, ...rest }: Props) { 12 | return ( 13 | 14 | 22 | 23 | { 24 | onClear && value && value?.length > 0 && 25 | 29 | 34 | 35 | } 36 | 37 | ); 38 | } -------------------------------------------------------------------------------- /src/components/Modal/index.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | import { BlurView } from 'expo-blur'; 3 | import { MaterialIcons } from '@expo/vector-icons'; 4 | import Animated, { FadeIn } from 'react-native-reanimated'; 5 | import { Modal as ReactNativeModal, ModalProps, View, Text, TouchableOpacity } from 'react-native'; 6 | 7 | const BlurViewAnimated = Animated.createAnimatedComponent(BlurView); 8 | 9 | import { styles } from './styles'; 10 | import { THEME } from '../../theme'; 11 | 12 | type Props = ModalProps & { 13 | title: string; 14 | children: ReactNode; 15 | onClose: () => void; 16 | } 17 | 18 | export function Modal({ title, children, onClose, ...rest }: Props) { 19 | return ( 20 | 21 | 26 | 27 | 28 | 29 | {title} 30 | 31 | 32 | 33 | 38 | 39 | 40 | 41 | {children} 42 | 43 | 44 | 45 | ); 46 | } -------------------------------------------------------------------------------- /src/components/ButtonIcon/index.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { MaterialIcons } from '@expo/vector-icons'; 3 | import { ActivityIndicator, Pressable, PressableProps } from 'react-native'; 4 | 5 | import { THEME } from '../../theme'; 6 | import { styles } from './styles'; 7 | 8 | type Props = PressableProps & { 9 | isLoading?: boolean; 10 | size?: 'primary_size' | 'secondary_size'; 11 | iconName: keyof typeof MaterialIcons.glyphMap; 12 | onPressIn?: () => void; 13 | onPressOut?: () => void; 14 | } 15 | 16 | export function ButtonIcon({ 17 | iconName, 18 | size = "primary_size", 19 | isLoading = false, 20 | onPressIn = () => { }, 21 | onPressOut = () => { }, 22 | ...rest 23 | }: Props) { 24 | const [isActive, setIsActive] = useState(false); 25 | 26 | function handleOnPressIn() { 27 | setIsActive(true); 28 | onPressIn(); 29 | } 30 | 31 | function handleOnPressOut() { 32 | setIsActive(false); 33 | onPressOut(); 34 | } 35 | 36 | return ( 37 | 48 | { 49 | isLoading 50 | ? 51 | 55 | : 56 | 61 | } 62 | 63 | ); 64 | } -------------------------------------------------------------------------------- /src/components/Tags/index.tsx: -------------------------------------------------------------------------------- 1 | import { View, Text, Alert } from 'react-native'; 2 | import Animated, { Layout } from 'react-native-reanimated'; 3 | import * as Clipboard from 'expo-clipboard'; 4 | 5 | import { ButtonIcon } from '../ButtonIcon'; 6 | import { Tag } from '../Tag'; 7 | 8 | import { styles } from './styles'; 9 | 10 | type Props = { 11 | tags: string[]; 12 | setTags: (tags: React.SetStateAction) => void; 13 | } 14 | 15 | export function Tags({ tags, setTags }: Props) { 16 | 17 | function handleCopyToClipboard() { 18 | const tagsFormatted = tags.toString().replaceAll(",", " "); 19 | 20 | Clipboard 21 | .setStringAsync(tagsFormatted) 22 | .then(() => Alert.alert('Copiado!')) 23 | .catch(() => Alert.alert('Não foi possível copiar!')); 24 | } 25 | 26 | function handleRemove(tag: string) { 27 | Alert.alert( 28 | 'Remover Tag', 29 | `Remover a tag "${tag}"?`, 30 | [ 31 | { 32 | text: 'Não', 33 | style: 'cancel' 34 | }, 35 | { 36 | text: 'Sim', 37 | onPress: () => setTags((prevState) => prevState.filter((item) => item !== tag)) 38 | } 39 | ] 40 | ); 41 | } 42 | 43 | return ( 44 | 45 | 46 | 47 | {tags.length} tags 48 | 49 | 50 | 54 | 55 | 56 | 60 | { 61 | tags.map((tag) => ( 62 | handleRemove(tag)} 66 | /> 67 | )) 68 | } 69 | 70 | 71 | ); 72 | } -------------------------------------------------------------------------------- /src/screens/Details/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { Alert, ScrollView, View } from "react-native"; 3 | import { SafeAreaView } from "react-native-safe-area-context"; 4 | import { Audio, InterruptionModeIOS, InterruptionModeAndroid } from 'expo-av'; 5 | import * as FileSystem from 'expo-file-system'; 6 | 7 | import { Tags } from "../../components/Tags"; 8 | import { Input } from "../../components/Input"; 9 | import { Modal } from "../../components/Modal"; 10 | import { Header } from "../../components/Header"; 11 | import { Button } from "../../components/Button"; 12 | import { TextArea } from "../../components/TextArea"; 13 | import { ButtonIcon } from "../../components/ButtonIcon"; 14 | 15 | import { styles } from "./styles"; 16 | import { Toast } from "../../components/Toast"; 17 | 18 | const CHAT_GPD_API_KEY = process.env.CHAT_GPD_API_KEY; 19 | const GCP_SPEECH_TO_TEXT_KEY = process.env.GCP_SPEECH_TO_TEXT_KEY; 20 | 21 | const RECORDING_OPTIONS = { 22 | android: { 23 | extension: '.m4a', 24 | outputFormat: Audio.AndroidOutputFormat.MPEG_4, 25 | audioEncoder: Audio.AndroidAudioEncoder.AAC, 26 | sampleRate: 44100, 27 | numberOfChannels: 2, 28 | bitRate: 128000, 29 | }, 30 | ios: { 31 | extension: '.wav', 32 | audioQuality: Audio.IOSAudioQuality.HIGH, 33 | sampleRate: 44100, 34 | numberOfChannels: 1, 35 | bitRate: 128000, 36 | linearPCMBitDepth: 16, 37 | linearPCMIsBigEndian: false, 38 | linearPCMIsFloat: false, 39 | }, 40 | web: { 41 | 42 | } 43 | }; 44 | 45 | export function Details() { 46 | const [tags, setTags] = useState([]); 47 | const [isLoading, setIsLoading] = useState(false); 48 | const [isConvertingSpeechToText, setIsConvertingSpeechToText] = useState(false); 49 | const [description, setDescription] = useState(''); 50 | const [collectionName, setCollectionName] = useState('Tags'); 51 | const [isModalFormVisible, setIsModalFormVisible] = useState(false); 52 | const [toastMessage, setToastMessage] = useState(null); 53 | 54 | const [recording, setRecording] = useState(null); 55 | 56 | function handleFetchTags() { 57 | setIsLoading(true); 58 | const prompt = ` 59 | Generate keywords in Portuguese for a post about ${description.trim()}. 60 | Replace the spaces in each word with the character "_". 61 | Return each item separated by a comma, in lowercase, and without a line break. 62 | `; 63 | 64 | fetch("https://api.openai.com/v1/engines/text-davinci-003-playground/completions", { 65 | method: 'POST', 66 | headers: { 67 | "Content-Type": "application/json", 68 | Authorization: `Bearer ${CHAT_GPD_API_KEY}` 69 | }, 70 | body: JSON.stringify({ 71 | prompt, 72 | temperature: 0.22, 73 | max_tokens: 500, 74 | top_p: 1, 75 | frequency_penalty: 0, 76 | presence_penalty: 0, 77 | }), 78 | }) 79 | .then(response => response.json()) 80 | .then((data) => saveTags(data.choices[0].text)) 81 | .catch(() => Alert.alert('Erro', 'Não foi possível buscar as tags.')) 82 | .finally(() => setIsLoading(false)); 83 | } 84 | 85 | function saveTags(data: string) { 86 | const tagsFormatted = data 87 | .trim() 88 | .split(',') 89 | .map((tag) => `#${tag}`); 90 | 91 | setTags(tagsFormatted); 92 | } 93 | 94 | function handleNameCollectionEdit() { 95 | setIsModalFormVisible(false); 96 | } 97 | 98 | async function handleRecordingStart() { 99 | const { granted } = await Audio.getPermissionsAsync(); 100 | 101 | if (granted) { 102 | try { 103 | setToastMessage('Gravando...'); 104 | 105 | const { recording } = await Audio.Recording.createAsync(RECORDING_OPTIONS); 106 | setRecording(recording); 107 | 108 | } catch (error) { 109 | console.log(error); 110 | } 111 | } 112 | } 113 | 114 | async function handleRecordingStop() { 115 | try { 116 | setToastMessage(null); 117 | 118 | await recording?.stopAndUnloadAsync(); 119 | const recordingFileUri = recording?.getURI(); 120 | 121 | if (recordingFileUri) { 122 | const base64File = await FileSystem.readAsStringAsync(recordingFileUri, { encoding: FileSystem?.EncodingType?.Base64 }); 123 | await FileSystem.deleteAsync(recordingFileUri); 124 | 125 | setRecording(null); 126 | getTranscription(base64File); 127 | } else { 128 | Alert.alert("Audio", "Não foi possível obter a gravação."); 129 | } 130 | } catch (error) { 131 | console.log(error); 132 | } 133 | } 134 | 135 | function getTranscription(base64File: string) { 136 | setIsConvertingSpeechToText(true); 137 | 138 | fetch(`https://speech.googleapis.com/v1/speech:recognize?key=${GCP_SPEECH_TO_TEXT_KEY}`, { 139 | method: 'POST', 140 | body: JSON.stringify({ 141 | config: { 142 | languageCode: "pt-BR", 143 | encoding: "LINEAR16", 144 | sampleRateHertz: 41000, 145 | }, 146 | audio: { 147 | content: base64File 148 | } 149 | }) 150 | }) 151 | .then(response => response.json()) 152 | .then((data) => { 153 | setDescription(data.results[0].alternatives[0].transcript); 154 | }) 155 | .catch((error) => console.log(error)) 156 | .finally(() => setIsConvertingSpeechToText(false)) 157 | } 158 | 159 | useEffect(() => { 160 | Audio 161 | .requestPermissionsAsync() 162 | .then((granted) => { 163 | if (granted) { 164 | Audio.setAudioModeAsync({ 165 | allowsRecordingIOS: true, 166 | interruptionModeIOS: InterruptionModeIOS.DoNotMix, 167 | playsInSilentModeIOS: true, 168 | shouldDuckAndroid: true, 169 | interruptionModeAndroid: InterruptionModeAndroid.DoNotMix, 170 | playThroughEarpieceAndroid: true, 171 | }); 172 | } 173 | }); 174 | }, []); 175 | 176 | return ( 177 | 178 | {toastMessage && } 179 | 180 |
181 | setIsModalFormVisible(true)} 184 | /> 185 |
186 | 187 | 188 | 189 |