Лабораторная работа №4: Сдана

This commit is contained in:
Сергей Полевой 2023-12-19 23:38:12 +04:00
parent 392b9e5c0a
commit 0f8abfbb99
34 changed files with 1175 additions and 392 deletions

View File

@ -4,6 +4,9 @@ plugins {
id 'kotlin-kapt' id 'kotlin-kapt'
} }
apply plugin: 'com.android.application'
apply plugin: 'com.google.dagger.hilt.android'
android { android {
namespace 'com.example.dtf' namespace 'com.example.dtf'
compileSdk 34 compileSdk 34
@ -71,4 +74,16 @@ dependencies {
implementation 'androidx.room:room-runtime:2.5.0' // Библиотека "Room" implementation 'androidx.room:room-runtime:2.5.0' // Библиотека "Room"
kapt "androidx.room:room-compiler:2.5.0" // Кодогенератор kapt "androidx.room:room-compiler:2.5.0" // Кодогенератор
implementation 'androidx.room:room-ktx:2.5.0' implementation 'androidx.room:room-ktx:2.5.0'
implementation "com.google.dagger:hilt-android:2.42"
kapt "com.google.dagger:hilt-android-compiler:2.42"
implementation("androidx.hilt:hilt-navigation-compose:1.0.0")
implementation 'androidx.compose.runtime:runtime-livedata:1.0.0-beta01'
implementation "androidx.datastore:datastore-preferences:1.0.0"
def paging_version = "3.2.1"
implementation "androidx.paging:paging-runtime-ktx:$paging_version"
implementation "androidx.paging:paging-compose:$paging_version"
implementation("androidx.room:room-paging:2.5.0")
} }

View File

@ -3,6 +3,7 @@
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<application <application
android:name=".DTFApp"
android:allowBackup="true" android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules" android:fullBackupContent="@xml/backup_rules"
@ -12,7 +13,7 @@
android:theme="@style/Theme.DTF" android:theme="@style/Theme.DTF"
tools:targetApi="31"> tools:targetApi="31">
<activity <activity
android:name="com.example.dtf.MainActivity" android:name=".MainActivity"
android:windowSoftInputMode="adjustResize" android:windowSoftInputMode="adjustResize"
android:exported="true" android:exported="true"
android:theme="@style/Theme.DTF"> android:theme="@style/Theme.DTF">

View File

@ -0,0 +1,52 @@
package com.example.dtf
import android.app.Application
import androidx.room.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import androidx.sqlite.db.SupportSQLiteDatabase
import com.example.dtf.db.AppDatabase
import com.example.dtf.models.User
import com.example.dtf.repositories.*
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides
@Singleton
fun provideDatabase(app: Application) : AppDatabase {
return Room.databaseBuilder(
app,
AppDatabase::class.java,
AppDatabase.DB_NAME
)
.fallbackToDestructiveMigration()
.build()
}
@Provides
@Singleton
fun provideCategoryRepository(db: AppDatabase) = CategoryRepository(db.categoryDao())
@Provides
@Singleton
fun provideCommentRepository(db: AppDatabase) = CommentRepository(db.commentDao())
@Provides
@Singleton
fun provideLikeRepository(db: AppDatabase) = LikeRepository(db.likeDao())
@Provides
@Singleton
fun providePostRepository(db: AppDatabase) = PostRepository(db.postDao())
@Provides
@Singleton
fun proviveUserRepository(db: AppDatabase) = UserRepository(db.userDao())
}

View File

@ -0,0 +1,11 @@
package com.example.dtf
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class DTFApp : Application() {
override fun onCreate() {
super.onCreate()
}
}

View File

@ -33,7 +33,9 @@ import com.example.dtf.screens.ProfileScreen
import com.example.dtf.screens.RegisterScreen import com.example.dtf.screens.RegisterScreen
import com.example.dtf.utils.ScreenPaths import com.example.dtf.utils.ScreenPaths
import com.example.dtf.widgets.BottomNavBar import com.example.dtf.widgets.BottomNavBar
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter", @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter",
"UnusedMaterialScaffoldPaddingParameter" "UnusedMaterialScaffoldPaddingParameter"
@ -100,7 +102,7 @@ class MainActivity : ComponentActivity() {
} }
composable(ScreenPaths.NewPost.route) { composable(ScreenPaths.NewPost.route) {
includeBackButton.value = true includeBackButton.value = true
NewPostScreen() NewPostScreen(navController)
} }
composable(ScreenPaths.Post.route) { navBackStackEntry -> composable(ScreenPaths.Post.route) { navBackStackEntry ->
includeBackButton.value = true includeBackButton.value = true
@ -115,6 +117,7 @@ class MainActivity : ComponentActivity() {
includeBackButton.value = true includeBackButton.value = true
navBackStackEntry.arguments?.getString("post")?.let { postId -> navBackStackEntry.arguments?.getString("post")?.let { postId ->
EditPostScreen( EditPostScreen(
navController,
postId.toInt() postId.toInt()
) )
} }

View File

@ -0,0 +1,23 @@
package com.example.dtf
import android.content.Context
import android.content.SharedPreferences
class PreferencesManager(context: Context) {
private val sharedPreferences: SharedPreferences =
context.getSharedPreferences("MyPrefs", Context.MODE_PRIVATE)
fun saveData(key: String, value: String) {
val editor = sharedPreferences.edit()
editor.putString(key, value)
editor.apply()
}
fun getData(key: String, defaultValue: String): String {
return sharedPreferences.getString(key, defaultValue) ?: defaultValue
}
fun deleteData(key: String) {
sharedPreferences.edit().remove(key).apply()
}
}

View File

@ -20,7 +20,4 @@ interface CategoryDao {
@Query("select * from category where category.id = :id") @Query("select * from category where category.id = :id")
fun getById(id: Int) : Flow<Category> fun getById(id: Int) : Flow<Category>
@Query("select * from category where category.name = :name")
fun getByName(name: String) : Flow<CategoryWithPosts>
} }

View File

@ -1,5 +1,6 @@
package com.example.dtf.dao package com.example.dtf.dao
import androidx.paging.PagingSource
import androidx.room.* import androidx.room.*
import com.example.dtf.models.* import com.example.dtf.models.*
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@ -21,6 +22,6 @@ interface CommentDao {
@Query("select * from comment where comment.id = :id") @Query("select * from comment where comment.id = :id")
fun getById(id: Int) : Flow<Comment> fun getById(id: Int) : Flow<Comment>
@Query("select * from comment where comment.post_id = :postId") @Query("select * from comment where comment.post_id = :postId ORDER BY date DESC")
fun getByPost(postId: Int) : Flow<List<Comment>> fun getByPost(postId: Int) : PagingSource<Int, Comment>
} }

View File

@ -0,0 +1,20 @@
package com.example.dtf.dao
import androidx.room.*
import com.example.dtf.models.Like
import kotlinx.coroutines.flow.Flow
@Dao
interface LikeDao {
@Insert
suspend fun insert(like: Like)
@Delete
suspend fun delete(like: Like)
@Query("SELECT COUNT(*) FROM likes WHERE post_id = :postId")
fun countByPost(postId: Int) : Flow<Int>
@Query("SELECT COUNT(*) = 1 FROM likes WHERE post_id = :postId AND user_id = :userId")
fun isLikedByUser(userId: Int, postId: Int) : Flow<Boolean>
}

View File

@ -1,5 +1,6 @@
package com.example.dtf.dao package com.example.dtf.dao
import androidx.paging.PagingSource
import androidx.room.* import androidx.room.*
import com.example.dtf.models.* import com.example.dtf.models.*
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@ -19,8 +20,8 @@ interface PostDao {
fun getAll() : Flow<List<Post>> fun getAll() : Flow<List<Post>>
@Query("select * from post where post.id = :id") @Query("select * from post where post.id = :id")
fun getById(id: Int) : Flow<PostWithComments> fun getById(id: Int) : Flow<Post>
@Query("select * from post where post.category_id = :categoryId") @Query("select * from post where post.category_id = :categoryId ORDER BY date DESC")
fun getByCategory(categoryId: String) : Flow<Post> fun getByCategory(categoryId: String) : PagingSource<Int, Post>
} }

