├── .gitignore
├── README.md
├── app
├── .gitignore
├── build.gradle.kts
├── proguard-rules.pro
├── release
│ ├── app-release-1.0.0.apk
│ └── output-metadata.json
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── paradoxo
│ │ └── threadscompose
│ │ └── ExampleInstrumentedTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ ├── ic_launcher-playstore.png
│ ├── java
│ │ └── com
│ │ │ └── paradoxo
│ │ │ └── threadscompose
│ │ │ ├── MainActivity.kt
│ │ │ ├── model
│ │ │ ├── Notification.kt
│ │ │ ├── Post.kt
│ │ │ └── UserAccount.kt
│ │ │ ├── network
│ │ │ └── firebase
│ │ │ │ ├── MediaFirebaseStorage.kt
│ │ │ │ ├── PostFirestore.kt
│ │ │ │ └── UserFirestore.kt
│ │ │ ├── sampleData
│ │ │ └── SampleData.kt
│ │ │ ├── ui
│ │ │ ├── Components.kt
│ │ │ ├── feed
│ │ │ │ ├── FeedScreen.kt
│ │ │ │ ├── FeedScreenState.kt
│ │ │ │ └── FeedViewModel.kt
│ │ │ ├── home
│ │ │ │ ├── SessionState.kt
│ │ │ │ └── SessionViewModel.kt
│ │ │ ├── login
│ │ │ │ ├── LoginScreen.kt
│ │ │ │ ├── LoginScreenState.kt
│ │ │ │ ├── LoginViewModel.kt
│ │ │ │ └── SplashScreen.kt
│ │ │ ├── navigation
│ │ │ │ ├── Destinations.kt
│ │ │ │ ├── ThreadsNavController.kt
│ │ │ │ └── ThreadsNavHost.kt
│ │ │ ├── notification
│ │ │ │ ├── NotificationScreenState.kt
│ │ │ │ └── NotificationsScreen.kt
│ │ │ ├── post
│ │ │ │ ├── PostScreen.kt
│ │ │ │ └── PostScreenState.kt
│ │ │ ├── profile
│ │ │ │ ├── ProfileEditScreen.kt
│ │ │ │ └── ProfileScreen.kt
│ │ │ ├── search
│ │ │ │ └── SearchScreen.kt
│ │ │ └── theme
│ │ │ │ ├── Color.kt
│ │ │ │ ├── Theme.kt
│ │ │ │ └── Type.kt
│ │ │ └── utils
│ │ │ ├── Extensions.kt
│ │ │ └── Utils.kt
│ └── res
│ │ ├── drawable-v24
│ │ └── ic_launcher_foreground.xml
│ │ ├── drawable
│ │ ├── bg_lines_1.png
│ │ ├── ic_attach_file.xml
│ │ ├── ic_circle.xml
│ │ ├── ic_comment.xml
│ │ ├── ic_facebook.xml
│ │ ├── ic_heart.xml
│ │ ├── ic_heart_outlined.xml
│ │ ├── ic_home.xml
│ │ ├── ic_home_outlined.xml
│ │ ├── ic_insta.xml
│ │ ├── ic_launcher_background.xml
│ │ ├── ic_logo_colors.xml
│ │ ├── ic_menu_config.xml
│ │ ├── ic_net.xml
│ │ ├── ic_post.xml
│ │ ├── ic_profile.xml
│ │ ├── ic_profile_outlined.xml
│ │ ├── ic_reply.xml
│ │ ├── ic_repost.xml
│ │ ├── ic_search.xml
│ │ ├── ic_send.xml
│ │ ├── placeholder_image.xml
│ │ ├── profile_pic_emoji_1.xml
│ │ ├── profile_pic_emoji_2.xml
│ │ ├── profile_pic_emoji_3.xml
│ │ └── profile_pic_emoji_4.xml
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.webp
│ │ ├── ic_launcher_foreground.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.webp
│ │ ├── ic_launcher_foreground.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.webp
│ │ ├── ic_launcher_foreground.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.webp
│ │ ├── ic_launcher_foreground.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.webp
│ │ ├── ic_launcher_foreground.webp
│ │ └── ic_launcher_round.webp
│ │ ├── raw
│ │ └── logo_lines_animated.json
│ │ ├── values
│ │ ├── colors.xml
│ │ ├── ic_launcher_background.xml
│ │ ├── strings.xml
│ │ └── themes.xml
│ │ └── xml
│ │ ├── backup_rules.xml
│ │ └── data_extraction_rules.xml
│ └── test
│ └── java
│ └── com
│ └── paradoxo
│ └── threadscompose
│ └── ExampleUnitTest.kt
├── build.gradle.kts
├── gradle.properties
├── gradle
├── libs.versions.toml
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── settings.gradle.kts
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | .DS_Store
11 | /build
12 | /captures
13 | .externalNativeBuild
14 | .cxx
15 | local.properties
16 | /.idea/
17 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | **Lines** é um aplicativo desenvolvido com propósito de se parecer ao máximo com o app [**Threads**][threads-net] da [Meta][meta], feito em Jetpack Composse assim como o original esse projeto também teve o objetivo de ser feito no menor tempo possível, em breve o [resultdo saí aqui][video-recriando-threads]
4 |
5 | 💻 As seguintes tecnologias estão em uso no momento:
6 | - [Jetpack Compose][compose] - Interface de usuário
7 | - [Facebook API][login-facebook] - Sistema de Login
8 | - [Firebase Auth][firebase-auth] - Integração com a API de autenticação do Facebook
9 | - [Firebase Firestore][firebase-firestore] - Banco de dado online
10 | - [Firebase Storage][firebase-storage] - Armazenamento de imagens que podem ser enviadas pelo app
11 | - [LottieFiles][lottie] - Animações controladas usando a API oficial do Airbnb
12 | - [Coil][coil] - Carregamento de imagens
13 | - [Jetpack Compose Animations][compose-animations] - Pequenas animações e transições de elementos de layout
14 |
15 | 📱 As seguintes funções estão disponíveis no momento:
16 | - Login com Facebook, permitindo trocar algumas informações pessoais do perfil.
17 | - Publicar posts únicos e claro as **Threads**
18 | - Visualizar posts únicos feito por outros usuários
19 | - Interagir com animações de movimento feitas através do Jetpack Compose e da API Lottie
20 | - Explorar a telas Feed, Busca, Post, Notificações e Perfil.
21 |
22 | ## 🎨 Previews
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | ## 🏃♂️ Algumas animações
34 | https://github.com/git-jr/Threads-Jetpack-Compose/assets/35709152/61c577a5-5b91-40c4-8999-962f793dffb5
35 |
36 |
37 |
38 | ## 📲 Testar o app
39 | Aviso: A versão atual deste projeto foi desenvolvida com o objetivo de criar, no menor tempo possível, a versão mais próxima do Threads. Você pode conferir o resultado desse desafio em breve [neste vídeo][video-recriando-threads], então ainda tem muita coisa pra ajustar 😉
40 |
41 | Vá até [Releases][releases], baixe o arquivo APK da última versão disponível e escolha a forma login:
42 | > Como convidado: Não precisa digitar nenhuma credencial, seu perfil dentro do app será gerado aleatoriamente com dados de teste, os posts de outros usuários não serão exibidos.
43 |
44 | > Com o Facebook: Você verá posts de outros usuários reais do app. Uma mensagem de "Permissões ainda não verificadas pelo Facebook" pode aparecer no início, mas não se preocupe. Este é um alerta padrão, pois o app ainda não foi revisado pela equipe do Facebook ainda. Você pode prosseguir com segurança.
45 |
46 |
47 |
48 | 💻 Como rodar o projeto
49 | Esse projeto precisa de 2 arquivos principais para ser compilado corremente no Android Studio:
50 |
51 | 1. `google-services.json`, arquivo de configuração do Firebase
52 | - Você pode aprender como gerar um [através da documentação oficial][tutorial-firebase]
53 | - Adicione o arquivo gerado dentro da pasta app:
54 |
55 |
56 |
57 |
58 | 2. `local.properties`, esse arquivo é gerado automaticamente pelo Android Studio, dentro será necessário adicionar 3 linhas de código para identificar o app perante a API de Login do Facebook
59 | - Na [documentação oficial do Facebook][tutorial-facebook-login-api], você encontrará instruções para criar `facebookAppId`, `fbLoginProtocolScheme` e `facebookClientToken`. Depois de obtê-los, adicione cada um desses valores ao arquivo de propriedades em linhas separadas e referenciando seus nomes.
60 |
61 |
62 |
63 | ## 😎 Gostou do app?
64 | Clica ali na estrela ⭐ do topo para dar aquela força!
65 |
66 | [compose]: https://developer.android.com/jetpack/compose
67 | [threads-net]: https://www.threads.net/
68 | [meta]: https://about.meta.com/
69 | [login-facebook]: https://developers.facebook.com/docs/facebook-login
70 | [lottie]: https://airbnb.io/lottie
71 |
72 | [firebase-auth]: https://firebase.google.com/docs/auth
73 | [firebase-storage]: https://firebase.google.com/docs/storage
74 | [firebase-firestore]: https://firebase.google.com/docs/firestore
75 |
76 | [coil]: https://coil-kt.github.io/coil/compose/
77 | [compose-animations]: https://developer.android.com/jetpack/compose/animation
78 |
79 | [releases]:https://github.com/git-jr/Threads-Jetpack-Compose/releases
80 |
81 | [tutorial-firebase]: https://firebase.google.com/docs/android/setup?hl=pt-br#create-firebase-project
82 | [tutorial-facebook-login-api]: https://developers.facebook.com/docs/facebook-login/android
83 |
84 | [video-recriando-threads]: https://youtu.be/Kr4Kn0ewnIw
85 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 | google-services.json
--------------------------------------------------------------------------------
/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import java.io.FileInputStream
2 | import java.util.Properties
3 |
4 | @Suppress("DSL_SCOPE_VIOLATION") // TODO: Remove once KTIJ-19369 is fixed
5 | plugins {
6 | alias(libs.plugins.androidApplication)
7 | alias(libs.plugins.kotlinAndroid)
8 | alias(libs.plugins.googleServices)
9 | }
10 |
11 | val properties = Properties().apply {
12 | FileInputStream(rootProject.file("local.properties")).use { load(it) }
13 | }
14 |
15 | android {
16 | namespace = "com.paradoxo.threadscompose"
17 | compileSdk = 34
18 |
19 | defaultConfig {
20 | applicationId = "com.paradoxo.threadscompose"
21 | minSdk = 21
22 | targetSdk = 34
23 | versionCode = 1
24 | versionName = "1.0.0"
25 |
26 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
27 | vectorDrawables {
28 | useSupportLibrary = true
29 | }
30 |
31 | // To generate a new facebook app id, go to https://developers.facebook.com/apps/
32 | // and create a new app. Then, go to Settings > Basic and copy the App ID.
33 | // Finally, paste the App ID in the facebookAppId on file local.properties
34 | val facebookAppId = properties.getProperty("facebookAppId") ?: "INSIRA O ID DO SEU APP AQUI"
35 | resValue("string", "facebook_app_id", facebookAppId)
36 |
37 | val facebookClientToken = properties.getProperty("facebookClientToken") ?: "INSIRA O TOKEN DO SEU APP AQUI"
38 | resValue("string", "facebook_client_token", facebookClientToken)
39 |
40 | val fbLoginProtocolScheme = "fb${facebookAppId}"
41 | resValue("string", "fb_login_protocol_scheme", fbLoginProtocolScheme)
42 |
43 | }
44 |
45 | buildTypes {
46 | release {
47 | isMinifyEnabled = false
48 | proguardFiles(
49 | getDefaultProguardFile("proguard-android-optimize.txt"),
50 | "proguard-rules.pro"
51 | )
52 | }
53 | }
54 | compileOptions {
55 | isCoreLibraryDesugaringEnabled = true
56 |
57 | sourceCompatibility = JavaVersion.VERSION_1_8
58 | targetCompatibility = JavaVersion.VERSION_1_8
59 | }
60 | kotlinOptions {
61 | jvmTarget = "1.8"
62 | }
63 | buildFeatures {
64 | compose = true
65 | }
66 | composeOptions {
67 | kotlinCompilerExtensionVersion = "1.4.3"
68 | }
69 | packaging {
70 | resources {
71 | excludes += "/META-INF/{AL2.0,LGPL2.1}"
72 | }
73 | }
74 | }
75 |
76 | dependencies {
77 |
78 | implementation (libs.lottie.compose)
79 | implementation(libs.androidx.foundation)
80 |
81 | implementation(platform(libs.firebase.bom))
82 | implementation(libs.coil.compose)
83 | implementation(libs.google.services)
84 | implementation (libs.facebook.android.sdk)
85 | implementation(libs.firebase.auth.ktx)
86 | implementation(libs.firebase.storage.ktx)
87 |
88 |
89 | implementation(libs.lifecycle.viewmodel)
90 | implementation(libs.navigation.compose)
91 | implementation(libs.firebase.firestore.ktx)
92 | implementation(libs.play.services.auth)
93 |
94 | coreLibraryDesugaring(libs.desugar.jdk.libs)
95 |
96 | implementation(libs.core.ktx)
97 | implementation(libs.lifecycle.runtime.ktx)
98 | implementation(libs.activity.compose)
99 | implementation(platform(libs.compose.bom))
100 | implementation(libs.ui)
101 | implementation(libs.ui.graphics)
102 | implementation(libs.ui.tooling.preview)
103 | implementation(libs.material3)
104 | testImplementation(libs.junit)
105 | androidTestImplementation(libs.androidx.test.ext.junit)
106 | androidTestImplementation(libs.espresso.core)
107 | androidTestImplementation(platform(libs.compose.bom))
108 | androidTestImplementation(libs.ui.test.junit4)
109 | debugImplementation(libs.ui.tooling)
110 | debugImplementation(libs.ui.test.manifest)
111 | }
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/app/release/app-release-1.0.0.apk:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/git-jr/Threads-Jetpack-Compose/6d5ee19ce593bdcdecda8eb4b4dd1252da50bed4/app/release/app-release-1.0.0.apk
--------------------------------------------------------------------------------
/app/release/output-metadata.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 3,
3 | "artifactType": {
4 | "type": "APK",
5 | "kind": "Directory"
6 | },
7 | "applicationId": "com.paradoxo.threadscompose",
8 | "variantName": "release",
9 | "elements": [
10 | {
11 | "type": "SINGLE",
12 | "filters": [],
13 | "attributes": [],
14 | "versionCode": 1,
15 | "versionName": "1.0.0",
16 | "outputFile": "app-release.apk"
17 | }
18 | ],
19 | "elementType": "File"
20 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/paradoxo/threadscompose/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.paradoxo.threadscompose
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("com.paradoxo.threadscompose", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
18 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
34 |
37 |
38 |
39 |
42 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
--------------------------------------------------------------------------------
/app/src/main/ic_launcher-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/git-jr/Threads-Jetpack-Compose/6d5ee19ce593bdcdecda8eb4b4dd1252da50bed4/app/src/main/ic_launcher-playstore.png
--------------------------------------------------------------------------------
/app/src/main/java/com/paradoxo/threadscompose/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.paradoxo.threadscompose
2 |
3 | import android.content.Intent
4 | import android.net.Uri
5 | import android.os.Bundle
6 | import androidx.activity.ComponentActivity
7 | import androidx.activity.compose.setContent
8 | import androidx.compose.foundation.layout.Arrangement
9 | import androidx.compose.foundation.layout.Box
10 | import androidx.compose.foundation.layout.Column
11 | import androidx.compose.foundation.layout.PaddingValues
12 | import androidx.compose.foundation.layout.fillMaxSize
13 | import androidx.compose.material3.Icon
14 | import androidx.compose.material3.NavigationBar
15 | import androidx.compose.material3.NavigationBarItem
16 | import androidx.compose.material3.NavigationBarItemDefaults
17 | import androidx.compose.material3.Scaffold
18 | import androidx.compose.runtime.Composable
19 | import androidx.compose.runtime.LaunchedEffect
20 | import androidx.compose.runtime.getValue
21 | import androidx.compose.runtime.mutableStateOf
22 | import androidx.compose.runtime.remember
23 | import androidx.compose.runtime.setValue
24 | import androidx.compose.ui.Alignment
25 | import androidx.compose.ui.Modifier
26 | import androidx.compose.ui.draw.alpha
27 | import androidx.compose.ui.graphics.Color
28 | import androidx.compose.ui.res.painterResource
29 | import androidx.core.view.WindowCompat
30 | import androidx.navigation.NavDestination
31 | import androidx.navigation.NavDestination.Companion.hierarchy
32 | import androidx.navigation.NavGraph.Companion.findStartDestination
33 | import androidx.navigation.NavHostController
34 | import androidx.navigation.compose.currentBackStackEntryAsState
35 | import androidx.navigation.compose.rememberNavController
36 | import com.paradoxo.threadscompose.ui.navigation.Destinations
37 | import com.paradoxo.threadscompose.ui.navigation.ThreadsNavHost
38 | import com.paradoxo.threadscompose.ui.navigation.screenItems
39 | import com.paradoxo.threadscompose.ui.theme.ThreadsComposeTheme
40 |
41 | class MainActivity : ComponentActivity() {
42 | private val testMode = false
43 |
44 | override fun onCreate(savedInstanceState: Bundle?) {
45 | super.onCreate(savedInstanceState)
46 | WindowCompat.setDecorFitsSystemWindows(window, false)
47 |
48 | setContent {
49 | if (testMode) {
50 | Box(
51 | Modifier.fillMaxSize(),
52 | contentAlignment = Alignment.Center
53 | ) {}
54 | } else {
55 | ThreadsComposeTheme {
56 | Box(
57 | Modifier.fillMaxSize()
58 | ) {
59 | val navController: NavHostController = rememberNavController()
60 |
61 | var showNavigationBar by remember { mutableStateOf(false) }
62 | val navBackStackEntry by navController.currentBackStackEntryAsState()
63 | val currentDestination = navBackStackEntry?.destination
64 |
65 | val destinysWithoutNavigationBar by remember {
66 | mutableStateOf(
67 | listOf(
68 | Destinations.Login.route,
69 | Destinations.ProfileEdit.route,
70 | Destinations.Post.route,
71 | )
72 | )
73 | }
74 |
75 | LaunchedEffect(currentDestination) {
76 | showNavigationBar =
77 | !destinysWithoutNavigationBar.contains(currentDestination?.route)
78 | }
79 |
80 | ThreadsApp(
81 | navController = navController,
82 | showNavigationBar = showNavigationBar,
83 | currentDestination = currentDestination,
84 | content = {
85 | ThreadsNavHost(
86 | navController = navController,
87 | navigateToInstagram = {
88 | val instagramIntent = Intent(
89 | Intent.ACTION_VIEW,
90 | Uri.parse("https://www.instagram.com/threadsapp/")
91 | )
92 | startActivity(instagramIntent)
93 | }
94 | )
95 | }
96 | )
97 | }
98 | }
99 | }
100 |
101 | }
102 | }
103 |
104 | @Composable
105 | fun ThreadsApp(
106 | content: @Composable (PaddingValues) -> Unit,
107 | navController: NavHostController,
108 | showNavigationBar: Boolean,
109 | currentDestination: NavDestination?
110 | ) {
111 | Scaffold(
112 | bottomBar = {
113 | if (showNavigationBar) {
114 | NavigationBar(
115 | containerColor = Color.White,
116 | ) {
117 | screenItems.forEach { screen ->
118 | val isSelected =
119 | currentDestination?.hierarchy?.any { it.route == screen.route } == true
120 |
121 | NavigationBarItem(
122 | icon = {
123 | screen.resourceId?.let { assetIcon ->
124 | val icon = painterResource(
125 | id = if (isSelected) {
126 | assetIcon.first
127 | } else {
128 | assetIcon.second
129 | },
130 | )
131 |
132 |
133 | Icon(
134 | icon,
135 | contentDescription = null,
136 | modifier = Modifier
137 | .alpha(
138 | if (isSelected) 1f else 0.5f
139 | )
140 | )
141 | }
142 | },
143 | selected = isSelected,
144 | onClick = {
145 | if (screen.route == Destinations.Post.route) {
146 | navController.navigate(screen.route) {
147 | launchSingleTop = true
148 | }
149 | } else {
150 | navController.navigate(screen.route) {
151 | popUpTo(navController.graph.findStartDestination().id) {
152 | saveState = true
153 | }
154 |
155 | launchSingleTop = true
156 | restoreState = true
157 | }
158 | }
159 | },
160 | colors = NavigationBarItemDefaults.colors(
161 | indicatorColor = Color.White
162 | )
163 | )
164 | }
165 | }
166 | }
167 | }
168 | ) { paddingValues ->
169 | Column(
170 | Modifier
171 | .fillMaxSize(),
172 | horizontalAlignment = Alignment.CenterHorizontally,
173 | verticalArrangement = Arrangement.Center
174 | ) {
175 | Box(Modifier.fillMaxSize()) {
176 | content(paddingValues)
177 | }
178 | }
179 | }
180 | }
181 |
182 |
183 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/paradoxo/threadscompose/model/Notification.kt:
--------------------------------------------------------------------------------
1 | package com.paradoxo.threadscompose.model
2 |
3 | data class Notification(
4 | val id: String = "",
5 | val title: String = "",
6 | val description: String = "",
7 | val image: Int = 0,
8 | val time: String = "",
9 | val extraContent: String? = null,
10 | val type: NotificationTypeEnum = NotificationTypeEnum.Follow,
11 | var isFollowing: Boolean = false,
12 | )
13 |
14 | enum class NotificationTypeEnum {
15 | All,
16 | Comment,
17 | Mention,
18 | Follow,
19 | Like,
20 | Verified,
21 | }
22 |
--------------------------------------------------------------------------------
/app/src/main/java/com/paradoxo/threadscompose/model/Post.kt:
--------------------------------------------------------------------------------
1 | package com.paradoxo.threadscompose.model
2 |
3 | data class Post(
4 | val id: String = "",
5 | val mainPost: Boolean = false,
6 | val userAccount: UserAccount = UserAccount(),
7 | val description: String = "",
8 | val date: Long = 0L,
9 | val medias: List = emptyList(),
10 | val likes: List = emptyList(),
11 | val comments: List = emptyList()
12 | )
13 |
14 | data class Like(
15 | val id: String = "",
16 | val profilePicAuthor: String = "",
17 | )
18 |
19 | data class Comment(
20 | val id: String = "",
21 | val profilePicAuthor: String = "",
22 | )
23 |
--------------------------------------------------------------------------------
/app/src/main/java/com/paradoxo/threadscompose/model/UserAccount.kt:
--------------------------------------------------------------------------------
1 | package com.paradoxo.threadscompose.model
2 |
3 | data class UserAccount(
4 | val id: String = "",
5 | val name: String = "",
6 | val userName: String = "",
7 | val bio: String = "",
8 | val link: String = "",
9 | val imageProfileUrl: String = "",
10 | val posts:List = emptyList(),
11 | val follows:List = emptyList(),
12 | val followers:List = emptyList()
13 | )
14 |
15 |
--------------------------------------------------------------------------------
/app/src/main/java/com/paradoxo/threadscompose/network/firebase/MediaFirebaseStorage.kt:
--------------------------------------------------------------------------------
1 | package com.paradoxo.threadscompose.network.firebase
2 |
3 | import android.net.Uri
4 | import android.util.Log
5 | import com.google.firebase.auth.ktx.auth
6 | import com.google.firebase.ktx.Firebase
7 | import com.google.firebase.storage.ktx.storage
8 | import kotlinx.coroutines.Dispatchers
9 | import kotlinx.coroutines.async
10 | import kotlinx.coroutines.awaitAll
11 | import kotlinx.coroutines.coroutineScope
12 | import kotlinx.coroutines.tasks.await
13 | import kotlinx.coroutines.withContext
14 | import java.util.UUID
15 |
16 | class MediaFirebaseStorage {
17 | private val idCurrentUser = Firebase.auth.currentUser?.uid
18 | private val storage = Firebase.storage
19 | private val storageRef = storage.reference
20 | private val ref = storageRef.child("users/$idCurrentUser/medias/posts/")
21 |
22 | suspend fun uploadMedia(
23 | medias: List,
24 | onSuccess: () -> Unit = {},
25 | onError: () -> Unit = {},
26 | ): List = coroutineScope {
27 | val dispatcher = Dispatchers.IO
28 |
29 | withContext(dispatcher) {
30 | val stackTasks = medias.map { imageUri ->
31 | async {
32 | val uuid = UUID.randomUUID().toString()
33 | val currentRef = ref.child(uuid)
34 | val file = Uri.parse(imageUri)
35 | val uploadTask = currentRef.putFile(file)
36 |
37 | uploadTask.await()
38 | val downloadUrl = currentRef.downloadUrl.await().toString()
39 |
40 | Log.i("imageUpload", "imageUpload link: $downloadUrl")
41 |
42 | downloadUrl
43 | }
44 | }
45 | try {
46 | val downloadUrls = stackTasks.awaitAll()
47 | withContext(Dispatchers.Main) {
48 | onSuccess()
49 | }
50 | downloadUrls
51 | } catch (e: Exception) {
52 | withContext(Dispatchers.Main) {
53 | onError()
54 | }
55 | Log.i("imageUpload", "imageUpload error: ${e.message}")
56 | emptyList()
57 | }
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/app/src/main/java/com/paradoxo/threadscompose/network/firebase/PostFirestore.kt:
--------------------------------------------------------------------------------
1 | package com.paradoxo.threadscompose.network.firebase
2 |
3 | import android.util.Log
4 | import com.google.firebase.firestore.FirebaseFirestore
5 | import com.google.firebase.firestore.Query
6 | import com.paradoxo.threadscompose.model.Comment
7 | import com.paradoxo.threadscompose.model.Post
8 | import kotlinx.coroutines.CoroutineScope
9 | import kotlinx.coroutines.Dispatchers.IO
10 | import kotlinx.coroutines.launch
11 | import java.util.UUID
12 |
13 | class PostFirestore {
14 | private val firebaseFirestore = FirebaseFirestore.getInstance()
15 | private val dbPosts = firebaseFirestore.collection("posts")
16 |
17 | private val mediaFirebaseStorage = MediaFirebaseStorage()
18 |
19 | fun getAllPosts(
20 | onSuccess: (List) -> Unit = {},
21 | onError: () -> Unit = {},
22 | ) {
23 | dbPosts
24 | .whereEqualTo("mainPost", true)
25 | .orderBy("date", Query.Direction.DESCENDING)
26 | .get()
27 | .addOnSuccessListener {
28 | val posts = it.toObjects(Post::class.java)
29 | onSuccess(posts)
30 | Log.i("getAllPosts", "Posts obtidos com sucesso ${posts.size}")
31 | }
32 | .addOnFailureListener {
33 | onError()
34 | Log.i("getAllPosts", "Erro ao obter posts ${it.message}")
35 | }
36 | }
37 |
38 | fun savePost(
39 | posts: List,
40 | onSuccess: () -> Unit = {},
41 | onError: () -> Unit = {},
42 | ) {
43 | if (posts.size > 1) {
44 | savePostThread(posts, onSuccess, onError)
45 | } else {
46 |
47 | CoroutineScope(IO).launch {
48 | val post = posts.first().copy(
49 | id = UUID.randomUUID().toString(),
50 | mainPost = true,
51 | medias = mediaFirebaseStorage.uploadMedia(posts.first().medias)
52 | )
53 | dbPosts.document()
54 | .set(post)
55 | .addOnSuccessListener {
56 | onSuccess()
57 | Log.i("savePost", "Post salvo com sucesso")
58 | }
59 | .addOnFailureListener {
60 | onError()
61 | Log.i("savePost", "Erro ao salvar post ${it.message}")
62 | }
63 | }
64 | }
65 | }
66 |
67 | private fun savePostThread(
68 | posts: List,
69 | onSuccess: () -> Unit,
70 | onError: () -> Unit
71 | ) {
72 | val batch = firebaseFirestore.batch()
73 |
74 | val postsInThreadFormat: List = generatePostsInThreadFormat(posts)
75 |
76 | CoroutineScope(IO).launch {
77 | try {
78 | postsInThreadFormat.forEach { post ->
79 | val urls = mediaFirebaseStorage.uploadMedia(post.medias)
80 | val newPost = post.copy(medias = urls)
81 | val documentReference = dbPosts.document()
82 | batch.set(documentReference, newPost)
83 | }
84 |
85 | batch.commit()
86 | .addOnSuccessListener {
87 | onSuccess()
88 | Log.i("savePostThread", "Thread salvo com sucesso")
89 | }
90 | .addOnFailureListener {
91 | onError()
92 | Log.i("savePostThread", "Erro ao salvar Thread ${it.message}")
93 | }
94 | } catch (e: Exception) {
95 | Log.e("savePostThread", "Erro ao salvar Thread ${e.message}")
96 | }
97 | }
98 | }
99 |
100 | private fun generatePostsInThreadFormat(posts: List): List {
101 | val postsInThreadFormat = mutableListOf()
102 | val randomIdList: List = mutableListOf().apply {
103 | repeat(posts.size) {
104 | add(UUID.randomUUID().toString())
105 | }
106 | }
107 |
108 | posts.forEachIndexed { index, post ->
109 |
110 | val commentList = mutableListOf()
111 | randomIdList.subList(index.plus(1), posts.size).forEach { id ->
112 | commentList.add(
113 | Comment(
114 | id = id,
115 | profilePicAuthor = post.userAccount.imageProfileUrl,
116 | )
117 | )
118 | }
119 |
120 | postsInThreadFormat.add(
121 | post.copy(
122 | id = randomIdList[index],
123 | mainPost = index == 0,
124 | comments = commentList,
125 | likes = emptyList()
126 | )
127 | )
128 | }
129 |
130 | return postsInThreadFormat
131 | }
132 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/paradoxo/threadscompose/network/firebase/UserFirestore.kt:
--------------------------------------------------------------------------------
1 | package com.paradoxo.threadscompose.network.firebase
2 |
3 | import android.util.Log
4 | import com.google.firebase.firestore.FirebaseFirestore
5 | import com.paradoxo.threadscompose.model.UserAccount
6 |
7 | class UserFirestore {
8 | private val firebaseFirestore = FirebaseFirestore.getInstance()
9 | private val dbUsers = firebaseFirestore.collection("users")
10 |
11 |
12 | fun getUserById(
13 | userId: String,
14 | onSuccess: (UserAccount) -> Unit = {},
15 | onError: () -> Unit = {},
16 | ) {
17 | dbUsers.document(userId)
18 | .get()
19 | .addOnSuccessListener {
20 | it.toObject(UserAccount::class.java)?.let { userAccount ->
21 | onSuccess(userAccount)
22 | Log.i("getUserById", "Usuário obtido com sucesso ${userAccount.name}")
23 | } ?: run {
24 | onError()
25 | Log.i("getUserById", "Usuário não encontrado")
26 | }
27 |
28 | }.addOnFailureListener {
29 | Log.i("getUserById", "Erro ao obter usuário ${it.message}")
30 | }
31 | }
32 |
33 |
34 | fun saveUser(
35 | userId: String,
36 | userAccount: UserAccount,
37 | onSuccess: () -> Unit = {},
38 | onError: () -> Unit = {},
39 | ) {
40 | dbUsers.document(userId)
41 | .set(userAccount)
42 | .addOnSuccessListener {
43 | onSuccess()
44 | Log.i("saveUser", "Usuário salvo com sucesso")
45 | }
46 | .addOnFailureListener {
47 | onError()
48 | Log.i("saveUser", "Erro ao salvar usuário ${it.message}")
49 | }
50 | }
51 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/paradoxo/threadscompose/sampleData/SampleData.kt:
--------------------------------------------------------------------------------
1 | package com.paradoxo.threadscompose.sampleData
2 |
3 | import com.paradoxo.threadscompose.R
4 | import com.paradoxo.threadscompose.model.Comment
5 | import com.paradoxo.threadscompose.model.Like
6 | import com.paradoxo.threadscompose.model.Notification
7 | import com.paradoxo.threadscompose.model.NotificationTypeEnum
8 | import com.paradoxo.threadscompose.model.Post
9 | import com.paradoxo.threadscompose.model.UserAccount
10 | import com.paradoxo.threadscompose.utils.getCurrentTime
11 | import java.util.UUID
12 |
13 | import kotlin.random.Random
14 |
15 | class SampleData {
16 | val notifications = mutableListOf()
17 | val posts = mutableListOf()
18 | val userAccounts = mutableListOf()
19 | private val comments = mutableListOf()
20 |
21 | private val descriptions = listOf(
22 | "Viver o momento é a verdadeira essência da vida!",
23 | "Tirando um tempo para admirar as pequenas coisas da vida.",
24 | "A melhor parte da vida são as pessoas que conhecemos ao longo do caminho.",
25 | "A natureza sempre veste as cores do espírito.",
26 | "Acredite em si mesmo e tudo é possível.",
27 | "Sonhe grande e ouse falhar.",
28 | "Desfrutando de alguns dos meus momentos favoritos.",
29 | "Faça o que você ama, ame o que você faz.",
30 | "É sempre melhor quando estamos juntos.",
31 | "Inspire-se, mas seja você mesmo!"
32 | )
33 | private val bios = listOf(
34 | "Apaixonado por viagens e fotografia.",
35 | "Viciado em café e aventuras.",
36 | "Admirador da beleza da natureza.",
37 | "Colecionador de momentos, não de coisas.",
38 | "Um sonhador que se recusa a acordar.",
39 | "Vivendo um dia de cada vez.",
40 | "Amante de todas as coisas belas e extraordinárias.",
41 | "Em busca de inspiração e felicidade.",
42 | "Buscando a magia em cada dia.",
43 | "Vivendo a vida ao máximo!"
44 | )
45 | val images = listOf(
46 | "https://raw.githubusercontent.com/git-jr/sample-files/7bc859dfa8a6241fa9c0d723ba6e7517bdfedd50/profile%20pics/profile_pic_emoji_1.png",
47 | "https://raw.githubusercontent.com/git-jr/sample-files/7bc859dfa8a6241fa9c0d723ba6e7517bdfedd50/profile%20pics/profile_pic_emoji_2.png",
48 | "https://raw.githubusercontent.com/git-jr/sample-files/7bc859dfa8a6241fa9c0d723ba6e7517bdfedd50/profile%20pics/profile_pic_emoji_3.png",
49 | "https://raw.githubusercontent.com/git-jr/sample-files/7bc859dfa8a6241fa9c0d723ba6e7517bdfedd50/profile%20pics/profile_pic_emoji_4.png",
50 | "https://raw.githubusercontent.com/git-jr/sample-files/7bc859dfa8a6241fa9c0d723ba6e7517bdfedd50/profile%20pics/profile_pic_emoji_5.png",
51 | )
52 |
53 | val names = listOf(
54 | "Sheldon Cooper",
55 | "Eleanor Shellstrop",
56 | "Sherlock Holmes",
57 | "John Watson",
58 | "Walter White",
59 | "Jesse Pinkman",
60 | "Daenerys Targaryen",
61 | "Jon Snow",
62 | "Michael Scott",
63 | "Chrisjen Avasarala",
64 | "Tyrion Lannister",
65 | "Arya Stark",
66 | "Tommy Shelby",
67 | "Gol D. Roger",
68 | "Jake Peralta",
69 | "Amy Santiago",
70 | "Chloe Morningstar",
71 | "Barry allen",
72 | "David Mailer",
73 | "Elizabeth Keen"
74 | )
75 |
76 | init {
77 | val randomNumbers = mutableListOf()
78 | repeat(1000) {
79 | randomNumbers.add(it.toLong())
80 | }
81 |
82 | for (i in 1..10) {
83 | val userAccount = UserAccount(
84 | id = i.toString(),
85 | name = names[Random.nextInt(0, names.size)],
86 | userName = "usuario$i",
87 | bio = bios[i - 1],
88 | imageProfileUrl = images.random(),
89 | posts = randomNumbers.subList(0, Random.nextInt(1, 1000)),
90 | follows = randomNumbers.subList(0, Random.nextInt(1, 1000)),
91 | followers = randomNumbers.subList(0, Random.nextInt(1, 1000)),
92 | )
93 | userAccounts.add(userAccount)
94 |
95 | val post = Post(
96 | id = i.toString(),
97 | userAccount = userAccount,
98 | description = descriptions[i - 1],
99 | date = getCurrentTime(),
100 | medias = if (Random.nextBoolean()) images.shuffled() else listOf(),
101 | likes = generateListSamplesLikes(),
102 | comments = generateSampleComments()
103 | )
104 | posts.add(post)
105 | }
106 |
107 | for (i in 1..10) {
108 | userAccounts[i - 1] =
109 | userAccounts[i - 1].copy(posts = posts.filter { it.userAccount.id == i.toString() }
110 | .map { it.id.toLong() })
111 | }
112 |
113 | repeat(10) {
114 | notifications.add(
115 | Notification(
116 | id = UUID.randomUUID().toString(),
117 | title = "Titulo $it",
118 | description = "Descrição $it",
119 | extraContent = if (Random.nextBoolean()) "Conteúdo extra $it" else null,
120 | image = R.drawable.profile_pic_emoji_4,
121 | time = "1d",
122 | type = if (it in 2..4) NotificationTypeEnum.values().sortedArray()[it] else
123 | NotificationTypeEnum.values().sortedArray()[Random.nextInt(0, 5)],
124 | isFollowing = Random.nextBoolean(),
125 | )
126 | )
127 |
128 | comments.add(
129 | Comment(
130 | id = UUID.randomUUID().toString(),
131 | profilePicAuthor = images.random(),
132 | )
133 | )
134 | }
135 |
136 | }
137 |
138 | private fun generateListRandomIdStrings(): List {
139 | if (Random.nextBoolean()) return emptyList()
140 | val randomList = mutableListOf()
141 | repeat(Random.nextInt(0, 1000)) {
142 | randomList.add(UUID.randomUUID().toString())
143 | }
144 | return randomList
145 | }
146 |
147 | private fun generateSampleComments(): List {
148 |
149 | if (Random.nextBoolean()) return emptyList()
150 |
151 | val comments = mutableListOf()
152 | repeat(Random.nextInt(0, 10)) {
153 | comments.add(
154 | Comment(
155 | id = it.toString(),
156 | profilePicAuthor = images.random(),
157 | )
158 | )
159 | }
160 | return comments
161 | }
162 |
163 | private fun generateListSamplesLikes(): List {
164 | if (Random.nextBoolean()) return emptyList()
165 |
166 | val likes = mutableListOf()
167 | repeat(Random.nextInt(0, 10)) {
168 | likes.add(
169 | Like(
170 | id = it.toString(),
171 | profilePicAuthor = images.random(),
172 | )
173 | )
174 | }
175 | return likes
176 | }
177 |
178 | fun generateSampleInvitedUser(): UserAccount {
179 | val name = names[Random.nextInt(0, names.size)]
180 | return UserAccount(
181 | id = UUID.randomUUID().toString(),
182 | name = name,
183 | userName = name.replace(" ", "").lowercase(),
184 | bio = "O tal do Lorem ipsum dolor sit amet, consectetur adipiscing elit",
185 | link = "https://github.com/git-jr",
186 | imageProfileUrl = images.random(),
187 | posts = (1L..Random.nextLong(100)).toList(),
188 | follows = (1L..Random.nextLong(100)).toList(),
189 | followers = (1L..Random.nextLong(100)).toList(),
190 | )
191 | }
192 | }
193 |
--------------------------------------------------------------------------------
/app/src/main/java/com/paradoxo/threadscompose/ui/Components.kt:
--------------------------------------------------------------------------------
1 | package com.paradoxo.threadscompose.ui
2 |
3 | import androidx.compose.animation.core.Spring
4 | import androidx.compose.animation.core.animateFloatAsState
5 | import androidx.compose.animation.core.spring
6 | import androidx.compose.foundation.border
7 | import androidx.compose.foundation.layout.Arrangement
8 | import androidx.compose.foundation.layout.Box
9 | import androidx.compose.foundation.layout.Column
10 | import androidx.compose.foundation.layout.IntrinsicSize
11 | import androidx.compose.foundation.layout.Row
12 | import androidx.compose.foundation.layout.Spacer
13 | import androidx.compose.foundation.layout.defaultMinSize
14 | import androidx.compose.foundation.layout.fillMaxHeight
15 | import androidx.compose.foundation.layout.fillMaxWidth
16 | import androidx.compose.foundation.layout.height
17 | import androidx.compose.foundation.layout.offset
18 | import androidx.compose.foundation.layout.padding
19 | import androidx.compose.foundation.layout.size
20 | import androidx.compose.foundation.layout.width
21 | import androidx.compose.foundation.lazy.LazyRow
22 | import androidx.compose.foundation.lazy.items
23 | import androidx.compose.foundation.shape.CircleShape
24 | import androidx.compose.foundation.shape.RoundedCornerShape
25 | import androidx.compose.material.icons.Icons
26 | import androidx.compose.material.icons.filled.MoreVert
27 | import androidx.compose.material3.Divider
28 | import androidx.compose.material3.Icon
29 | import androidx.compose.material3.MaterialTheme
30 | import androidx.compose.material3.Text
31 | import androidx.compose.runtime.Composable
32 | import androidx.compose.runtime.getValue
33 | import androidx.compose.ui.Alignment
34 | import androidx.compose.ui.Modifier
35 | import androidx.compose.ui.draw.clip
36 | import androidx.compose.ui.draw.scale
37 | import androidx.compose.ui.graphics.Color
38 | import androidx.compose.ui.layout.ContentScale
39 | import androidx.compose.ui.platform.LocalContext
40 | import androidx.compose.ui.res.painterResource
41 | import androidx.compose.ui.text.font.FontWeight
42 | import androidx.compose.ui.tooling.preview.Preview
43 | import androidx.compose.ui.unit.dp
44 | import coil.compose.AsyncImage
45 | import coil.request.ImageRequest
46 | import com.paradoxo.threadscompose.R
47 | import com.paradoxo.threadscompose.model.Post
48 | import com.paradoxo.threadscompose.sampleData.SampleData
49 | import com.paradoxo.threadscompose.utils.formatTimeElapsed
50 | import com.paradoxo.threadscompose.utils.getCurrentTime
51 | import com.paradoxo.threadscompose.utils.noRippleClickable
52 |
53 | @Composable
54 | fun PostItem(
55 | post: Post,
56 | isLiked: Boolean = false,
57 | onLikeClick: (String) -> Unit = {},
58 | ) {
59 | val dividerColor = Color.Gray.copy(alpha = 0.2f)
60 |
61 | val commentsSize = post.comments.size
62 | val likesSize = post.likes.size
63 | val groupImageOffsetSize = if (commentsSize > 0) 8.dp else 0.dp
64 | val hasMedia = post.medias.isNotEmpty()
65 |
66 | Column {
67 | Row(
68 | Modifier
69 | .height(IntrinsicSize.Min)
70 | .fillMaxWidth()
71 | ) {
72 | Column(
73 | Modifier
74 | .padding(start = 8.dp, top = 8.dp, bottom = 8.dp)
75 | .weight(0.2f),
76 | horizontalAlignment = Alignment.CenterHorizontally
77 | ) {
78 | AsyncImage(
79 | model = ImageRequest.Builder(LocalContext.current)
80 | .data(post.userAccount.imageProfileUrl)
81 | .crossfade(true)
82 | .build(),
83 | placeholder = painterResource(id = R.drawable.placeholder_image),
84 | error = painterResource(id = R.drawable.placeholder_image),
85 | contentDescription = "avatar",
86 | modifier = Modifier
87 | .padding(horizontal = 8.dp)
88 | .clip(CircleShape)
89 | )
90 |
91 | if (commentsSize > 0 || likesSize > 0) {
92 | Divider(
93 | color = dividerColor,
94 | modifier = Modifier
95 | .padding(top = 8.dp)
96 | .fillMaxHeight()
97 | .width(3.dp)
98 | .clip(CircleShape)
99 | )
100 | }
101 | }
102 | Column(
103 | Modifier
104 | .padding(vertical = 8.dp)
105 | .weight(0.8f)
106 | ) {
107 | Row(
108 | Modifier
109 | .padding(end = 8.dp)
110 | .fillMaxWidth(),
111 | verticalAlignment = Alignment.CenterVertically
112 | ) {
113 | Text(
114 | text = post.userAccount.name,
115 | fontWeight = FontWeight.Bold,
116 | modifier = Modifier.weight(0.8f)
117 | )
118 |
119 | Row(
120 | verticalAlignment = Alignment.CenterVertically
121 | ) {
122 | Text(
123 | text = formatTimeElapsed(post.date, getCurrentTime()),
124 | fontWeight = FontWeight.Light
125 | )
126 | Icon(
127 | imageVector = Icons.Default.MoreVert,
128 | contentDescription = "more"
129 | )
130 | }
131 | }
132 |
133 | Text(
134 | text = post.description,
135 | modifier = Modifier.padding(end = 8.dp)
136 | )
137 |
138 |
139 | if (hasMedia) {
140 | Row(
141 | modifier = Modifier
142 | .height(height = 200.dp)
143 | .fillMaxWidth()
144 | ) {
145 | LazyRow(
146 | Modifier
147 | .fillMaxWidth()
148 | ) {
149 | items(post.medias) { media ->
150 | Row(
151 | Modifier
152 | .defaultMinSize(minHeight = 200.dp)
153 | ) {
154 | AsyncImage(
155 | contentScale = ContentScale.Crop,
156 | model = ImageRequest.Builder(LocalContext.current)
157 | .data(media)
158 | .crossfade(true)
159 | .build(),
160 | placeholder = painterResource(id = R.drawable.placeholder_image),
161 | error = painterResource(id = R.drawable.placeholder_image),
162 | contentDescription = "avatar",
163 | modifier = Modifier
164 | .fillMaxHeight()
165 | .padding(vertical = 8.dp)
166 | .clip(RoundedCornerShape(10))
167 | .border(
168 | width = 1.dp,
169 | color = Color.Gray.copy(alpha = 0.5f),
170 | shape = RoundedCornerShape(10)
171 | )
172 |
173 | )
174 | Spacer(modifier = Modifier.width(8.dp))
175 | }
176 | }
177 | }
178 | }
179 | }
180 |
181 | Row(
182 | Modifier.padding(vertical = 8.dp),
183 | ) {
184 |
185 | LikeButton(
186 | isLiked = isLiked,
187 | onLikeClick = onLikeClick,
188 | postId = post.id
189 | )
190 |
191 | Spacer(modifier = Modifier.width(10.dp))
192 | Icon(
193 | painterResource(id = R.drawable.ic_comment),
194 | contentDescription = "comment"
195 | )
196 |
197 | Spacer(modifier = Modifier.width(10.dp))
198 | Icon(
199 | painterResource(id = R.drawable.ic_repost),
200 | contentDescription = "retweet"
201 | )
202 |
203 | Spacer(modifier = Modifier.width(10.dp))
204 | Icon(
205 | painterResource(id = R.drawable.ic_send),
206 | contentDescription = "share"
207 | )
208 | }
209 | }
210 | }
211 | if (commentsSize > 0 || likesSize > 0) {
212 | Row(
213 | Modifier
214 | .fillMaxWidth(),
215 | verticalAlignment = Alignment.CenterVertically,
216 | ) {
217 | Column(
218 | Modifier
219 | .weight(0.2f)
220 | .padding(start = 8.dp)
221 | .offset(y = (-groupImageOffsetSize * 2)),
222 | horizontalAlignment = Alignment.CenterHorizontally,
223 | verticalArrangement = Arrangement.Center
224 | ) {
225 | if (commentsSize > 0) {
226 | when (post.comments.size) {
227 | 1 -> {
228 | ContainerOneProfilePic(post.comments.first().profilePicAuthor)
229 | }
230 |
231 | 2 -> {
232 | val profilePics = post.comments.take(2).map { it.profilePicAuthor }
233 | if (profilePics.distinct().size == 1) {
234 | ContainerOneProfilePic(profilePics.first())
235 | } else {
236 | ContainerTwoProfilePics(profilePics[0], profilePics[1])
237 | }
238 | }
239 |
240 | else -> {
241 | val profilePics = post.comments.take(3).map { it.profilePicAuthor }
242 | if (profilePics.distinct().size == 1) {
243 | ContainerOneProfilePic(profilePics.first())
244 | } else {
245 | ContainerMoreTwoProfilePics(
246 | profilePics[0],
247 | profilePics[1],
248 | profilePics[2]
249 | )
250 | }
251 | }
252 | }
253 | } else {
254 | when (post.likes.size) {
255 | 1 -> {
256 | ContainerOneProfilePic(post.likes.first().profilePicAuthor)
257 | }
258 |
259 | 2 -> {
260 | val profilePics = post.likes.take(2).map { it.profilePicAuthor }
261 | if (profilePics.distinct().size == 1) {
262 | ContainerOneProfilePic(profilePics.first())
263 | } else {
264 | ContainerTwoProfilePics(profilePics[0], profilePics[1])
265 | }
266 | }
267 |
268 | else -> {
269 | val profilePics = post.likes.take(3).map { it.profilePicAuthor }
270 | if (profilePics.distinct().size == 1) {
271 | ContainerOneProfilePic(profilePics.first())
272 | } else {
273 | ContainerMoreTwoProfilePics(
274 | profilePics[0],
275 | profilePics[1],
276 | profilePics[2]
277 | )
278 | }
279 | }
280 | }
281 | }
282 | }
283 | Row(
284 | Modifier
285 | .weight(0.8f)
286 | .offset(y = (-groupImageOffsetSize * 2)),
287 | verticalAlignment = Alignment.CenterVertically
288 | ) {
289 | if (commentsSize > 0) {
290 | val textCommentsSize =
291 | if (commentsSize == 1) "1 Resposta" else "$commentsSize Respostas"
292 | Text(
293 | text = textCommentsSize,
294 | style = MaterialTheme.typography.bodyLarge.copy(
295 | color = Color.Gray.copy(alpha = 0.8f)
296 | )
297 | )
298 | Spacer(modifier = Modifier.width(4.dp))
299 | }
300 |
301 | if (likesSize > 0) {
302 | val separator = if (commentsSize > 0) " • " else ""
303 | val textLikesSize =
304 | separator + if (likesSize == 1) "1 Curtida" else "$likesSize Curtidas"
305 |
306 | Text(
307 | text = textLikesSize,
308 | style = MaterialTheme.typography.bodyLarge.copy(
309 | color = Color.Gray.copy(alpha = 0.8f)
310 | )
311 | )
312 | Spacer(modifier = Modifier.width(4.dp))
313 | }
314 | }
315 | }
316 | }
317 | }
318 |
319 | Divider(
320 | color = dividerColor,
321 | modifier = Modifier
322 | .fillMaxWidth()
323 | .offset(y = -groupImageOffsetSize * 2)
324 |
325 | )
326 | }
327 |
328 |
329 | @Composable
330 | private fun ContainerOneProfilePic(
331 | profilePicAuthor: String
332 | ) {
333 | Box(
334 | Modifier.padding(horizontal = 8.dp, vertical = 16.dp)
335 | ) {
336 | AsyncImage(
337 | model = ImageRequest.Builder(LocalContext.current)
338 | .data(profilePicAuthor)
339 | .crossfade(true)
340 | .build(),
341 | contentDescription = "avatar",
342 | modifier = Modifier
343 | .size(22.dp)
344 | .border(
345 | width = 2.dp,
346 | color = Color.White,
347 | shape = CircleShape
348 | )
349 | .clip(CircleShape)
350 | .padding(2.dp)
351 | )
352 | }
353 | }
354 |
355 | @Composable
356 | private fun ContainerTwoProfilePics(
357 | firstProfilePic: String,
358 | secondProfilePic: String
359 | ) {
360 | Box(
361 | Modifier.padding(horizontal = 8.dp, vertical = 16.dp)
362 | ) {
363 | AsyncImage(
364 | model = firstProfilePic,
365 | contentDescription = "avatar",
366 | modifier = Modifier
367 | .size(22.dp)
368 | .offset(x = (-5).dp)
369 | .border(
370 | width = 2.dp,
371 | color = Color.White,
372 | shape = CircleShape
373 | )
374 | .clip(CircleShape)
375 | .padding(2.dp)
376 | )
377 |
378 | AsyncImage(
379 | model = secondProfilePic,
380 | contentDescription = "avatar",
381 | modifier = Modifier
382 | .size(22.dp)
383 | .offset(x = 5.dp)
384 | .border(
385 | width = 2.dp,
386 | color = Color.White,
387 | shape = CircleShape
388 | )
389 | .clip(CircleShape)
390 | .padding(2.dp)
391 | )
392 | }
393 | }
394 |
395 | @Composable
396 | private fun ContainerMoreTwoProfilePics(
397 | firstProfilePic: String,
398 | secondProfilePic: String,
399 | thirdProfilePic: String
400 | ) {
401 | Box(
402 | Modifier.padding(horizontal = 8.dp)
403 | ) {
404 | Column(
405 | Modifier
406 | .fillMaxWidth()
407 | .padding(start = 8.dp, end = 8.dp, top = 8.dp),
408 | verticalArrangement = Arrangement.Center,
409 | ) {
410 | AsyncImage(
411 | model = firstProfilePic,
412 | contentDescription = "avatar",
413 | modifier = Modifier
414 | .size(22.dp)
415 | .border(
416 | width = 2.dp,
417 | color = Color.White,
418 | shape = CircleShape
419 | )
420 | .clip(CircleShape)
421 | .padding(2.dp)
422 | .align(Alignment.End),
423 | )
424 |
425 | AsyncImage(
426 | model = secondProfilePic,
427 | contentDescription = "avatar",
428 | modifier = Modifier
429 | .size(16.dp)
430 | .offset(y = (-14).dp)
431 | .border(
432 | width = 2.dp,
433 | color = Color.White,
434 | shape = CircleShape
435 | )
436 | .clip(CircleShape)
437 | .padding(2.dp)
438 | .align(Alignment.Start)
439 | )
440 |
441 | AsyncImage(
442 | model = thirdProfilePic,
443 | contentDescription = "avatar",
444 | modifier = Modifier
445 | .size(14.dp)
446 | .offset(y = (-18).dp)
447 | .border(
448 | width = 2.dp,
449 | color = Color.White,
450 | shape = CircleShape
451 | )
452 | .clip(CircleShape)
453 | .padding(2.dp)
454 | .align(Alignment.CenterHorizontally)
455 | )
456 | }
457 | }
458 | }
459 |
460 |
461 | @Composable
462 | fun LikeButton(
463 | isLiked: Boolean, onLikeClick: (String) -> Unit,
464 | postId: String
465 | ) {
466 | val scale: Float by animateFloatAsState(
467 | targetValue = if (isLiked) 1.1f else 1f,
468 | animationSpec = spring(
469 | dampingRatio = Spring.DampingRatioHighBouncy,
470 | stiffness = Spring.StiffnessMedium
471 | ), label = ""
472 | )
473 |
474 | val resourceId = if (isLiked) R.drawable.ic_heart else R.drawable.ic_heart_outlined
475 | Icon(
476 | painter = painterResource(id = resourceId),
477 | contentDescription = "like",
478 | modifier = Modifier
479 | .noRippleClickable {
480 | onLikeClick(postId)
481 | }
482 | .scale(scale),
483 | tint = if (isLiked) Color.Red else Color.Black
484 | )
485 | }
486 |
487 | @Preview(showBackground = true)
488 | @Composable
489 | private fun PostItemPreview() {
490 | PostItem(
491 | post = SampleData().posts.first(),
492 | isLiked = true
493 | )
494 | }
495 |
--------------------------------------------------------------------------------
/app/src/main/java/com/paradoxo/threadscompose/ui/feed/FeedScreen.kt:
--------------------------------------------------------------------------------
1 | package com.paradoxo.threadscompose.ui.feed
2 |
3 | import androidx.compose.animation.core.Animatable
4 | import androidx.compose.animation.core.Spring
5 | import androidx.compose.animation.core.spring
6 | import androidx.compose.foundation.background
7 | import androidx.compose.foundation.gestures.detectVerticalDragGestures
8 | import androidx.compose.foundation.layout.Arrangement
9 | import androidx.compose.foundation.layout.Box
10 | import androidx.compose.foundation.layout.Row
11 | import androidx.compose.foundation.layout.Spacer
12 | import androidx.compose.foundation.layout.fillMaxSize
13 | import androidx.compose.foundation.layout.fillMaxWidth
14 | import androidx.compose.foundation.layout.height
15 | import androidx.compose.foundation.layout.size
16 | import androidx.compose.foundation.layout.systemBarsPadding
17 | import androidx.compose.foundation.lazy.LazyColumn
18 | import androidx.compose.foundation.lazy.items
19 | import androidx.compose.runtime.Composable
20 | import androidx.compose.runtime.LaunchedEffect
21 | import androidx.compose.runtime.getValue
22 | import androidx.compose.runtime.mutableFloatStateOf
23 | import androidx.compose.runtime.mutableStateOf
24 | import androidx.compose.runtime.remember
25 | import androidx.compose.runtime.rememberCoroutineScope
26 | import androidx.compose.runtime.saveable.rememberSaveable
27 | import androidx.compose.ui.Modifier
28 | import androidx.compose.ui.graphics.Color
29 | import androidx.compose.ui.input.pointer.pointerInput
30 | import androidx.compose.ui.tooling.preview.Preview
31 | import androidx.compose.ui.unit.dp
32 | import com.airbnb.lottie.compose.LottieAnimation
33 | import com.airbnb.lottie.compose.LottieCompositionSpec
34 | import com.airbnb.lottie.compose.LottieConstants
35 | import com.airbnb.lottie.compose.rememberLottieAnimatable
36 | import com.airbnb.lottie.compose.rememberLottieComposition
37 | import com.paradoxo.threadscompose.R
38 | import com.paradoxo.threadscompose.model.Post
39 | import com.paradoxo.threadscompose.ui.PostItem
40 | import kotlinx.coroutines.launch
41 |
42 |
43 | @Composable
44 | fun FeedScreen(
45 | modifier: Modifier = Modifier,
46 | posts: List = emptyList(),
47 | onLikeClick: (Post) -> Unit = {},
48 | onReload: () -> Unit = {},
49 | idCurrentUserProfile: String = "",
50 | ) {
51 | LazyColumn(
52 | modifier = modifier
53 | .background(color = Color.White)
54 | .fillMaxSize()
55 | .systemBarsPadding()
56 | ) {
57 | item {
58 | ExpandableAppLogoLottie(onReload = onReload)
59 | }
60 |
61 | items(
62 | posts,
63 | key = { post -> post.id }
64 | ) { post ->
65 | val isLiked = rememberSaveable {
66 | mutableStateOf(post.likes.any { it.id == idCurrentUserProfile })
67 | }
68 | PostItem(
69 | post,
70 | isLiked.value,
71 | onLikeClick = {
72 | onLikeClick(post)
73 | isLiked.value = !isLiked.value
74 | }
75 | )
76 | }
77 |
78 | item {
79 | Spacer(modifier = Modifier.height(56.dp))
80 | }
81 | }
82 | }
83 |
84 | @Composable
85 | private fun ExpandableAppLogoLottie(onReload: () -> Unit = {}) {
86 | val maxHeightImage = 200.dp
87 | val defaultSizeImage = 72.dp
88 | val coroutineScope = rememberCoroutineScope()
89 | val animatedImageSize = remember { Animatable(defaultSizeImage.value) }
90 |
91 |
92 | val lottiePreviousProgress by remember { mutableFloatStateOf(0f) }
93 | val lottieSpeed by remember { mutableFloatStateOf(1f) }
94 | val lottieComposition by rememberLottieComposition(
95 | LottieCompositionSpec.RawRes(
96 | R.raw.logo_lines_animated
97 | )
98 | )
99 | val lottieAnimatable = rememberLottieAnimatable()
100 |
101 | LaunchedEffect(lottieSpeed) {
102 | lottieAnimatable.animate(
103 | lottieComposition,
104 | iteration = LottieConstants.IterateForever,
105 | speed = lottieSpeed,
106 | )
107 | }
108 |
109 | Box(
110 | modifier = Modifier
111 | .pointerInput(Unit) {
112 | detectVerticalDragGestures(
113 | onVerticalDrag = { change, offset ->
114 | coroutineScope.launch {
115 | val newSize =
116 | (animatedImageSize.value + offset / 8).coerceAtLeast(
117 | defaultSizeImage.value
118 | )
119 | if (newSize < maxHeightImage.value) {
120 | animatedImageSize.snapTo(newSize)
121 | }
122 | change.consume()
123 | }
124 | },
125 | onDragEnd = {
126 | coroutineScope.launch {
127 | animatedImageSize.animateTo(
128 | defaultSizeImage.value,
129 | animationSpec = spring(
130 | dampingRatio = Spring.DampingRatioLowBouncy,
131 | stiffness = Spring.StiffnessLow
132 | )
133 | )
134 | }
135 | coroutineScope.launch {
136 | lottieAnimatable.animate(
137 | lottieComposition,
138 | iteration = 1,
139 | speed = lottieSpeed,
140 | reverseOnRepeat = true,
141 | initialProgress = lottiePreviousProgress,
142 | )
143 | }
144 |
145 | onReload()
146 | },
147 | onDragStart = {
148 | coroutineScope.launch {
149 | lottieAnimatable.animate(
150 | lottieComposition,
151 | iteration = LottieConstants.IterateForever,
152 | speed = lottieSpeed,
153 | initialProgress = lottiePreviousProgress,
154 | )
155 | }
156 | }
157 | )
158 | }
159 | ) {
160 | Row(
161 | modifier = Modifier
162 | .fillMaxWidth(),
163 | horizontalArrangement = Arrangement.Center,
164 | ) {
165 |
166 | LottieAnimation(
167 | composition = lottieComposition,
168 | progress = { lottieAnimatable.progress },
169 | modifier = Modifier
170 | .size(animatedImageSize.value.dp)
171 | )
172 |
173 | }
174 | }
175 | }
176 |
177 | @Preview(showBackground = true)
178 | @Composable
179 | fun FeedScreenPreview() {
180 | FeedScreen()
181 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/paradoxo/threadscompose/ui/feed/FeedScreenState.kt:
--------------------------------------------------------------------------------
1 | package com.paradoxo.threadscompose.ui.feed
2 |
3 | import com.paradoxo.threadscompose.model.Post
4 | import com.paradoxo.threadscompose.model.UserAccount
5 |
6 | data class FeedScreenState(
7 | var posts: List = emptyList(),
8 | val currentUserProfile: UserAccount = UserAccount(),
9 | )
10 |
--------------------------------------------------------------------------------
/app/src/main/java/com/paradoxo/threadscompose/ui/feed/FeedViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.paradoxo.threadscompose.ui.feed
2 |
3 | import androidx.lifecycle.ViewModel
4 | import com.google.firebase.auth.ktx.auth
5 | import com.google.firebase.ktx.Firebase
6 | import com.paradoxo.threadscompose.model.Like
7 | import com.paradoxo.threadscompose.model.Post
8 | import com.paradoxo.threadscompose.model.UserAccount
9 | import com.paradoxo.threadscompose.network.firebase.PostFirestore
10 | import com.paradoxo.threadscompose.sampleData.SampleData
11 | import kotlinx.coroutines.flow.MutableStateFlow
12 | import kotlinx.coroutines.flow.StateFlow
13 | import kotlinx.coroutines.flow.asStateFlow
14 |
15 | internal class FeedViewModel : ViewModel() {
16 |
17 | private val _uiState = MutableStateFlow(FeedScreenState())
18 | val uiState: StateFlow = _uiState.asStateFlow()
19 | private val postFirestore = PostFirestore()
20 |
21 | init {
22 | // val postFirestore = PostFirestore()
23 | searchNewPosts()
24 |
25 | _uiState.value = _uiState.value.copy(
26 | currentUserProfile = UserAccount(
27 | id = Firebase.auth.currentUser?.uid ?: "",
28 | userName = Firebase.auth.currentUser?.displayName ?: "",
29 | imageProfileUrl = Firebase.auth.currentUser?.photoUrl.toString()
30 | )
31 | )
32 | }
33 |
34 | fun searchNewPosts() {
35 | if (Firebase.auth.currentUser == null) {
36 | _uiState.value = _uiState.value.copy(posts = SampleData().posts)
37 | return
38 | }
39 |
40 | postFirestore.getAllPosts(
41 | onSuccess = { posts ->
42 | _uiState.value = _uiState.value.copy(posts = posts)
43 | },
44 | onError = {
45 | _uiState.value = _uiState.value.copy(posts = SampleData().posts)
46 | }
47 | )
48 | }
49 |
50 | fun likePost(it: Post) {
51 |
52 | if (it.likes.any { like -> like.id == _uiState.value.currentUserProfile.id }) {
53 | _uiState.value = _uiState.value.copy(posts = _uiState.value.posts.map { post ->
54 | if (post.id == it.id) {
55 | post.copy(
56 | likes = post.likes.toMutableList().apply {
57 | removeIf { like -> like.id == _uiState.value.currentUserProfile.id }
58 | }
59 | )
60 | } else {
61 | post
62 | }
63 | })
64 | return
65 | }
66 |
67 | _uiState.value = _uiState.value.copy(posts = _uiState.value.posts.map { post ->
68 | if (post.id == it.id) {
69 | post.copy(
70 | likes = post.likes.toMutableList().apply {
71 | add(
72 | Like(
73 | id = _uiState.value.currentUserProfile.id,
74 | profilePicAuthor = _uiState.value.currentUserProfile.imageProfileUrl
75 | )
76 | )
77 | }
78 | )
79 | } else {
80 | post
81 | }
82 | })
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/app/src/main/java/com/paradoxo/threadscompose/ui/home/SessionState.kt:
--------------------------------------------------------------------------------
1 | package com.paradoxo.threadscompose.ui.home
2 |
3 | import com.paradoxo.threadscompose.model.UserAccount
4 | import com.paradoxo.threadscompose.ui.login.AppState
5 |
6 | data class SessionState(
7 | var appState: AppState = AppState.Loading,
8 | var userAccount: UserAccount = UserAccount()
9 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/paradoxo/threadscompose/ui/home/SessionViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.paradoxo.threadscompose.ui.home
2 |
3 | import androidx.lifecycle.ViewModel
4 | import com.paradoxo.threadscompose.model.UserAccount
5 | import kotlinx.coroutines.flow.MutableStateFlow
6 | import kotlinx.coroutines.flow.StateFlow
7 | import kotlinx.coroutines.flow.asStateFlow
8 |
9 | internal class SessionViewModel : ViewModel() {
10 |
11 | private val _uiState = MutableStateFlow(SessionState())
12 | val uiState: StateFlow = _uiState.asStateFlow()
13 |
14 | fun setCurrentUser(currentUser: UserAccount) {
15 | _uiState.value = _uiState.value.copy(userAccount = currentUser)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/app/src/main/java/com/paradoxo/threadscompose/ui/login/LoginScreen.kt:
--------------------------------------------------------------------------------
1 | package com.paradoxo.threadscompose.ui.login
2 |
3 | import android.util.Log
4 | import androidx.activity.compose.rememberLauncherForActivityResult
5 | import androidx.activity.result.contract.ActivityResultContracts
6 | import androidx.compose.foundation.Image
7 | import androidx.compose.foundation.background
8 | import androidx.compose.foundation.border
9 | import androidx.compose.foundation.layout.Arrangement
10 | import androidx.compose.foundation.layout.Box
11 | import androidx.compose.foundation.layout.Column
12 | import androidx.compose.foundation.layout.Row
13 | import androidx.compose.foundation.layout.Spacer
14 | import androidx.compose.foundation.layout.fillMaxHeight
15 | import androidx.compose.foundation.layout.fillMaxSize
16 | import androidx.compose.foundation.layout.fillMaxWidth
17 | import androidx.compose.foundation.layout.height
18 | import androidx.compose.foundation.layout.padding
19 | import androidx.compose.foundation.layout.size
20 | import androidx.compose.foundation.shape.RoundedCornerShape
21 | import androidx.compose.material3.MaterialTheme
22 | import androidx.compose.material3.Text
23 | import androidx.compose.runtime.Composable
24 | import androidx.compose.runtime.DisposableEffect
25 | import androidx.compose.runtime.collectAsState
26 | import androidx.compose.runtime.getValue
27 | import androidx.compose.runtime.remember
28 | import androidx.compose.runtime.rememberCoroutineScope
29 | import androidx.compose.ui.Alignment
30 | import androidx.compose.ui.Modifier
31 | import androidx.compose.ui.graphics.Color
32 | import androidx.compose.ui.platform.LocalContext
33 | import androidx.compose.ui.res.painterResource
34 | import androidx.compose.ui.res.stringResource
35 | import androidx.compose.ui.text.font.FontWeight
36 | import androidx.compose.ui.text.style.TextAlign
37 | import androidx.compose.ui.tooling.preview.Preview
38 | import androidx.compose.ui.unit.dp
39 | import androidx.compose.ui.unit.sp
40 | import androidx.lifecycle.viewmodel.compose.viewModel
41 | import coil.compose.AsyncImage
42 | import com.facebook.CallbackManager
43 | import com.facebook.FacebookCallback
44 | import com.facebook.FacebookException
45 | import com.facebook.Profile
46 | import com.facebook.login.LoginManager
47 | import com.facebook.login.LoginResult
48 | import com.google.android.gms.auth.api.signin.GoogleSignIn
49 | import com.google.android.gms.auth.api.signin.GoogleSignInOptions
50 | import com.google.firebase.auth.FacebookAuthProvider
51 | import com.google.firebase.auth.GoogleAuthProvider
52 | import com.google.firebase.auth.ktx.auth
53 | import com.google.firebase.ktx.Firebase
54 | import com.paradoxo.threadscompose.R
55 | import com.paradoxo.threadscompose.ui.theme.ThreadsComposeTheme
56 | import com.paradoxo.threadscompose.utils.noRippleClickable
57 | import com.paradoxo.threadscompose.utils.showMessage
58 | import kotlinx.coroutines.launch
59 | import kotlinx.coroutines.tasks.await
60 |
61 | @Composable
62 | internal fun LoginScreen(
63 | loginViewModel: LoginViewModel = viewModel(),
64 | onAuthComplete: (String?) -> Unit = {},
65 | ) {
66 | val loginState by loginViewModel.uiState.collectAsState()
67 |
68 | val context = LocalContext.current
69 |
70 | Column(
71 | Modifier
72 | .fillMaxSize()
73 | ) {
74 | when (loginState.appState) {
75 | AppState.Loading -> {
76 | SplashScreen()
77 | }
78 |
79 | AppState.LoggedIn -> {
80 | if (loginState.profileInServer) {
81 | onAuthComplete(loginState.currentUserName)
82 | } else {
83 | onAuthComplete(null)
84 | }
85 | }
86 |
87 | AppState.LoggedOut -> {
88 | LoggedOutScreen(
89 | onAuthComplete = { profileName ->
90 | if (profileName == null) {
91 | onAuthComplete(null)
92 | } else {
93 | onAuthComplete(profileName)
94 | }
95 | Log.i("login", "onAuthComplete: $profileName")
96 | },
97 | onAuthError = {
98 | context.showMessage("Erro ao fazer login, tente novamente")
99 | },
100 | onAuthCancel = {
101 | context.showMessage("Login cancelado")
102 | }
103 | )
104 | }
105 | }
106 | }
107 | }
108 |
109 | @Composable
110 | fun LoggedOutScreen(
111 | onAuthComplete: (String?) -> Unit = {},
112 | onAuthError: () -> Unit = {},
113 | onAuthCancel: () -> Unit = {}
114 | ) {
115 | val scope = rememberCoroutineScope()
116 | val loginManager = LoginManager.getInstance()
117 | val callbackManager = remember { CallbackManager.Factory.create() }
118 |
119 | val context = LocalContext.current
120 |
121 | val facebookAuthLauncher = rememberLauncherForActivityResult(
122 | contract = loginManager.createLogInActivityResultContract(callbackManager, null),
123 | onResult = {
124 | // The result are handled by the callbackManager
125 | }
126 | )
127 |
128 | val googleAuthLauncher = rememberLauncherForActivityResult(
129 | contract = ActivityResultContracts.StartActivityForResult(),
130 | onResult = {
131 | val data = it.data
132 | val account = GoogleSignIn.getSignedInAccountFromIntent(data).result
133 | val credential = GoogleAuthProvider.getCredential(account.idToken, null)
134 |
135 | Firebase.auth.signInWithCredential(credential).addOnCompleteListener {
136 | if (it.isSuccessful) {
137 | onAuthComplete(account?.displayName)
138 |
139 | } else {
140 | Log.e("googleAuth", "Erro ao logar com o Google", it.exception)
141 | onAuthError()
142 | }
143 | }
144 | }
145 | )
146 |
147 | val gso =
148 | GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
149 | .requestIdToken(stringResource(R.string.default_web_client_id))
150 | .requestEmail()
151 | .build()
152 |
153 | val client = GoogleSignIn.getClient(context, gso)
154 |
155 | DisposableEffect(Unit) {
156 | loginManager.registerCallback(callbackManager, object : FacebookCallback {
157 | override fun onCancel() {
158 | onAuthCancel()
159 | }
160 |
161 | override fun onError(error: FacebookException) {
162 | onAuthError()
163 | }
164 |
165 | override fun onSuccess(result: LoginResult) {
166 | scope.launch {
167 | val token = result.accessToken.token
168 | val credential = FacebookAuthProvider.getCredential(token)
169 | val authResult = Firebase.auth.signInWithCredential(credential).await()
170 | if (authResult.user != null) {
171 | Profile.getCurrentProfile()?.name?.let { profileName ->
172 | onAuthComplete(profileName)
173 | } ?: run {
174 | Log.e("login", "Login error: Profile name is null")
175 | onAuthError()
176 | }
177 | } else {
178 | Log.i("login", "Unable to sign in with Facebook")
179 | onAuthError()
180 | }
181 | }
182 | }
183 | })
184 |
185 | onDispose {
186 | loginManager.unregisterCallback(callbackManager)
187 | }
188 | }
189 |
190 | Column(
191 | Modifier
192 | .fillMaxSize(),
193 | horizontalAlignment = Alignment.CenterHorizontally,
194 | ) {
195 | Image(
196 | painter = painterResource(id = R.drawable.bg_lines_1),
197 | contentDescription = "app logo"
198 | )
199 |
200 | Column(
201 | Modifier.padding(16.dp),
202 | horizontalAlignment = Alignment.CenterHorizontally,
203 | ) {
204 | Row(
205 | modifier = Modifier
206 | .noRippleClickable {
207 | facebookAuthLauncher.launch(listOf("email", "public_profile"))
208 | }
209 | .fillMaxWidth()
210 | .background(
211 | color = Color.White,
212 | shape = RoundedCornerShape(25)
213 | )
214 | .border(
215 | width = 1.dp,
216 | color = Color.LightGray,
217 | shape = RoundedCornerShape(25)
218 | )
219 | .padding(horizontal = 16.dp, vertical = 4.dp),
220 | horizontalArrangement = Arrangement.SpaceBetween,
221 | verticalAlignment = Alignment.CenterVertically
222 | ) {
223 | Column {
224 | Text(
225 | text = "Entrar com Facebook",
226 | style = MaterialTheme.typography.bodyLarge.copy(
227 | color = Color.Gray.copy(alpha = 0.8f)
228 | )
229 | )
230 | Text(
231 | text = "Fazer login",
232 | style = MaterialTheme.typography.titleMedium
233 | )
234 | }
235 |
236 | Image(
237 | painter = painterResource(id = R.drawable.ic_facebook),
238 | contentDescription = "logo instagram",
239 | modifier = Modifier
240 | .padding(16.dp)
241 | .size(50.dp)
242 | )
243 | }
244 |
245 | Spacer(modifier = Modifier.height(16.dp))
246 |
247 | Column(
248 | modifier = Modifier
249 | .fillMaxWidth()
250 | .height(48.dp)
251 | .background(Color(66, 133, 244, 255), shape = RoundedCornerShape(8.dp))
252 | .padding(8.dp)
253 | .noRippleClickable {
254 | googleAuthLauncher.launch(client.signInIntent)
255 | }
256 | ) {
257 | Box(
258 | Modifier.fillMaxHeight(),
259 | contentAlignment = Alignment.CenterStart
260 | ) {
261 | AsyncImage(
262 | model = "https://lh3.googleusercontent.com/COxitqgJr1sJnIDe8-jiKhxDx1FrYbtRHKJ9z_hELisAlapwE9LUPh6fcXIfb5vwpbMl4xl9H9TRFPc5NOO8Sb3VSgIBrfRYvW6cUA",
263 | contentDescription = "Logo do Google",
264 | modifier = Modifier
265 | .background(Color.White, shape = RoundedCornerShape(4.dp))
266 | .padding(8.dp)
267 | )
268 | Text(
269 | text = "Entrar com o Google",
270 | modifier = Modifier.fillMaxWidth(),
271 | textAlign = TextAlign.Center,
272 | color = Color.White,
273 | fontSize = 14.sp,
274 | fontWeight = FontWeight.Bold
275 | )
276 | }
277 | }
278 |
279 | Spacer(modifier = Modifier.height(16.dp))
280 | Text(
281 | text = "Entrar como convidado",
282 | style = MaterialTheme.typography.bodyLarge.copy(
283 | color = Color.Gray.copy(alpha = 0.8f),
284 | textAlign = TextAlign.Center
285 | ),
286 | modifier = Modifier
287 | .fillMaxWidth()
288 | .noRippleClickable {
289 | onAuthComplete(null)
290 | }
291 | .padding(horizontal = 16.dp, vertical = 22.dp)
292 | )
293 | }
294 |
295 | }
296 | }
297 |
298 | @Preview(showBackground = true)
299 | @Composable
300 | fun LoggedOutScreenPreview() {
301 | ThreadsComposeTheme {
302 | LoggedOutScreen()
303 | }
304 | }
305 |
306 | @Preview(showBackground = true)
307 | @Composable
308 | fun LoginScreenPreview() {
309 | ThreadsComposeTheme {
310 | LoginScreen()
311 | }
312 | }
313 |
--------------------------------------------------------------------------------
/app/src/main/java/com/paradoxo/threadscompose/ui/login/LoginScreenState.kt:
--------------------------------------------------------------------------------
1 | package com.paradoxo.threadscompose.ui.login
2 |
3 | sealed class AppState {
4 | object Loading : AppState()
5 | object LoggedIn : AppState()
6 | object LoggedOut : AppState()
7 | }
8 |
9 | data class LoginState(
10 | var appState: AppState = AppState.Loading,
11 | var profileInServer: Boolean = false,
12 | val currentUserName: String = "",
13 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/paradoxo/threadscompose/ui/login/LoginViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.paradoxo.threadscompose.ui.login
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import com.facebook.Profile
6 | import com.google.firebase.auth.FirebaseAuth
7 | import com.google.firebase.auth.ktx.auth
8 | import com.google.firebase.ktx.Firebase
9 | import com.paradoxo.threadscompose.model.UserAccount
10 | import com.paradoxo.threadscompose.network.firebase.UserFirestore
11 | import kotlinx.coroutines.delay
12 | import kotlinx.coroutines.flow.MutableStateFlow
13 | import kotlinx.coroutines.flow.StateFlow
14 | import kotlinx.coroutines.flow.asStateFlow
15 | import kotlinx.coroutines.launch
16 | import kotlin.random.Random
17 |
18 |
19 | internal class LoginViewModel : ViewModel() {
20 | private val _uiState = MutableStateFlow(LoginState())
21 | val uiState: StateFlow = _uiState.asStateFlow()
22 |
23 | private var auth: FirebaseAuth = Firebase.auth
24 | private var userFirestore = UserFirestore()
25 |
26 | init {
27 | verifyLoginState()
28 | }
29 |
30 | private fun verifyLoginState() {
31 | viewModelScope.launch {
32 | delay(Random.nextLong(600, 1200))
33 | if (auth.currentUser != null) {
34 | _uiState.value = _uiState.value.copy(
35 | appState = AppState.LoggedIn,
36 | profileInServer = true,
37 | currentUserName = auth.currentUser?.displayName ?: ""
38 | )
39 | } else {
40 | _uiState.value = LoginState(AppState.LoggedOut)
41 | }
42 | }
43 | }
44 |
45 |
46 | fun getProfile(
47 | onSuccess: (UserAccount) -> Unit = {},
48 | onError: () -> Unit = {},
49 | ) {
50 | val userId = auth.currentUser?.uid
51 | userId?.let {
52 | userFirestore.getUserById(
53 | userId = userId,
54 | onSuccess = {
55 | onSuccess(it)
56 | },
57 | onError = onError
58 | )
59 | } ?: onError()
60 | }
61 |
62 | fun saveNewUser(
63 | userAccount: UserAccount,
64 | onSuccess: () -> Unit,
65 | onError: () -> Unit
66 | ) {
67 | auth.currentUser?.let {
68 | val user = userAccount.copy(
69 | id = it.uid,
70 | name = it.displayName ?: "",
71 | userName = it.displayName?.replace(" ", "") ?: "",
72 | )
73 |
74 | userFirestore.saveUser(
75 | userId = it.uid,
76 | userAccount = user,
77 | onSuccess = onSuccess,
78 | onError = onError
79 | )
80 | } ?: onError()
81 | }
82 |
83 | fun getCurrentUser(): UserAccount {
84 | val currentUser = Firebase.auth.currentUser
85 | val profile = Profile.getCurrentProfile()
86 | val profileFacebookPicUri = profile?.getProfilePictureUri(200, 200).toString()
87 |
88 | val photoUrl = currentUser?.photoUrl.toString().replace("=s96", "=s200")
89 | val profileImage = profileFacebookPicUri.ifEmpty { photoUrl }
90 |
91 | val userAccount = UserAccount(
92 | id = currentUser?.uid ?: "",
93 | name = currentUser?.displayName ?: "",
94 | userName = currentUser?.displayName?.lowercase()?.replace(" ", "_") ?: "",
95 | imageProfileUrl = profileImage
96 | )
97 |
98 | return userAccount
99 | }
100 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/paradoxo/threadscompose/ui/login/SplashScreen.kt:
--------------------------------------------------------------------------------
1 | package com.paradoxo.threadscompose.ui.login
2 |
3 | import androidx.compose.foundation.Image
4 | import androidx.compose.foundation.layout.Arrangement
5 | import androidx.compose.foundation.layout.Column
6 | import androidx.compose.foundation.layout.fillMaxSize
7 | import androidx.compose.foundation.layout.padding
8 | import androidx.compose.foundation.layout.size
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.ui.Alignment
11 | import androidx.compose.ui.Modifier
12 | import androidx.compose.ui.res.painterResource
13 | import androidx.compose.ui.tooling.preview.Preview
14 | import androidx.compose.ui.unit.dp
15 | import com.paradoxo.threadscompose.R
16 | import com.paradoxo.threadscompose.ui.theme.ThreadsComposeTheme
17 |
18 | @Composable
19 | internal fun SplashScreen() {
20 | Column(
21 | Modifier
22 | .fillMaxSize()
23 | .padding(16.dp),
24 | horizontalAlignment = Alignment.CenterHorizontally,
25 | verticalArrangement = Arrangement.Center
26 | ) {
27 | Image(
28 | painter = painterResource(id = R.drawable.ic_logo_colors),
29 | contentDescription = "app logo",
30 | modifier = Modifier.size(200.dp)
31 | )
32 | }
33 | }
34 |
35 | @Preview(showBackground = true)
36 | @Composable
37 | private fun SplashScreenPreview() {
38 | ThreadsComposeTheme {
39 | SplashScreen()
40 | }
41 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/paradoxo/threadscompose/ui/navigation/Destinations.kt:
--------------------------------------------------------------------------------
1 | package com.paradoxo.threadscompose.ui.navigation
2 |
3 | import com.paradoxo.threadscompose.R
4 |
5 |
6 | sealed class Destinations(val route: String, val resourceId: Pair? = null) {
7 | object Login : Destinations("login")
8 | object Feed : Destinations("feed", Pair(R.drawable.ic_home, R.drawable.ic_home_outlined))
9 | object Search : Destinations("search", Pair(R.drawable.ic_search, R.drawable.ic_search))
10 | object Post : Destinations("post", Pair(R.drawable.ic_post, R.drawable.ic_post))
11 | object Notifications :
12 | Destinations("notifications", Pair(R.drawable.ic_heart, R.drawable.ic_heart_outlined))
13 |
14 | object Profile :
15 | Destinations("profile", Pair(R.drawable.ic_profile, R.drawable.ic_profile_outlined))
16 |
17 | object ProfileEdit : Destinations("profileEdit")
18 |
19 | object LoginNavigation : Destinations("loginNavigation")
20 | object HomeNavigation : Destinations("homeNavigation")
21 | }
22 |
23 | val screenItems = listOf(
24 | Destinations.Feed,
25 | Destinations.Search,
26 | Destinations.Post,
27 | Destinations.Notifications,
28 | Destinations.Profile
29 | )
30 |
--------------------------------------------------------------------------------
/app/src/main/java/com/paradoxo/threadscompose/ui/navigation/ThreadsNavController.kt:
--------------------------------------------------------------------------------
1 | package com.paradoxo.threadscompose.ui.navigation
2 |
3 | import androidx.compose.foundation.layout.PaddingValues
4 | import androidx.compose.foundation.layout.padding
5 | import androidx.compose.runtime.State
6 | import androidx.compose.runtime.collectAsState
7 | import androidx.compose.runtime.getValue
8 | import androidx.compose.runtime.mutableStateOf
9 | import androidx.compose.runtime.remember
10 | import androidx.compose.runtime.rememberCoroutineScope
11 | import androidx.compose.runtime.toMutableStateList
12 | import androidx.compose.ui.Modifier
13 | import androidx.compose.ui.platform.LocalContext
14 | import androidx.compose.ui.unit.dp
15 | import androidx.lifecycle.viewmodel.compose.viewModel
16 | import androidx.navigation.NavGraphBuilder
17 | import androidx.navigation.compose.composable
18 | import androidx.navigation.navigation
19 | import com.google.firebase.auth.ktx.auth
20 | import com.google.firebase.ktx.Firebase
21 | import com.paradoxo.threadscompose.model.UserAccount
22 | import com.paradoxo.threadscompose.network.firebase.PostFirestore
23 | import com.paradoxo.threadscompose.sampleData.SampleData
24 | import com.paradoxo.threadscompose.ui.feed.FeedScreen
25 | import com.paradoxo.threadscompose.ui.feed.FeedViewModel
26 | import com.paradoxo.threadscompose.ui.home.SessionState
27 | import com.paradoxo.threadscompose.ui.login.LoginScreen
28 | import com.paradoxo.threadscompose.ui.login.LoginViewModel
29 | import com.paradoxo.threadscompose.ui.notification.NotificationScreenState
30 | import com.paradoxo.threadscompose.ui.notification.NotificationsScreen
31 | import com.paradoxo.threadscompose.ui.post.PostScreen
32 | import com.paradoxo.threadscompose.ui.profile.ProfileEditScreen
33 | import com.paradoxo.threadscompose.ui.profile.ProfileScreen
34 | import com.paradoxo.threadscompose.ui.search.SearchScreen
35 | import com.paradoxo.threadscompose.utils.showMessage
36 | import kotlinx.coroutines.launch
37 |
38 | internal fun NavGraphBuilder.loginGraph(
39 | onNavigateToHome: (UserAccount) -> Unit = {},
40 | onNavigateToProfileEdit: () -> Unit = {}
41 | ) {
42 | navigation(
43 | startDestination = Destinations.Login.route,
44 | route = Destinations.LoginNavigation.route
45 | ) {
46 | composable(Destinations.Login.route) {
47 | val loginViewModel: LoginViewModel = viewModel()
48 |
49 | LoginScreen(
50 | loginViewModel = loginViewModel,
51 | onAuthComplete = {
52 | it?.let {
53 | loginViewModel.getProfile(
54 | onSuccess = { userAccountOnFirebase ->
55 | if (userAccountOnFirebase.userName.isNotEmpty()) {
56 | onNavigateToHome(userAccountOnFirebase)
57 | } else {
58 | onNavigateToProfileEdit()
59 | }
60 | },
61 | onError = {
62 | onNavigateToProfileEdit()
63 | }
64 | )
65 | } ?: run {
66 | onNavigateToHome(SampleData().generateSampleInvitedUser())
67 | }
68 | },
69 | )
70 | }
71 |
72 | composable(Destinations.ProfileEdit.route) {
73 | val loginViewModel: LoginViewModel = viewModel()
74 | val context = LocalContext.current
75 |
76 | ProfileEditScreen(
77 | userAccount = loginViewModel.getCurrentUser(),
78 | onSave = { userAccountUpdated ->
79 | loginViewModel.saveNewUser(
80 | userAccount = userAccountUpdated,
81 | onSuccess = {
82 | onNavigateToHome(userAccountUpdated)
83 | },
84 | onError = {
85 | context.showMessage("Erro ao salvar informações do perfil")
86 | }
87 | )
88 | }
89 | )
90 | }
91 | }
92 | }
93 |
94 | internal fun NavGraphBuilder.homeGraph(
95 | state: State,
96 | onNavigateToInstagram: () -> Unit = {},
97 | onBack: () -> Unit = {},
98 | paddingValues: PaddingValues = PaddingValues(0.dp)
99 | ) {
100 | navigation(
101 | startDestination = Destinations.Feed.route,
102 | route = Destinations.HomeNavigation.route
103 | ) {
104 | composable(Destinations.Feed.route) {
105 | val postViewModel: FeedViewModel = viewModel()
106 | val postState by postViewModel.uiState.collectAsState()
107 |
108 | FeedScreen(
109 | posts = postState.posts,
110 | idCurrentUserProfile = state.value.userAccount.id,
111 | onLikeClick = {
112 | postViewModel.likePost(it)
113 | },
114 | onReload = {
115 | postViewModel.searchNewPosts()
116 | }
117 | )
118 | }
119 | composable(Destinations.Search.route) { SearchScreen() }
120 | composable(Destinations.Post.route) {
121 |
122 | val scope = rememberCoroutineScope()
123 | val context = LocalContext.current
124 |
125 | PostScreen(
126 | currentUser = state.value.userAccount,
127 | onBack = {
128 | onBack()
129 | },
130 | onSendPost = { posts ->
131 | if (Firebase.auth.currentUser == null) {
132 | context.showMessage("Você precisa estar logado para publicar")
133 | return@PostScreen
134 | }
135 | context.showMessage("Publicando")
136 | val postFirestore = PostFirestore()
137 | scope.launch {
138 | postFirestore.savePost(
139 | posts = posts,
140 | onSuccess = {
141 | context.showMessage("Publicado com sucesso")
142 | onBack()
143 | },
144 | onError = {
145 | context.showMessage("Erro ao publicar")
146 | onBack()
147 | }
148 | )
149 | }
150 | }
151 | )
152 | }
153 | composable(Destinations.Notifications.route) {
154 | val notificationState by remember { mutableStateOf(NotificationScreenState()) }
155 | notificationState.notifications.addAll(SampleData().notifications)
156 |
157 | val allNotifications = notificationState.notifications
158 | val notifications = allNotifications.toMutableStateList()
159 |
160 | NotificationsScreen(
161 | allNotifications = allNotifications,
162 | notifications = notifications
163 | )
164 | }
165 | composable(Destinations.Profile.route) {
166 |
167 | val postLists = remember { SampleData().posts }.toMutableStateList()
168 |
169 | ProfileScreen(
170 | currentUser = state.value.userAccount,
171 | postLists = postLists,
172 | repliesList = postLists.shuffled().toMutableList(),
173 | modifier = Modifier.padding(paddingValues),
174 | onNavigateToInstagram = {
175 | onNavigateToInstagram()
176 | }
177 | )
178 | }
179 | }
180 | }
181 |
--------------------------------------------------------------------------------
/app/src/main/java/com/paradoxo/threadscompose/ui/navigation/ThreadsNavHost.kt:
--------------------------------------------------------------------------------
1 | package com.paradoxo.threadscompose.ui.navigation
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.collectAsState
5 | import androidx.lifecycle.viewmodel.compose.viewModel
6 | import androidx.navigation.NavGraph.Companion.findStartDestination
7 | import androidx.navigation.NavHostController
8 | import androidx.navigation.compose.NavHost
9 | import com.paradoxo.threadscompose.ui.home.SessionViewModel
10 |
11 | @Composable
12 | fun ThreadsNavHost(
13 | navController: NavHostController,
14 | navigateToInstagram: () -> Unit = {}
15 | ) {
16 | val sessionViewModel: SessionViewModel = viewModel()
17 | val state = sessionViewModel.uiState.collectAsState()
18 |
19 | NavHost(
20 | navController = navController,
21 | startDestination = Destinations.LoginNavigation.route
22 | ) {
23 | loginGraph(
24 | onNavigateToHome = { currentUser ->
25 | sessionViewModel.setCurrentUser(currentUser)
26 |
27 | navController.navigate(Destinations.HomeNavigation.route) {
28 | popUpTo(navController.graph.findStartDestination().id) {
29 | saveState = true
30 | }
31 | launchSingleTop = true
32 | restoreState = true
33 | }
34 | },
35 | onNavigateToProfileEdit = {
36 | navController.navigate(Destinations.ProfileEdit.route) {
37 | popUpTo(navController.graph.findStartDestination().id) {
38 | saveState = true
39 | }
40 |
41 | launchSingleTop = true
42 | restoreState = true
43 | }
44 | }
45 | )
46 | homeGraph(
47 | state = state,
48 | onNavigateToInstagram = {
49 | navigateToInstagram()
50 | },
51 | onBack = {
52 | navController.popBackStack()
53 | }
54 | )
55 | }
56 | }
57 |
58 |
--------------------------------------------------------------------------------
/app/src/main/java/com/paradoxo/threadscompose/ui/notification/NotificationScreenState.kt:
--------------------------------------------------------------------------------
1 | package com.paradoxo.threadscompose.ui.notification
2 |
3 | import com.paradoxo.threadscompose.model.Notification
4 |
5 | internal data class NotificationScreenState(
6 | val notifications: MutableList = mutableListOf(),
7 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/paradoxo/threadscompose/ui/notification/NotificationsScreen.kt:
--------------------------------------------------------------------------------
1 | package com.paradoxo.threadscompose.ui.notification
2 |
3 | import androidx.compose.foundation.Image
4 | import androidx.compose.foundation.background
5 | import androidx.compose.foundation.border
6 | import androidx.compose.foundation.clickable
7 | import androidx.compose.foundation.horizontalScroll
8 | import androidx.compose.foundation.layout.Arrangement
9 | import androidx.compose.foundation.layout.Box
10 | import androidx.compose.foundation.layout.Column
11 | import androidx.compose.foundation.layout.IntrinsicSize
12 | import androidx.compose.foundation.layout.Row
13 | import androidx.compose.foundation.layout.Spacer
14 | import androidx.compose.foundation.layout.fillMaxHeight
15 | import androidx.compose.foundation.layout.fillMaxSize
16 | import androidx.compose.foundation.layout.fillMaxWidth
17 | import androidx.compose.foundation.layout.height
18 | import androidx.compose.foundation.layout.offset
19 | import androidx.compose.foundation.layout.padding
20 | import androidx.compose.foundation.layout.size
21 | import androidx.compose.foundation.layout.sizeIn
22 | import androidx.compose.foundation.layout.width
23 | import androidx.compose.foundation.lazy.LazyColumn
24 | import androidx.compose.foundation.lazy.items
25 | import androidx.compose.foundation.rememberScrollState
26 | import androidx.compose.foundation.shape.CircleShape
27 | import androidx.compose.foundation.shape.RoundedCornerShape
28 | import androidx.compose.material.icons.Icons
29 | import androidx.compose.material.icons.filled.Notifications
30 | import androidx.compose.material.icons.filled.Person
31 | import androidx.compose.material3.Divider
32 | import androidx.compose.material3.ExperimentalMaterial3Api
33 | import androidx.compose.material3.FilterChip
34 | import androidx.compose.material3.FilterChipDefaults
35 | import androidx.compose.material3.Icon
36 | import androidx.compose.material3.MaterialTheme
37 | import androidx.compose.material3.Scaffold
38 | import androidx.compose.material3.Text
39 | import androidx.compose.material3.TopAppBar
40 | import androidx.compose.runtime.Composable
41 | import androidx.compose.runtime.LaunchedEffect
42 | import androidx.compose.runtime.getValue
43 | import androidx.compose.runtime.mutableIntStateOf
44 | import androidx.compose.runtime.mutableStateOf
45 | import androidx.compose.runtime.remember
46 | import androidx.compose.runtime.saveable.rememberSaveable
47 | import androidx.compose.runtime.setValue
48 | import androidx.compose.ui.Alignment
49 | import androidx.compose.ui.Modifier
50 | import androidx.compose.ui.draw.clip
51 | import androidx.compose.ui.graphics.Color
52 | import androidx.compose.ui.graphics.painter.Painter
53 | import androidx.compose.ui.graphics.vector.rememberVectorPainter
54 | import androidx.compose.ui.res.painterResource
55 | import androidx.compose.ui.text.font.FontWeight
56 | import androidx.compose.ui.text.style.TextAlign
57 | import androidx.compose.ui.tooling.preview.Preview
58 | import androidx.compose.ui.unit.dp
59 | import androidx.compose.ui.unit.sp
60 | import com.paradoxo.threadscompose.R
61 | import com.paradoxo.threadscompose.model.Notification
62 | import com.paradoxo.threadscompose.model.NotificationTypeEnum
63 | import com.paradoxo.threadscompose.sampleData.SampleData
64 | import com.paradoxo.threadscompose.ui.theme.ThreadsComposeTheme
65 | import com.paradoxo.threadscompose.utils.noRippleClickable
66 |
67 | @OptIn(ExperimentalMaterial3Api::class)
68 | @Composable
69 | fun NotificationsScreen(
70 | modifier: Modifier = Modifier,
71 | allNotifications: MutableList,
72 | notifications: MutableList
73 | ) {
74 |
75 | val tabItems = listOf("Tudo", "Respostas", "Menções", "Verificado")
76 |
77 | Scaffold(
78 | modifier = modifier,
79 | topBar = {
80 | Column {
81 | TopAppBar(
82 | title = {
83 | Text(
84 | text = "Atividade",
85 | style = MaterialTheme.typography.titleLarge.copy(
86 | fontWeight = FontWeight.Bold,
87 | fontSize = 32.sp,
88 | ),
89 | )
90 | })
91 | NotificationTabs(
92 | tabItems = tabItems,
93 | onItemSelected = { selectedItem ->
94 | if (selectedItem == 0) {
95 | notifications.clear()
96 | notifications.addAll(allNotifications)
97 | return@NotificationTabs
98 | } else {
99 | val filterList = allNotifications.filter {
100 | it.type == NotificationTypeEnum.values()[selectedItem]
101 | }
102 | notifications.clear()
103 | notifications.addAll(filterList)
104 | }
105 | }
106 | )
107 | }
108 | },
109 | ) { paddingValues ->
110 | Column(
111 | Modifier
112 | .fillMaxWidth()
113 | .padding(paddingValues = paddingValues)
114 |
115 | ) {
116 | if (notifications.isNotEmpty()) {
117 |
118 | LazyColumn(
119 | modifier = modifier
120 | .fillMaxSize()
121 | ) {
122 | items(
123 | notifications,
124 | key = { notification -> notification.id }
125 | ) { notification ->
126 | val isFollowing = rememberSaveable {
127 | mutableStateOf(notification.isFollowing)
128 | }
129 | NotificationItem(
130 | notification = notification,
131 | onFollowClick = {
132 | isFollowing.value = !isFollowing.value
133 | },
134 | isFollowing = isFollowing.value
135 | )
136 | }
137 | }
138 | } else {
139 | Box(
140 | modifier = Modifier
141 | .fillMaxSize()
142 | .padding(16.dp),
143 | contentAlignment = Alignment.Center
144 | ) {
145 | Text(
146 | text = "Ainda não há nada para ver aqui",
147 | color = Color.Black.copy(alpha = 0.4f),
148 | )
149 | }
150 | }
151 | }
152 | }
153 | }
154 |
155 | @Composable
156 | @OptIn(ExperimentalMaterial3Api::class)
157 | private fun NotificationTabs(
158 | tabItems: List,
159 | onItemSelected: (Int) -> Unit = {}
160 | ) {
161 | val scrollState = rememberScrollState()
162 | var selectedItem by remember { mutableIntStateOf(0) }
163 |
164 | LaunchedEffect(selectedItem) {
165 | scrollState.animateScrollTo(selectedItem * 100)
166 | }
167 |
168 | Row(
169 | Modifier
170 | .fillMaxWidth()
171 | .horizontalScroll(scrollState),
172 | horizontalArrangement = Arrangement.spacedBy(8.dp)
173 | ) {
174 |
175 | Spacer(modifier = Modifier.width(4.dp))
176 | tabItems.forEachIndexed { index, item ->
177 | FilterChip(
178 | selected = selectedItem == index,
179 | onClick = {
180 | selectedItem = index
181 | onItemSelected(selectedItem)
182 | },
183 | colors = FilterChipDefaults.filterChipColors(
184 | selectedLabelColor = MaterialTheme.colorScheme.background,
185 | selectedContainerColor = MaterialTheme.colorScheme.onPrimaryContainer,
186 | ),
187 | border = FilterChipDefaults.filterChipBorder(
188 | borderColor = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.2f),
189 | ),
190 | shape = RoundedCornerShape(30),
191 | label = {
192 | Text(
193 | text = item,
194 | fontSize = 14.sp,
195 | textAlign = TextAlign.Center,
196 | modifier = Modifier
197 | .sizeIn(minWidth = 68.dp)
198 | .padding(
199 | vertical = 8.dp
200 | )
201 | )
202 | },
203 |
204 | )
205 | }
206 | Spacer(modifier = Modifier.width(4.dp))
207 | }
208 | }
209 |
210 | @Composable
211 | private fun NotificationItem(
212 | notification: Notification,
213 | onFollowClick: (Notification) -> Unit = {},
214 | isFollowing: Boolean = false,
215 | ) {
216 | val dividerColor = Color.Gray.copy(alpha = 0.2f)
217 | val iconSpecs = getIconByType(notification.type)
218 |
219 | Column {
220 | Row(
221 | Modifier
222 | .fillMaxWidth()
223 | .height(IntrinsicSize.Min)
224 | ) {
225 | Box(
226 | Modifier
227 | .padding(vertical = 8.dp, horizontal = 10.dp),
228 | contentAlignment = Alignment.BottomEnd,
229 | ) {
230 | Image(
231 | painter = painterResource(id = notification.image),
232 | contentDescription = "avatar",
233 | modifier = Modifier
234 | .size(42.dp)
235 | .clip(CircleShape),
236 | )
237 |
238 | Box(
239 | modifier = Modifier
240 | .offset(x = 2.dp, y = 2.dp)
241 | .background(
242 | Color.White,
243 | CircleShape
244 | )
245 | .padding(2.dp)
246 | ) {
247 | Box(
248 | modifier = Modifier
249 | .background(
250 | iconSpecs.second,
251 | CircleShape
252 | )
253 | ) {
254 | Icon(
255 | painter = iconSpecs.first,
256 | contentDescription = "Action",
257 | tint = Color.White,
258 | modifier = Modifier
259 | .size(20.dp)
260 | .clip(CircleShape)
261 | .padding(5.dp)
262 |
263 | )
264 | }
265 | }
266 | }
267 |
268 | Column(
269 | Modifier
270 | .padding(vertical = 8.dp)
271 | .weight(0.5f)
272 | ) {
273 | Row(
274 | Modifier.fillMaxWidth(),
275 | verticalAlignment = Alignment.CenterVertically
276 | ) {
277 | Text(
278 | text = notification.title,
279 | fontWeight = FontWeight.Bold,
280 | )
281 |
282 | Text(
283 | text = " ${notification.time}",
284 | color = Color.Black.copy(alpha = 0.4f),
285 | modifier = Modifier.padding(start = 2.dp)
286 | )
287 | }
288 |
289 | Text(
290 | text = notification.description,
291 | color = Color.Black.copy(alpha = 0.4f),
292 | )
293 | notification.extraContent?.let {
294 | Spacer(
295 | modifier = Modifier.height(4.dp)
296 | )
297 | Text(
298 | text = it,
299 | )
300 | }
301 | }
302 |
303 | Row(
304 | modifier = Modifier
305 | .fillMaxWidth()
306 | .fillMaxHeight()
307 | .padding(vertical = 8.dp, horizontal = 8.dp)
308 | .weight(0.3f),
309 | verticalAlignment = Alignment.CenterVertically,
310 | ) {
311 | val textIsFollowing = if (isFollowing) {
312 | "Seguindo"
313 | } else {
314 | "Seguir"
315 | }
316 |
317 | val textColorByIsFollowState = if (notification.isFollowing) {
318 | Color.Gray
319 | } else {
320 | Color.Black
321 | }
322 |
323 | if (notification.type == NotificationTypeEnum.Follow) {
324 | Box(
325 | contentAlignment = Alignment.Center,
326 | modifier = Modifier
327 | .clickable {}
328 | .fillMaxWidth()
329 | .border(
330 | width = 1.dp,
331 | color = Color.LightGray,
332 | shape = RoundedCornerShape(10.dp)
333 | )
334 | .clip(RoundedCornerShape(40))
335 | .noRippleClickable {
336 | onFollowClick(notification)
337 | }
338 | ) {
339 | Text(
340 | text = textIsFollowing,
341 | fontWeight = FontWeight.Bold,
342 | color = textColorByIsFollowState,
343 | modifier = Modifier
344 | .padding(vertical = 6.dp),
345 | )
346 | }
347 | }
348 | }
349 | }
350 |
351 | Row {
352 | Spacer(
353 | modifier = Modifier
354 | .fillMaxWidth()
355 | .weight(0.2f)
356 |
357 | )
358 | Divider(
359 | color = dividerColor,
360 | modifier = Modifier
361 | .weight(0.9f)
362 | .height(1.dp)
363 | .fillMaxWidth()
364 | )
365 | }
366 | }
367 | }
368 |
369 | @Composable
370 | fun getIconByType(type: NotificationTypeEnum): Pair {
371 | return when (type) {
372 | NotificationTypeEnum.Follow -> Pair(
373 | rememberVectorPainter(Icons.Default.Person),
374 | Color(0xFF6B3AEE)
375 | )
376 |
377 | NotificationTypeEnum.Like -> Pair(
378 | painterResource(id = R.drawable.ic_heart),
379 | Color(0xFFFB0169)
380 | )
381 |
382 | NotificationTypeEnum.Comment -> Pair(
383 | painterResource(id = R.drawable.ic_reply),
384 | Color(0xFF1FC1FC)
385 | )
386 |
387 | NotificationTypeEnum.Mention -> Pair(
388 | painterResource(id = R.drawable.ic_attach_file),
389 | Color(0xFF18C686)
390 | )
391 |
392 | else -> Pair(rememberVectorPainter(Icons.Default.Notifications), Color.Black)
393 | }
394 | }
395 |
396 |
397 | @Preview(showBackground = true)
398 | @Composable
399 | private fun NotificationItemPreview() {
400 | NotificationItem(
401 | SampleData().notifications.first()
402 | )
403 | }
404 |
405 | @Preview(showBackground = true)
406 | @Composable
407 | fun NotificationsScreenPreview() {
408 | ThreadsComposeTheme {
409 | NotificationsScreen(
410 | allNotifications = mutableListOf(),
411 | notifications = mutableListOf(),
412 | )
413 | }
414 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/paradoxo/threadscompose/ui/post/PostScreenState.kt:
--------------------------------------------------------------------------------
1 | package com.paradoxo.threadscompose.ui.post
2 |
3 | import com.paradoxo.threadscompose.model.Post
4 | import com.paradoxo.threadscompose.model.UserAccount
5 | import com.paradoxo.threadscompose.utils.getCurrentTime
6 |
7 | internal data class PostScreenState(
8 | val userAccount: UserAccount,
9 | var content: String,
10 | var medias: MutableList = mutableListOf(),
11 | val date: String,
12 | val isFirstPost: Boolean = false
13 | ) {
14 | fun toPost() = Post(
15 | userAccount = userAccount,
16 | date = getCurrentTime(),
17 | description = content,
18 | medias = medias,
19 | likes = emptyList(),
20 | comments = emptyList(),
21 | )
22 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/paradoxo/threadscompose/ui/profile/ProfileEditScreen.kt:
--------------------------------------------------------------------------------
1 | package com.paradoxo.threadscompose.ui.profile
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.border
5 | import androidx.compose.foundation.layout.Arrangement
6 | import androidx.compose.foundation.layout.Box
7 | import androidx.compose.foundation.layout.Column
8 | import androidx.compose.foundation.layout.Row
9 | import androidx.compose.foundation.layout.Spacer
10 | import androidx.compose.foundation.layout.fillMaxSize
11 | import androidx.compose.foundation.layout.fillMaxWidth
12 | import androidx.compose.foundation.layout.height
13 | import androidx.compose.foundation.layout.imePadding
14 | import androidx.compose.foundation.layout.padding
15 | import androidx.compose.foundation.layout.size
16 | import androidx.compose.foundation.rememberScrollState
17 | import androidx.compose.foundation.shape.CircleShape
18 | import androidx.compose.foundation.shape.RoundedCornerShape
19 | import androidx.compose.foundation.text.BasicTextField
20 | import androidx.compose.foundation.verticalScroll
21 | import androidx.compose.material.icons.Icons
22 | import androidx.compose.material.icons.filled.Lock
23 | import androidx.compose.material3.Button
24 | import androidx.compose.material3.ButtonDefaults
25 | import androidx.compose.material3.Divider
26 | import androidx.compose.material3.Icon
27 | import androidx.compose.material3.MaterialTheme
28 | import androidx.compose.material3.Text
29 | import androidx.compose.runtime.Composable
30 | import androidx.compose.runtime.getValue
31 | import androidx.compose.runtime.mutableStateOf
32 | import androidx.compose.runtime.remember
33 | import androidx.compose.runtime.setValue
34 | import androidx.compose.ui.Alignment
35 | import androidx.compose.ui.Modifier
36 | import androidx.compose.ui.draw.clip
37 | import androidx.compose.ui.graphics.Color
38 | import androidx.compose.ui.graphics.SolidColor
39 | import androidx.compose.ui.layout.ContentScale
40 | import androidx.compose.ui.res.painterResource
41 | import androidx.compose.ui.text.TextStyle
42 | import androidx.compose.ui.text.font.FontWeight
43 | import androidx.compose.ui.tooling.preview.Preview
44 | import androidx.compose.ui.unit.dp
45 | import androidx.compose.ui.unit.sp
46 | import coil.compose.AsyncImage
47 | import com.paradoxo.threadscompose.R
48 | import com.paradoxo.threadscompose.model.UserAccount
49 |
50 |
51 | @Composable
52 | fun ProfileEditScreen(
53 | modifier: Modifier = Modifier,
54 | userAccount: UserAccount,
55 | onSave: (UserAccount) -> Unit = {}
56 | ) {
57 | var bioEditTextState by remember {
58 | mutableStateOf(userAccount.bio)
59 | }
60 |
61 | var linkEditTextState by remember {
62 | mutableStateOf(userAccount.link)
63 | }
64 |
65 | val scrollState = rememberScrollState()
66 | Column(
67 | modifier = modifier
68 | .imePadding()
69 | .fillMaxSize()
70 | .verticalScroll(scrollState)
71 | .padding(16.dp),
72 | horizontalAlignment = Alignment.CenterHorizontally,
73 | verticalArrangement = Arrangement.SpaceEvenly
74 | ) {
75 |
76 | Column(
77 | horizontalAlignment = Alignment.CenterHorizontally,
78 | ) {
79 | Text(
80 | text = "Perfil",
81 | style = MaterialTheme.typography.headlineLarge,
82 | fontWeight = FontWeight.Bold,
83 | )
84 | Text(
85 | text = "Personalize seu perfil no Lines",
86 | color = Color.Gray.copy(alpha = 0.8f)
87 | )
88 | }
89 |
90 | Column {
91 | Column(
92 | modifier = Modifier
93 | .border(
94 | width = 1.dp,
95 | color = Color.Black.copy(alpha = 0.15f),
96 | shape = RoundedCornerShape(12.dp)
97 | )
98 | .background(Color.White, shape = RoundedCornerShape(12.dp))
99 | .padding(16.dp)
100 | ) {
101 | Row(
102 | Modifier.fillMaxWidth(),
103 | horizontalArrangement = Arrangement.SpaceBetween,
104 | ) {
105 | Column {
106 | Text(
107 | text = "Nome",
108 | fontWeight = FontWeight.Bold,
109 | )
110 | Spacer(modifier = Modifier.padding(4.dp))
111 | Row(
112 | verticalAlignment = Alignment.CenterVertically
113 | ) {
114 | Icon(
115 | Icons.Default.Lock,
116 | contentDescription = "lock",
117 | Modifier.size(14.dp)
118 | )
119 |
120 | Spacer(modifier = Modifier.padding(horizontal = 2.dp))
121 |
122 | Text("${userAccount.name} (${userAccount.userName})")
123 | }
124 | }
125 |
126 | AsyncImage(
127 | model = userAccount.imageProfileUrl,
128 | placeholder = painterResource(id = R.drawable.placeholder_image),
129 | error = painterResource(id = R.drawable.placeholder_image),
130 | contentDescription = "logo imagem perfil",
131 | contentScale = ContentScale.Crop,
132 | modifier = Modifier
133 | .clip(CircleShape)
134 | .size(50.dp)
135 | )
136 | }
137 | Row {
138 | Divider(
139 | color = Color.Gray.copy(alpha = 0.15f),
140 | modifier = Modifier.weight(0.8f)
141 | )
142 | Spacer(modifier = Modifier.weight(0.2f))
143 | }
144 | Spacer(modifier = Modifier.height(16.dp))
145 |
146 | Row(
147 | Modifier.fillMaxWidth(),
148 | ) {
149 | Column {
150 | Text(
151 | text = "Bio",
152 | fontWeight = FontWeight.Bold,
153 | )
154 | Spacer(modifier = Modifier.padding(4.dp))
155 |
156 | BasicTextField(
157 | value = bioEditTextState,
158 | onValueChange = { bioEditTextState = it },
159 | modifier = Modifier.fillMaxWidth(),
160 | maxLines = 3,
161 | textStyle = TextStyle.Default.copy(
162 | fontSize = 16.sp
163 | ),
164 | decorationBox = { innerValue ->
165 | Box {
166 | if (bioEditTextState.isEmpty()) {
167 | Text(
168 | text = "+Escrever bio",
169 | color = Color.Gray.copy(alpha = 0.8f)
170 | )
171 | }
172 | innerValue()
173 | }
174 | },
175 | cursorBrush = SolidColor(MaterialTheme.colorScheme.surfaceVariant)
176 | )
177 |
178 | }
179 | }
180 |
181 | Divider(color = Color.Gray.copy(alpha = 0.15f))
182 | Spacer(modifier = Modifier.height(16.dp))
183 |
184 | Row(
185 | Modifier.fillMaxWidth(),
186 | ) {
187 | Column {
188 | Text(
189 | text = "Link",
190 | fontWeight = FontWeight.Bold,
191 | )
192 | Spacer(modifier = Modifier.padding(4.dp))
193 |
194 | BasicTextField(
195 | value = linkEditTextState,
196 | onValueChange = { linkEditTextState = it },
197 | modifier = Modifier.fillMaxWidth(),
198 | maxLines = 3,
199 | textStyle = TextStyle.Default.copy(
200 | fontSize = 16.sp
201 | ),
202 | decorationBox = { innerValue ->
203 | Box {
204 | if (linkEditTextState.isEmpty()) {
205 | Text(
206 | text = "+Adicionar link",
207 | color = Color.Gray.copy(alpha = 0.8f)
208 | )
209 | }
210 | innerValue()
211 | }
212 | },
213 | cursorBrush = SolidColor(MaterialTheme.colorScheme.surfaceVariant)
214 | )
215 |
216 | }
217 | }
218 | }
219 |
220 | Spacer(modifier = Modifier.height(16.dp))
221 | Button(
222 | onClick = {
223 | onSave(
224 | userAccount.copy(
225 | bio = bioEditTextState,
226 | link = linkEditTextState
227 | )
228 | )
229 | },
230 | modifier = Modifier
231 | .fillMaxWidth(),
232 | shape = RoundedCornerShape(25),
233 | colors = ButtonDefaults.buttonColors(
234 | containerColor = Color.Black,
235 | contentColor = Color.White
236 | )
237 | ) {
238 | Text(
239 | text = "Link Start!",
240 | style = MaterialTheme.typography.titleMedium.copy(
241 | color = Color.White,
242 | fontWeight = FontWeight.Bold
243 | ),
244 | modifier = Modifier.padding(16.dp)
245 | )
246 | }
247 | }
248 | }
249 | }
250 |
251 | @Preview(showBackground = true)
252 | @Composable
253 | fun ProfileEditScreenPreview() {
254 | ProfileEditScreen(
255 | userAccount = UserAccount(
256 | name = "Nome",
257 | bio = "Bio",
258 | link = "Link",
259 | imageProfileUrl = "https://bit.ly/43CtVSz"
260 | )
261 | )
262 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/paradoxo/threadscompose/ui/search/SearchScreen.kt:
--------------------------------------------------------------------------------
1 | package com.paradoxo.threadscompose.ui.search
2 |
3 | import android.view.ViewTreeObserver
4 | import androidx.compose.foundation.ExperimentalFoundationApi
5 | import androidx.compose.foundation.background
6 | import androidx.compose.foundation.border
7 | import androidx.compose.foundation.clickable
8 | import androidx.compose.foundation.layout.Arrangement
9 | import androidx.compose.foundation.layout.Box
10 | import androidx.compose.foundation.layout.Column
11 | import androidx.compose.foundation.layout.Row
12 | import androidx.compose.foundation.layout.Spacer
13 | import androidx.compose.foundation.layout.fillMaxSize
14 | import androidx.compose.foundation.layout.fillMaxWidth
15 | import androidx.compose.foundation.layout.height
16 | import androidx.compose.foundation.layout.offset
17 | import androidx.compose.foundation.layout.padding
18 | import androidx.compose.foundation.layout.size
19 | import androidx.compose.foundation.layout.width
20 | import androidx.compose.foundation.lazy.LazyColumn
21 | import androidx.compose.foundation.lazy.items
22 | import androidx.compose.foundation.lazy.rememberLazyListState
23 | import androidx.compose.foundation.shape.CircleShape
24 | import androidx.compose.foundation.shape.RoundedCornerShape
25 | import androidx.compose.foundation.text.BasicTextField
26 | import androidx.compose.material.icons.Icons
27 | import androidx.compose.material.icons.filled.ArrowBack
28 | import androidx.compose.material.icons.filled.Close
29 | import androidx.compose.material.icons.filled.Search
30 | import androidx.compose.material3.Divider
31 | import androidx.compose.material3.Icon
32 | import androidx.compose.material3.MaterialTheme
33 | import androidx.compose.material3.Scaffold
34 | import androidx.compose.material3.Text
35 | import androidx.compose.runtime.Composable
36 | import androidx.compose.runtime.DisposableEffect
37 | import androidx.compose.runtime.LaunchedEffect
38 | import androidx.compose.runtime.derivedStateOf
39 | import androidx.compose.runtime.getValue
40 | import androidx.compose.runtime.mutableStateOf
41 | import androidx.compose.runtime.remember
42 | import androidx.compose.runtime.setValue
43 | import androidx.compose.ui.Alignment
44 | import androidx.compose.ui.Modifier
45 | import androidx.compose.ui.draw.alpha
46 | import androidx.compose.ui.draw.clip
47 | import androidx.compose.ui.graphics.Color
48 | import androidx.compose.ui.platform.LocalSoftwareKeyboardController
49 | import androidx.compose.ui.platform.LocalView
50 | import androidx.compose.ui.res.painterResource
51 | import androidx.compose.ui.text.TextStyle
52 | import androidx.compose.ui.text.font.FontWeight
53 | import androidx.compose.ui.tooling.preview.Preview
54 | import androidx.compose.ui.unit.dp
55 | import androidx.compose.ui.unit.sp
56 | import androidx.core.view.ViewCompat
57 | import androidx.core.view.WindowInsetsCompat
58 | import coil.compose.AsyncImage
59 | import com.paradoxo.threadscompose.R
60 | import com.paradoxo.threadscompose.model.UserAccount
61 | import com.paradoxo.threadscompose.sampleData.SampleData
62 | import com.paradoxo.threadscompose.ui.theme.ThreadsComposeTheme
63 |
64 | @OptIn(ExperimentalFoundationApi::class)
65 | @Composable
66 | fun SearchScreen(
67 | modifier: Modifier = Modifier,
68 | ) {
69 | val accountLists = SampleData().userAccounts
70 | val listState = rememberLazyListState()
71 |
72 | var showBackIcon by remember {
73 | mutableStateOf(false)
74 | }
75 |
76 | var showHeaderTitle by remember {
77 | mutableStateOf(false)
78 | }
79 |
80 | LaunchedEffect(showHeaderTitle) {
81 | if (showHeaderTitle) listState.animateScrollToItem(1) else listState.animateScrollToItem(0)
82 | }
83 |
84 | val alphaHeaderTitle by remember {
85 | derivedStateOf {
86 | val isFirstItem = listState.firstVisibleItemIndex == 0
87 | val offset = listState.firstVisibleItemScrollOffset
88 |
89 | if (isFirstItem) {
90 | if (offset > 0) {
91 | 1 - (offset / 100f).coerceIn(0f, 1f)
92 | } else {
93 | 1f
94 | }
95 | } else {
96 | 0f
97 | }
98 | }
99 | }
100 |
101 | Scaffold { paddingValues ->
102 |
103 | LazyColumn(
104 | state = listState,
105 | modifier = modifier
106 | .fillMaxSize()
107 | .padding(paddingValues)
108 | ) {
109 |
110 | item {
111 | Row(
112 | modifier = Modifier
113 | .padding(horizontal = 8.dp)
114 | .alpha(alphaHeaderTitle)
115 | ) {
116 | Text(
117 | text = "Pesquisar",
118 | style = MaterialTheme.typography.titleLarge.copy(
119 | fontWeight = FontWeight.Bold,
120 | fontSize = 32.sp,
121 | ),
122 | )
123 | }
124 | }
125 |
126 | stickyHeader {
127 | SearchBar(showBackIcon = showBackIcon,
128 | onBackClick = {
129 | showHeaderTitle = it
130 | showBackIcon = it
131 | })
132 | }
133 |
134 | items(accountLists) { account ->
135 | AccountItem(account)
136 | }
137 | }
138 | }
139 | }
140 |
141 | @Composable
142 | private fun SearchBar(
143 | modifier: Modifier = Modifier,
144 | onBackClick: (Boolean) -> Unit = {},
145 | showBackIcon: Boolean = false,
146 | ) {
147 | var searchEditTextState by remember {
148 | mutableStateOf("")
149 | }
150 |
151 | val keyboard = LocalSoftwareKeyboardController.current
152 | LaunchedEffect(showBackIcon) {
153 | if (!showBackIcon) {
154 | keyboard?.hide()
155 | }
156 | onBackClick(showBackIcon)
157 | }
158 |
159 | val view = LocalView.current
160 | val viewTreeObserver = view.viewTreeObserver
161 |
162 | DisposableEffect(viewTreeObserver) {
163 | val listener = ViewTreeObserver.OnGlobalLayoutListener {
164 | val isKeyboardOpen = ViewCompat.getRootWindowInsets(view)
165 | ?.isVisible(WindowInsetsCompat.Type.ime()) ?: true
166 | if (isKeyboardOpen) {
167 | onBackClick(true)
168 | }
169 | }
170 |
171 | viewTreeObserver.addOnGlobalLayoutListener(listener)
172 |
173 | onDispose {
174 | if (viewTreeObserver.isAlive) {
175 | viewTreeObserver.removeOnGlobalLayoutListener(listener)
176 | } else {
177 | view.viewTreeObserver.removeOnGlobalLayoutListener(listener)
178 | }
179 | }
180 | }
181 |
182 | Row(
183 | modifier = modifier
184 | .fillMaxWidth()
185 | .background(
186 | MaterialTheme.colorScheme.background
187 | ),
188 | verticalAlignment = Alignment.CenterVertically,
189 | ) {
190 | if (showBackIcon) {
191 | Icon(
192 | Icons.Default.ArrowBack,
193 | contentDescription = "back",
194 | modifier = Modifier
195 | .padding(horizontal = 16.dp, vertical = 8.dp)
196 | .size(24.dp)
197 | .clickable {
198 | searchEditTextState = ""
199 | onBackClick(false)
200 | },
201 | tint = Color.Gray,
202 | )
203 | }
204 |
205 | BasicTextField(
206 | value = searchEditTextState,
207 | onValueChange = {
208 | searchEditTextState = it
209 | },
210 | maxLines = 1,
211 | textStyle = TextStyle.Default.copy(
212 | fontSize = 18.sp,
213 | color = MaterialTheme.colorScheme.onBackground
214 | ),
215 | decorationBox = { innerValue ->
216 | Row(
217 | modifier = Modifier
218 | .fillMaxWidth()
219 | .padding(8.dp)
220 | .clip(RoundedCornerShape(8.dp))
221 | .background(Color.LightGray.copy(alpha = 0.4f)),
222 | verticalAlignment = Alignment.CenterVertically,
223 | horizontalArrangement = Arrangement.SpaceBetween
224 | ) {
225 | Row(
226 | verticalAlignment = Alignment.CenterVertically,
227 | ) {
228 | Icon(
229 | Icons.Default.Search,
230 | contentDescription = "search",
231 | modifier = Modifier
232 | .padding(horizontal = 16.dp, vertical = 8.dp)
233 | .size(24.dp),
234 | tint = Color.Gray.copy(alpha = 0.8f)
235 | )
236 |
237 | Box {
238 | if (searchEditTextState.isEmpty()) {
239 | Text(
240 | text = "Pesquisar",
241 | color = Color.Gray.copy(alpha = 0.8f),
242 | )
243 | }
244 | innerValue()
245 | }
246 | }
247 |
248 | if (searchEditTextState.isNotEmpty()) {
249 | Icon(
250 | Icons.Default.Close,
251 | contentDescription = "close",
252 | modifier = Modifier
253 | .padding(horizontal = 16.dp, vertical = 8.dp)
254 | .size(24.dp)
255 | .clickable {
256 | searchEditTextState = ""
257 | },
258 | tint = Color.Gray,
259 | )
260 | }
261 | }
262 | }
263 | )
264 | }
265 | }
266 |
267 | @Composable
268 | private fun AccountItem(account: UserAccount) {
269 | val dividerColor = Color.Gray.copy(alpha = 0.2f)
270 |
271 | Row(
272 | Modifier
273 | .fillMaxWidth()
274 | ) {
275 | Column(
276 | Modifier
277 | .padding(start = 8.dp, top = 8.dp, bottom = 8.dp)
278 | .weight(0.2f),
279 | horizontalAlignment = Alignment.CenterHorizontally
280 | ) {
281 | AsyncImage(
282 | model = account.imageProfileUrl,
283 | placeholder = painterResource(id = R.drawable.placeholder_image),
284 | error = painterResource(id = R.drawable.placeholder_image),
285 | contentDescription = "avatar",
286 | modifier = Modifier
287 | .padding(horizontal = 10.dp)
288 | .clip(CircleShape)
289 | )
290 | }
291 |
292 | Column(
293 | Modifier
294 | .padding(vertical = 8.dp)
295 | .weight(0.5f)
296 | ) {
297 |
298 | Text(
299 | text = account.userName,
300 | fontWeight = FontWeight.Bold,
301 | )
302 |
303 | Text(
304 | text = account.name,
305 | color = Color.Gray
306 | )
307 |
308 | Spacer(
309 | modifier = Modifier.height(8.dp)
310 | )
311 |
312 | Row(
313 | Modifier.fillMaxWidth(),
314 | verticalAlignment = Alignment.CenterVertically
315 | ) {
316 | Row {
317 | AsyncImage(
318 | model = R.drawable.profile_pic_emoji_1,
319 | placeholder = painterResource(id = R.drawable.placeholder_image),
320 | error = painterResource(id = R.drawable.placeholder_image),
321 | contentDescription = "avatar",
322 | modifier = Modifier
323 | .size(22.dp)
324 | .offset(x = (-2).dp)
325 | .border(
326 | width = 2.dp,
327 | color = Color.White,
328 | shape = CircleShape
329 | )
330 | .clip(CircleShape)
331 | .padding(2.dp)
332 | )
333 |
334 | AsyncImage(
335 | model = R.drawable.profile_pic_emoji_3,
336 | placeholder = painterResource(id = R.drawable.placeholder_image),
337 | error = painterResource(id = R.drawable.placeholder_image),
338 | contentDescription = "avatar",
339 | modifier = Modifier
340 | .size(22.dp)
341 | .offset(x = (-10).dp)
342 | .border(
343 | width = 2.dp,
344 | color = Color.White,
345 | shape = CircleShape
346 | )
347 | .clip(CircleShape)
348 | .padding(2.dp)
349 | )
350 | }
351 |
352 | Text(
353 | text = account.followers.size.toString(),
354 | )
355 | Spacer(modifier = Modifier.width(4.dp))
356 | Text(
357 | text = "Seguidores",
358 | )
359 | }
360 | }
361 |
362 | Row(
363 | modifier = Modifier
364 | .fillMaxWidth()
365 | .padding(vertical = 8.dp, horizontal = 8.dp)
366 | .weight(0.3f),
367 | verticalAlignment = Alignment.CenterVertically,
368 | horizontalArrangement = Arrangement.Center
369 | ) {
370 | val isFollowing by remember {
371 | mutableStateOf(true)
372 | }
373 |
374 | val textIsFollowing = if (isFollowing) {
375 | "Seguindo"
376 | } else {
377 | "Seguir"
378 | }
379 |
380 | val textColorByIsFollowState = if (isFollowing) {
381 | Color.Gray
382 | } else {
383 | Color.Black
384 | }
385 |
386 | Box(
387 | contentAlignment = Alignment.Center,
388 | modifier = Modifier
389 | .fillMaxWidth()
390 | .background(
391 | color = Color.Transparent,
392 | shape = RoundedCornerShape(10.dp)
393 | )
394 | .border(
395 | width = 1.dp,
396 | color = Color.LightGray,
397 | shape = RoundedCornerShape(10.dp)
398 | )
399 | .clip(RoundedCornerShape(40))
400 |
401 | ) {
402 | Text(
403 | text = textIsFollowing,
404 | fontWeight = FontWeight.Bold,
405 | color = textColorByIsFollowState,
406 | modifier = Modifier
407 | .padding(vertical = 6.dp),
408 | )
409 | }
410 | }
411 | }
412 |
413 | Divider(
414 | color = dividerColor,
415 | modifier = Modifier
416 | .fillMaxWidth()
417 | )
418 | }
419 |
420 | @Preview(showBackground = true)
421 | @Composable
422 | fun SearchScreenPreview() {
423 | ThreadsComposeTheme {
424 | SearchScreen()
425 | }
426 | }
427 |
428 | @Preview(showBackground = true)
429 | @Composable
430 | private fun AccountItemPreview() {
431 | AccountItem(
432 | SampleData().userAccounts.first()
433 | )
434 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/paradoxo/threadscompose/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package com.paradoxo.threadscompose.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val Purple80 = Color(0xFFD0BCFF)
6 | val PurpleGrey80 = Color(0xFFCCC2DC)
7 | val Pink80 = Color(0xFFEFB8C8)
8 |
9 | val Purple40 = Color(0xFF6650a4)
10 | val PurpleGrey40 = Color(0xFF625b71)
11 | val Pink40 = Color(0xFF7D5260)
--------------------------------------------------------------------------------
/app/src/main/java/com/paradoxo/threadscompose/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package com.paradoxo.threadscompose.ui.theme
2 |
3 | import android.app.Activity
4 | import android.os.Build
5 | import androidx.compose.material3.MaterialTheme
6 | import androidx.compose.material3.darkColorScheme
7 | import androidx.compose.material3.dynamicDarkColorScheme
8 | import androidx.compose.material3.dynamicLightColorScheme
9 | import androidx.compose.material3.lightColorScheme
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.runtime.SideEffect
12 | import androidx.compose.ui.platform.LocalContext
13 | import androidx.compose.ui.platform.LocalView
14 | import androidx.core.view.WindowCompat
15 |
16 | private val DarkColorScheme = darkColorScheme(
17 | primary = Purple80,
18 | secondary = PurpleGrey80,
19 | tertiary = Pink80
20 | )
21 |
22 | private val LightColorScheme = lightColorScheme(
23 | primary = Purple40,
24 | secondary = PurpleGrey40,
25 | tertiary = Pink40
26 |
27 | /* Other default colors to override
28 | background = Color(0xFFFFFBFE),
29 | surface = Color(0xFFFFFBFE),
30 | onPrimary = Color.White,
31 | onSecondary = Color.White,
32 | onTertiary = Color.White,
33 | onBackground = Color(0xFF1C1B1F),
34 | onSurface = Color(0xFF1C1B1F),
35 | */
36 | )
37 |
38 | @Composable
39 | fun ThreadsComposeTheme(
40 | // darkTheme: Boolean = isSystemInDarkTheme(),
41 | darkTheme: Boolean = false,
42 | // Dynamic color is available on Android 12+
43 | dynamicColor: Boolean = true,
44 | content: @Composable () -> Unit
45 | ) {
46 | val colorScheme = when {
47 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
48 | val context = LocalContext.current
49 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
50 | }
51 |
52 | darkTheme -> DarkColorScheme
53 | else -> LightColorScheme
54 | }
55 | val view = LocalView.current
56 | if (!view.isInEditMode) {
57 | SideEffect {
58 | val window = (view.context as Activity).window
59 | WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = true
60 | WindowCompat.setDecorFitsSystemWindows(window, false)
61 | }
62 | }
63 |
64 | MaterialTheme(
65 | colorScheme = colorScheme,
66 | typography = Typography,
67 | content = content
68 | )
69 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/paradoxo/threadscompose/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package com.paradoxo.threadscompose.ui.theme
2 |
3 | import androidx.compose.material3.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.FontFamily
6 | import androidx.compose.ui.text.font.FontWeight
7 | import androidx.compose.ui.unit.sp
8 |
9 | // Set of Material typography styles to start with
10 | val Typography = Typography(
11 | bodyLarge = TextStyle(
12 | fontFamily = FontFamily.Default,
13 | fontWeight = FontWeight.Normal,
14 | fontSize = 16.sp,
15 | lineHeight = 24.sp,
16 | letterSpacing = 0.5.sp
17 | )
18 | /* Other default text styles to override
19 | titleLarge = TextStyle(
20 | fontFamily = FontFamily.Default,
21 | fontWeight = FontWeight.Normal,
22 | fontSize = 22.sp,
23 | lineHeight = 28.sp,
24 | letterSpacing = 0.sp
25 | ),
26 | labelSmall = TextStyle(
27 | fontFamily = FontFamily.Default,
28 | fontWeight = FontWeight.Medium,
29 | fontSize = 11.sp,
30 | lineHeight = 16.sp,
31 | letterSpacing = 0.5.sp
32 | )
33 | */
34 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/paradoxo/threadscompose/utils/Extensions.kt:
--------------------------------------------------------------------------------
1 | package com.paradoxo.threadscompose.utils
2 |
3 | import android.content.Context
4 | import android.widget.Toast
5 | import androidx.compose.foundation.clickable
6 | import androidx.compose.foundation.interaction.MutableInteractionSource
7 | import androidx.compose.runtime.remember
8 | import androidx.compose.ui.Modifier
9 | import androidx.compose.ui.composed
10 |
11 | fun Context.showMessage(message: String) {
12 | Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
13 | }
14 |
15 | fun Modifier.noRippleClickable(onClick: () -> Unit): Modifier = composed {
16 | clickable(indication = null,
17 | interactionSource = remember { MutableInteractionSource() }) {
18 | onClick()
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/app/src/main/java/com/paradoxo/threadscompose/utils/Utils.kt:
--------------------------------------------------------------------------------
1 | package com.paradoxo.threadscompose.utils
2 |
3 | import java.time.LocalDateTime
4 | import java.time.ZoneOffset
5 |
6 | fun getCurrentTime(): Long {
7 | return LocalDateTime.now().toEpochSecond(ZoneOffset.UTC)
8 | }
9 |
10 | fun formatTimeElapsed(start: Long, end: Long): String {
11 | val elapsedSeconds = end - start
12 |
13 | val secondsInMinute = 60
14 | val secondsInHour = 60 * secondsInMinute
15 | val secondsInDay = 24 * secondsInHour
16 | val secondsInWeek = 7 * secondsInDay
17 | val secondsInMonth = 30 * secondsInDay
18 |
19 | return when {
20 | elapsedSeconds < secondsInHour -> "${elapsedSeconds / secondsInMinute} min"
21 | elapsedSeconds < secondsInDay -> "${elapsedSeconds / secondsInHour} h"
22 | elapsedSeconds < secondsInWeek -> "${elapsedSeconds / secondsInDay} d"
23 | elapsedSeconds < secondsInMonth -> "${elapsedSeconds / secondsInWeek} sem"
24 | elapsedSeconds < secondsInMonth * 12 -> "${elapsedSeconds / secondsInMonth} m"
25 | else -> "${elapsedSeconds / (secondsInMonth * 12)} a"
26 | }
27 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/bg_lines_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/git-jr/Threads-Jetpack-Compose/6d5ee19ce593bdcdecda8eb4b4dd1252da50bed4/app/src/main/res/drawable/bg_lines_1.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_attach_file.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_circle.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_facebook.xml:
--------------------------------------------------------------------------------
1 |
6 |
13 |
20 |
27 |
34 |
41 |
48 |
55 |
62 |
69 |
76 |
83 |
90 |
97 |
104 |
111 |
118 |
125 |
132 |
139 |
146 |
153 |
160 |
167 |
174 |
181 |
188 |
195 |
202 |
209 |
216 |
223 |
230 |
237 |
244 |
251 |
258 |
265 |
272 |
279 |
284 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_heart.xml:
--------------------------------------------------------------------------------
1 |
6 |
13 |
20 |
27 |
34 |
41 |
48 |
55 |
62 |
69 |
76 |
83 |
90 |
97 |
104 |
111 |
118 |
125 |
132 |
139 |
146 |
153 |
160 |
167 |
174 |
181 |
188 |
195 |
200 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_insta.xml:
--------------------------------------------------------------------------------
1 |
6 |
13 |
20 |
27 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_menu_config.xml:
--------------------------------------------------------------------------------
1 |
6 |
13 |
20 |
27 |
34 |
41 |
48 |
55 |
62 |
69 |
76 |
83 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_reply.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/placeholder_image.xml:
--------------------------------------------------------------------------------
1 |
6 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/git-jr/Threads-Jetpack-Compose/6d5ee19ce593bdcdecda8eb4b4dd1252da50bed4/app/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/git-jr/Threads-Jetpack-Compose/6d5ee19ce593bdcdecda8eb4b4dd1252da50bed4/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/git-jr/Threads-Jetpack-Compose/6d5ee19ce593bdcdecda8eb4b4dd1252da50bed4/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/git-jr/Threads-Jetpack-Compose/6d5ee19ce593bdcdecda8eb4b4dd1252da50bed4/app/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/git-jr/Threads-Jetpack-Compose/6d5ee19ce593bdcdecda8eb4b4dd1252da50bed4/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/git-jr/Threads-Jetpack-Compose/6d5ee19ce593bdcdecda8eb4b4dd1252da50bed4/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/git-jr/Threads-Jetpack-Compose/6d5ee19ce593bdcdecda8eb4b4dd1252da50bed4/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/git-jr/Threads-Jetpack-Compose/6d5ee19ce593bdcdecda8eb4b4dd1252da50bed4/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/git-jr/Threads-Jetpack-Compose/6d5ee19ce593bdcdecda8eb4b4dd1252da50bed4/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/git-jr/Threads-Jetpack-Compose/6d5ee19ce593bdcdecda8eb4b4dd1252da50bed4/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/git-jr/Threads-Jetpack-Compose/6d5ee19ce593bdcdecda8eb4b4dd1252da50bed4/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/git-jr/Threads-Jetpack-Compose/6d5ee19ce593bdcdecda8eb4b4dd1252da50bed4/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/git-jr/Threads-Jetpack-Compose/6d5ee19ce593bdcdecda8eb4b4dd1252da50bed4/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/git-jr/Threads-Jetpack-Compose/6d5ee19ce593bdcdecda8eb4b4dd1252da50bed4/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/git-jr/Threads-Jetpack-Compose/6d5ee19ce593bdcdecda8eb4b4dd1252da50bed4/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/app/src/main/res/values/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #000000
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Threads Compose
3 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/app/src/test/java/com/paradoxo/threadscompose/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.paradoxo.threadscompose
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | @Suppress("DSL_SCOPE_VIOLATION") // TODO: Remove once KTIJ-19369 is fixed
3 | plugins {
4 | alias(libs.plugins.androidApplication) apply false
5 | alias(libs.plugins.kotlinAndroid) apply false
6 | alias(libs.plugins.googleServices) apply false
7 | }
8 | true // Needed to make the Suppress annotation work for the plugins block
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | agp = "8.2.0-alpha11"
3 | desugar_jdk_libs = "2.0.3"
4 | foundation = "1.6.0-alpha02"
5 | google-services = "4.3.15"
6 | kotlin = "1.8.10"
7 | core-ktx = "1.10.1"
8 | junit = "4.13.2"
9 | androidx-test-ext-junit = "1.1.5"
10 | espresso-core = "3.5.1"
11 | lifecycle-runtime-ktx = "2.6.1"
12 | activity-compose = "1.7.2"
13 | compose-bom = "2023.06.01"
14 | navigation-compose = "2.7.0-rc01"
15 | viewmodel-compose = "2.6.1"
16 | firebase-auth-ktx = "22.1.0"
17 | facebook-android-sdk = "16.1.3"
18 | coil-compose = "2.4.0"
19 | firebase-bom = "32.2.0"
20 | firebase-firestore-ktx = "24.7.0"
21 | firebase-storage-ktx = "20.2.1"
22 | lottie-compose = "6.1.0"
23 | play-services-auth = "20.7.0"
24 |
25 |
26 | [libraries]
27 | androidx-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "foundation" }
28 | coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil-compose" }
29 | firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref = "firebase-bom" }
30 | facebook-android-sdk = { module = "com.facebook.android:facebook-android-sdk", version.ref = "facebook-android-sdk" }
31 | google-services = { module = "com.google.gms:google-services", version.ref = "google-services" }
32 |
33 |
34 | lottie-compose = { module = "com.airbnb.android:lottie-compose", version.ref = "lottie-compose" }
35 | navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigation-compose" }
36 | lifecycle-viewmodel = { group = "androidx.lifecycle", name = "lifecycle-viewmodel", version.ref = "viewmodel-compose" }
37 | core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "core-ktx" }
38 | desugar_jdk_libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar_jdk_libs" }
39 | junit = { group = "junit", name = "junit", version.ref = "junit" }
40 | androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-ext-junit" }
41 | espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso-core" }
42 | lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle-runtime-ktx" }
43 | activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activity-compose" }
44 | compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" }
45 | ui = { group = "androidx.compose.ui", name = "ui" }
46 | ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
47 | ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
48 | ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
49 | ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
50 | ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
51 | material3 = { group = "androidx.compose.material3", name = "material3" }
52 | firebase-auth-ktx = { group = "com.google.firebase", name = "firebase-auth-ktx", version.ref = "firebase-auth-ktx" }
53 | firebase-firestore-ktx = { group = "com.google.firebase", name = "firebase-firestore-ktx", version.ref = "firebase-firestore-ktx" }
54 | firebase-storage-ktx = { group = "com.google.firebase", name = "firebase-storage-ktx", version.ref = "firebase-storage-ktx" }
55 | play-services-auth = { group = "com.google.android.gms", name = "play-services-auth", version.ref = "play-services-auth" }
56 |
57 |
58 |
59 | [plugins]
60 | androidApplication = { id = "com.android.application", version.ref = "agp" }
61 | kotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
62 | googleServices = { id = "com.google.gms.google-services", version.ref = "google-services" }
63 | [bundles]
64 |
65 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/git-jr/Threads-Jetpack-Compose/6d5ee19ce593bdcdecda8eb4b4dd1252da50bed4/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Tue Jul 11 23:21:02 BRT 2023
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | gradlePluginPortal()
6 | }
7 | }
8 | dependencyResolutionManagement {
9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
10 | repositories {
11 | google()
12 | mavenCentral()
13 | }
14 | }
15 |
16 | rootProject.name = "Threads Compose"
17 | include(":app")
18 |
--------------------------------------------------------------------------------