├── .gitignore ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── paradoxo │ │ └── materialgram │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── ic_launcher-playstore.png │ ├── java │ │ └── com │ │ │ └── paradoxo │ │ │ └── materialgram │ │ │ ├── MaterialGramApplication.kt │ │ │ ├── data │ │ │ ├── PostRepository.kt │ │ │ ├── VideoRepository.kt │ │ │ ├── local │ │ │ │ ├── PostLocalDataSource.kt │ │ │ │ └── VideoLocalDataSource.kt │ │ │ ├── model │ │ │ │ ├── PostResponse.kt │ │ │ │ └── VideoResponse.kt │ │ │ └── network │ │ │ │ ├── PostService.kt │ │ │ │ └── VideoService.kt │ │ │ ├── di │ │ │ ├── PostModule.kt │ │ │ ├── RetrofitModule.kt │ │ │ └── VideoModule.kt │ │ │ ├── domain │ │ │ ├── PostMapper.kt │ │ │ ├── PostUseCase.kt │ │ │ ├── ReelsMapper.kt │ │ │ ├── VideoUseCase.kt │ │ │ └── model │ │ │ │ ├── BasePost.kt │ │ │ │ ├── Media.kt │ │ │ │ ├── Post.kt │ │ │ │ ├── Reels.kt │ │ │ │ └── User.kt │ │ │ └── presentation │ │ │ ├── MainActivity.kt │ │ │ ├── adapter │ │ │ └── ImageAdapter.kt │ │ │ ├── components │ │ │ ├── AsyncImageWithShimmer.kt │ │ │ ├── HomeBottomBar.kt │ │ │ ├── HomeFAB.kt │ │ │ ├── HomeSearchAppBar.kt │ │ │ ├── ItemCarouselView.kt │ │ │ ├── ItemSingleImage.kt │ │ │ ├── ItemStory.kt │ │ │ └── ReelsVideoHud.kt │ │ │ ├── screens │ │ │ ├── feed │ │ │ │ └── ListPostsScreen.kt │ │ │ ├── home │ │ │ │ ├── HomeScreen.kt │ │ │ │ ├── HomeUiState.kt │ │ │ │ └── HomeViewModel.kt │ │ │ └── reels │ │ │ │ ├── ReelsScreen.kt │ │ │ │ ├── ReelsUiState.kt │ │ │ │ └── ReelsViewModel.kt │ │ │ └── theme │ │ │ ├── Color.kt │ │ │ ├── Theme.kt │ │ │ └── Type.kt │ └── res │ │ ├── drawable │ │ ├── custom_foreground.xml │ │ ├── ic_error.xml │ │ ├── ic_launcher_background.xml │ │ ├── ic_launcher_foreground.xml │ │ └── shimmer_animation.gif │ │ ├── layout │ │ └── image_item.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── values │ │ ├── colors.xml │ │ ├── ic_launcher_background.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ └── data_extraction_rules.xml │ └── test │ └── java │ └── com │ └── paradoxo │ └── materialgram │ └── 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![thumb-github](https://github.com/git-jr/MaterialGram/assets/35709152/ce3feeda-2656-4f86-9145-034a55c00f98) 2 | 3 | 💻 As seguintes tecnologias estão em uso no momento: 4 | - [Jetpack Compose][compose] - Interface de usuário 5 | - [ExoPlayer][exoplpayer] - Reprodução de vídeos 6 | - [Coil][coil] - Carregamento de imagens e gifs 7 | - [Jetpack Compose Animations][compose-animations] - Pequenas animações e transições de elementos de layout 8 | 9 | 10 | 🎨 Design de inspiração: [@jvepng][jvepng] 11 | 12 | ## 😎 Gostou do app? 13 | Clica ali na estrela ⭐ do topo para dar aquela força! 14 | 15 | [compose]: https://developer.android.com/jetpack/compose 16 | [exoplpayer]: https://developer.android.com/guide/topics/media/exoplayer 17 | [coil]: https://coil-kt.github.io/coil/compose/ 18 | [compose-animations]: https://developer.android.com/jetpack/compose/animation 19 | [jvepng]: https://twitter.com/jvepng/status/1699047334766862654?t=Sp-1FiShpAHPpvqkamad8w&s=19 20 | 21 | [releases]:https://github.com/git-jr/Threads-Jetpack-Compose/releases 22 | 23 | [tutorial-firebase]: https://firebase.google.com/docs/android/setup?hl=pt-br#create-firebase-project 24 | [tutorial-facebook-login-api]: https://developers.facebook.com/docs/facebook-login/android 25 | 26 | [video-recriando-threads]: https://youtu.be/Kr4Kn0ewnIw 27 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | @Suppress("DSL_SCOPE_VIOLATION") // TODO: Remove once KTIJ-19369 is fixed 2 | plugins { 3 | alias(libs.plugins.androidApplication) 4 | alias(libs.plugins.kotlinAndroid) 5 | alias(libs.plugins.ksp) 6 | alias(libs.plugins.hilt) 7 | } 8 | 9 | android { 10 | namespace = "com.paradoxo.materialgram" 11 | compileSdk = 34 12 | 13 | defaultConfig { 14 | applicationId = "com.paradoxo.materialgram" 15 | minSdk = 24 16 | targetSdk = 33 17 | versionCode = 1 18 | versionName = "1.0" 19 | 20 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 21 | vectorDrawables { 22 | useSupportLibrary = true 23 | } 24 | } 25 | 26 | buildTypes { 27 | release { 28 | isMinifyEnabled = false 29 | proguardFiles( 30 | getDefaultProguardFile("proguard-android-optimize.txt"), 31 | "proguard-rules.pro" 32 | ) 33 | } 34 | } 35 | compileOptions { 36 | sourceCompatibility = JavaVersion.VERSION_1_8 37 | targetCompatibility = JavaVersion.VERSION_1_8 38 | } 39 | kotlinOptions { 40 | jvmTarget = "1.8" 41 | } 42 | buildFeatures { 43 | compose = true 44 | } 45 | composeOptions { 46 | kotlinCompilerExtensionVersion = "1.5.1" 47 | } 48 | packaging { 49 | resources { 50 | excludes += "/META-INF/{AL2.0,LGPL2.1}" 51 | } 52 | } 53 | buildFeatures { 54 | viewBinding = true 55 | } 56 | } 57 | 58 | dependencies { 59 | 60 | implementation(libs.core.ktx) 61 | implementation(libs.lifecycle.runtime.ktx) 62 | implementation(libs.activity.compose) 63 | implementation(platform(libs.compose.bom)) 64 | implementation(libs.ui) 65 | implementation(libs.ui.graphics) 66 | implementation(libs.ui.tooling.preview) 67 | implementation(libs.material3) 68 | implementation(libs.appcompat) 69 | implementation(libs.constraintlayout) 70 | 71 | implementation(libs.material) 72 | implementation(libs.material3) 73 | implementation(libs.coil) 74 | implementation(libs.coil.compose) 75 | implementation(libs.coil.gif) 76 | 77 | implementation(libs.material.icons.extended) 78 | implementation(libs.viewmodel.compose) 79 | implementation(libs.bundles.retrofit) 80 | 81 | implementation(libs.hilt.android) 82 | ksp (libs.dagger.compiler) 83 | ksp (libs.hilt.compiler) 84 | 85 | implementation(libs.bundles.media3) 86 | 87 | 88 | testImplementation(libs.junit) 89 | androidTestImplementation(libs.androidx.test.ext.junit) 90 | androidTestImplementation(libs.espresso.core) 91 | androidTestImplementation(platform(libs.compose.bom)) 92 | androidTestImplementation(libs.ui.test.junit4) 93 | debugImplementation(libs.ui.tooling) 94 | debugImplementation(libs.ui.test.manifest) 95 | } -------------------------------------------------------------------------------- /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/src/androidTest/java/com/paradoxo/materialgram/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.paradoxo.materialgram 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.materialgram", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 18 | 19 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-jr/MaterialGram/d6291086d4c0ccbb9faa3e77ad8023d730973688/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/com/paradoxo/materialgram/MaterialGramApplication.kt: -------------------------------------------------------------------------------- 1 | package com.paradoxo.materialgram 2 | 3 | import android.app.Application 4 | import dagger.hilt.android.HiltAndroidApp 5 | 6 | @HiltAndroidApp 7 | class MaterialGramApplication : Application() -------------------------------------------------------------------------------- /app/src/main/java/com/paradoxo/materialgram/data/PostRepository.kt: -------------------------------------------------------------------------------- 1 | package com.paradoxo.materialgram.data 2 | 3 | import com.paradoxo.materialgram.data.local.PostLocalDataSource 4 | import com.paradoxo.materialgram.data.network.PostService 5 | import com.paradoxo.materialgram.domain.model.Post 6 | import com.paradoxo.materialgram.domain.toPost 7 | import kotlinx.coroutines.flow.Flow 8 | import kotlinx.coroutines.flow.flow 9 | import javax.inject.Inject 10 | 11 | interface PostRepository { 12 | suspend fun getPosts(): Flow> 13 | } 14 | 15 | class PostRepositoryImpl @Inject constructor( 16 | private val postLocalDataSource: PostLocalDataSource, 17 | private val postService: PostService 18 | ) : PostRepository { 19 | override suspend fun getPosts(): Flow> { 20 | 21 | return flow { 22 | val localPosts = postLocalDataSource.posts 23 | 24 | if (localPosts.isNotEmpty()) { 25 | emit(localPosts) 26 | } 27 | 28 | val remotePosts = postService.getPosts().map { it.toPost() } 29 | emit(localPosts + remotePosts) 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /app/src/main/java/com/paradoxo/materialgram/data/VideoRepository.kt: -------------------------------------------------------------------------------- 1 | package com.paradoxo.materialgram.data 2 | 3 | import com.paradoxo.materialgram.data.local.VideoLocalDataSource 4 | import com.paradoxo.materialgram.data.network.VideoService 5 | import com.paradoxo.materialgram.domain.model.Reels 6 | import com.paradoxo.materialgram.domain.toReels 7 | import kotlinx.coroutines.flow.Flow 8 | import kotlinx.coroutines.flow.flow 9 | import javax.inject.Inject 10 | 11 | interface VideoRepository { 12 | suspend fun getVideos(): Flow> 13 | } 14 | 15 | class VideoRepositoryImpl @Inject constructor( 16 | private val videoLocalDataSource: VideoLocalDataSource, 17 | private val videoService: VideoService 18 | ) : VideoRepository { 19 | override suspend fun getVideos(): Flow> { 20 | 21 | return flow { 22 | val localVideos = videoLocalDataSource.reels 23 | 24 | if (localVideos.isNotEmpty()) { 25 | emit(localVideos) 26 | } 27 | 28 | val remoteVideos = videoService.getVideos().map { it.toReels() } 29 | emit(localVideos + remoteVideos) 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /app/src/main/java/com/paradoxo/materialgram/data/local/PostLocalDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.paradoxo.materialgram.data.local 2 | 3 | import com.paradoxo.materialgram.domain.model.BasePost 4 | import com.paradoxo.materialgram.domain.model.Media 5 | import com.paradoxo.materialgram.domain.model.Post 6 | import com.paradoxo.materialgram.domain.model.User 7 | 8 | class PostLocalDataSource { 9 | 10 | private val imageArrayList = arrayListOf( 11 | "https://raw.githubusercontent.com/K6pkus/sample-api/main/loki-files/images/image%20(1).jpeg", 12 | "https://raw.githubusercontent.com/K6pkus/sample-api/main/loki-files/images/image%20(1).jpg", 13 | "https://raw.githubusercontent.com/K6pkus/sample-api/main/loki-files/images/image%20(2).jpg", 14 | "https://raw.githubusercontent.com/K6pkus/sample-api/main/loki-files/images/image%20(3).jpg", 15 | "https://raw.githubusercontent.com/K6pkus/sample-api/main/loki-files/images/image%20(4).jpg", 16 | "https://raw.githubusercontent.com/K6pkus/sample-api/main/loki-files/images/image%20(5).jpeg", 17 | "https://raw.githubusercontent.com/K6pkus/sample-api/main/loki-files/images/image%20(5).jpg", 18 | "https://raw.githubusercontent.com/K6pkus/sample-api/main/loki-files/images/image%20(10).jpg", 19 | "https://raw.githubusercontent.com/K6pkus/sample-api/main/loki-files/images/image%20(6).jpg", 20 | "https://raw.githubusercontent.com/K6pkus/sample-api/main/loki-files/images/image%20(7).jpg", 21 | "https://raw.githubusercontent.com/K6pkus/sample-api/main/loki-files/images/image%20(8).jpg", 22 | "https://raw.githubusercontent.com/K6pkus/sample-api/main/loki-files/images/image%20(9).jpg", 23 | "https://raw.githubusercontent.com/K6pkus/sample-api/main/loki-files/images/image%20(4).jpeg" 24 | ) 25 | 26 | private val medias = listOf( 27 | Media( 28 | imageArrayList[0], 29 | "" 30 | ), Media( 31 | imageArrayList[1], 32 | "" 33 | ), Media( 34 | imageArrayList[2], 35 | "" 36 | ), Media( 37 | imageArrayList[3], 38 | "" 39 | ), Media( 40 | imageArrayList[4], 41 | "" 42 | ), Media( 43 | imageArrayList[5], 44 | "" 45 | ) 46 | ) 47 | 48 | val users = listOf( 49 | User( 50 | name = "Thor", 51 | avatar = "https://raw.githubusercontent.com/K6pkus/sample-api/main/loki-files/images/profile-pics/thor.png", 52 | followers = 5000000, 53 | following = 100, 54 | posts = 1000, 55 | description = "God of Thunder", 56 | website = "https://www.example.com/thor", 57 | isFollowing = false, 58 | isMe = false 59 | ), 60 | User( 61 | name = "Odin", 62 | avatar = "https://raw.githubusercontent.com/K6pkus/sample-api/main/loki-files/images/profile-pics/odin.png", 63 | followers = 10000, 64 | following = 10, 65 | posts = 500, 66 | description = "King of Asgard", 67 | website = "https://www.example.com/odin", 68 | isFollowing = false, 69 | isMe = false 70 | ), 71 | User( 72 | name = "Lady Sif", 73 | avatar = "https://raw.githubusercontent.com/K6pkus/sample-api/main/loki-files/images/profile-pics/lady-sif.png", 74 | followers = 5000, 75 | following = 20, 76 | posts = 200, 77 | description = "Warrior", 78 | website = "", 79 | isFollowing = false, 80 | isMe = false 81 | ), 82 | User( 83 | name = "Mobius", 84 | avatar = "https://raw.githubusercontent.com/K6pkus/sample-api/main/loki-files/images/profile-pics/mobius.png", 85 | followers = 100000, 86 | following = 50, 87 | posts = 800, 88 | description = "Time Variance Authority", 89 | website = "https://www.example.com/Mobius", 90 | isFollowing = false, 91 | isMe = false 92 | ), 93 | User( 94 | name = "Hela", 95 | avatar = "https://raw.githubusercontent.com/K6pkus/sample-api/main/loki-files/images/profile-pics/hela.png", 96 | followers = 50000, 97 | following = 30, 98 | posts = 600, 99 | description = "Goddess of Death", 100 | website = "", 101 | isFollowing = false, 102 | isMe = false 103 | ) 104 | ) 105 | 106 | val basePosts = listOf( 107 | BasePost( 108 | description = "hj nem Odin na causa", 109 | likes = 616, 110 | comments = 110, 111 | time = "1h", 112 | user = users.last() 113 | ), 114 | BasePost( 115 | description = "Plural Strings em ação", 116 | likes = 1, 117 | comments = 10, 118 | time = "1h", 119 | user = users.first() 120 | ), 121 | BasePost( 122 | description = "O personagem Loki foi introduzido no Universo Cinematográfico Marvel (MCU) no filme Thor (2011), e Hiddleston foi escalado para o papel depois que o diretor Kenneth Branagh ficou impressionado com sua audição. Hiddleston voltou a interpretar Loki em Thor: The Dark World (2013), Thor: Ragnarok (2017), Avengers: Infinity War (2018) e Avengers: Endgame (2019). Em setembro de 2018, a Marvel Studios estava desenvolvendo uma série limitada centrada em Loki e estrelada por Hiddleston. Waldron foi contratado em fevereiro de 2019, e Herron foi contratada no mês seguinte. A fotografia principal começou em janeiro de 2020, mas foi interrompida em março devido à pandemia COVID-19. A produção foi retomada em setembro e foi concluída em dezembro.", 123 | likes = 5500, 124 | comments = 100, 125 | time = "1h", 126 | user = users[1] 127 | ), 128 | BasePost( 129 | description = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec euismod, nisl eget ultricies aliquam, nunc nisl aliquet nunc, quis aliquam nisl", 130 | likes = 100, 131 | comments = 10, 132 | time = "1h", 133 | user = users[2] 134 | ), 135 | BasePost( 136 | description = "O tempo está acabando para o Deus da Trapaça. ⌛\n Assista ao novo trailer de #Loki, uma série Original da Marvel Studios.", 137 | likes = 5464, 138 | comments = 87, 139 | time = "1h", 140 | user = users[3] 141 | ) 142 | ) 143 | 144 | val posts = listOf( 145 | Post( 146 | basePost = basePosts[0], 147 | images = medias, 148 | ) 149 | ) 150 | 151 | } -------------------------------------------------------------------------------- /app/src/main/java/com/paradoxo/materialgram/data/local/VideoLocalDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.paradoxo.materialgram.data.local 2 | 3 | import com.paradoxo.materialgram.domain.model.Reels 4 | 5 | class VideoLocalDataSource { 6 | val reels = listOf( 7 | Reels( 8 | video ="https://github.com/K6pkus/sample-api/raw/main/loki-files/reels/reels_1.mp4", 9 | thumbnail = "", 10 | PostLocalDataSource().basePosts[4] 11 | ), 12 | ) 13 | } -------------------------------------------------------------------------------- /app/src/main/java/com/paradoxo/materialgram/data/model/PostResponse.kt: -------------------------------------------------------------------------------- 1 | package com.paradoxo.materialgram.data.model 2 | 3 | import com.paradoxo.materialgram.domain.model.BasePost 4 | import com.paradoxo.materialgram.domain.model.Media 5 | 6 | data class PostResponse( 7 | val basePost: BasePost, 8 | val medias: List, 9 | ) 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/paradoxo/materialgram/data/model/VideoResponse.kt: -------------------------------------------------------------------------------- 1 | package com.paradoxo.materialgram.data.model 2 | 3 | import com.paradoxo.materialgram.domain.model.BasePost 4 | 5 | 6 | data class VideoResponse( 7 | val video: String, 8 | val thumbnail: String, 9 | val basePost: BasePost, 10 | ) 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/paradoxo/materialgram/data/network/PostService.kt: -------------------------------------------------------------------------------- 1 | package com.paradoxo.materialgram.data.network 2 | 3 | import com.paradoxo.materialgram.data.model.PostResponse 4 | import retrofit2.http.GET 5 | 6 | interface PostService { 7 | 8 | @GET("posts.json") // ".json" in the and because api is a mock from github pages 9 | suspend fun getPosts(): List 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/paradoxo/materialgram/data/network/VideoService.kt: -------------------------------------------------------------------------------- 1 | package com.paradoxo.materialgram.data.network 2 | 3 | import com.paradoxo.materialgram.data.model.VideoResponse 4 | import retrofit2.http.GET 5 | 6 | interface VideoService { 7 | 8 | @GET("reels.json") // ".json" in the and because api is a mock from github pages 9 | suspend fun getVideos(): List 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/paradoxo/materialgram/di/PostModule.kt: -------------------------------------------------------------------------------- 1 | package com.paradoxo.materialgram.di 2 | 3 | import com.paradoxo.materialgram.data.PostRepository 4 | import com.paradoxo.materialgram.data.PostRepositoryImpl 5 | import com.paradoxo.materialgram.data.local.PostLocalDataSource 6 | import com.paradoxo.materialgram.data.network.PostService 7 | import com.paradoxo.materialgram.domain.PostUseCase 8 | import com.paradoxo.materialgram.domain.PostUseCaseImpl 9 | import dagger.Module 10 | import dagger.Provides 11 | import dagger.hilt.InstallIn 12 | import dagger.hilt.components.SingletonComponent 13 | 14 | @Module 15 | @InstallIn(SingletonComponent::class) 16 | class PostModule { 17 | 18 | @Provides 19 | fun providePostUseCase(postRepository: PostRepository): PostUseCase { 20 | return PostUseCaseImpl(postRepository) 21 | } 22 | 23 | @Provides 24 | fun providePostRepository( 25 | postLocalDataSource: PostLocalDataSource, 26 | postService: PostService 27 | ): PostRepository { 28 | return PostRepositoryImpl(postLocalDataSource, postService) 29 | } 30 | 31 | @Provides 32 | fun providePostLocalDataSource(): PostLocalDataSource { 33 | return PostLocalDataSource() 34 | } 35 | } -------------------------------------------------------------------------------- /app/src/main/java/com/paradoxo/materialgram/di/RetrofitModule.kt: -------------------------------------------------------------------------------- 1 | package com.paradoxo.materialgram.di 2 | 3 | import com.paradoxo.materialgram.data.network.PostService 4 | import com.paradoxo.materialgram.data.network.VideoService 5 | import dagger.Module 6 | import dagger.Provides 7 | import dagger.hilt.InstallIn 8 | import dagger.hilt.components.SingletonComponent 9 | import retrofit2.Retrofit 10 | import retrofit2.converter.moshi.MoshiConverterFactory 11 | import javax.inject.Singleton 12 | 13 | @Module 14 | @InstallIn(SingletonComponent::class) 15 | object RetrofitModule { 16 | 17 | private const val BASE_URL = "https://k6pkus.github.io/sample-api/loki-files/" 18 | 19 | @Provides 20 | @Singleton 21 | fun provideRetrofit(): Retrofit { 22 | return Retrofit.Builder() 23 | .baseUrl(BASE_URL) 24 | .addConverterFactory(MoshiConverterFactory.create()) 25 | .build() 26 | } 27 | 28 | @Provides 29 | @Singleton 30 | fun provideVideoService(retrofit: Retrofit): VideoService { 31 | return retrofit.create(VideoService::class.java) 32 | } 33 | 34 | @Provides 35 | @Singleton 36 | fun providePostService(retrofit: Retrofit): PostService { 37 | return retrofit.create(PostService::class.java) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/com/paradoxo/materialgram/di/VideoModule.kt: -------------------------------------------------------------------------------- 1 | package com.paradoxo.materialgram.di 2 | 3 | import android.app.Application 4 | import androidx.media3.common.Player 5 | import androidx.media3.exoplayer.ExoPlayer 6 | import com.paradoxo.materialgram.data.VideoRepository 7 | import com.paradoxo.materialgram.data.VideoRepositoryImpl 8 | import com.paradoxo.materialgram.data.local.VideoLocalDataSource 9 | import com.paradoxo.materialgram.data.network.VideoService 10 | import com.paradoxo.materialgram.domain.VideoUseCase 11 | import com.paradoxo.materialgram.domain.VideoUseCaseImpl 12 | import dagger.Module 13 | import dagger.Provides 14 | import dagger.hilt.InstallIn 15 | import dagger.hilt.components.SingletonComponent 16 | 17 | @Module 18 | @InstallIn(SingletonComponent::class) 19 | object VideoModule { 20 | 21 | @Provides 22 | fun provideVideoUseCase(videoRepository: VideoRepository): VideoUseCase { 23 | return VideoUseCaseImpl(videoRepository) 24 | } 25 | 26 | @Provides 27 | fun provideVideoRepository( 28 | videoLocalDataSource: VideoLocalDataSource, 29 | videoService: VideoService 30 | ): VideoRepository { 31 | return VideoRepositoryImpl(videoLocalDataSource, videoService) 32 | } 33 | 34 | @Provides 35 | fun provideVideoDataSource(): VideoLocalDataSource { 36 | return VideoLocalDataSource() 37 | } 38 | 39 | 40 | @Provides 41 | fun provideExpoVideoPlayer(application: Application): Player { 42 | return ExoPlayer.Builder(application) 43 | .build().apply { 44 | playWhenReady = true 45 | repeatMode = Player.REPEAT_MODE_ALL 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /app/src/main/java/com/paradoxo/materialgram/domain/PostMapper.kt: -------------------------------------------------------------------------------- 1 | package com.paradoxo.materialgram.domain 2 | 3 | import com.paradoxo.materialgram.data.model.PostResponse 4 | import com.paradoxo.materialgram.domain.model.Post 5 | 6 | fun PostResponse.toPost(): Post = Post( 7 | basePost = basePost, 8 | images = medias 9 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/paradoxo/materialgram/domain/PostUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.paradoxo.materialgram.domain 2 | 3 | import com.paradoxo.materialgram.data.PostRepository 4 | import com.paradoxo.materialgram.domain.model.Post 5 | import kotlinx.coroutines.flow.Flow 6 | import javax.inject.Inject 7 | 8 | interface PostUseCase { 9 | suspend fun getPosts(): Flow> 10 | } 11 | 12 | class PostUseCaseImpl @Inject constructor( 13 | private val postRepository: PostRepository 14 | ) : PostUseCase { 15 | override suspend fun getPosts(): Flow> { 16 | return postRepository.getPosts() 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/com/paradoxo/materialgram/domain/ReelsMapper.kt: -------------------------------------------------------------------------------- 1 | package com.paradoxo.materialgram.domain 2 | 3 | import com.paradoxo.materialgram.data.model.VideoResponse 4 | import com.paradoxo.materialgram.domain.model.Reels 5 | 6 | fun VideoResponse.toReels(): Reels = Reels( 7 | video = video, 8 | thumbnail = thumbnail, 9 | basePost = basePost 10 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/paradoxo/materialgram/domain/VideoUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.paradoxo.materialgram.domain 2 | 3 | import com.paradoxo.materialgram.data.VideoRepository 4 | import com.paradoxo.materialgram.domain.model.Reels 5 | import kotlinx.coroutines.flow.Flow 6 | import javax.inject.Inject 7 | 8 | interface VideoUseCase { 9 | suspend fun getVideos(): Flow> 10 | } 11 | 12 | class VideoUseCaseImpl @Inject constructor( 13 | private val repository: VideoRepository 14 | ) : VideoUseCase { 15 | override suspend fun getVideos(): Flow> { 16 | return repository.getVideos() 17 | } 18 | } -------------------------------------------------------------------------------- /app/src/main/java/com/paradoxo/materialgram/domain/model/BasePost.kt: -------------------------------------------------------------------------------- 1 | package com.paradoxo.materialgram.domain.model 2 | 3 | data class BasePost( 4 | val description: String, 5 | val likes: Int, 6 | val comments: Int, 7 | val time: String, 8 | val user: User, 9 | ) 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/paradoxo/materialgram/domain/model/Media.kt: -------------------------------------------------------------------------------- 1 | package com.paradoxo.materialgram.domain.model 2 | 3 | data class Media( 4 | val url: String, 5 | val description: String 6 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/paradoxo/materialgram/domain/model/Post.kt: -------------------------------------------------------------------------------- 1 | package com.paradoxo.materialgram.domain.model 2 | 3 | data class Post( 4 | val basePost: BasePost, 5 | val images: List, 6 | ) 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/paradoxo/materialgram/domain/model/Reels.kt: -------------------------------------------------------------------------------- 1 | package com.paradoxo.materialgram.domain.model 2 | 3 | 4 | data class Reels( 5 | val video: String, 6 | val thumbnail: String, 7 | val basePost: BasePost, 8 | ) 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/java/com/paradoxo/materialgram/domain/model/User.kt: -------------------------------------------------------------------------------- 1 | package com.paradoxo.materialgram.domain.model 2 | 3 | data class User( 4 | val name: String, 5 | val avatar: String, 6 | val followers: Int, 7 | val following: Int, 8 | val posts: Int, 9 | val description: String, 10 | val website: String, 11 | val isFollowing: Boolean, 12 | val isMe: Boolean, 13 | ) 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/paradoxo/materialgram/presentation/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.paradoxo.materialgram.presentation 2 | 3 | 4 | import android.os.Bundle 5 | import androidx.activity.ComponentActivity 6 | import androidx.activity.compose.setContent 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.material3.MaterialTheme 9 | import androidx.compose.material3.Surface 10 | import androidx.compose.ui.Modifier 11 | import com.paradoxo.materialgram.presentation.screens.home.HomeScreen 12 | import com.paradoxo.materialgram.presentation.theme.MaterialGramTheme 13 | import dagger.hilt.android.AndroidEntryPoint 14 | 15 | @AndroidEntryPoint 16 | class MainActivity : ComponentActivity() { 17 | override fun onCreate(savedInstanceState: Bundle?) { 18 | super.onCreate(savedInstanceState) 19 | setContent { 20 | MaterialGramTheme { 21 | Surface( 22 | modifier = Modifier.fillMaxSize(), 23 | color = MaterialTheme.colorScheme.background 24 | ) { 25 | HomeScreen( 26 | onBack = { 27 | onBackPressed() 28 | }) 29 | } 30 | } 31 | } 32 | } 33 | } 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /app/src/main/java/com/paradoxo/materialgram/presentation/adapter/ImageAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.paradoxo.materialgram.presentation.adapter 2 | 3 | 4 | import android.content.Context 5 | import android.os.Build 6 | import android.view.LayoutInflater 7 | import android.view.ViewGroup 8 | import android.widget.ImageView 9 | import androidx.recyclerview.widget.RecyclerView 10 | import coil.ImageLoader 11 | import coil.decode.GifDecoder 12 | import coil.decode.ImageDecoderDecoder 13 | import coil.load 14 | import com.paradoxo.materialgram.R 15 | import com.paradoxo.materialgram.databinding.ImageItemBinding 16 | import com.paradoxo.materialgram.domain.model.Media 17 | 18 | 19 | class ImageAdapter( 20 | private var context: Context, 21 | private var mediaArrayList: List 22 | ) : 23 | RecyclerView.Adapter() { 24 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { 25 | 26 | val binding = ImageItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) 27 | return ViewHolder(binding) 28 | } 29 | 30 | override fun onBindViewHolder(holder: ViewHolder, position: Int) { 31 | holder.imageView.load(mediaArrayList[position].url, imageLoaderGif()) 32 | } 33 | 34 | private fun imageLoaderGif(): ImageLoader { 35 | val imageLoader = ImageLoader.Builder(context) 36 | .components { 37 | if (Build.VERSION.SDK_INT >= 28) { 38 | add(ImageDecoderDecoder.Factory()) 39 | } else { 40 | add(GifDecoder.Factory()) 41 | } 42 | } 43 | .placeholder(R.drawable.shimmer_animation) 44 | .error(R.drawable.ic_error) 45 | .build() 46 | return imageLoader 47 | } 48 | 49 | override fun getItemCount(): Int { 50 | return mediaArrayList.size 51 | } 52 | 53 | class ViewHolder(itemView: ImageItemBinding) : 54 | RecyclerView.ViewHolder(itemView.root) { 55 | var imageView: ImageView 56 | 57 | init { 58 | imageView = itemView.carouselImageView 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /app/src/main/java/com/paradoxo/materialgram/presentation/components/AsyncImageWithShimmer.kt: -------------------------------------------------------------------------------- 1 | package com.paradoxo.materialgram.presentation.components 2 | 3 | import android.os.Build 4 | import androidx.compose.foundation.Image 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.Modifier 7 | import androidx.compose.ui.layout.ContentScale 8 | import androidx.compose.ui.platform.LocalContext 9 | import coil.ImageLoader 10 | import coil.compose.rememberAsyncImagePainter 11 | import coil.decode.GifDecoder 12 | import coil.decode.ImageDecoderDecoder 13 | import coil.request.ImageRequest 14 | import com.paradoxo.materialgram.R 15 | 16 | 17 | /** 18 | * @git-jr - 04/10/2023 19 | * Allows you to load an image asynchronously with shimmer effect and gif support 20 | * @param data: image url 21 | * @param contentDescription: image description 22 | * @param modifier: Modifier = Modifier 23 | * @return Unit 24 | */ 25 | 26 | @Composable 27 | fun AsyncImageWithShimmer( 28 | data: String, 29 | contentDescription: String, 30 | modifier: Modifier = Modifier, 31 | placeHolder: Int = R.drawable.shimmer_animation, 32 | error: Int = R.drawable.ic_error, 33 | contentScale: ContentScale = ContentScale.Crop 34 | ) { 35 | val imageLoader = ImageLoader.Builder(LocalContext.current) 36 | .components { 37 | if (Build.VERSION.SDK_INT >= 28) { 38 | add(ImageDecoderDecoder.Factory()) 39 | } else { 40 | add(GifDecoder.Factory()) 41 | } 42 | } 43 | .build() 44 | 45 | Image( 46 | painter = rememberAsyncImagePainter( 47 | ImageRequest.Builder(LocalContext.current) 48 | .data(data) 49 | .placeholder(placeHolder) 50 | .error(error) 51 | .build(), 52 | imageLoader = imageLoader 53 | ), 54 | modifier = modifier, 55 | contentScale = contentScale, 56 | contentDescription = contentDescription 57 | ) 58 | } -------------------------------------------------------------------------------- /app/src/main/java/com/paradoxo/materialgram/presentation/components/HomeBottomBar.kt: -------------------------------------------------------------------------------- 1 | package com.paradoxo.materialgram.presentation.components 2 | 3 | import androidx.compose.material.icons.Icons 4 | import androidx.compose.material.icons.filled.Email 5 | import androidx.compose.material.icons.filled.Favorite 6 | import androidx.compose.material.icons.filled.Home 7 | import androidx.compose.material.icons.filled.Search 8 | import androidx.compose.material.icons.outlined.Email 9 | import androidx.compose.material.icons.outlined.FavoriteBorder 10 | import androidx.compose.material.icons.outlined.Home 11 | import androidx.compose.material.icons.outlined.Search 12 | import androidx.compose.material3.Icon 13 | import androidx.compose.material3.MaterialTheme 14 | import androidx.compose.material3.NavigationBar 15 | import androidx.compose.material3.NavigationBarItem 16 | import androidx.compose.material3.NavigationBarItemDefaults 17 | import androidx.compose.material3.Text 18 | import androidx.compose.runtime.Composable 19 | import androidx.compose.runtime.getValue 20 | import androidx.compose.runtime.mutableIntStateOf 21 | import androidx.compose.runtime.remember 22 | import androidx.compose.runtime.setValue 23 | 24 | 25 | @Composable 26 | fun HomeBottomBar() { 27 | 28 | var selectedItem by remember { mutableIntStateOf(0) } 29 | 30 | NavigationBar { 31 | screenItems.forEachIndexed { index, item -> 32 | NavigationBarItem( 33 | icon = { 34 | Icon( 35 | if (selectedItem == index) item.second.first else item.second.second, 36 | contentDescription = item.first, 37 | ) 38 | }, 39 | label = { Text(item.first) }, 40 | selected = selectedItem == index, 41 | onClick = { selectedItem = index }, 42 | colors = NavigationBarItemDefaults.colors( 43 | selectedIconColor = MaterialTheme.colorScheme.onSecondaryContainer, 44 | unselectedIconColor = MaterialTheme.colorScheme.onSurface, 45 | selectedTextColor = MaterialTheme.colorScheme.onSecondaryContainer, 46 | indicatorColor = MaterialTheme.colorScheme.primaryContainer, 47 | unselectedTextColor = MaterialTheme.colorScheme.onSurface, 48 | ) 49 | ) 50 | } 51 | } 52 | } 53 | 54 | val screenItems = listOf( 55 | Pair("Home", Pair(Icons.Filled.Home, Icons.Outlined.Home)), 56 | Pair("Explore", Pair(Icons.Filled.Search, Icons.Outlined.Search)), 57 | Pair("Notifications", Pair(Icons.Filled.Favorite, Icons.Outlined.FavoriteBorder)), 58 | Pair("Chat", Pair(Icons.Filled.Email, Icons.Outlined.Email)) 59 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/paradoxo/materialgram/presentation/components/HomeFAB.kt: -------------------------------------------------------------------------------- 1 | package com.paradoxo.materialgram.presentation.components 2 | 3 | import androidx.compose.material.icons.Icons 4 | import androidx.compose.material.icons.filled.Add 5 | import androidx.compose.material3.FloatingActionButton 6 | import androidx.compose.material3.Icon 7 | import androidx.compose.material3.MaterialTheme 8 | import androidx.compose.runtime.Composable 9 | 10 | @Composable 11 | fun HomeFAB() { 12 | FloatingActionButton( 13 | onClick = { /*TODO*/ }, 14 | ) { 15 | Icon( 16 | imageVector = Icons.Filled.Add, 17 | contentDescription = "Add", 18 | tint = MaterialTheme.colorScheme.onSecondaryContainer 19 | ) 20 | } 21 | } -------------------------------------------------------------------------------- /app/src/main/java/com/paradoxo/materialgram/presentation/components/HomeSearchAppBar.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalMaterial3Api::class) 2 | 3 | package com.paradoxo.materialgram.presentation.components 4 | 5 | import androidx.compose.foundation.background 6 | import androidx.compose.foundation.layout.Arrangement 7 | import androidx.compose.foundation.layout.Row 8 | import androidx.compose.foundation.layout.Spacer 9 | import androidx.compose.foundation.layout.fillMaxWidth 10 | import androidx.compose.foundation.layout.height 11 | import androidx.compose.foundation.layout.padding 12 | import androidx.compose.foundation.layout.size 13 | import androidx.compose.foundation.layout.width 14 | import androidx.compose.foundation.shape.CircleShape 15 | import androidx.compose.material.icons.Icons 16 | import androidx.compose.material.icons.filled.Menu 17 | import androidx.compose.material3.ExperimentalMaterial3Api 18 | import androidx.compose.material3.Icon 19 | import androidx.compose.material3.MaterialTheme 20 | import androidx.compose.material3.Tab 21 | import androidx.compose.material3.TabRow 22 | import androidx.compose.material3.Text 23 | import androidx.compose.material3.TopAppBar 24 | import androidx.compose.material3.TopAppBarDefaults 25 | import androidx.compose.material3.TopAppBarScrollBehavior 26 | import androidx.compose.runtime.Composable 27 | import androidx.compose.runtime.getValue 28 | import androidx.compose.runtime.mutableIntStateOf 29 | import androidx.compose.runtime.remember 30 | import androidx.compose.runtime.setValue 31 | import androidx.compose.ui.Alignment 32 | import androidx.compose.ui.Modifier 33 | import androidx.compose.ui.draw.clip 34 | import androidx.compose.ui.draw.shadow 35 | import androidx.compose.ui.unit.dp 36 | 37 | @ExperimentalMaterial3Api 38 | @Composable 39 | fun HomeSearchAppBar(scrollBehavior: TopAppBarScrollBehavior) { 40 | 41 | TopAppBar( 42 | title = {}, 43 | scrollBehavior = scrollBehavior, 44 | actions = { 45 | Row( 46 | modifier = Modifier 47 | .height(56.dp) 48 | .padding(top = 8.dp) 49 | .fillMaxWidth() 50 | .padding(horizontal = 16.dp) 51 | .background(MaterialTheme.colorScheme.surface, CircleShape) 52 | .shadow( 53 | 1.dp, CircleShape, 54 | spotColor = MaterialTheme.colorScheme.secondaryContainer, 55 | ), 56 | verticalAlignment = Alignment.CenterVertically, 57 | horizontalArrangement = Arrangement.SpaceBetween, 58 | ) { 59 | Row( 60 | verticalAlignment = Alignment.CenterVertically, 61 | horizontalArrangement = Arrangement.Center 62 | ) { 63 | Spacer(modifier = Modifier.width(16.dp)) 64 | Icon( 65 | imageVector = Icons.Filled.Menu, 66 | contentDescription = "Search", 67 | modifier = Modifier 68 | .size(24.dp), 69 | tint = MaterialTheme.colorScheme.onSurface 70 | ) 71 | Spacer(modifier = Modifier.width(16.dp)) 72 | } 73 | Text( 74 | text = "Pesquisar", 75 | color = MaterialTheme.colorScheme.onSurface, 76 | ) 77 | Row( 78 | verticalAlignment = Alignment.CenterVertically, 79 | horizontalArrangement = Arrangement.Center 80 | ) { 81 | Spacer(modifier = Modifier.width(16.dp)) 82 | 83 | AsyncImageWithShimmer( 84 | data = "https://raw.githubusercontent.com/K6pkus/sample-api/main/loki-files/images/image%20(3).jpeg", 85 | contentDescription = "imagem de perfil", 86 | modifier = Modifier 87 | .clip(CircleShape) 88 | .size(32.dp) 89 | ) 90 | 91 | Spacer(modifier = Modifier.width(16.dp)) 92 | } 93 | } 94 | }, 95 | colors = TopAppBarDefaults.topAppBarColors( 96 | containerColor = MaterialTheme.colorScheme.background, 97 | ), 98 | ) 99 | 100 | } 101 | 102 | @ExperimentalMaterial3Api 103 | @Composable 104 | fun HomeTabsAppBar( 105 | scrollBehavior: TopAppBarScrollBehavior, 106 | onSelectedTab: (Int) -> Unit 107 | ) { 108 | var selectedTabIndex by remember { mutableIntStateOf(0) } 109 | 110 | val tabs = listOf("Home", "Reels") 111 | 112 | TopAppBar( 113 | title = {}, 114 | scrollBehavior = scrollBehavior, 115 | actions = { 116 | TabRow( 117 | selectedTabIndex = selectedTabIndex, 118 | containerColor = MaterialTheme.colorScheme.background, 119 | ) { 120 | tabs.forEachIndexed { index, title -> 121 | Tab( 122 | selected = index == selectedTabIndex, 123 | onClick = { 124 | selectedTabIndex = index 125 | onSelectedTab(index) 126 | }, 127 | text = { Text(text = title) } 128 | ) 129 | } 130 | } 131 | }, 132 | colors = TopAppBarDefaults.topAppBarColors( 133 | containerColor = MaterialTheme.colorScheme.background, 134 | ), 135 | ) 136 | } 137 | 138 | -------------------------------------------------------------------------------- /app/src/main/java/com/paradoxo/materialgram/presentation/components/ItemCarouselView.kt: -------------------------------------------------------------------------------- 1 | package com.paradoxo.materialgram.presentation.components 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.fillMaxSize 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.foundation.layout.height 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.tooling.preview.Preview 10 | import androidx.compose.ui.unit.dp 11 | import androidx.compose.ui.viewinterop.AndroidView 12 | import androidx.recyclerview.widget.RecyclerView 13 | import com.google.android.material.carousel.CarouselLayoutManager 14 | import com.paradoxo.materialgram.presentation.adapter.ImageAdapter 15 | import com.paradoxo.materialgram.domain.model.Media 16 | 17 | @Composable 18 | fun ItemCarouselView(medias: List) { 19 | AndroidView( 20 | modifier = Modifier 21 | .height(300.dp) 22 | .fillMaxWidth(), 23 | factory = { context -> 24 | RecyclerView(context).apply { 25 | val adapter = ImageAdapter(context, medias) 26 | 27 | this.layoutManager = CarouselLayoutManager() 28 | this.adapter = adapter 29 | } 30 | } 31 | ) 32 | } 33 | 34 | 35 | @Preview(showBackground = true) 36 | @Composable 37 | fun CustomViewPreview() { 38 | Column(Modifier.fillMaxSize()) { 39 | ItemCarouselView(emptyList()) 40 | } 41 | } -------------------------------------------------------------------------------- /app/src/main/java/com/paradoxo/materialgram/presentation/components/ItemSingleImage.kt: -------------------------------------------------------------------------------- 1 | package com.paradoxo.materialgram.presentation.components 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.fillMaxWidth 5 | import androidx.compose.foundation.layout.height 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.foundation.shape.RoundedCornerShape 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Alignment 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.draw.clip 12 | import androidx.compose.ui.unit.dp 13 | 14 | @Composable 15 | fun ItemSingleImage(imageUrl: String, description: String) { 16 | Column( 17 | horizontalAlignment = Alignment.CenterHorizontally, 18 | ) { 19 | AsyncImageWithShimmer( 20 | imageUrl, description, 21 | modifier = Modifier 22 | .fillMaxWidth() 23 | .height(300.dp) 24 | .padding(horizontal = 4.dp) 25 | .clip(RoundedCornerShape(30.dp)), 26 | ) 27 | } 28 | } 29 | 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/paradoxo/materialgram/presentation/components/ItemStory.kt: -------------------------------------------------------------------------------- 1 | package com.paradoxo.materialgram.presentation.components 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.Spacer 9 | import androidx.compose.foundation.layout.fillMaxWidth 10 | import androidx.compose.foundation.layout.height 11 | import androidx.compose.foundation.layout.padding 12 | import androidx.compose.foundation.layout.size 13 | import androidx.compose.foundation.layout.width 14 | import androidx.compose.foundation.lazy.LazyRow 15 | import androidx.compose.foundation.lazy.items 16 | import androidx.compose.foundation.shape.CircleShape 17 | import androidx.compose.material.icons.Icons 18 | import androidx.compose.material.icons.filled.Add 19 | import androidx.compose.material3.Badge 20 | import androidx.compose.material3.Divider 21 | import androidx.compose.material3.ExperimentalMaterial3Api 22 | import androidx.compose.material3.Icon 23 | import androidx.compose.material3.MaterialTheme 24 | import androidx.compose.material3.Text 25 | import androidx.compose.runtime.Composable 26 | import androidx.compose.ui.Alignment 27 | import androidx.compose.ui.Modifier 28 | import androidx.compose.ui.draw.clip 29 | import androidx.compose.ui.unit.dp 30 | import com.paradoxo.materialgram.data.local.PostLocalDataSource 31 | 32 | @Composable 33 | @OptIn(ExperimentalMaterial3Api::class) 34 | fun StoryContainer() { 35 | Column { 36 | LazyRow( 37 | modifier = Modifier 38 | .padding(horizontal = 8.dp, vertical = 8.dp), 39 | content = { 40 | item { 41 | ItemStory( 42 | profilePic = "https://raw.githubusercontent.com/K6pkus/sample-api/main/loki-files/images/image%20(3).jpeg", 43 | profilePicDescription = "foto de perfil da conta atual", 44 | name = "Seu story", 45 | ringOpacity = 0f, 46 | badge = { 47 | Badge( 48 | Modifier 49 | .size(22.dp) 50 | .padding(bottom = 2.dp, end = 2.dp) 51 | .border( 52 | 2.dp, 53 | MaterialTheme.colorScheme.background, 54 | CircleShape 55 | ), 56 | containerColor = MaterialTheme.colorScheme.primary, 57 | ) { 58 | Icon( 59 | imageVector = Icons.Default.Add, 60 | contentDescription = "add", 61 | modifier = Modifier 62 | .size(20.dp), 63 | ) 64 | } 65 | } 66 | ) 67 | } 68 | 69 | items(PostLocalDataSource().users) { user -> 70 | Spacer(modifier = Modifier.width(8.dp)) 71 | val ringOpacity = 72 | if (PostLocalDataSource().users.indexOf(user) % 2 == 1) 0.2f else 1f 73 | ItemStory( 74 | profilePic = user.avatar, 75 | profilePicDescription = "foto de perfil de ${user.name}", 76 | name = user.name, 77 | ringOpacity = ringOpacity 78 | ) 79 | } 80 | } 81 | ) 82 | Divider( 83 | color = MaterialTheme.colorScheme.onSurface.copy(0.4f), 84 | modifier = Modifier 85 | .padding(top = 16.dp, bottom = 4.dp) 86 | .fillMaxWidth() 87 | ) 88 | } 89 | } 90 | 91 | @Composable 92 | private fun ItemStory( 93 | profilePic: String, 94 | profilePicDescription: String, 95 | name: String, 96 | ringOpacity: Float, 97 | badge: @Composable () -> Unit = {} 98 | ) { 99 | Column( 100 | verticalArrangement = Arrangement.Center, 101 | horizontalAlignment = Alignment.CenterHorizontally 102 | ) { 103 | 104 | Box( 105 | contentAlignment = Alignment.BottomEnd, 106 | ) { 107 | Box( 108 | modifier = Modifier 109 | .background( 110 | MaterialTheme.colorScheme.primary.copy(ringOpacity), CircleShape 111 | ) 112 | .padding(2.dp) 113 | .clip(CircleShape) 114 | ) { 115 | AsyncImageWithShimmer( 116 | data = profilePic, 117 | contentDescription = profilePicDescription, 118 | modifier = Modifier 119 | .clip(CircleShape) 120 | .size(62.dp) 121 | .border( 122 | 3.dp, MaterialTheme.colorScheme.background, CircleShape 123 | ) 124 | ) 125 | } 126 | badge() 127 | } 128 | 129 | Spacer(modifier = Modifier.height(4.dp)) 130 | 131 | Text( 132 | text = name, 133 | style = MaterialTheme.typography.labelSmall, 134 | color = MaterialTheme.colorScheme.onSurface, 135 | ) 136 | } 137 | } -------------------------------------------------------------------------------- /app/src/main/java/com/paradoxo/materialgram/presentation/components/ReelsVideoHud.kt: -------------------------------------------------------------------------------- 1 | package com.paradoxo.materialgram.presentation.components 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.Row 8 | import androidx.compose.foundation.layout.Spacer 9 | import androidx.compose.foundation.layout.fillMaxSize 10 | import androidx.compose.foundation.layout.fillMaxWidth 11 | import androidx.compose.foundation.layout.height 12 | import androidx.compose.foundation.layout.padding 13 | import androidx.compose.foundation.layout.size 14 | import androidx.compose.foundation.layout.width 15 | import androidx.compose.foundation.shape.CircleShape 16 | import androidx.compose.foundation.shape.RoundedCornerShape 17 | import androidx.compose.material.icons.Icons 18 | import androidx.compose.material.icons.filled.Comment 19 | import androidx.compose.material.icons.filled.Send 20 | import androidx.compose.material.icons.filled.ThumbDown 21 | import androidx.compose.material.icons.filled.ThumbUp 22 | import androidx.compose.material3.Icon 23 | import androidx.compose.material3.MaterialTheme 24 | import androidx.compose.material3.Text 25 | import androidx.compose.runtime.Composable 26 | import androidx.compose.ui.Alignment 27 | import androidx.compose.ui.Modifier 28 | import androidx.compose.ui.draw.clip 29 | import androidx.compose.ui.graphics.vector.ImageVector 30 | import androidx.compose.ui.res.colorResource 31 | import androidx.compose.ui.text.font.FontWeight 32 | import androidx.compose.ui.text.style.TextOverflow 33 | import androidx.compose.ui.unit.dp 34 | import com.paradoxo.materialgram.R 35 | import com.paradoxo.materialgram.domain.model.BasePost 36 | 37 | 38 | @Composable 39 | fun VideoHud(reels: BasePost) { 40 | Box( 41 | Modifier.fillMaxSize(), 42 | contentAlignment = Alignment.BottomEnd 43 | ) { 44 | 45 | Column( 46 | horizontalAlignment = Alignment.CenterHorizontally, 47 | verticalArrangement = Arrangement.SpaceEvenly, 48 | modifier = Modifier 49 | .width(70.dp) 50 | .padding(vertical = 16.dp) 51 | ) { 52 | ItemIcon(Icons.Default.ThumbUp, reels.likes.toString()) 53 | 54 | ItemIcon(Icons.Default.ThumbDown, "Não gostei") 55 | 56 | ItemIcon(Icons.Default.Comment, reels.comments.toString()) 57 | 58 | ItemIcon(Icons.Default.Send, "Compartilhar") 59 | 60 | AsyncImageWithShimmer( 61 | data = reels.user.avatar, 62 | contentDescription = "foto de perfil da conta", 63 | modifier = Modifier 64 | .size(50.dp) 65 | .clip(RoundedCornerShape(20)) 66 | ) 67 | 68 | } 69 | 70 | Row( 71 | Modifier 72 | .fillMaxWidth() 73 | .padding( 74 | top = 16.dp, bottom = 16.dp, 75 | start = 10.dp, end = 80.dp 76 | ) 77 | ) { 78 | Column { 79 | Row( 80 | verticalAlignment = Alignment.CenterVertically, 81 | horizontalArrangement = Arrangement.spacedBy(8.dp), 82 | ) { 83 | AsyncImageWithShimmer( 84 | data = reels.user.avatar, 85 | contentDescription = "Foto de perfil da conta", 86 | modifier = Modifier 87 | .size(30.dp) 88 | .clip(CircleShape) 89 | ) 90 | 91 | Text( 92 | text = reels.user.name, 93 | color = colorResource(id = R.color.reels_icon), 94 | maxLines = 1, 95 | overflow = TextOverflow.Ellipsis, 96 | fontWeight = FontWeight.Bold, 97 | fontSize = MaterialTheme.typography.labelLarge.fontSize, 98 | ) 99 | 100 | Text( 101 | text = "Inscrever-se", 102 | fontSize = MaterialTheme.typography.bodySmall.fontSize, 103 | color = colorResource(id = R.color.reels_text), 104 | modifier = Modifier 105 | .background( 106 | shape = CircleShape, 107 | color = colorResource(id = R.color.reels_icon) 108 | ) 109 | .padding(8.dp) 110 | ) 111 | 112 | } 113 | 114 | Spacer(modifier = Modifier.height(4.dp)) 115 | 116 | Text( 117 | text = reels.description, 118 | color = colorResource(id = R.color.reels_icon), 119 | maxLines = 2, 120 | overflow = TextOverflow.Ellipsis, 121 | fontSize = MaterialTheme.typography.titleMedium.fontSize 122 | ) 123 | } 124 | } 125 | } 126 | } 127 | 128 | 129 | @Composable 130 | private fun ItemIcon(icon: ImageVector, text: String) { 131 | Column( 132 | modifier = Modifier.size(70.dp), 133 | horizontalAlignment = Alignment.CenterHorizontally 134 | ) { 135 | Icon( 136 | imageVector = icon, contentDescription = text, 137 | tint = colorResource(id = R.color.reels_icon), 138 | ) 139 | Spacer(modifier = Modifier.height(8.dp)) 140 | Text( 141 | text = text, 142 | maxLines = 1, 143 | overflow = TextOverflow.Ellipsis, 144 | fontWeight = FontWeight.Bold, 145 | color = colorResource(id = R.color.reels_icon), 146 | fontSize = MaterialTheme.typography.labelMedium.fontSize 147 | ) 148 | Spacer(modifier = Modifier.height(8.dp)) 149 | } 150 | } -------------------------------------------------------------------------------- /app/src/main/java/com/paradoxo/materialgram/presentation/screens/feed/ListPostsScreen.kt: -------------------------------------------------------------------------------- 1 | package com.paradoxo.materialgram.presentation.screens.feed 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.Row 7 | import androidx.compose.foundation.layout.Spacer 8 | import androidx.compose.foundation.layout.fillMaxSize 9 | import androidx.compose.foundation.layout.fillMaxWidth 10 | import androidx.compose.foundation.layout.height 11 | import androidx.compose.foundation.layout.padding 12 | import androidx.compose.foundation.layout.size 13 | import androidx.compose.foundation.layout.width 14 | import androidx.compose.foundation.lazy.LazyColumn 15 | import androidx.compose.foundation.lazy.items 16 | import androidx.compose.foundation.shape.RoundedCornerShape 17 | import androidx.compose.material.icons.Icons 18 | import androidx.compose.material.icons.outlined.BookmarkBorder 19 | import androidx.compose.material.icons.outlined.Favorite 20 | import androidx.compose.material.icons.outlined.ModeComment 21 | import androidx.compose.material.icons.outlined.MoreVert 22 | import androidx.compose.material.icons.outlined.Reply 23 | import androidx.compose.material3.Icon 24 | import androidx.compose.material3.ListItem 25 | import androidx.compose.material3.ListItemDefaults 26 | import androidx.compose.material3.MaterialTheme 27 | import androidx.compose.material3.Text 28 | import androidx.compose.runtime.Composable 29 | import androidx.compose.ui.Modifier 30 | import androidx.compose.ui.draw.clip 31 | import androidx.compose.ui.graphics.graphicsLayer 32 | import androidx.compose.ui.res.pluralStringResource 33 | import androidx.compose.ui.text.SpanStyle 34 | import androidx.compose.ui.text.buildAnnotatedString 35 | import androidx.compose.ui.text.font.FontWeight 36 | import androidx.compose.ui.text.style.TextOverflow 37 | import androidx.compose.ui.text.withStyle 38 | import androidx.compose.ui.unit.dp 39 | import androidx.compose.ui.unit.sp 40 | import com.paradoxo.materialgram.R 41 | import com.paradoxo.materialgram.domain.model.BasePost 42 | import com.paradoxo.materialgram.domain.model.Media 43 | import com.paradoxo.materialgram.domain.model.Post 44 | import com.paradoxo.materialgram.presentation.components.AsyncImageWithShimmer 45 | import com.paradoxo.materialgram.presentation.components.ItemCarouselView 46 | import com.paradoxo.materialgram.presentation.components.ItemSingleImage 47 | import com.paradoxo.materialgram.presentation.components.StoryContainer 48 | 49 | 50 | @Composable 51 | fun ListPosts(posts: List) { 52 | LazyColumn( 53 | modifier = Modifier 54 | .background(MaterialTheme.colorScheme.background) 55 | .fillMaxSize(), 56 | ) { 57 | item { 58 | StoryContainer() 59 | } 60 | items(posts) { post -> 61 | ItemPost(post) 62 | } 63 | } 64 | } 65 | 66 | @Composable 67 | private fun ItemPost(post: Post) { 68 | Spacer(modifier = Modifier.height(8.dp)) 69 | 70 | Column { 71 | PostHeader(post) 72 | Spacer(modifier = Modifier.height(8.dp)) 73 | PostMedias(post.images) 74 | PostMetadata(post.basePost) 75 | } 76 | } 77 | 78 | @Composable 79 | private fun PostHeader(post: Post) { 80 | ListItem( 81 | headlineContent = { 82 | Text( 83 | text = post.basePost.user.name, 84 | color = MaterialTheme.colorScheme.onSecondaryContainer.copy(alpha = 0.8f), 85 | fontWeight = FontWeight.Medium, 86 | ) 87 | }, 88 | leadingContent = { 89 | AsyncImageWithShimmer( 90 | post.basePost.user.avatar, "foto de perfil ${post.basePost.user.name}", 91 | modifier = Modifier 92 | .size(40.dp) 93 | .clip(RoundedCornerShape(30.dp)), 94 | ) 95 | }, 96 | trailingContent = { 97 | Icon( 98 | imageVector = Icons.Outlined.MoreVert, 99 | contentDescription = "Mais opções", 100 | modifier = Modifier.size(24.dp), 101 | tint = MaterialTheme.colorScheme.onSecondaryContainer.copy(alpha = 0.8f), 102 | ) 103 | }, 104 | colors = ListItemDefaults.colors( 105 | containerColor = MaterialTheme.colorScheme.background, 106 | ) 107 | ) 108 | } 109 | 110 | @Composable 111 | private fun PostMedias(medias: List) { 112 | Column(Modifier.padding(horizontal = 8.dp)) { 113 | if (medias.size > 1) { 114 | ItemCarouselView(medias) 115 | } else { 116 | ItemSingleImage( 117 | medias.first().url, 118 | medias.first().description 119 | ) 120 | } 121 | } 122 | } 123 | 124 | @Composable 125 | private fun PostMetadata(post: BasePost) { 126 | Row( 127 | Modifier 128 | .padding(horizontal = 16.dp, vertical = 8.dp) 129 | .fillMaxWidth(), 130 | horizontalArrangement = Arrangement.SpaceBetween 131 | ) { 132 | Row { 133 | Icon( 134 | imageVector = Icons.Outlined.Favorite, 135 | contentDescription = "Like", 136 | modifier = Modifier.size(24.dp), 137 | tint = MaterialTheme.colorScheme.primary, 138 | ) 139 | Spacer(modifier = Modifier.width(16.dp)) 140 | Icon( 141 | imageVector = Icons.Outlined.ModeComment, 142 | contentDescription = "Comment", 143 | modifier = Modifier.size(24.dp), 144 | tint = MaterialTheme.colorScheme.onSecondaryContainer 145 | ) 146 | Spacer(modifier = Modifier.width(16.dp)) 147 | 148 | Icon( 149 | imageVector = Icons.Outlined.Reply, 150 | contentDescription = "Share", 151 | modifier = Modifier 152 | .size(24.dp) 153 | .graphicsLayer { 154 | rotationY = 180f 155 | }, 156 | tint = MaterialTheme.colorScheme.onSecondaryContainer 157 | ) 158 | } 159 | Icon( 160 | imageVector = Icons.Outlined.BookmarkBorder, 161 | contentDescription = "Save", 162 | modifier = Modifier.size(24.dp), 163 | tint = MaterialTheme.colorScheme.onSecondaryContainer 164 | ) 165 | } 166 | Text( 167 | text = pluralStringResource( 168 | id = R.plurals.likes_count, 169 | count = post.likes, 170 | post.likes 171 | ), 172 | modifier = Modifier.padding(start = 16.dp), 173 | color = MaterialTheme.colorScheme.onSurface, 174 | fontSize = 14.sp, 175 | fontWeight = FontWeight.Bold 176 | ) 177 | Row { 178 | 179 | val nameWithDescription = buildAnnotatedString { 180 | withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { 181 | append(post.user.name) 182 | } 183 | append(" ") 184 | append(post.description) 185 | } 186 | 187 | Text( 188 | text = nameWithDescription, 189 | modifier = Modifier.padding(start = 16.dp), 190 | color = MaterialTheme.colorScheme.onSurface, 191 | fontSize = 14.sp, 192 | overflow = TextOverflow.Ellipsis, 193 | maxLines = 2, 194 | ) 195 | } 196 | 197 | Text( 198 | text = "Há 6 horas", 199 | modifier = Modifier.padding(start = 16.dp), 200 | color = MaterialTheme.colorScheme.onSurface, 201 | fontSize = 12.sp, 202 | ) 203 | Spacer(modifier = Modifier.height(8.dp)) 204 | } 205 | -------------------------------------------------------------------------------- /app/src/main/java/com/paradoxo/materialgram/presentation/screens/home/HomeScreen.kt: -------------------------------------------------------------------------------- 1 | package com.paradoxo.materialgram.presentation.screens.home 2 | 3 | import androidx.activity.compose.BackHandler 4 | import androidx.compose.animation.Crossfade 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.material3.ExperimentalMaterial3Api 8 | import androidx.compose.material3.Scaffold 9 | import androidx.compose.material3.TopAppBarDefaults 10 | import androidx.compose.material3.TopAppBarScrollBehavior 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.collectAsState 13 | import androidx.compose.runtime.getValue 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.input.nestedscroll.nestedScroll 16 | import androidx.compose.ui.tooling.preview.Preview 17 | import androidx.lifecycle.viewmodel.compose.viewModel 18 | import com.paradoxo.materialgram.presentation.components.HomeBottomBar 19 | import com.paradoxo.materialgram.presentation.components.HomeFAB 20 | import com.paradoxo.materialgram.presentation.components.HomeSearchAppBar 21 | import com.paradoxo.materialgram.presentation.components.HomeTabsAppBar 22 | import com.paradoxo.materialgram.presentation.screens.feed.ListPosts 23 | import com.paradoxo.materialgram.presentation.screens.reels.ReelsScreen 24 | import com.paradoxo.materialgram.presentation.theme.MaterialGramTheme 25 | 26 | @OptIn(ExperimentalMaterial3Api::class) 27 | @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) 28 | @Composable 29 | fun HomeScreen(onBack: () -> Unit) { 30 | val homeViewModel = viewModel() 31 | val state by homeViewModel.uiState.collectAsState() 32 | val showFeed = state.showFeed 33 | 34 | SetupOnBackPress( 35 | showFeed = showFeed, 36 | onBack = onBack, 37 | onChangeSelectedTab = { 38 | homeViewModel.changeSelectedTab(it) 39 | } 40 | ) 41 | 42 | val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() 43 | val scrollBehaviorTabs = TopAppBarDefaults.enterAlwaysScrollBehavior() 44 | 45 | Scaffold( 46 | modifier = Modifier 47 | .nestedScroll(scrollBehaviorTabs.nestedScrollConnection) 48 | .nestedScroll(scrollBehavior.nestedScrollConnection), 49 | topBar = { 50 | if (showFeed) { 51 | HomeAppBar(scrollBehavior, scrollBehaviorTabs, onSelectedTab = { tab -> 52 | homeViewModel.changeSelectedTab(tab) 53 | }) 54 | } 55 | }, 56 | floatingActionButton = { 57 | if (showFeed) { 58 | HomeFAB() 59 | } 60 | }, 61 | bottomBar = { 62 | HomeBottomBar() 63 | }, 64 | ) { paddingValues -> 65 | Column( 66 | modifier = Modifier 67 | .padding(paddingValues) 68 | ) { 69 | Crossfade(targetState = showFeed, label = "") { showFeed -> 70 | if (showFeed) { 71 | ListPosts(state.posts) 72 | } else { 73 | ReelsScreen() 74 | } 75 | } 76 | } 77 | } 78 | } 79 | 80 | @Composable 81 | private fun SetupOnBackPress( 82 | showFeed: Boolean, 83 | onChangeSelectedTab: (Int) -> Unit, 84 | onBack: () -> Unit 85 | ) { 86 | BackHandler { 87 | if (!showFeed) { 88 | onChangeSelectedTab(0) 89 | } else { 90 | onBack() 91 | } 92 | } 93 | } 94 | 95 | @Preview(showBackground = true) 96 | @Composable 97 | fun HomeScreenPreview() { 98 | MaterialGramTheme { 99 | HomeScreen {} 100 | } 101 | } 102 | 103 | @OptIn(ExperimentalMaterial3Api::class) 104 | @Composable 105 | fun HomeAppBar( 106 | scrollBehavior: TopAppBarScrollBehavior, 107 | scrollBehaviorTabs: TopAppBarScrollBehavior, 108 | onSelectedTab: (Int) -> Unit 109 | ) { 110 | Column { 111 | HomeSearchAppBar(scrollBehavior) 112 | HomeTabsAppBar( 113 | scrollBehavior = scrollBehaviorTabs, 114 | onSelectedTab = { index -> 115 | onSelectedTab(index) 116 | } 117 | ) 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /app/src/main/java/com/paradoxo/materialgram/presentation/screens/home/HomeUiState.kt: -------------------------------------------------------------------------------- 1 | package com.paradoxo.materialgram.presentation.screens.home 2 | 3 | import com.paradoxo.materialgram.domain.model.Post 4 | 5 | data class HomeUiState( 6 | val posts: List, 7 | val showFeed: Boolean = true 8 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/paradoxo/materialgram/presentation/screens/home/HomeViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.paradoxo.materialgram.presentation.screens.home 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import com.paradoxo.materialgram.domain.PostUseCase 6 | import dagger.hilt.android.lifecycle.HiltViewModel 7 | import kotlinx.coroutines.flow.MutableStateFlow 8 | import kotlinx.coroutines.flow.asStateFlow 9 | import kotlinx.coroutines.launch 10 | import javax.inject.Inject 11 | 12 | @HiltViewModel 13 | class HomeViewModel @Inject constructor( 14 | private var postUseCase: PostUseCase 15 | ) : ViewModel() { 16 | private val _uiState = MutableStateFlow( 17 | HomeUiState( 18 | posts = emptyList() 19 | ) 20 | ) 21 | var uiState = _uiState.asStateFlow() 22 | 23 | init { 24 | loadPosts() 25 | } 26 | 27 | private fun loadPosts() { 28 | viewModelScope.launch { 29 | postUseCase.getPosts().collect { posts -> 30 | _uiState.value = _uiState.value.copy(posts = posts) 31 | } 32 | } 33 | } 34 | 35 | fun changeSelectedTab(indexTab: Int) { 36 | _uiState.value = _uiState.value.copy(showFeed = indexTab == 0) 37 | } 38 | } -------------------------------------------------------------------------------- /app/src/main/java/com/paradoxo/materialgram/presentation/screens/reels/ReelsScreen.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalFoundationApi::class) 2 | 3 | package com.paradoxo.materialgram.presentation.screens.reels 4 | 5 | import android.view.ViewGroup 6 | import android.widget.FrameLayout 7 | import androidx.compose.animation.AnimatedVisibility 8 | import androidx.compose.animation.core.tween 9 | import androidx.compose.animation.fadeIn 10 | import androidx.compose.animation.fadeOut 11 | import androidx.compose.foundation.ExperimentalFoundationApi 12 | import androidx.compose.foundation.background 13 | import androidx.compose.foundation.layout.Box 14 | import androidx.compose.foundation.layout.Column 15 | import androidx.compose.foundation.layout.fillMaxSize 16 | import androidx.compose.foundation.pager.VerticalPager 17 | import androidx.compose.foundation.pager.rememberPagerState 18 | import androidx.compose.material.icons.Icons 19 | import androidx.compose.material.icons.outlined.CameraAlt 20 | import androidx.compose.material.icons.outlined.MoreVert 21 | import androidx.compose.material.icons.outlined.Search 22 | import androidx.compose.material3.ExperimentalMaterial3Api 23 | import androidx.compose.material3.Icon 24 | import androidx.compose.material3.IconButton 25 | import androidx.compose.material3.MaterialTheme 26 | import androidx.compose.material3.Text 27 | import androidx.compose.material3.TopAppBar 28 | import androidx.compose.material3.TopAppBarDefaults 29 | import androidx.compose.runtime.Composable 30 | import androidx.compose.runtime.DisposableEffect 31 | import androidx.compose.runtime.LaunchedEffect 32 | import androidx.compose.runtime.collectAsState 33 | import androidx.compose.runtime.getValue 34 | import androidx.compose.runtime.mutableStateOf 35 | import androidx.compose.runtime.remember 36 | import androidx.compose.runtime.setValue 37 | import androidx.compose.ui.Modifier 38 | import androidx.compose.ui.graphics.Color 39 | import androidx.compose.ui.platform.LocalLifecycleOwner 40 | import androidx.compose.ui.res.colorResource 41 | import androidx.compose.ui.text.font.FontWeight 42 | import androidx.compose.ui.viewinterop.AndroidView 43 | import androidx.lifecycle.Lifecycle 44 | import androidx.lifecycle.LifecycleEventObserver 45 | import androidx.lifecycle.viewmodel.compose.viewModel 46 | import androidx.media3.common.util.UnstableApi 47 | import androidx.media3.ui.AspectRatioFrameLayout 48 | import androidx.media3.ui.PlayerView 49 | import com.paradoxo.materialgram.R 50 | import com.paradoxo.materialgram.presentation.components.VideoHud 51 | 52 | @OptIn(ExperimentalMaterial3Api::class) 53 | @UnstableApi 54 | @Composable 55 | fun ReelsScreen() { 56 | val viewModel = viewModel() 57 | val state by viewModel.uiState.collectAsState() 58 | 59 | Box( 60 | modifier = Modifier 61 | .fillMaxSize() 62 | ) { 63 | VideoPlayer(viewModel) 64 | 65 | LoadOverlay(state) 66 | 67 | Column(Modifier.fillMaxSize()) { 68 | val pagerState = rememberPagerState(pageCount = { 69 | state.reels.size 70 | }) 71 | 72 | val currentReels = state.reels[pagerState.currentPage] 73 | 74 | LaunchedEffect(pagerState.getOffsetFractionForPage(0)) { 75 | val pageIsTotalVisible = pagerState.currentPageOffsetFraction == 0.0f 76 | if (pageIsTotalVisible) { 77 | viewModel.playVideo(currentReels.video) 78 | viewModel.showLoadAnimation() 79 | } 80 | } 81 | 82 | 83 | Box(Modifier.fillMaxSize()) { 84 | TopAppBar( 85 | title = { 86 | Text( 87 | text = "Reels", 88 | fontWeight = FontWeight.Bold, 89 | fontSize = MaterialTheme.typography.headlineLarge.fontSize, 90 | color = colorResource(id = R.color.reels_icon) 91 | ) 92 | }, 93 | actions = { 94 | IconButton(onClick = { /*TODO*/ }) { 95 | Icon( 96 | imageVector = Icons.Outlined.Search, 97 | contentDescription = "Buscar", 98 | tint = colorResource(id = R.color.reels_icon), 99 | ) 100 | } 101 | 102 | 103 | IconButton(onClick = { /*TODO*/ }) { 104 | Icon( 105 | imageVector = Icons.Outlined.CameraAlt, 106 | contentDescription = "Camêra", 107 | tint = colorResource(id = R.color.reels_icon), 108 | ) 109 | } 110 | 111 | IconButton(onClick = { /*TODO*/ }) { 112 | Icon( 113 | imageVector = Icons.Outlined.MoreVert, 114 | contentDescription = "Mais opções", 115 | tint = colorResource(id = R.color.reels_icon), 116 | ) 117 | } 118 | }, 119 | colors = TopAppBarDefaults.topAppBarColors( 120 | containerColor = Color.Transparent 121 | ), 122 | ) 123 | 124 | VerticalPager( 125 | state = pagerState, 126 | ) { 127 | VideoHud(currentReels.basePost) 128 | } 129 | } 130 | 131 | } 132 | } 133 | } 134 | 135 | 136 | @UnstableApi 137 | @Composable 138 | private fun VideoPlayer(viewModel: ReelsViewModel) { 139 | var lifecycle by remember { 140 | mutableStateOf(Lifecycle.Event.ON_CREATE) 141 | } 142 | val lifecycleOwner = LocalLifecycleOwner.current 143 | DisposableEffect(lifecycleOwner) { 144 | val observer = LifecycleEventObserver { _, event -> 145 | lifecycle = event 146 | } 147 | lifecycleOwner.lifecycle.addObserver(observer) 148 | 149 | onDispose { 150 | lifecycleOwner.lifecycle.removeObserver(observer) 151 | } 152 | } 153 | 154 | Column(Modifier.fillMaxSize()) { 155 | AndroidView( 156 | factory = { context -> 157 | PlayerView(context).apply { 158 | player = viewModel.player 159 | useController = false 160 | resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FILL 161 | layoutParams = FrameLayout.LayoutParams( 162 | ViewGroup.LayoutParams.MATCH_PARENT, 163 | ViewGroup.LayoutParams.MATCH_PARENT 164 | ) 165 | } 166 | }, 167 | update = { 168 | when (lifecycle) { 169 | Lifecycle.Event.ON_PAUSE -> { 170 | it.onPause() 171 | it.player?.pause() 172 | } 173 | 174 | Lifecycle.Event.ON_RESUME -> { 175 | it.onResume() 176 | it.player?.play() 177 | } 178 | 179 | else -> Unit 180 | } 181 | } 182 | ) 183 | 184 | } 185 | } 186 | 187 | @Composable 188 | private fun LoadOverlay(state: ReelsUiState) { 189 | AnimatedVisibility( 190 | visible = state.showLoadAnimation, 191 | enter = fadeIn(tween(state.loadAnimationTime / 3)), 192 | exit = fadeOut(tween(state.loadAnimationTime)), 193 | ) { 194 | Box( 195 | modifier = Modifier 196 | .fillMaxSize() 197 | .background(Color.Black) 198 | ) 199 | } 200 | } -------------------------------------------------------------------------------- /app/src/main/java/com/paradoxo/materialgram/presentation/screens/reels/ReelsUiState.kt: -------------------------------------------------------------------------------- 1 | package com.paradoxo.materialgram.presentation.screens.reels 2 | 3 | import com.paradoxo.materialgram.domain.model.Reels 4 | 5 | data class ReelsUiState( 6 | val reels: List, 7 | val showLoadAnimation: Boolean = false, 8 | val loadAnimationTime: Int = 800 9 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/paradoxo/materialgram/presentation/screens/reels/ReelsViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.paradoxo.materialgram.presentation.screens.reels 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import androidx.media3.common.MediaItem 6 | import androidx.media3.common.Player 7 | import com.paradoxo.materialgram.domain.VideoUseCase 8 | import dagger.hilt.android.lifecycle.HiltViewModel 9 | import kotlinx.coroutines.delay 10 | import kotlinx.coroutines.flow.MutableStateFlow 11 | import kotlinx.coroutines.flow.asStateFlow 12 | import kotlinx.coroutines.launch 13 | import javax.inject.Inject 14 | 15 | @HiltViewModel 16 | class ReelsViewModel @Inject constructor( 17 | private val videoUseCase: VideoUseCase, 18 | val player: Player 19 | ) : ViewModel() { 20 | 21 | private val _uiState = MutableStateFlow(ReelsUiState(emptyList())) 22 | val uiState = _uiState.asStateFlow() 23 | 24 | init { 25 | player.prepare() 26 | 27 | viewModelScope.launch { 28 | loadVideos() 29 | } 30 | } 31 | 32 | private suspend fun loadVideos() { 33 | videoUseCase.getVideos().collect { videos -> 34 | _uiState.value = _uiState.value.copy( 35 | reels = videos 36 | ) 37 | } 38 | } 39 | 40 | fun playVideo(path: String) { 41 | val mediaItem = MediaItem.Builder() 42 | .setUri(path) 43 | .build() 44 | player.setMediaItem(mediaItem) 45 | } 46 | 47 | 48 | private fun resetLoadAnimation() { 49 | _uiState.value = _uiState.value.copy( 50 | showLoadAnimation = !_uiState.value.showLoadAnimation 51 | ) 52 | } 53 | 54 | fun showLoadAnimation() { 55 | _uiState.value = _uiState.value.copy( 56 | showLoadAnimation = true 57 | ) 58 | 59 | viewModelScope.launch { 60 | with(_uiState.value) { 61 | if (showLoadAnimation) { 62 | delay(loadAnimationTime.toLong()) 63 | resetLoadAnimation() 64 | } 65 | } 66 | } 67 | } 68 | 69 | override fun onCleared() { 70 | super.onCleared() 71 | player.release() 72 | } 73 | } -------------------------------------------------------------------------------- /app/src/main/java/com/paradoxo/materialgram/presentation/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package com.paradoxo.materialgram.presentation.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val Purple80 = Color(0xFF60412E) 6 | val PurpleGrey80 = Color(0xFFCCC2DC) 7 | val Pink80 = Color(0xFFEFB8C8) 8 | 9 | val PrimaryContainerDark = Color(0xFF271205) 10 | val SurfaceBeigeDark = Color(0xFF312620) 11 | val OnSurfaceBeigeDark = Color(0xFFD6C5B9) 12 | 13 | val Purple40 = Color(0xFF8f4f24) 14 | val PurpleGrey40 = Color(0xFF625b71) 15 | val Pink40 = Color(0xFF7D5260) 16 | 17 | val PrimaryContainerLight = Color(0xFFFEDBC8) 18 | val SurfaceBeigeLight = Color(0xFFF5ECED) 19 | val OnSurfaceBeigeLight = Color(0xFF807D7C) 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/paradoxo/materialgram/presentation/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.paradoxo.materialgram.presentation.theme 2 | 3 | import android.app.Activity 4 | import android.os.Build 5 | import androidx.compose.foundation.isSystemInDarkTheme 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.material3.darkColorScheme 8 | import androidx.compose.material3.dynamicDarkColorScheme 9 | import androidx.compose.material3.dynamicLightColorScheme 10 | import androidx.compose.material3.lightColorScheme 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.SideEffect 13 | import androidx.compose.ui.graphics.toArgb 14 | import androidx.compose.ui.platform.LocalContext 15 | import androidx.compose.ui.platform.LocalView 16 | import androidx.core.view.WindowCompat 17 | 18 | private val DarkColorScheme = darkColorScheme( 19 | primary = Purple80, 20 | secondary = PurpleGrey80, 21 | tertiary = Pink80, 22 | primaryContainer = PrimaryContainerDark, 23 | onSecondaryContainer = PrimaryContainerLight, 24 | secondaryContainer = PrimaryContainerDark, 25 | 26 | surface = SurfaceBeigeDark, 27 | onSurface = OnSurfaceBeigeDark 28 | ) 29 | 30 | private val LightColorScheme = lightColorScheme( 31 | primary = Purple40, 32 | secondary = PurpleGrey40, 33 | tertiary = Pink40, 34 | primaryContainer = PrimaryContainerLight, 35 | onSecondaryContainer = PrimaryContainerDark, 36 | secondaryContainer = PrimaryContainerLight, 37 | 38 | surface = SurfaceBeigeLight, 39 | onSurface = OnSurfaceBeigeLight 40 | 41 | /* Other default colors to override 42 | background = Color(0xFFFFFBFE), 43 | surface = Color(0xFFFFFBFE), 44 | onPrimary = Color.White, 45 | onSecondary = Color.White, 46 | onTertiary = Color.White, 47 | onBackground = Color(0xFF1C1B1F), 48 | onSurface = Color(0xFF1C1B1F), 49 | */ 50 | ) 51 | 52 | @Composable 53 | fun MaterialGramTheme( 54 | darkTheme: Boolean = isSystemInDarkTheme(), 55 | dynamicColor: Boolean = false, 56 | content: @Composable () -> Unit 57 | ) { 58 | val colorScheme = when { 59 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { 60 | val context = LocalContext.current 61 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) 62 | } 63 | 64 | darkTheme -> DarkColorScheme 65 | else -> LightColorScheme 66 | } 67 | val view = LocalView.current 68 | if (!view.isInEditMode) { 69 | SideEffect { 70 | val window = (view.context as Activity).window 71 | window.statusBarColor = colorScheme.background.toArgb() 72 | window.navigationBarColor = colorScheme.surface.toArgb() 73 | WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme 74 | WindowCompat.getInsetsController(window, view).isAppearanceLightNavigationBars = 75 | !darkTheme 76 | } 77 | } 78 | 79 | MaterialTheme( 80 | colorScheme = colorScheme, 81 | typography = Typography, 82 | content = content 83 | ) 84 | } -------------------------------------------------------------------------------- /app/src/main/java/com/paradoxo/materialgram/presentation/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package com.paradoxo.materialgram.presentation.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/res/drawable/custom_foreground.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_error.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /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/shimmer_animation.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-jr/MaterialGram/d6291086d4c0ccbb9faa3e77ad8023d730973688/app/src/main/res/drawable/shimmer_animation.gif -------------------------------------------------------------------------------- /app/src/main/res/layout/image_item.xml: -------------------------------------------------------------------------------- 1 | 10 | 11 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /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/MaterialGram/d6291086d4c0ccbb9faa3e77ad8023d730973688/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-jr/MaterialGram/d6291086d4c0ccbb9faa3e77ad8023d730973688/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/MaterialGram/d6291086d4c0ccbb9faa3e77ad8023d730973688/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-jr/MaterialGram/d6291086d4c0ccbb9faa3e77ad8023d730973688/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/MaterialGram/d6291086d4c0ccbb9faa3e77ad8023d730973688/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-jr/MaterialGram/d6291086d4c0ccbb9faa3e77ad8023d730973688/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/MaterialGram/d6291086d4c0ccbb9faa3e77ad8023d730973688/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-jr/MaterialGram/d6291086d4c0ccbb9faa3e77ad8023d730973688/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/MaterialGram/d6291086d4c0ccbb9faa3e77ad8023d730973688/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-jr/MaterialGram/d6291086d4c0ccbb9faa3e77ad8023d730973688/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 | 11 | #FFFEDBC8 12 | #FF271205 13 | -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFD0B6 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | MaterialGram 3 | 4 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sit amet nunc eget nunc aliquam ultricies. Donec euismod, nisl eget aliquet ultricies, nunc nisl aliquet nunc, eget aliquam nisl nisl quis nisl. Donec euismod, nisl eget aliquet ultricies, nunc nisl aliquet nunc, eget aliquam nisl nisl quis nisl. 5 | 6 | 7 | %d like 8 | %d likes 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |