├── .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 | 
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 |
5 |
--------------------------------------------------------------------------------
/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/materialgram/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.paradoxo.materialgram
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.hilt) apply false
7 | alias(libs.plugins.ksp) apply false
8 | }
9 | 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.3.0-alpha05"
3 | dagger-compiler = "2.48"
4 | hilt-android = "2.48"
5 | kotlin = "1.9.0"
6 | ksp = "1.9.0-1.0.13"
7 | core-ktx = "1.12.0"
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.2"
12 | activity-compose = "1.7.2"
13 | compose-bom = "2023.09.01"
14 | lifecycle-viewmodel-compose = "2.6.2"
15 | material = "1.9.0"
16 | coil = "2.4.0"
17 | appcompat = "1.6.1"
18 | constraintlayout = "2.1.4"
19 | media3 = "1.1.1"
20 | retrofit = "2.9.0"
21 |
22 |
23 | [libraries]
24 |
25 | dagger-compiler = { module = "com.google.dagger:dagger-compiler", version.ref = "dagger-compiler" }
26 | hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt-android" }
27 | hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "dagger-compiler" }
28 | viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycle-viewmodel-compose" }
29 | material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" }
30 |
31 | coil = { module = "io.coil-kt:coil", version.ref = "coil" }
32 | coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" }
33 | coil-gif = { module = "io.coil-kt:coil-gif", version.ref = "coil" }
34 | core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "core-ktx" }
35 | junit = { group = "junit", name = "junit", version.ref = "junit" }
36 | androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-ext-junit" }
37 | espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso-core" }
38 | lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle-runtime-ktx" }
39 | activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activity-compose" }
40 | compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" }
41 | material = { module = "com.google.android.material:material", version.ref = "material" }
42 | ui = { group = "androidx.compose.ui", name = "ui" }
43 | ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
44 | ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
45 | ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
46 | ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
47 | ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
48 | material3 = { group = "androidx.compose.material3", name = "material3" }
49 | appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
50 | constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
51 | retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
52 | moshi = { group = "com.squareup.retrofit2", name ="converter-moshi", version.ref = "retrofit" }
53 |
54 | media3_base = { group = "androidx.media3", name = "media3-exoplayer", version.ref = "media3" }
55 | media3_ui = { group = "androidx.media3", name = "media3-ui", version.ref = "media3" }
56 |
57 | [plugins]
58 | androidApplication = { id = "com.android.application", version.ref = "agp" }
59 | kotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
60 | ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
61 | hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt-android" }
62 |
63 | [bundles]
64 | media3 = ["media3_base", "media3_ui" ]
65 | retrofit = ["retrofit", "moshi"]
66 |
67 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/git-jr/MaterialGram/d6291086d4c0ccbb9faa3e77ad8023d730973688/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Wed Sep 06 09:00:57 BRT 2023
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-rc-2-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 = "MaterialGram"
17 | include(":app")
18 |
--------------------------------------------------------------------------------