building business logic

This commit is contained in:
Erik Cavazos 2023-02-11 23:41:23 -06:00
parent 82170c4edc
commit 34c61df362
72 changed files with 1245 additions and 62 deletions

View File

@ -63,7 +63,7 @@ dependencies {
// Material 3 // Material 3
implementation "com.google.android.material:material:1.9.0-alpha01" implementation "com.google.android.material:material:1.8.0"
// Navigation // Navigation
def nav_version = "2.5.3" def nav_version = "2.5.3"
@ -77,6 +77,14 @@ dependencies {
// Data security // Data security
implementation "androidx.security:security-crypto:1.0.0" implementation "androidx.security:security-crypto:1.0.0"
// Markwon
final def markwon_version = '4.6.2'
implementation "io.noties.markwon:core:$markwon_version"
implementation "io.noties.markwon:editor:$markwon_version"
implementation "io.noties.markwon:image-picasso:$markwon_version"
implementation "io.noties.markwon:linkify:$markwon_version"
} }
kapt { kapt {

View File

@ -24,6 +24,8 @@
</activity> </activity>
<activity android:name=".home.HomeActivity" android:theme="@style/Theme.Isolaatti" /> <activity android:name=".home.HomeActivity" android:theme="@style/Theme.Isolaatti" />
<activity android:name=".login.LogInActivity" android:theme="@style/Theme.Isolaatti" /> <activity android:name=".login.LogInActivity" android:theme="@style/Theme.Isolaatti" />
<activity android:name=".profile.ui.ProfileActivity" android:theme="@style/Theme.Isolaatti"/>
<activity android:name=".settings.ui.SettingsActivity" android:theme="@style/Theme.Isolaatti"/>
</application> </application>
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
</manifest> </manifest>

View File

@ -25,7 +25,9 @@ class MainActivity : ComponentActivity() {
val currentToken = authRepository.getCurrentToken() val currentToken = authRepository.getCurrentToken()
if(currentToken == null) { if(currentToken == null) {
startActivity(Intent(this@MainActivity, LogInActivity::class.java)) startActivity(Intent(this@MainActivity, LogInActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NO_HISTORY
})
} else { } else {
startActivity(Intent(this@MainActivity, HomeActivity::class.java)) startActivity(Intent(this@MainActivity, HomeActivity::class.java))
} }

View File

@ -1,27 +1,29 @@
package com.isolaatti package com.isolaatti
import com.isolaatti.auth.data.AuthRepositoryImpl
import com.isolaatti.auth.data.local.TokenStorage
import com.isolaatti.auth.data.remote.AuthApi
import com.isolaatti.auth.domain.AuthRepository import com.isolaatti.auth.domain.AuthRepository
import com.isolaatti.connectivity.AuthenticationInterceptor
import com.isolaatti.connectivity.RetrofitClient import com.isolaatti.connectivity.RetrofitClient
import com.squareup.picasso.Picasso
import com.squareup.picasso.RequestCreator
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityComponent
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import io.noties.markwon.image.AsyncDrawable
import io.noties.markwon.image.picasso.PicassoImagesPlugin
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
class MainModule { class MainModule {
@Provides @Provides
fun provideAuthApi():AuthApi { fun provideAuthenticationInterceptor(authRepository: dagger.Lazy<AuthRepository>): AuthenticationInterceptor {
return RetrofitClient.client.create(AuthApi::class.java) return AuthenticationInterceptor(authRepository)
}
@Provides
fun provideRetrofitClient(authenticationInterceptor: AuthenticationInterceptor) : RetrofitClient {
return RetrofitClient(authenticationInterceptor)
} }
@Provides
fun provideAuthRepository(tokenStorage: TokenStorage, authApi: AuthApi): AuthRepository {
return AuthRepositoryImpl(tokenStorage, authApi)
}
} }

View File

@ -5,12 +5,14 @@ import com.isolaatti.auth.data.AuthRepositoryImpl
import com.isolaatti.auth.data.local.TokenStorage import com.isolaatti.auth.data.local.TokenStorage
import com.isolaatti.auth.data.remote.AuthApi import com.isolaatti.auth.data.remote.AuthApi
import com.isolaatti.auth.domain.AuthRepository import com.isolaatti.auth.domain.AuthRepository
import com.isolaatti.connectivity.RetrofitClient
import dagger.Provides import dagger.Provides
import dagger.hilt.android.HiltAndroidApp import dagger.hilt.android.HiltAndroidApp
import javax.inject.Singleton import javax.inject.Singleton
@HiltAndroidApp @HiltAndroidApp
class MyApplication : Application() { class MyApplication : Application() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
} }

View File

@ -0,0 +1,24 @@
package com.isolaatti.auth
import com.isolaatti.auth.data.AuthRepositoryImpl
import com.isolaatti.auth.data.local.TokenStorage
import com.isolaatti.auth.data.remote.AuthApi
import com.isolaatti.auth.domain.AuthRepository
import com.isolaatti.connectivity.RetrofitClient
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
class Module {
@Provides
fun provideAuthApi(retrofitClient: RetrofitClient): AuthApi {
return retrofitClient.client.create(AuthApi::class.java)
}
@Provides
fun provideAuthRepository(tokenStorage: TokenStorage, authApi: AuthApi): AuthRepository {
return AuthRepositoryImpl(tokenStorage, authApi)
}
}

View File

@ -0,0 +1,4 @@
package com.isolaatti.comments
class Module {
}

View File

@ -0,0 +1,8 @@
package com.isolaatti.comments.data.remote
import com.isolaatti.comments.domain.model.Comment
data class CommentDto(
val comment: Comment,
val username: String
)

View File

@ -0,0 +1,6 @@
package com.isolaatti.comments.data.remote
data class CommentToPostDto(
val content: String,
val audioId: String?
)

View File

@ -0,0 +1,10 @@
package com.isolaatti.comments.data.remote
import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.POST
interface CommentsApi {
@POST("Posting/Post/{postId}/Comment")
fun postComment(@Body commentToPost: CommentToPostDto): Call<Nothing>
}

View File

@ -0,0 +1,8 @@
package com.isolaatti.comments.data.repository
import com.isolaatti.comments.data.remote.CommentsApi
import com.isolaatti.comments.domain.CommentRepository
import javax.inject.Inject
class CommentsRepositoryImpl @Inject constructor(commentsApi: CommentsApi) : CommentRepository {
}

View File

@ -0,0 +1,5 @@
package com.isolaatti.comments.domain
interface CommentRepository {
}

View File

@ -0,0 +1,12 @@
package com.isolaatti.comments.domain.model
data class Comment(
val id: Long,
val textContent: String,
val userId: Int,
val postId: Long,
val date: String,
val responseForCommentId: Long?,
val linkedDiscussionId: Long?,
val linkedCommentId: Long?
)

View File

@ -0,0 +1,26 @@
package com.isolaatti.connectivity
import com.isolaatti.auth.domain.AuthRepository
import okhttp3.Interceptor
import okhttp3.Response
class AuthenticationInterceptor(private val authRepository: dagger.Lazy<AuthRepository>) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val url = chain.request().url()
val path = url.url().path
if(RetrofitClient.excludedUrlsFromAuthentication.contains(path)){
return chain.proceed(chain.request())
}
// Add auth header here
val tokenDto = authRepository.get().getCurrentToken()
tokenDto?.token?.let {
val request = chain.request().newBuilder().addHeader("sessionToken", it) .build()
return chain.proceed(request)
}
return chain.proceed(chain.request())
}
}