View File

@ -19,5 +19,8 @@ interface UserDao {
fun getAll() : Flow<List<User>> fun getAll() : Flow<List<User>>
@Query("select * from user where user.id = :id") @Query("select * from user where user.id = :id")
fun getById(id: Int): User fun getById(id: Int): Flow<User>
@Query("select * from user where user.username = :username and user.password = :password")
fun getByUsernameAndPassword(username: String, password: String): Flow<User?>
} }

View File

@ -11,6 +11,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import androidx.sqlite.db.SupportSQLiteDatabase import androidx.sqlite.db.SupportSQLiteDatabase
import com.example.dtf.dao.* import com.example.dtf.dao.*
import com.example.dtf.models.Like
import java.util.Date import java.util.Date
@Database( @Database(
@ -18,9 +19,10 @@ import java.util.Date
User::class, User::class,
Category::class, Category::class,
Post::class, Post::class,
Like::class,
Comment::class Comment::class
], ],
version = 1, version = 2,
exportSchema = false exportSchema = false
) )
@TypeConverters(Converter::class) @TypeConverters(Converter::class)
@ -29,119 +31,9 @@ abstract class AppDatabase : RoomDatabase() {
abstract fun categoryDao() : CategoryDao abstract fun categoryDao() : CategoryDao
abstract fun commentDao() : CommentDao abstract fun commentDao() : CommentDao
abstract fun postDao() : PostDao abstract fun postDao() : PostDao
abstract fun likeDao() : LikeDao
companion object { companion object {
private const val DB_NAME: String = "news-db" public const val DB_NAME: String = "news-db"
@Volatile
private var INSTANCE: AppDatabase? = null
private suspend fun populateDatabase() {
INSTANCE?.let { database ->
val userDao = database.userDao()
userDao.insert(User(1, "Sheriff", "123456", true))
userDao.insert(User(2, "Человек", "123456", true))
val categoryDao = database.categoryDao()
categoryDao.insert(Category(1, "Игры"))
categoryDao.insert(Category(2, "Кино"))
categoryDao.insert(Category(3, "Аниме"))
val postDao = database.postDao()
postDao.insert(
Post(
1,
"Что не так с half-life 2",
"Да всё не так",
Date(2023, 10, 22),
1,
1
)
)
postDao.insert(
Post(
2,
"Я действительно ненавижу фильм про титаник",
"Пруфов не будет",
Date(2023, 9, 22),
1,
2
)
)
postDao.insert(
Post(
3,
"\"Госпожа Кагуя\" это переоценённый тайтл",
"Я, конечно, не смотрел, но мне так кажется. А всё потому, что там происходит такое, что даже Аристотелю не снилось. А может даже Платону. Об этом можно рассуждать тысячи лет, но я смогу уложиться всего в пару слов. И первое слово - этот тайтл полное днище. Ладно, не слово",
Date(2023, 9, 22),
1,
3
)
)
postDao.insert(
Post(
4,
"\"Восхождение в тени\" это переоценённый тайтл",
"Я, конечно, не смотрел, но мне так кажется. А всё потому, что там происходит такое, что даже Аристотелю не снилось. А может даже Платону. Об этом можно рассуждать тысячи лет, но я смогу уложиться всего в пару слов. И первое слово - этот тайтл полное днище. Ладно, не слово",
Date(2023, 9, 22),
1,
3
)
)
postDao.insert(
Post(
5,
"\"Наруто\" это переоценённый тайтл",
"Я, конечно, не смотрел, но мне так кажется. А всё потому, что там происходит такое, что даже Аристотелю не снилось. А может даже Платону. Об этом можно рассуждать тысячи лет, но я смогу уложиться всего в пару слов. И первое слово - этот тайтл полное днище. Ладно, не слово",
Date(2023, 9, 22),
1,
3
)
)
postDao.insert(
Post(
6,
"\"Блич\" это переоценённый тайтл",
"Я, конечно, не смотрел, но мне так кажется. А всё потому, что там происходит такое, что даже Аристотелю не снилось. А может даже Платону. Об этом можно рассуждать тысячи лет, но я смогу уложиться всего в пару слов. И первое слово - этот тайтл полное днище. Ладно, не слово",
Date(2023, 9, 22),
1,
3
)
)
val commentDao = database.commentDao()
commentDao.insert(Comment(1, 2, 1, "Пост бред. Начнём с того, что вот эта твоя манера речи клоунская...", Date(2023, 10, 20)))
commentDao.insert(Comment(2, 1, 1, "Да какой бред, чел, я всё по факту сказал", Date(2023, 10, 20)))
commentDao.insert(Comment(3, 2, 3,"Автора на увольнение", Date(2023, 9, 20)))
commentDao.insert(Comment(4, 2, 4,"Автора на увольнение дважды", Date(2023, 9, 20)))
commentDao.insert(Comment(5, 2, 5,"Автора на увольнение трижды", Date(2023, 9, 20)))
commentDao.insert(Comment(6, 2, 6,"Автора на увольнение четырежды", Date(2023, 9, 20)))
}
}
fun getInstance(appContext: Context): AppDatabase {
return INSTANCE ?: synchronized(this) {
Room.databaseBuilder(
appContext,
AppDatabase::class.java,
DB_NAME
)
.addCallback(object : Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
CoroutineScope(Dispatchers.IO).launch {
populateDatabase()
}
}
})
.build()
.also { INSTANCE = it }
}
}
} }
} }

View File

@ -0,0 +1,11 @@
package com.example.dtf.models
import androidx.room.*
@Entity(tableName = "likes", primaryKeys = ["user_id", "post_id"])
data class Like (
@ColumnInfo(name = "user_id", index = true)
val userId: Int,
@ColumnInfo(name = "post_id", index = true)
val postId: Int
)

View File

@ -0,0 +1,19 @@
package com.example.dtf.repositories
import com.example.dtf.dao.CategoryDao
import com.example.dtf.models.Category
import javax.inject.Inject
class CategoryRepository @Inject constructor(
private val categoryDao: CategoryDao
) {
suspend fun insert(category: Category) = categoryDao.insert(category)
suspend fun update(category: Category) = categoryDao.update(category)
suspend fun delete(category: Category) = categoryDao.delete(category)
fun getAll() = categoryDao.getAll()
fun getById(id: Int) = categoryDao.getById(id)
}

View File

@ -0,0 +1,28 @@
package com.example.dtf.repositories
import androidx.paging.*
import com.example.dtf.dao.CommentDao
import com.example.dtf.models.Comment
import javax.inject.Inject
class CommentRepository @Inject constructor(
private val commentDao: CommentDao
) {
suspend fun insert(comment: Comment) = commentDao.insert(comment)
suspend fun update(comment: Comment) = commentDao.update(comment)
suspend fun delete(comment: Comment) = commentDao.delete(comment)
fun getAll() = commentDao.getAll()
fun getById(id: Int) = commentDao.getById(id)
fun getByPost(postId: Int) = Pager(
PagingConfig(
pageSize = 5,
enablePlaceholders = false
),
pagingSourceFactory = { commentDao.getByPost(postId) }
).flow
}

