├── .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 |
213 |
214 |
218 |
219 |
220 | setIsModalFormVisible(false)}
223 | title="Editar nome"
224 | >
225 | <>
226 |
231 |
232 |
236 | >
237 |
238 |
239 | );
240 | }
--------------------------------------------------------------------------------