View File

@ -1,53 +1,28 @@
package com.isolaatti.connectivity package com.isolaatti.connectivity
import android.util.Log
import com.isolaatti.auth.domain.AuthRepository
import okhttp3.Interceptor
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Response
import retrofit2.Retrofit import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory import retrofit2.converter.gson.GsonConverterFactory
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Provider
object RetrofitClient {
class RetrofitClient @Inject constructor(private val authenticationInterceptor: AuthenticationInterceptor) {
companion object {
// These urls don't need auth header // These urls don't need auth header
private val excludedUrlsFromAuthentication = listOf( val excludedUrlsFromAuthentication = listOf(
"/api/LogIn" "/api/LogIn"
) )
const val BASE_URL = "https://isolaatti.com/api/"
class AuthenticationInterceptor : Interceptor {
@Inject lateinit var authRepository: Provider<AuthRepository>
override fun intercept(chain: Interceptor.Chain): Response {
val url = chain.request().url()
val path = url.url().path
if(excludedUrlsFromAuthentication.contains(path)){
return chain.proceed(chain.request())
} }
// Add auth header here
val tokenDto = authRepository.get().getCurrentToken()
tokenDto?.token?.let {
val request = chain.request().newBuilder().addHeader("sessionToken", it) .build()
return chain.proceed(request)
}
return chain.proceed(chain.request())
}
}
private val okHttpClient get() = OkHttpClient.Builder() private val okHttpClient get() = OkHttpClient.Builder()
.addInterceptor(AuthenticationInterceptor()) .addInterceptor(authenticationInterceptor)
.build() .build()
val client: Retrofit get() = Retrofit.Builder() val client: Retrofit get() = Retrofit.Builder()
.baseUrl("https://isolaatti.com/api/") .baseUrl(BASE_URL)
.client(okHttpClient) .client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create()) .addConverterFactory(GsonConverterFactory.create())
.build() .build()

View File

@ -0,0 +1,4 @@
package com.isolaatti.followers
class Module {
}

View File

@ -0,0 +1,23 @@
package com.isolaatti.followers.data.remote
import com.isolaatti.profile.data.remote.ProfileListItemDto
import com.isolaatti.utils.IntIdentificationWrapper
import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.Path
interface FollowersApi {
@GET("Following/FollowersOf/{userId}")
fun getFollowersOfUser(@Path("userId") userId: Int): Call<List<ProfileListItemDto>>
@GET("Following/FollowingsOf/{userId}")
fun getFollowingsOfUser(@Path("userId") userId: Int): Call<List<ProfileListItemDto>>
@POST("Following/Follow")
fun followUser(@Body id: IntIdentificationWrapper): Call<Nothing>
@POST("Following/Unfollow")
fun unfollowUser(@Body id: IntIdentificationWrapper): Call<Nothing>
}

View File

@ -2,19 +2,24 @@ package com.isolaatti.home
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.view.Menu
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.content.res.AppCompatResources
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver
import androidx.navigation.fragment.NavHostFragment import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.setupWithNavController import androidx.navigation.ui.setupWithNavController
import com.google.android.material.search.SearchBar import com.google.android.material.search.SearchBar
import com.isolaatti.R import com.isolaatti.R
import com.isolaatti.databinding.ActivityHomeBinding import com.isolaatti.databinding.ActivityHomeBinding
import com.isolaatti.home.feed.presentation.FeedViewModel
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint @AndroidEntryPoint
class HomeActivity : AppCompatActivity() { class HomeActivity : AppCompatActivity() {
lateinit var viewBinding: ActivityHomeBinding lateinit var viewBinding: ActivityHomeBinding
val feedViewModel: FeedViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -22,6 +27,8 @@ class HomeActivity : AppCompatActivity() {
setContentView(viewBinding.root) setContentView(viewBinding.root)
val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
viewBinding.bottomNavigation.setupWithNavController(navHostFragment.navController) viewBinding.bottomNavigation.setupWithNavController(navHostFragment.navController)
feedViewModel.getFeed()
} }
override fun onCreateOptionsMenu(menu: Menu?): Boolean { override fun onCreateOptionsMenu(menu: Menu?): Boolean {

View File

@ -0,0 +1,27 @@
package com.isolaatti.home
import com.isolaatti.connectivity.RetrofitClient
import com.isolaatti.home.feed.data.remote.FeedApi
import com.isolaatti.home.feed.data.repository.FeedRepositoryImpl
import com.isolaatti.home.feed.domain.repository.FeedRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityComponent
import dagger.hilt.android.scopes.ActivityScoped
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
class Module {
@Provides
fun provideFeedApi(retrofitClient: RetrofitClient): FeedApi {
return retrofitClient.client.create(FeedApi::class.java)
}
@Provides
fun provideFeedRepository(feedApi: FeedApi): FeedRepository {
return FeedRepositoryImpl(feedApi)
}
}

View File

@ -0,0 +1,10 @@
package com.isolaatti.home.feed.data.remote
import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Query
interface FeedApi {
@GET("Feed")
fun fetchFeed(@Query("lastId") lastId: Long, @Query("length") length: Int): Call<FeedDto>
}

View File

@ -0,0 +1,6 @@
package com.isolaatti.home.feed.data.remote
data class FeedDto(
val data: List<PostDto>,
val moreContent: Boolean
)

View File

@ -0,0 +1,12 @@
package com.isolaatti.home.feed.data.remote
import com.isolaatti.home.feed.domain.model.Post
data class PostDto(
val post: Post,
val numberOfLikes: Int,
val numberOfComments: Int,
val userName: String,
val squadName: String?,
val liked: Boolean
)

View File

@ -0,0 +1,20 @@
package com.isolaatti.home.feed.data.repository
import com.isolaatti.home.feed.data.remote.FeedApi
import com.isolaatti.home.feed.data.remote.FeedDto
import com.isolaatti.home.feed.domain.repository.FeedRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import retrofit2.awaitResponse
import javax.inject.Inject
class FeedRepositoryImpl @Inject constructor(private val feedApi: FeedApi): FeedRepository {
override fun getNextPage(lastId: Long, count: Int): Flow<FeedDto?> = flow {
val response = feedApi.fetchFeed(lastId, count).awaitResponse().body()
if(response != null){
emit(response)
} else {
emit(null)
}
}
}

View File

@ -0,0 +1,13 @@
package com.isolaatti.home.feed.domain.model
data class Post(
val id: Long,
val textContent: String,
val userId: Int,
val privacy: Int,
val date: String,
val audioId: String,
val squadId: String,
val linkedDiscussionId: Long,
val linkedCommentId: Long
)

View File

@ -0,0 +1,8 @@
package com.isolaatti.home.feed.domain.repository
import com.isolaatti.home.feed.data.remote.FeedDto
import kotlinx.coroutines.flow.Flow
interface FeedRepository {
fun getNextPage(lastId: Long, count: Int): Flow<FeedDto?>
}

View File

@ -1,7 +1,29 @@
package com.isolaatti.home.feed.presentation package com.isolaatti.home.feed.presentation
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.isolaatti.home.feed.data.remote.FeedDto
import com.isolaatti.home.feed.domain.repository.FeedRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import javax.inject.Inject
class FeedViewModel : ViewModel() { @HiltViewModel
// TODO: Implement the ViewModel class FeedViewModel @Inject constructor(private val feedRepository: FeedRepository) : ViewModel() {
private val _feed: MutableLiveData<FeedDto> = MutableLiveData()
val feed: LiveData<FeedDto> get() = _feed
private fun getLastId(): Long = try {_feed.value?.data?.last()?.post?.id ?: 0} catch (e: NoSuchElementException) { 0 }
fun getFeed() {
viewModelScope.launch {
feedRepository.getNextPage(0, 20).collect {
_feed.postValue(it)
}
}
}
} }

View File

@ -1,23 +1,37 @@
package com.isolaatti.home.feed.ui package com.isolaatti.home.feed.ui
import android.content.Intent
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import android.os.Bundle import android.os.Bundle
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.LinearLayoutManager
import com.isolaatti.R import com.isolaatti.R
import com.isolaatti.databinding.FragmentFeedBinding import com.isolaatti.databinding.FragmentFeedBinding
import com.isolaatti.home.feed.presentation.FeedViewModel import com.isolaatti.home.feed.presentation.FeedViewModel
import com.isolaatti.profile.ui.ProfileActivity
import com.isolaatti.settings.ui.SettingsActivity
import com.isolaatti.utils.PicassoImagesPluginDef
import dagger.hilt.android.AndroidEntryPoint
import io.noties.markwon.AbstractMarkwonPlugin
import io.noties.markwon.Markwon
import io.noties.markwon.MarkwonConfiguration
import io.noties.markwon.image.destination.ImageDestinationProcessorRelativeToAbsolute
import io.noties.markwon.linkify.LinkifyPlugin
@AndroidEntryPoint
class FeedFragment : Fragment() { class FeedFragment : Fragment() {
companion object { companion object {
fun newInstance() = FeedFragment() fun newInstance() = FeedFragment()
} }
private lateinit var viewModel: FeedViewModel private val viewModel: FeedViewModel by activityViewModels()
private lateinit var viewBinding: FragmentFeedBinding private lateinit var viewBinding: FragmentFeedBinding
private lateinit var adapter: FeedRecyclerViewAdapter
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater, container: ViewGroup?,
@ -25,15 +39,46 @@ class FeedFragment : Fragment() {
): View? { ): View? {
viewBinding = FragmentFeedBinding.inflate(inflater) viewBinding = FragmentFeedBinding.inflate(inflater)
viewBinding.searchBar.setNavigationOnClickListener {
viewBinding.drawerLayout.openDrawer(viewBinding.homeDrawer)
}
return viewBinding.root return viewBinding.root
} }
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProvider(this).get(FeedViewModel::class.java) viewBinding.topAppBar.setNavigationOnClickListener {
// TODO: Use the ViewModel viewBinding.drawerLayout.openDrawer(viewBinding.homeDrawer)
}
viewBinding.homeDrawer.setNavigationItemSelectedListener {
when(it.itemId) {
R.id.my_profile_menu_item -> {
startActivity(Intent(requireActivity(), ProfileActivity::class.java))
true
}
R.id.settings_menu_item -> {
startActivity(Intent(requireActivity(), SettingsActivity::class.java))
true
}
else -> {true}
}
}
val markwon = Markwon.builder(requireContext())
.usePlugin(object: AbstractMarkwonPlugin() {
override fun configureConfiguration(builder: MarkwonConfiguration.Builder) {
builder
.imageDestinationProcessor(ImageDestinationProcessorRelativeToAbsolute
.create("https://isolaatti.com/"))
}
})
.usePlugin(PicassoImagesPluginDef.picassoImagePlugin)
.usePlugin(LinkifyPlugin.create())
.build()
adapter = FeedRecyclerViewAdapter(markwon)
viewBinding.feedRecyclerView.adapter = adapter
viewBinding.feedRecyclerView.layoutManager = LinearLayoutManager(requireContext())
viewModel.feed.observe(viewLifecycleOwner){
adapter.submitList(it.data)
}
} }
} }