View File

@ -0,0 +1,17 @@
package com.example.dtf.repositories
import com.example.dtf.dao.LikeDao
import com.example.dtf.models.Like
import javax.inject.Inject
class LikeRepository @Inject constructor(
private val likeDao: LikeDao
) {
suspend fun insert(like: Like) = likeDao.insert(like)
suspend fun delete(like: Like) = likeDao.delete(like)
fun countByPost(postId: Int) = likeDao.countByPost(postId)
fun isLikedByUser(userId: Int, postId: Int) = likeDao.isLikedByUser(userId, postId)
}

View File

@ -0,0 +1,29 @@
package com.example.dtf.repositories
import androidx.paging.*
import com.example.dtf.dao.PostDao
import com.example.dtf.models.Post
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
class PostRepository @Inject constructor(
private val postDao: PostDao
) {
suspend fun insert(post: Post) = postDao.insert(post)
suspend fun update(post: Post) = postDao.update(post)
suspend fun delete(post: Post) = postDao.delete(post)
fun getAll() = postDao.getAll()
fun getById(id: Int) = postDao.getById(id)
fun getByCategory(categoryId: Int) = Pager(
PagingConfig(
pageSize = 3,
enablePlaceholders = false
),
pagingSourceFactory = { postDao.getByCategory(categoryId.toString()) }
).flow
}

View File

@ -0,0 +1,22 @@
package com.example.dtf.repositories
import com.example.dtf.dao.UserDao
import com.example.dtf.models.User
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
class UserRepository @Inject constructor(
private val userDao: UserDao
) {
suspend fun insert(user: User) = userDao.insert(user)
suspend fun update(user: User) = userDao.update(user)
suspend fun delete(user: User) = userDao.delete(user)
fun getAll() = userDao.getAll()
fun getById(id: Int) = userDao.getById(id)
fun getByUsernameAndPassword(username: String, password: String) = userDao.getByUsernameAndPassword(username, password)
}

View File