View File

@ -0,0 +1,62 @@
package com.isolaatti.home.feed.ui
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import com.isolaatti.R
import com.isolaatti.home.feed.data.remote.PostDto
import com.isolaatti.utils.UrlGen.userProfileImage
import com.squareup.picasso.Picasso
import io.noties.markwon.Markwon
import java.text.DateFormat
import java.util.Date
class FeedRecyclerViewAdapter(private val markwon: Markwon) : ListAdapter<PostDto, FeedRecyclerViewAdapter.FeedViewHolder>(DIFF_CALLBACK) {
inner class FeedViewHolder(itemView: View) : ViewHolder(itemView) {
fun bindView(postDto: PostDto) {
val username: TextView = itemView.findViewById(R.id.text_view_username)
username.text = postDto.userName
val profileImageView: ImageView = itemView.findViewById(R.id.avatar_picture)
Picasso.get().load(userProfileImage(postDto.post.userId)).into(profileImageView)
val dateTextView: TextView = itemView.findViewById(R.id.text_view_date)
dateTextView.text = postDto.post.date
val content: TextView = itemView.findViewById(R.id.post_content)
markwon.setMarkdown(content, postDto.post.textContent)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FeedViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.post_layout, parent, false)
return FeedViewHolder(view)
}
override fun onBindViewHolder(holder: FeedViewHolder, position: Int) {
holder.bindView(getItem(position))
}
companion object {
val DIFF_CALLBACK = object: DiffUtil.ItemCallback<PostDto>() {
override fun areItemsTheSame(oldItem: PostDto, newItem: PostDto): Boolean {
return oldItem.post.id == newItem.post.id
}
override fun areContentsTheSame(oldItem: PostDto, newItem: PostDto): Boolean {
return oldItem.post.id == newItem.post.id && oldItem.post.textContent == newItem.post.textContent
}
}
}
}

View File

@ -14,7 +14,7 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
class LogInViewModel @Inject constructor(private val authRepository: AuthRepository): ViewModel() { class LogInViewModel @Inject constructor(private val authRepository: AuthRepository): ViewModel() {
val signInSuccess: MutableLiveData<Boolean> = MutableLiveData(false) val signInSuccess: MutableLiveData<Boolean> = MutableLiveData()
val formIsValid: MutableLiveData<Boolean> = MutableLiveData(false) val formIsValid: MutableLiveData<Boolean> = MutableLiveData(false)
val emailUserInputIsValid: MutableLiveData<Boolean> = MutableLiveData(false) val emailUserInputIsValid: MutableLiveData<Boolean> = MutableLiveData(false)
val passwordUserInputIsValid: MutableLiveData<Boolean> = MutableLiveData(false) val passwordUserInputIsValid: MutableLiveData<Boolean> = MutableLiveData(false)

View File

@ -0,0 +1,4 @@
package com.isolaatti.posts
class Module {
}

View File

@ -0,0 +1,23 @@
package com.isolaatti.posts.data.remote
import com.isolaatti.home.feed.data.remote.FeedDto
import com.isolaatti.home.feed.data.remote.PostDto
import com.isolaatti.profile.data.remote.ProfileListItemDto
import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Path
import retrofit2.http.Query
interface PostsApi {
@GET("Fetch/PostsOfUser/{userId}")
fun postsOfUser(@Path("userId") userId: Int,
@Query("length") length: Int,
@Query("lastId") lastId: Long,
@Query("olderFirst") olderFirst: Boolean): Call<FeedDto>
@GET("Fetch/Post/{postId}")
fun getPost(@Path("postId") postId: Long): Call<PostDto>
@GET("Fetch/Post/{postId}/LikedBy")
fun getLikedBy(@Path("postId") postId: Long): Call<List<ProfileListItemDto>>
}

View File

@ -0,0 +1,23 @@
package com.isolaatti.profile
import com.isolaatti.connectivity.RetrofitClient
import com.isolaatti.profile.data.remote.ProfileApi
import com.isolaatti.profile.data.repository.ProfileRepositoryImpl
import com.isolaatti.profile.domain.ProfileRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@Module
@InstallIn
class Module {
@Provides
fun provideProfileApi(retrofitClient: RetrofitClient): ProfileApi {
return retrofitClient.client.create(ProfileApi::class.java)
}
@Provides
fun provideProfileRepository(profileApi: ProfileApi): ProfileRepository {
return ProfileRepositoryImpl(profileApi)
}
}

View File

@ -0,0 +1,14 @@
package com.isolaatti.profile.data.remote
import com.isolaatti.home.feed.data.remote.FeedDto
import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Path
interface ProfileApi {
@GET("Fetch/UserProfile/{userId}")
fun userProfile(@Path("userId") userId: Int): Call<UserProfileDto>
}

View File

@ -0,0 +1,7 @@
package com.isolaatti.profile.data.remote
data class ProfileListItemDto(
val id: Int,
val name: String,
val profileImageId: String?
)

View File

@ -0,0 +1,17 @@
package com.isolaatti.profile.data.remote
data class UserProfileDto(
val id: Int,
val name: String,
val email: String,
val numberOfFollowers: Int,
val numberOfFollowing: Int,
val numberOfLikes: Int,
val numberOfPosts: Int,
val isUserItself: Boolean,
val followingThisUser: Boolean,
val thisUserIsFollowingMe: Boolean,
val profileImageId: String?,
val descriptionText: String,
val descriptionAudioId: String?
)

View File

@ -0,0 +1,14 @@
package com.isolaatti.profile.data.repository
import com.isolaatti.profile.data.remote.ProfileApi
import com.isolaatti.profile.data.remote.UserProfileDto
import com.isolaatti.profile.domain.ProfileRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import retrofit2.await
class ProfileRepositoryImpl(private val profileApi: ProfileApi) : ProfileRepository {
override fun getProfile(): Flow<UserProfileDto> = flow {
}
}

View File

@ -0,0 +1,9 @@
package com.isolaatti.profile.domain
import com.isolaatti.profile.data.remote.ProfileApi
import com.isolaatti.profile.data.remote.UserProfileDto
import kotlinx.coroutines.flow.Flow
interface ProfileRepository {
fun getProfile(): Flow<UserProfileDto>
}

View File

@ -0,0 +1,9 @@
package com.isolaatti.profile.presentation
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class ProfileViewModel @Inject constructor() : ViewModel() {
}

View File

@ -0,0 +1,21 @@
package com.isolaatti.profile.ui
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import com.isolaatti.databinding.FragmentAudiosBinding
class AudiosFragment : Fragment() {
lateinit var viewBinding: FragmentAudiosBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
viewBinding = FragmentAudiosBinding.inflate(inflater)
return viewBinding.root
}
}

View File

@ -0,0 +1,22 @@
package com.isolaatti.profile.ui
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import com.isolaatti.databinding.FragmentDiscussionsBinding
class DiscussionsFragment : Fragment() {
lateinit var viewBinding: FragmentDiscussionsBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
viewBinding = FragmentDiscussionsBinding.inflate(inflater)
return viewBinding.root
}
}