@ -7,6 +7,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.* import androidx.compose.ui.*
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@ -14,27 +15,72 @@ import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.* import androidx.compose.ui.unit.*
import com.example.dtf.db.AppDatabase import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import com.example.dtf.PreferencesManager
import com.example.dtf.utils.ScreenPaths
import com.example.dtf.viewmodels.EditPostViewModel
import com.example.dtf.widgets.MyTextField import com.example.dtf.widgets.MyTextField
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@Composable @Composable
fun EditPostScreen(postId: Int) { fun EditPostScreen(navController: NavHostController, postId: Int) {
val context = LocalContext.current val sharedPref = PreferencesManager(LocalContext.current)
val title = remember { mutableStateOf(TextFieldValue("")) } val title = remember { mutableStateOf(TextFieldValue("")) }
val content = remember { mutableStateOf(TextFieldValue("")) } val content = remember { mutableStateOf(TextFieldValue("")) }
LaunchedEffect(Unit) { val viewModel = hiltViewModel<EditPostViewModel>()
withContext(Dispatchers.IO) {
AppDatabase.getInstance(context).postDao().getById(postId).collect { data -> val post = viewModel.post.observeAsState().value
title.value = TextFieldValue(data.post.title)
content.value = TextFieldValue(data.post.content) val success = viewModel.editingPostState.observeAsState().value
viewModel.retrievePost(postId)
if (post != null) {
title.value = TextFieldValue(post.title)
content.value = TextFieldValue(post.content)
}
if (success == true) {
navController.navigate(ScreenPaths.Post.route.replace("{post}", postId.toString())) {
popUpTo(ScreenPaths.Post.route.replace("{post}", postId.toString())) {
inclusive = true
} }
} }
} }
if (success == false) {
AlertDialog(
title = {
Text(
text = "Ошибка",
fontWeight = FontWeight.Bold,
fontSize = 20.sp
)
},
text = {
Text(
text = "Нельзя сохранить новость с пустым заголовком или содержанием",
fontWeight = FontWeight.Normal,
fontSize = 16.sp
)
},
onDismissRequest = {
viewModel.calmEditingState()
},
buttons = {
TextButton(
onClick = {
viewModel.calmEditingState()
}
) {
Text("ОК")
}
}
)
}
Column( Column(
modifier = Modifier.verticalScroll(rememberScrollState()), modifier = Modifier.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(10.dp), verticalArrangement = Arrangement.spacedBy(10.dp),
@ -57,7 +103,7 @@ fun EditPostScreen(postId: Int) {
) )
Button( Button(
{ {
viewModel.editPost(sharedPref, postId, title.value.text, content.value.text)
}, },
colors = ButtonDefaults.buttonColors(Color(0xFF388DF2)), colors = ButtonDefaults.buttonColors(Color(0xFF388DF2)),
border = BorderStroke(1.dp, Color(0xFF0085FF)), border = BorderStroke(1.dp, Color(0xFF0085FF)),

View File

@ -1,33 +1,77 @@
package com.example.dtf.screens package com.example.dtf.screens
import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Button import androidx.compose.material.*
import androidx.compose.material.ButtonDefaults import androidx.compose.runtime.*
import androidx.compose.material.Text import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.* import androidx.compose.ui.*
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.*
import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import com.example.dtf.PreferencesManager
import com.example.dtf.utils.ScreenPaths import com.example.dtf.utils.ScreenPaths
import com.example.dtf.viewmodels.LoginViewModel
import com.example.dtf.widgets.MyTextField import com.example.dtf.widgets.MyTextField
@Composable @Composable
fun LoginScreen(navController: NavHostController) { fun LoginScreen(navController: NavHostController) {
val sharedPref = PreferencesManager(LocalContext.current)
val login = remember { mutableStateOf(TextFieldValue(""))} val login = remember { mutableStateOf(TextFieldValue(""))}
val password = remember { mutableStateOf(TextFieldValue(""))} val password = remember { mutableStateOf(TextFieldValue(""))}
val viewModel = hiltViewModel<LoginViewModel>()
val success = viewModel.successState.observeAsState().value
if (success == true) {
navController.navigate(ScreenPaths.Posts.route) {
popUpTo(ScreenPaths.Posts.route) {
inclusive = true
}
}
}
if (success == false) {
AlertDialog(
title = {
Text(
text = "Ошибка авторизации",
fontWeight = FontWeight.Bold,
fontSize = 20.sp
)
},
text = {
Text(
text = "Неверный логин или пароль",
fontWeight = FontWeight.Normal,
fontSize = 16.sp
)
},
onDismissRequest = {
viewModel.calmSuccessState()
},
buttons = {
TextButton(
onClick = {
viewModel.calmSuccessState()
}
) {
Text("ОК")
}
}
)
}
Row( Row(
horizontalArrangement = Arrangement.Center, horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
@ -65,7 +109,7 @@ fun LoginScreen(navController: NavHostController) {
) )
Button( Button(
{ {
navController.navigate(ScreenPaths.Posts.route) viewModel.login(sharedPref, login.value.text, password.value.text)
}, },
colors = ButtonDefaults.buttonColors(Color(0xFF388DF2)), colors = ButtonDefaults.buttonColors(Color(0xFF388DF2)),
border = BorderStroke(1.dp, Color(0xFF0085FF)), border = BorderStroke(1.dp, Color(0xFF0085FF)),

View File

@ -7,6 +7,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.* import androidx.compose.ui.*
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@ -15,32 +16,69 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.* import androidx.compose.ui.unit.*
import com.example.dtf.db.AppDatabase import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import com.example.dtf.PreferencesManager
import com.example.dtf.models.Category import com.example.dtf.models.Category
import com.example.dtf.utils.ScreenPaths
import com.example.dtf.viewmodels.NewPostViewModel
import com.example.dtf.widgets.MyTextField import com.example.dtf.widgets.MyTextField
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@Composable @Composable
fun NewPostScreen() { fun NewPostScreen(navController: NavHostController) {
val categories = remember { mutableStateListOf<Category>() } val sharedPref = PreferencesManager(LocalContext.current)
val context = LocalContext.current
LaunchedEffect(Unit) { val selectedCategory = remember { mutableStateOf(Category(null, "")) }
withContext(Dispatchers.IO) {
AppDatabase.getInstance(context).categoryDao().getAll().collect {data ->
categories.clear()
categories.addAll(data)
}
}
}
val selectedCategory = remember { mutableStateOf("") }
val expanded = remember { mutableStateOf(false) } val expanded = remember { mutableStateOf(false) }
val title = remember { mutableStateOf(TextFieldValue()) } val title = remember { mutableStateOf(TextFieldValue()) }
val content = remember { mutableStateOf(TextFieldValue()) } val content = remember { mutableStateOf(TextFieldValue()) }
val viewModel = hiltViewModel<NewPostViewModel>()
val success = viewModel.addingPostState.observeAsState().value
val categories = viewModel.categories.observeAsState().value
viewModel.retrieveCategories()
if (success == true) {
navController.navigate(ScreenPaths.Posts.route) {
popUpTo(ScreenPaths.Posts.route) {
inclusive = true
}
}
}
if (success == false) {
AlertDialog(
title = {
Text(
text = "Ошибка",
fontWeight = FontWeight.Bold,
fontSize = 20.sp
)
},
text = {
Text(
text = "Нельзя создать новость с пустым заголовком или содержанием",
fontWeight = FontWeight.Normal,
fontSize = 16.sp
)
},
onDismissRequest = {
viewModel.calmAddingState()
},
buttons = {
TextButton(
onClick = {
viewModel.calmAddingState()
}
) {
Text("ОК")
}
}
)
}
Column( Column(
modifier = Modifier.verticalScroll(rememberScrollState()), modifier = Modifier.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(10.dp), verticalArrangement = Arrangement.spacedBy(10.dp),
@ -56,7 +94,7 @@ fun NewPostScreen() {
modifier = Modifier.fillMaxWidth().padding(15.dp, 10.dp, 15.dp, 0.dp) modifier = Modifier.fillMaxWidth().padding(15.dp, 10.dp, 15.dp, 0.dp)
) { ) {
Text( Text(
text = selectedCategory.value, text = selectedCategory.value.name,
style = TextStyle( style = TextStyle(
fontSize = 24.sp, fontSize = 24.sp,
fontWeight = FontWeight(400), fontWeight = FontWeight(400),
@ -68,10 +106,10 @@ fun NewPostScreen() {
expanded = expanded.value, expanded = expanded.value,
onDismissRequest = {expanded.value = false} onDismissRequest = {expanded.value = false}
) { ) {
categories.forEach {category -> categories?.forEach { category ->
DropdownMenuItem( DropdownMenuItem(
onClick = { onClick = {
selectedCategory.value = category.name selectedCategory.value = category
expanded.value = false expanded.value = false
}, },
) { ) {
@ -101,7 +139,9 @@ fun NewPostScreen() {
) )
Button( Button(
{ {
if (selectedCategory.value.id != null) {
viewModel.addPost(sharedPref, selectedCategory.value.id!!, title.value.text, content.value.text)
}
}, },
colors = ButtonDefaults.buttonColors(Color(0xFF388DF2)), colors = ButtonDefaults.buttonColors(Color(0xFF388DF2)),
border = BorderStroke(1.dp, Color(0xFF0085FF)), border = BorderStroke(1.dp, Color(0xFF0085FF)),

View File

@ -2,6 +2,7 @@ package com.example.dtf.screens
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.* import androidx.compose.foundation.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@ -9,6 +10,7 @@ import androidx.compose.material.icons.filled.AddCircle
import androidx.compose.material.icons.filled.ThumbUp import androidx.compose.material.icons.filled.ThumbUp
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.* import androidx.compose.ui.*
import androidx.compose.ui.draw.* import androidx.compose.ui.draw.*
import androidx.compose.ui.graphics.* import androidx.compose.ui.graphics.*
@ -16,144 +18,176 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.* import androidx.compose.ui.unit.*
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.viewModelScope
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.itemKey
import com.example.dtf.PreferencesManager
import com.example.dtf.db.AppDatabase import com.example.dtf.db.AppDatabase
import com.example.dtf.models.Comment import com.example.dtf.models.Comment
import com.example.dtf.models.Post import com.example.dtf.models.Post
import com.example.dtf.models.PostWithComments import com.example.dtf.models.PostWithComments
import com.example.dtf.models.User import com.example.dtf.models.User
import com.example.dtf.utils.ScreenPaths import com.example.dtf.utils.ScreenPaths
import com.example.dtf.viewmodels.PostViewModel
import com.example.dtf.widgets.MyTextField import com.example.dtf.widgets.MyTextField
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.util.Date import java.util.Date
@Composable @Composable
fun PostScreen(postId: Int, navController: NavHostController) { fun PostScreen(postId: Int, navController: NavHostController) {
val post = remember { mutableStateOf(PostWithComments(Post(null, "", "", Date(), -1, -1), listOf())) } val sharedPref = PreferencesManager(LocalContext.current)
val user = remember { mutableStateOf(User(-1, "", "", false)) } val viewModel = hiltViewModel<PostViewModel>()
val context = LocalContext.current val post = viewModel.post.observeAsState().value
val comments = viewModel.getCommentsListUiState(postId).collectAsLazyPagingItems()
val likes = remember { mutableIntStateOf(0) }
val isLiked = remember { mutableStateOf(false) }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
withContext(Dispatchers.IO) { viewModel.getLikes(postId).collect {
user.value = AppDatabase.getInstance(context).userDao().getById(1) likes.intValue = it
AppDatabase.getInstance(context).postDao().getById(postId).collect {data ->
post.value = data
}
} }
} }
LaunchedEffect(Unit) {
viewModel.isLiked(sharedPref, postId).collect {
isLiked.value = it
}
}
viewModel.retrievePost(postId)
val content = remember { mutableStateOf(TextFieldValue("")) } val content = remember { mutableStateOf(TextFieldValue("")) }
Column( LazyColumn(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxWidth()
.background(Color.White) .background(Color.White),
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(5.dp), verticalArrangement = Arrangement.spacedBy(5.dp),
horizontalAlignment = Alignment.Start horizontalAlignment = Alignment.Start
) { ) {
Text( item {
modifier = Modifier.padding(10.dp),
text = post.value.post.title,
fontSize = 26.sp
)
Text(
modifier = Modifier.fillMaxHeight().padding(10.dp, 0.dp, 10.dp, 0.dp),
text = post.value.post.content,
fontSize = 20.sp
)
Row(
modifier = Modifier.fillMaxWidth().padding(10.dp),
verticalAlignment = Alignment.Bottom,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text( Text(
text = "day.month.year".replace( modifier = Modifier.padding(10.dp),
"day", text = post?.title ?: "Loading...",
post.value.post.date.day.toString() fontSize = 26.sp
).replace(
"month",
post.value.post.date.month.toString()
).replace(
"year",
post.value.post.date.year.toString()
),
fontSize = 14.sp,
color = Color(0xFFCECCCC)
) )
if (user.value.isModerator) { Text(
Text( modifier = Modifier.fillMaxHeight().padding(10.dp, 0.dp, 10.dp, 0.dp),
text = post?.content ?: "Loading...",
fontSize = 20.sp
)
Row(
modifier = Modifier.fillMaxWidth().padding(10.dp),
verticalAlignment = Alignment.Bottom,
horizontalArrangement = Arrangement.SpaceBetween
) {
if (post != null) {
Text(
text = "day.month.year".replace(
"day",
post.date.day.toString()
).replace(
"month",
post.date.month.toString()
).replace(
"year",
post.date.year.toString()
),
fontSize = 14.sp,
color = Color(0xFFCECCCC)
)
}
if (sharedPref.getData("isModerator", "false").toBooleanStrict()) {
Text(
modifier = Modifier.clickable {
navController.navigate(ScreenPaths.EditPost.route.replace("{post}", postId.toString()))
},
text = "Изменить",
fontSize = 18.sp,
color = Color(0xFFAFAFAF)
)
}
Row (
modifier = Modifier.clickable { modifier = Modifier.clickable {
navController.navigate(ScreenPaths.EditPost.route.replace("{post}", postId.toString())) if (isLiked.value) {
viewModel.unlikePost(sharedPref, postId)
likes.intValue--
} else {
viewModel.likePost(sharedPref, postId)
likes.intValue++
}
isLiked.value = !isLiked.value
}, },
text = "Изменить", horizontalArrangement = Arrangement.End
fontSize = 18.sp, ) {
color = Color(0xFFAFAFAF) Text(
) text = likes.intValue.toString(),
fontSize = 16.sp,
color = Color.Green
)
Icon(
modifier = Modifier.padding(start = 8.dp),
imageVector = Icons.Default.ThumbUp,
contentDescription = null,
tint = if (isLiked.value) { Color(40, 200, 40, 255) } else {Color.Black}
)
}
} }
Row ( Divider()
modifier = Modifier, Text(
horizontalArrangement = Arrangement.End modifier = Modifier.fillMaxWidth().padding(0.dp, 0.dp, 0.dp, 10.dp),
) { text = "Комментарии",
Text( fontSize = 22.sp,
text = "0", textAlign = TextAlign.Center
fontSize = 16.sp,
color = Color.Green
)
Icon(
modifier = Modifier.padding(start = 8.dp),
imageVector = Icons.Default.ThumbUp,
contentDescription = null
)
}
}
Divider()
Text(
modifier = Modifier.fillMaxWidth().padding(0.dp, 0.dp, 0.dp, 10.dp),
text = "Комментарии",
fontSize = 22.sp,
textAlign = TextAlign.Center
)
Row(
modifier = Modifier.fillMaxSize().padding(horizontal = 15.dp),
verticalAlignment = Alignment.CenterVertically
) {
MyTextField(
value = content,
placeholder = "Комментарий",
onChanged = {content.value = it},
modifier = Modifier.fillMaxWidth(0.9f),
backgroundColor = Color.White
) )
IconButton( Row(
modifier = Modifier.fillMaxWidth().scale(1.5f), modifier = Modifier.fillMaxSize().padding(horizontal = 15.dp),
onClick = {}, verticalAlignment = Alignment.CenterVertically
) { ) {
Icon( MyTextField(
imageVector = Icons.Default.AddCircle, value = content,
contentDescription = null placeholder = "Комментарий",
onChanged = {content.value = it},
modifier = Modifier.fillMaxWidth(0.9f),
backgroundColor = Color.White
) )
IconButton(
modifier = Modifier.fillMaxWidth().scale(1.5f),
onClick = {
viewModel.addComment(sharedPref, postId, content.value.text)
content.value = TextFieldValue("")
},
) {
Icon(
imageVector = Icons.Default.AddCircle,
contentDescription = null
)
}
} }
} }
post.value.comments.reversed().forEach() {comment -> items(
Comment(comment) comments.itemCount,
key = comments.itemKey()
) {
Comment(comments[it]!!)
}
item {
Spacer(modifier = Modifier.height(60.dp))
} }
Spacer(modifier = Modifier.height(60.dp))
} }
} }
@Composable @Composable
private fun Comment(comment: Comment) { private fun Comment(comment: Comment) {
val user = remember { mutableStateOf(User(-1, "", "", false)) } val viewModel = hiltViewModel<PostViewModel>()
val context = LocalContext.current val user = viewModel.getCommentsAuthor(comment).collectAsState(null).value
LaunchedEffect(Unit) {
withContext(Dispatchers.IO) {
user.value = AppDatabase.getInstance(context).userDao().getById(comment.userId)
}
}
Column( Column(
modifier = Modifier modifier = Modifier
@ -168,7 +202,7 @@ private fun Comment(comment: Comment) {
horizontalArrangement = Arrangement.SpaceBetween horizontalArrangement = Arrangement.SpaceBetween
) { ) {
Text( Text(
text = user.value.username, text = user?.username ?: "Loading...",
fontSize = 20.sp fontSize = 20.sp
) )
Text( Text(

View File

@ -2,6 +2,7 @@ package com.example.dtf.screens
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.* import androidx.compose.foundation.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ThumbUp import androidx.compose.material.icons.filled.ThumbUp
@ -14,36 +15,33 @@ import androidx.compose.ui.graphics.*
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.* import androidx.compose.ui.unit.*
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.paging.PagingData
import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.itemKey
import com.example.dtf.PreferencesManager
import com.example.dtf.db.AppDatabase import com.example.dtf.db.AppDatabase
import com.example.dtf.models.Category import com.example.dtf.models.Category
import com.example.dtf.models.CategoryWithPosts import com.example.dtf.models.CategoryWithPosts
import com.example.dtf.models.Post
import com.example.dtf.utils.ScreenPaths import com.example.dtf.utils.ScreenPaths
import com.example.dtf.viewmodels.PostsViewModel
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@Composable @Composable
fun PostsScreen(navController: NavHostController) { fun PostsScreen(navController: NavHostController) {
val currentCategory = remember { mutableStateOf("") } val sharedPref = PreferencesManager(LocalContext.current)
val categories = remember { mutableListOf<Category> () }
val categoryWithPosts = remember { mutableStateOf(CategoryWithPosts(Category(-1, ""), listOf()))}
val context = LocalContext.current
val scope = rememberCoroutineScope()
LaunchedEffect(Unit) { val currentCategory = remember { mutableStateOf<Category?>(null) }
withContext(Dispatchers.IO) {
AppDatabase.getInstance(context).categoryDao().getAll().collect {data -> val viewModel = hiltViewModel<PostsViewModel>()
currentCategory.value = data.first().name val posts = remember { mutableStateOf<Flow<PagingData<Post>>?>(null) }
categories.clear()
categories.addAll(data)
AppDatabase.getInstance(context).categoryDao().getByName(currentCategory.value).collect {data ->
categoryWithPosts.value = data
}
}
}
}
Column( Column(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
@ -60,42 +58,37 @@ fun PostsScreen(navController: NavHostController) {
horizontalArrangement = Arrangement.spacedBy(25.dp), horizontalArrangement = Arrangement.spacedBy(25.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Categories(scope, categoryWithPosts, currentCategory, categories) Categories(viewModel, currentCategory, posts)
} }
Row( Row(
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) { ) {
Column( if (currentCategory.value != null && posts.value != null) {
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()), PostsByCategory(viewModel, navController, posts)
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
if (currentCategory.value.isNotEmpty()) {
PostsByCategory(navController, categoryWithPosts)
}
Spacer(modifier = Modifier.height(60.dp))
} }
} }
} }
} }
@Composable @Composable
private fun Categories(scope: CoroutineScope, categoryWithPosts: MutableState<CategoryWithPosts>, categoryState: MutableState<String>, categories: MutableList<Category>) { private fun Categories(viewModel: PostsViewModel, currentCategory: MutableState<Category?>, posts: MutableState<Flow<PagingData<Post>>?>) {
val context = LocalContext.current val categories = viewModel.getCategories().collectAsState(listOf()).value
if (categories.isNotEmpty() && currentCategory.value == null) {
currentCategory.value = categories[0]
posts.value = viewModel.getPostsListUiState(currentCategory.value!!.id!!)
}
Spacer(modifier = Modifier.width(5.dp)) Spacer(modifier = Modifier.width(5.dp))
categories.forEach {category -> categories.forEach {category ->
Text( Text(
modifier = Modifier modifier = Modifier
.clickable { .clickable {
categoryState.value = category.name currentCategory.value = category
scope.launch { posts.value = viewModel.getPostsListUiState(category.id!!)
AppDatabase.getInstance(context).categoryDao().getByName(categoryState.value).collect { data ->
categoryWithPosts.value = data
}
}
} }
.drawBehind { .drawBehind {
if (category.name == categoryState.value) { if (category.name == currentCategory.value?.name) {
val strokeWidthPx = 2.dp.toPx() val strokeWidthPx = 2.dp.toPx()
val verticalOffset = size.height + 2.sp.toPx() val verticalOffset = size.height + 2.sp.toPx()
drawLine( drawLine(
@ -113,65 +106,110 @@ private fun Categories(scope: CoroutineScope, categoryWithPosts: MutableState<Ca
} }
@Composable @Composable
private fun PostsByCategory(navController: NavHostController, categoryWithPosts: MutableState<CategoryWithPosts>) { private fun PostsByCategory(viewModel: PostsViewModel, navController: NavHostController, posts: MutableState<Flow<PagingData<Post>>?>) {
categoryWithPosts.value.posts.forEach { post -> val postsItems = posts.value!!.collectAsLazyPagingItems()
Column(
modifier = Modifier LazyColumn(
.fillMaxHeight(0.3f) modifier = Modifier.fillMaxSize(),
.heightIn(max = 250.dp) verticalArrangement = Arrangement.spacedBy(10.dp)
.fillMaxWidth() ) {
.background(Color.White) items(
.clickable { count = postsItems.itemCount,
navController.navigate(ScreenPaths.Post.route.replace("{post}", post.id.toString())) key = postsItems.itemKey()
}, ) {
verticalArrangement = Arrangement.spacedBy(5.dp), Post(viewModel, navController, postsItems[it]!!)
horizontalAlignment = Alignment.Start }
item {
Spacer(modifier = Modifier.height(60.dp))
}
}
}
@Composable
private fun Post(viewModel: PostsViewModel, navController: NavHostController, post: Post) {
val sharedPref = PreferencesManager(LocalContext.current)
val likes = remember { mutableIntStateOf(0) }
val isLiked = remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
viewModel.getLikes(post.id!!).collect {
likes.intValue = it
}
}
LaunchedEffect(Unit) {
viewModel.isLiked(sharedPref, post.id!!).collect {
isLiked.value = it
}
}
Column(
modifier = Modifier
.fillMaxHeight(0.3f)
.heightIn(max = 250.dp)
.fillMaxWidth()
.background(Color.White)
.clickable {
navController.navigate(ScreenPaths.Post.route.replace("{post}", post.id.toString()))
},
verticalArrangement = Arrangement.spacedBy(5.dp),
horizontalAlignment = Alignment.Start
) {
Text(
modifier = Modifier.padding(10.dp),
text = post.title,
fontSize = 26.sp
)
Text(
modifier = Modifier.fillMaxHeight(0.6f).padding(10.dp, 0.dp, 10.dp, 0.dp),
text = post.content,
fontSize = 20.sp,
overflow = TextOverflow.Ellipsis
)
Row(
modifier = Modifier.fillMaxWidth().padding(10.dp),
verticalAlignment = Alignment.Bottom,
horizontalArrangement = Arrangement.SpaceBetween
) { ) {
Text( Text(
modifier = Modifier.padding(10.dp), text = "day.month.year".replace(
text = post.title, "day",
fontSize = 26.sp post.date.day.toString()
).replace(
"month",
post.date.month.toString()
).replace(
"year",
post.date.year.toString()
),
fontSize = 14.sp,
color = Color(0xFFCECCCC)
) )
Text( Row (
modifier = Modifier.fillMaxHeight(0.6f).padding(10.dp, 0.dp, 10.dp, 0.dp), modifier = Modifier.fillMaxWidth().clickable {
text = post.content, if (isLiked.value) {
fontSize = 20.sp, viewModel.unlikePost(sharedPref, post.id!!)
overflow = TextOverflow.Ellipsis likes.intValue--
) } else {
Row( viewModel.likePost(sharedPref, post.id!!)
modifier = Modifier.fillMaxWidth().padding(10.dp), likes.intValue++
verticalAlignment = Alignment.Bottom, }
horizontalArrangement = Arrangement.SpaceBetween isLiked.value = !isLiked.value
},
horizontalArrangement = Arrangement.End
) { ) {
Text( Text(
text = "day.month.year".replace( text = likes.intValue.toString(),
"day", fontSize = 16.sp,
post.date.day.toString() color = Color.Green
).replace( )
"month", Icon(
post.date.month.toString() modifier = Modifier.padding(start = 8.dp),
).replace( imageVector = Icons.Default.ThumbUp,
"year", contentDescription = null,
post.date.year.toString() tint = if (isLiked.value) { Color(40, 200, 40, 255) } else {Color.Black}
),
fontSize = 14.sp,
color = Color(0xFFCECCCC)
) )
Row (
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End
) {
Text(
text = "0",
fontSize = 16.sp,
color = Color.Green
)
Icon(
modifier = Modifier.padding(start = 8.dp),
imageVector = Icons.Default.ThumbUp,
contentDescription = null
)
}
} }
} }
} }

View File

@ -5,30 +5,25 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.* import androidx.compose.ui.*
import androidx.compose.ui.graphics.* import androidx.compose.ui.graphics.*
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.* import androidx.compose.ui.unit.*
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import com.example.dtf.db.AppDatabase import com.example.dtf.PreferencesManager
import com.example.dtf.models.User
import com.example.dtf.utils.ScreenPaths import com.example.dtf.utils.ScreenPaths
import kotlinx.coroutines.Dispatchers import com.example.dtf.viewmodels.ProfileViewModel
import kotlinx.coroutines.withContext
@Composable @Composable
fun ProfileScreen(navController: NavHostController) { fun ProfileScreen(navController: NavHostController) {
val user = remember { mutableStateOf(User(-1, "", "", false)) } val sharedPref = PreferencesManager(LocalContext.current)
val context = LocalContext.current
LaunchedEffect(Unit) { val viewModel = hiltViewModel<ProfileViewModel>()
withContext(Dispatchers.IO) { val user = viewModel.user.observeAsState().value
user.value = AppDatabase.getInstance(context).userDao().getById(1)
} viewModel.retrieveUser(sharedPref)
}
Column( Column(
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
@ -52,14 +47,14 @@ fun ProfileScreen(navController: NavHostController) {
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Text( Text(
text = if (user.value.username.isNotEmpty()) { user.value.username[0].toString() } else {""}, text = if (user != null) { user.username[0].toString() } else {""},
fontSize = 30.sp, fontSize = 30.sp,
) )
} }
} }
Spacer(modifier = Modifier.fillMaxHeight(0.1f)) Spacer(modifier = Modifier.fillMaxHeight(0.1f))
Text( Text(
text = user.value.username, text = user?.username ?: "",
fontSize = 30.sp, fontSize = 30.sp,
color = Color.White color = Color.White
) )
@ -84,6 +79,7 @@ fun ProfileScreen(navController: NavHostController) {
Text( Text(
modifier = Modifier.clickable { modifier = Modifier.clickable {
viewModel.logout(sharedPref)
navController.navigate(ScreenPaths.Login.route) navController.navigate(ScreenPaths.Login.route)
}, },
text = "Выйти", text = "Выйти",

View File

@ -7,10 +7,13 @@ import androidx.compose.foundation.border
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.AlertDialog
import androidx.compose.material.Button import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.* import androidx.compose.ui.*
@ -23,9 +26,11 @@ import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import com.example.dtf.utils.ScreenPaths import com.example.dtf.utils.ScreenPaths
import com.example.dtf.viewmodels.RegisterViewModel
import com.example.dtf.widgets.MyTextField import com.example.dtf.widgets.MyTextField
@Composable @Composable
@ -33,6 +38,49 @@ fun RegisterScreen(navController: NavHostController) {
val login = remember { mutableStateOf(TextFieldValue(""))} val login = remember { mutableStateOf(TextFieldValue(""))}
val password = remember { mutableStateOf(TextFieldValue(""))} val password = remember { mutableStateOf(TextFieldValue(""))}
val confirmPassword = remember { mutableStateOf(TextFieldValue(""))} val confirmPassword = remember { mutableStateOf(TextFieldValue(""))}
val viewModel = hiltViewModel<RegisterViewModel>()
val success = viewModel.successState.observeAsState().value
if (success == true) {
navController.navigate(ScreenPaths.Login.route) {
popUpTo(ScreenPaths.Login.route) {
inclusive = true
}
}
}
if (success == false) {
AlertDialog(
title = {
Text(
text = "Ошибка регистрации",
fontWeight = FontWeight.Bold,
fontSize = 20.sp
)
},
text = {
Text(
text = "Проверьте корректность введенных данных",
fontWeight = FontWeight.Normal,
fontSize = 16.sp
)
},
onDismissRequest = {
viewModel.calmSuccessState()
},
buttons = {
TextButton(
onClick = {
viewModel.calmSuccessState()
}
) {
Text("ОК")
}
}
)
}
Row( Row(
horizontalArrangement = Arrangement.Center, horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
@ -78,7 +126,9 @@ fun RegisterScreen(navController: NavHostController) {
.heightIn(max=55.dp) .heightIn(max=55.dp)
) )
Button( Button(
{}, {
viewModel.register(login.value.text, password.value.text, confirmPassword.value.text)
},
colors = ButtonDefaults.buttonColors(Color(0xFF388DF2)), colors = ButtonDefaults.buttonColors(Color(0xFF388DF2)),
border = BorderStroke(1.dp, Color(0xFF0085FF)), border = BorderStroke(1.dp, Color(0xFF0085FF)),
shape = RoundedCornerShape(15.dp), shape = RoundedCornerShape(15.dp),

View File

@ -0,0 +1,52 @@
package com.example.dtf.viewmodels
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.dtf.PreferencesManager
import com.example.dtf.models.Post
import com.example.dtf.repositories.PostRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class EditPostViewModel @Inject constructor(
private val postRepository: PostRepository
) : ViewModel() {
private val _editingPostState = MutableLiveData<Boolean?>(null)
val editingPostState: LiveData<Boolean?>
get() = _editingPostState
private val _post = MutableLiveData<Post>()
val post: LiveData<Post>
get() = _post
fun retrievePost(postId: Int) {
viewModelScope.launch {
postRepository.getById(postId).collect {
_post.postValue(it)
}
}
}
fun calmEditingState() {
_editingPostState.value = null
}
fun editPost(sharedPref: PreferencesManager, postId: Int, title: String, content: String) {
if (!sharedPref.getData("isModerator", "false").toBooleanStrict()) return
viewModelScope.launch {
if (title.isNotEmpty() && content.isNotEmpty()) {
postRepository.getById(postId).collect {
postRepository.update(Post(postId, title, content, it.date, it.userId, it.categoryId))
_editingPostState.postValue(true)
}
} else {
_editingPostState.postValue(false)
}
}
}
}

View File

@ -0,0 +1,59 @@
package com.example.dtf.viewmodels
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.dtf.PreferencesManager
import com.example.dtf.models.Category
import com.example.dtf.models.User
import com.example.dtf.repositories.CategoryRepository
import com.example.dtf.repositories.UserRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class LoginViewModel @Inject constructor(
private val userRepository: UserRepository,
private val categoryRepository: CategoryRepository
) : ViewModel() {
private val _successState = MutableLiveData<Boolean?>()
val successState: LiveData<Boolean?>
get() = _successState
init {
addAdmin()
}
fun calmSuccessState() {
_successState.postValue(null)
}
fun login(sharedPref: PreferencesManager, username: String, password: String) {
viewModelScope.launch {
userRepository.getByUsernameAndPassword(username, password).collect {
if (it == null) {
_successState.postValue(false)
} else {
sharedPref.saveData("userId", it.id.toString())
sharedPref.saveData("isModerator", it.isModerator.toString())
_successState.postValue(true)
}
}
}
}
private fun addAdmin() {
viewModelScope.launch {
userRepository.getAll().collect {
if (it.size == 0) {
userRepository.insert(User(1, "admin", "admin", true))
categoryRepository.insert(Category(1, "Аниме"))
categoryRepository.insert(Category(2, "Игры"))
categoryRepository.insert(Category(3, "Фильмы"))
}
}
}
}
}

View File

@ -0,0 +1,65 @@
package com.example.dtf.viewmodels
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.dtf.PreferencesManager
import com.example.dtf.models.Category
import com.example.dtf.models.Post
import com.example.dtf.repositories.CategoryRepository
import com.example.dtf.repositories.PostRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import java.time.LocalDate
import java.util.Date
import javax.inject.Inject
@HiltViewModel
class NewPostViewModel @Inject constructor(
private val postRepository: PostRepository,
private val categoryRepository: CategoryRepository
) : ViewModel() {
private val _addingPostState = MutableLiveData<Boolean?>(null)
val addingPostState: LiveData<Boolean?>
get() = _addingPostState
private val _categories = MutableLiveData<List<Category>>(listOf())
val categories: LiveData<List<Category>>
get() = _categories
fun retrieveCategories() {
viewModelScope.launch {
categoryRepository.getAll().collect {
_categories.postValue(it)
}
}
}
fun calmAddingState() {
_addingPostState.value = null
}
fun addPost(sharedPref: PreferencesManager, categoryId: Int, title: String, content: String) {
if (!sharedPref.getData("isModerator", "false").toBooleanStrict()) return
viewModelScope.launch {
if (title.isNotEmpty() && content.isNotEmpty()) {
postRepository.insert(
Post(
null,
title,
content,
Date(),
sharedPref.getData("userId", "0").toInt(),
categoryId
)
)
_addingPostState.postValue(true)
} else {
_addingPostState.postValue(false)
}
}
}
}

View File

@ -0,0 +1,91 @@
package com.example.dtf.viewmodels
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.paging.PagingData
import com.example.dtf.PreferencesManager
import com.example.dtf.models.Comment
import com.example.dtf.models.Like
import com.example.dtf.models.Post
import com.example.dtf.repositories.CommentRepository
import com.example.dtf.repositories.LikeRepository
import com.example.dtf.repositories.PostRepository
import com.example.dtf.repositories.UserRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
import java.util.Date
import javax.inject.Inject
@HiltViewModel
class PostViewModel @Inject constructor(
private val postRepository: PostRepository,
private val commentRepository: CommentRepository,
private val likeRepository: LikeRepository,
private val userRepository: UserRepository
) : ViewModel() {
private val _post = MutableLiveData<Post>()
val post: LiveData<Post>
get() = _post
fun retrievePost(postId: Int) {
viewModelScope.launch {
postRepository.getById(postId).collect {
_post.postValue(it)
}
}
}
fun getLikes(postId: Int) = likeRepository.countByPost(postId)
fun likePost(sharedPref: PreferencesManager, postId: Int) {
val userId = sharedPref.getData("userId", "-1").toInt()
if (userId == -1) return
viewModelScope.launch {
likeRepository.insert(Like(userId, postId))
}
}
fun unlikePost(sharedPref: PreferencesManager, postId: Int) {
val userId = sharedPref.getData("userId", "-1").toInt()
if (userId == -1) return
viewModelScope.launch {
likeRepository.delete(Like(userId, postId))
}
}
fun isLiked(sharedPref: PreferencesManager, postId: Int)
= likeRepository.isLikedByUser(
sharedPref.getData("userId", "-1").toInt(),
postId
)
fun addComment(sharedPref: PreferencesManager, postId: Int, content: String) {
val userId = sharedPref.getData("userId", "-1").toInt()
if (userId == -1 || content.trim().isEmpty()) return
viewModelScope.launch {
commentRepository.insert(
Comment(
null,
userId,
postId,
content,
Date()
)
)
}
}
fun getCommentsAuthor(comment: Comment) = userRepository.getById(comment.userId)
fun getCommentsListUiState(postId: Int): Flow<PagingData<Comment>> = commentRepository.getByPost(postId)
}

View File

@ -0,0 +1,51 @@
package com.example.dtf.viewmodels
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.dtf.PreferencesManager
import com.example.dtf.models.Like
import com.example.dtf.repositories.CategoryRepository
import com.example.dtf.repositories.LikeRepository
import com.example.dtf.repositories.PostRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class PostsViewModel @Inject constructor(
private val postRepository: PostRepository,
private val categoryRepository: CategoryRepository,
private val likeRepository: LikeRepository
) : ViewModel() {
fun getLikes(postId: Int) = likeRepository.countByPost(postId)
fun likePost(sharedPref: PreferencesManager, postId: Int) {
val userId = sharedPref.getData("userId", "-1").toInt()
if (userId == -1) return
viewModelScope.launch {
likeRepository.insert(Like(userId, postId))
}
}
fun unlikePost(sharedPref: PreferencesManager, postId: Int) {
val userId = sharedPref.getData("userId", "-1").toInt()
if (userId == -1) return
viewModelScope.launch {
likeRepository.delete(Like(userId, postId))
}
}
fun isLiked(sharedPref: PreferencesManager, postId: Int)
= likeRepository.isLikedByUser(
sharedPref.getData("userId", "-1").toInt(),
postId
)
fun getCategories() = categoryRepository.getAll()
fun getPostsListUiState(categoryId: Int) = postRepository.getByCategory(categoryId)
}

View File

@ -0,0 +1,38 @@
package com.example.dtf.viewmodels
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.dtf.PreferencesManager
import com.example.dtf.models.User
import com.example.dtf.repositories.UserRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class ProfileViewModel @Inject constructor(
private val userRepository: UserRepository
) : ViewModel() {
private val _user = MutableLiveData<User>()
val user: LiveData<User>
get() = _user
fun retrieveUser(sharedPref: PreferencesManager) {
val userId = sharedPref.getData("userId", "-1").toInt()
if (userId == -1) return
viewModelScope.launch {
userRepository.getById(userId).collect {
_user.postValue(it)
}
}
}
fun logout(sharedPref: PreferencesManager) {
sharedPref.deleteData("userId")
sharedPref.deleteData("isModerator")
}
}

View File

@ -0,0 +1,53 @@
package com.example.dtf.viewmodels
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.dtf.PreferencesManager
import com.example.dtf.models.User
import com.example.dtf.repositories.UserRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class RegisterViewModel @Inject constructor(
private val userRepository: UserRepository
) : ViewModel() {
private val _successState = MutableLiveData<Boolean?>()
val successState: LiveData<Boolean?>
get() = _successState
fun calmSuccessState() {
_successState.postValue(null)
}
fun register(username: String, password: String, confirmPassword: String) {
if (username.length < 5 || username.length > 20) {
_successState.postValue(false)
return
}
if (password.length < 8 || password.length > 30) {
_successState.postValue(false)
return
}
if (password != confirmPassword) {
_successState.postValue(false)
return
}
viewModelScope.launch {
userRepository.getByUsernameAndPassword(username, password).collect {
if (it != null) {
_successState.postValue(false)
} else {
userRepository.insert(User(null, username, password, false))
_successState.postValue(true)
}
}
}
}
}

View File

@ -29,6 +29,7 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.currentBackStackEntryAsState
import com.example.dtf.PreferencesManager
import com.example.dtf.db.AppDatabase import com.example.dtf.db.AppDatabase
import com.example.dtf.models.User import com.example.dtf.models.User
import com.example.dtf.utils.ScreenPaths import com.example.dtf.utils.ScreenPaths
@ -37,28 +38,7 @@ import kotlinx.coroutines.withContext
@Composable @Composable
fun BottomNavBar(navController: NavController) { fun BottomNavBar(navController: NavController) {
val user = remember { mutableStateOf(User(-1, "", "", false)) } val sharedPref = PreferencesManager(LocalContext.current)
val buttons = remember { mutableListOf(
Triple(ScreenPaths.Posts.route, Icons.Default.Home, "Новости"),
Triple(ScreenPaths.Profile.route, Icons.Default.Person, "Профиль")
) }
val context = LocalContext.current
LaunchedEffect(Unit) {
withContext(Dispatchers.IO) {
user.value = AppDatabase.getInstance(context).userDao().getById(1)
if (user.value.isModerator) {
buttons.clear()
buttons.addAll(
listOf(
Triple(ScreenPaths.Posts.route, Icons.Default.Home, "Новости"),
Triple(ScreenPaths.NewPost.route, Icons.Default.Edit, "Создать"),
Triple(ScreenPaths.Profile.route, Icons.Default.Person, "Профиль")
)
)
}
}
}
BottomNavigation( BottomNavigation(
modifier = Modifier.height(70.dp), modifier = Modifier.height(70.dp),
@ -69,7 +49,7 @@ fun BottomNavBar(navController: NavController) {
val currentRoute = navBackStackEntry?.destination?.route val currentRoute = navBackStackEntry?.destination?.route
listOfNotNull( listOfNotNull(
Triple(ScreenPaths.Posts.route, Icons.Default.Home, "Новости"), Triple(ScreenPaths.Posts.route, Icons.Default.Home, "Новости"),
if (user.value.isModerator) { if (sharedPref.getData("isModerator", "false").toBooleanStrict()) {
Triple(ScreenPaths.NewPost.route, Icons.Default.Edit, "Создать") Triple(ScreenPaths.NewPost.route, Icons.Default.Edit, "Создать")
} else { null }, } else { null },
Triple(ScreenPaths.Profile.route, Icons.Default.Person, "Профиль") Triple(ScreenPaths.Profile.route, Icons.Default.Person, "Профиль")

View File

@ -7,6 +7,7 @@ buildscript {
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:4.0.0' classpath 'com.android.tools.build:gradle:4.0.0'
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.42'
} }
}// 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 {