View File

@ -0,0 +1,22 @@
package com.isolaatti.profile.ui
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import com.isolaatti.databinding.FragmentImagesBinding
class ImagesFragment : Fragment() {
lateinit var viewBinding: FragmentImagesBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
viewBinding = FragmentImagesBinding.inflate(inflater)
return viewBinding.root
}
}

View File

@ -0,0 +1,70 @@
package com.isolaatti.profile.ui
import android.os.Bundle
import androidx.activity.addCallback
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.viewpager.widget.PagerAdapter
import androidx.viewpager2.adapter.FragmentStateAdapter
import androidx.viewpager2.adapter.FragmentViewHolder
import com.google.android.material.tabs.TabLayoutMediator
import com.isolaatti.R
import com.isolaatti.databinding.ActivityProfileBinding
import com.squareup.picasso.Picasso
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class ProfileActivity : AppCompatActivity() {
class ViewPagerAdapter(activity: FragmentActivity) : FragmentStateAdapter(activity) {
override fun getItemCount(): Int = 3
override fun createFragment(position: Int): Fragment {
return when(position) {
0 -> {
DiscussionsFragment()
}
1 -> {
AudiosFragment()
}
2 -> {
ImagesFragment()
}
else -> {Fragment()}
}
}
}
lateinit var viewBinding: ActivityProfileBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewBinding = ActivityProfileBinding.inflate(layoutInflater)
setContentView(viewBinding.root)
Picasso.get().load("https://isolaatti.com/api/images/image/63a2a6c5270ecc2be2512799?mode=reduced").into(viewBinding.profileImageView)
viewBinding.textViewUsername.text = "Erik Everardo"
viewBinding.textViewDescription.text = "Hola"
viewBinding.profileViewPager2.adapter = ViewPagerAdapter(this)
viewBinding.topAppBar.setNavigationOnClickListener {
onBackPressed()
}
TabLayoutMediator(viewBinding.profileTabLayout, viewBinding.profileViewPager2) {tab, position ->
when(position) {
0 -> {
tab.text = getText(R.string.discussions)
}
1 -> {
tab.text = getText(R.string.audios)
}
2 -> {
tab.text = getText(R.string.images)
}
}
}.attach()
}
}

View File

@ -0,0 +1,21 @@
package com.isolaatti.settings.ui
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import com.isolaatti.databinding.FragmentSettingsChangePasswordBinding
class ChangePasswordFragment : Fragment() {
lateinit var viewBinding: FragmentSettingsChangePasswordBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
viewBinding = FragmentSettingsChangePasswordBinding.inflate(inflater)
return viewBinding.root
}
}

View File

@ -0,0 +1,22 @@
package com.isolaatti.settings.ui
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import com.isolaatti.databinding.FragmentSettingsFeedSettingsBinding
class FeedSettingsFragment : Fragment() {
lateinit var viewBinding: FragmentSettingsFeedSettingsBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
viewBinding = FragmentSettingsFeedSettingsBinding.inflate(inflater)
return viewBinding.root
}
}

View File

@ -0,0 +1,22 @@
package com.isolaatti.settings.ui
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import com.isolaatti.databinding.FragmentSettingsSessionsBinding
class SessionsFragment : Fragment() {
lateinit var viewBinding: FragmentSettingsSessionsBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
viewBinding = FragmentSettingsSessionsBinding.inflate(inflater)
return viewBinding.root
}
}

View File

@ -0,0 +1,17 @@
package com.isolaatti.settings.ui
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.isolaatti.databinding.ActivitySettingsBinding
class SettingsActivity : AppCompatActivity() {
lateinit var viewBinding: ActivitySettingsBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewBinding = ActivitySettingsBinding.inflate(layoutInflater)
setContentView(viewBinding.root)
}
}

View File

@ -0,0 +1,22 @@
package com.isolaatti.settings.ui
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import com.isolaatti.databinding.FragmentSettingsBinding
class SettingsFragment : Fragment() {
lateinit var viewBinding: FragmentSettingsBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
viewBinding = FragmentSettingsBinding.inflate(inflater)
return viewBinding.root
}
}

View File

@ -0,0 +1,3 @@
package com.isolaatti.utils
data class IntIdentificationWrapper(val id: Int)

View File

@ -0,0 +1,19 @@
package com.isolaatti.utils
import com.squareup.picasso.Picasso
import com.squareup.picasso.RequestCreator
import io.noties.markwon.image.AsyncDrawable
import io.noties.markwon.image.picasso.PicassoImagesPlugin
object PicassoImagesPluginDef {
val picassoImagePlugin = PicassoImagesPlugin.create(object: PicassoImagesPlugin.PicassoStore {
override fun load(drawable: AsyncDrawable): RequestCreator {
return Picasso.get().load(drawable.destination).tag(drawable)
}
override fun cancel(drawable: AsyncDrawable) {
Picasso.get().cancelTag(drawable)
}
})
}

View File

@ -0,0 +1,7 @@
package com.isolaatti.utils
import com.isolaatti.connectivity.RetrofitClient.Companion.BASE_URL
object UrlGen {
fun userProfileImage(userId: Int) = "${BASE_URL}images/profile_image/of_user/1?mode=small"
}

View File

@ -0,0 +1,5 @@
<vector android:autoMirrored="true" android:height="24dp"
android:tint="#000000" android:viewportHeight="24"
android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="640dp"
android:height="512dp"
android:viewportWidth="640"
android:viewportHeight="512">
<path
android:fillColor="#FF000000"
android:pathData="M208,352c114.9,0 208,-78.8 208,-176S322.9,0 208,0S0,78.8 0,176c0,38.6 14.7,74.3 39.6,103.4c-3.5,9.4 -8.7,17.7 -14.2,24.7c-4.8,6.2 -9.7,11 -13.3,14.3c-1.8,1.6 -3.3,2.9 -4.3,3.7c-0.5,0.4 -0.9,0.7 -1.1,0.8l-0.2,0.2 0,0 0,0C1,327.2 -1.4,334.4 0.8,340.9S9.1,352 16,352c21.8,0 43.8,-5.6 62.1,-12.5c9.2,-3.5 17.8,-7.4 25.3,-11.4C134.1,343.3 169.8,352 208,352zM448,176c0,112.3 -99.1,196.9 -216.5,207C255.8,457.4 336.4,512 432,512c38.2,0 73.9,-8.7 104.7,-23.9c7.5,4 16,7.9 25.2,11.4c18.3,6.9 40.3,12.5 62.1,12.5c6.9,0 13.1,-4.5 15.2,-11.1c2.1,-6.6 -0.2,-13.8 -5.8,-17.9l0,0 0,0 -0.2,-0.2c-0.2,-0.2 -0.6,-0.4 -1.1,-0.8c-1,-0.8 -2.5,-2 -4.3,-3.7c-3.6,-3.3 -8.5,-8.1 -13.3,-14.3c-5.5,-7 -10.7,-15.4 -14.2,-24.7c24.9,-29 39.6,-64.7 39.6,-103.4c0,-92.8 -84.9,-168.9 -192.6,-175.5c0.4,5.1 0.6,10.3 0.6,15.5z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="512dp"
android:height="512dp"
android:viewportWidth="512"
android:viewportHeight="512">
<path
android:fillColor="#FF000000"
android:pathData="M336,16L336,80c0,8.8 -7.2,16 -16,16s-16,-7.2 -16,-16L304,16c0,-8.8 7.2,-16 16,-16s16,7.2 16,16zM237.3,23.1l32,48c4.9,7.4 2.9,17.3 -4.4,22.2s-17.3,2.9 -22.2,-4.4l-32,-48c-4.9,-7.4 -2.9,-17.3 4.4,-22.2s17.3,-2.9 22.2,4.4zM135,119c9.4,-9.4 24.6,-9.4 33.9,0L292.7,242.7c10.1,10.1 27.3,2.9 27.3,-11.3L320,192c0,-17.7 14.3,-32 32,-32s32,14.3 32,32L384,345.6c0,57.1 -30,110 -78.9,139.4c-64,38.4 -145.8,28.3 -198.5,-24.4L7,361c-9.4,-9.4 -9.4,-24.6 0,-33.9s24.6,-9.4 33.9,0l53,53c6.1,6.1 16,6.1 22.1,0s6.1,-16 0,-22.1L23,265c-9.4,-9.4 -9.4,-24.6 0,-33.9s24.6,-9.4 33.9,0l93,93c6.1,6.1 16,6.1 22.1,0s6.1,-16 0,-22.1L55,185c-9.4,-9.4 -9.4,-24.6 0,-33.9s24.6,-9.4 33.9,0l117,117c6.1,6.1 16,6.1 22.1,0s6.1,-16 0,-22.1l-93,-93c-9.4,-9.4 -9.4,-24.6 0,-33.9zM433.1,484.9c-24.2,14.5 -50.9,22.1 -77.7,23.1c48.1,-39.6 76.6,-99 76.6,-162.4l0,-98.1c8.2,-0.1 16,-6.4 16,-16L448,192c0,-17.7 14.3,-32 32,-32s32,14.3 32,32L512,345.6c0,57.1 -30,110 -78.9,139.4zM424.9,18.7c7.4,4.9 9.3,14.8 4.4,22.2l-32,48c-4.9,7.4 -14.8,9.3 -22.2,4.4s-9.3,-14.8 -4.4,-22.2l32,-48c4.9,-7.4 14.8,-9.3 22.2,-4.4z"/>
</vector>

View File

@ -0,0 +1,84 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/topAppBar_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/top_app_bar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:titleCentered="true"
style="@style/Theme.Isolaatti"
app:navigationIcon="@drawable/baseline_arrow_back_24"
app:title="Profile"/>
</com.google.android.material.appbar.AppBarLayout>
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/profile_image_view"
android:layout_width="120dp"
android:layout_height="120dp"
android:layout_weight="1"
android:layout_marginTop="16dp"
app:shapeAppearance="@style/ShapeAppearanceOverlay.Avatar"
tools:srcCompat="@tools:sample/avatars" />
<TextView
android:id="@+id/text_view_username"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="20sp"
android:layout_marginTop="20dp"
tools:text="Erik Cavazos" />
<TextView
android:id="@+id/text_view_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="20dp"
android:textAlignment="center"
tools:text="Hi there, I am software developer!"/>
</LinearLayout>
<com.google.android.material.tabs.TabLayout
android:id="@+id/profile_tab_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:tabMode="fixed"
app:tabContentStart="56dp"/>
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/profile_view_pager2"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/settings_fragment_container"
android:layout_width="0dp"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/settings_navigation"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/audios"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/discussions"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -16,8 +16,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="bottom" android:layout_gravity="bottom"
android:hint="@string/searchbar_hint" android:hint="@string/searchbar_hint" />
app:navigationIcon="@drawable/baseline_menu_24" />
<com.google.android.material.search.SearchView <com.google.android.material.search.SearchView
android:layout_width="match_parent" android:layout_width="match_parent"
@ -33,6 +32,49 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_marginBottom="80dp"> android:layout_marginBottom="80dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/topAppBar_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/topAppBar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:titleCentered="true"
style="@style/Theme.Isolaatti"
app:navigationIcon="@drawable/baseline_menu_24"
app:title="Feed"/>
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/feed_recycler_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="@id/topAppBar_layout"
app:layout_constraintBottom_toBottomOf="parent"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="80dp"/>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</LinearLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton <com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/floating_action_button" android:id="@+id/floating_action_button"
android:layout_width="wrap_content" android:layout_width="wrap_content"

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/textView3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/images"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.appbar.MaterialToolbar
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"/>
</com.google.android.material.appbar.AppBarLayout>
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/account"
android:padding="10dp"
style="?attr/textAppearanceBodyLarge"/>
</LinearLayout>
</ScrollView>
</LinearLayout>

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/textView4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Change password"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/textView5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Feed settings"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/textView6"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Sessions"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,75 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<com.google.android.material.card.MaterialCardView
android:id="@+id/card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="?attr/materialCardViewFilledStyle"
android:layout_margin="8dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="4dp">
<LinearLayout android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/avatar_picture"
android:layout_width="40dp"
android:layout_height="40dp"
app:shapeAppearance="@style/ShapeAppearanceOverlay.Avatar"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginStart="8dp"
android:layout_gravity="center_vertical">
<TextView
android:id="@+id/text_view_username"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textStyle="bold"/>
<TextView
android:id="@+id/text_view_date"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
</LinearLayout>
<TextView
android:id="@+id/post_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:layout_marginHorizontal="8dp"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="end">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="?attr/materialIconButtonFilledTonalStyle"
app:icon="@drawable/comments_solid"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="?attr/materialIconButtonFilledTonalStyle"
app:icon="@drawable/hands_clapping_solid"/>
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>

View File

@ -2,12 +2,15 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"> <menu xmlns:android="http://schemas.android.com/apk/res/android">
<item <item
android:id="@+id/my_profile_menu_item"
android:icon="@drawable/baseline_person_24" android:icon="@drawable/baseline_person_24"
android:title="My Profile" /> android:title="My Profile" />
<item <item
android:id="@+id/squads_menu_item"
android:icon="@drawable/baseline_people_24" android:icon="@drawable/baseline_people_24"
android:title="Squads" /> android:title="Squads" />
<item <item
android:id="@+id/settings_menu_item"
android:icon="@drawable/baseline_settings_24" android:icon="@drawable/baseline_settings_24"
android:title="Settings" /> android:title="Settings" />
</menu> </menu>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:title="Item" />
</menu>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/profile_navigation"
app:startDestination="@id/discussionsFragment">
<fragment
android:id="@+id/discussionsFragment"
android:name="com.isolaatti.profile.ui.DiscussionsFragment"
android:label="DiscussionsFragment" />
<fragment
android:id="@+id/audiosFragment"
android:name="com.isolaatti.profile.ui.AudiosFragment"
android:label="AudiosFragment" />
<fragment
android:id="@+id/imagesFragment"
android:name="com.isolaatti.profile.ui.ImagesFragment"
android:label="ImagesFragment" />
</navigation>

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/settings_navigation"
app:startDestination="@id/settingsFragment">
<fragment
android:id="@+id/settingsFragment"
android:name="com.isolaatti.settings.ui.SettingsFragment"
android:label="SettingsFragment" >
<action
android:id="@+id/action_settingsFragment_to_changePasswordFragment"
app:destination="@id/changePasswordFragment" />
<action
android:id="@+id/action_settingsFragment_to_sessionsFragment"
app:destination="@id/sessionsFragment" />
<action
android:id="@+id/action_settingsFragment_to_feedSettingsFragment"
app:destination="@id/feedSettingsFragment" />
</fragment>
<fragment
android:id="@+id/changePasswordFragment"
android:name="com.isolaatti.settings.ui.ChangePasswordFragment"
android:label="ChangePasswordFragment" />
<fragment
android:id="@+id/sessionsFragment"
android:name="com.isolaatti.settings.ui.SessionsFragment"
android:label="SessionsFragment" />
<fragment
android:id="@+id/feedSettingsFragment"
android:name="com.isolaatti.settings.ui.FeedSettingsFragment"
android:label="FeedSettingsFragment" />
</navigation>

View File

@ -7,4 +7,8 @@
<string name="forgot_password">Forgot password</string> <string name="forgot_password">Forgot password</string>
<string name="or_you_can_sign_up">Or you can sign up</string> <string name="or_you_can_sign_up">Or you can sign up</string>
<string name="sign_up">Sign up</string> <string name="sign_up">Sign up</string>
<string name="discussions">Discussions</string>
<string name="audios">Audios</string>
<string name="images">Images</string>
<string name="account">Account</string>
</resources> </resources>

View File

@ -8,4 +8,9 @@
</style> </style>
<style name="Theme.Isolaatti.Splash" parent="Theme.SplashScreen"/> <style name="Theme.Isolaatti.Splash" parent="Theme.SplashScreen"/>
<style name="ShapeAppearanceOverlay.Avatar" parent="ShapeAppearance.MaterialComponents.SmallComponent">
<item name="cornerFamily">rounded</item>
<item name="cornerSize">50%</item>
</style>
</resources> </resources>

View File

@ -1,7 +1,7 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins { plugins {
id 'com.android.application' version '7.4.0' apply false id 'com.android.application' version '7.4.1' apply false
id 'com.android.library' version '7.4.0' apply false id 'com.android.library' version '7.4.1' apply false
id 'org.jetbrains.kotlin.android' version '1.8.0' apply false id 'org.jetbrains.kotlin.android' version '1.8.0' apply false
id 'com.google.dagger.hilt.android' version '2.44' apply false id 'com.google.dagger.hilt.android' version '2.44' apply false
} }

View File

@ -21,3 +21,4 @@ kotlin.code.style=official
# resources declared in the library itself and none from the library's dependencies, # resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library # thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true android.nonTransitiveRClass=true
android.enableJetifier=true