Лабораторная работа №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'
}
apply plugin: 'com.android.application'
apply plugin: 'com.google.dagger.hilt.android'
android {
namespace 'com.example.dtf'
compileSdk 34
@ -71,4 +74,16 @@ dependencies {
implementation 'androidx.room:room-runtime:2.5.0' // Библиотека "Room"
kapt "androidx.room:room-compiler: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">
<application
android:name=".DTFApp"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
@ -12,7 +13,7 @@
android:theme="@style/Theme.DTF"
tools:targetApi="31">
<activity
android:name="com.example.dtf.MainActivity"
android:name=".MainActivity"
android:windowSoftInputMode="adjustResize"
android:exported="true"
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.utils.ScreenPaths
import com.example.dtf.widgets.BottomNavBar
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter",
"UnusedMaterialScaffoldPaddingParameter"
@ -100,7 +102,7 @@ class MainActivity : ComponentActivity() {
}
composable(ScreenPaths.NewPost.route) {
includeBackButton.value = true
NewPostScreen()
NewPostScreen(navController)
}
composable(ScreenPaths.Post.route) { navBackStackEntry ->
includeBackButton.value = true
@ -115,6 +117,7 @@ class MainActivity : ComponentActivity() {
includeBackButton.value = true
navBackStackEntry.arguments?.getString("post")?.let { postId ->
EditPostScreen(
navController,
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")
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
import androidx.paging.PagingSource
import androidx.room.*
import com.example.dtf.models.*
import kotlinx.coroutines.flow.Flow
@ -21,6 +22,6 @@ interface CommentDao {
@Query("select * from comment where comment.id = :id")
fun getById(id: Int) : Flow<Comment>
@Query("select * from comment where comment.post_id = :postId")
fun getByPost(postId: Int) : Flow<List<Comment>>
@Query("select * from comment where comment.post_id = :postId ORDER BY date DESC")
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
import androidx.paging.PagingSource
import androidx.room.*
import com.example.dtf.models.*
import kotlinx.coroutines.flow.Flow
@ -19,8 +20,8 @@ interface PostDao {
fun getAll() : Flow<List<Post>>
@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")
fun getByCategory(categoryId: String) : Flow<Post>
@Query("select * from post where post.category_id = :categoryId ORDER BY date DESC")
fun getByCategory(categoryId: String) : PagingSource<Int, Post>
}

View File

@ -19,5 +19,8 @@ interface UserDao {
fun getAll() : Flow<List<User>>
@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 androidx.sqlite.db.SupportSQLiteDatabase
import com.example.dtf.dao.*
import com.example.dtf.models.Like
import java.util.Date
@Database(
@ -18,9 +19,10 @@ import java.util.Date
User::class,
Category::class,
Post::class,
Like::class,
Comment::class
],
version = 1,
version = 2,
exportSchema = false
)
@TypeConverters(Converter::class)
@ -29,119 +31,9 @@ abstract class AppDatabase : RoomDatabase() {
abstract fun categoryDao() : CategoryDao
abstract fun commentDao() : CommentDao
abstract fun postDao() : PostDao
abstract fun likeDao() : LikeDao
companion object {
private 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 }
}
}
public const val DB_NAME: String = "news-db"
}
}

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.material.*
import androidx.compose.runtime.*
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.*
import androidx.compose.ui.graphics.Color
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.input.TextFieldValue
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 kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@Composable
fun EditPostScreen(postId: Int) {
val context = LocalContext.current
fun EditPostScreen(navController: NavHostController, postId: Int) {
val sharedPref = PreferencesManager(LocalContext.current)
val title = remember { mutableStateOf(TextFieldValue("")) }
val content = remember { mutableStateOf(TextFieldValue("")) }
LaunchedEffect(Unit) {
withContext(Dispatchers.IO) {
AppDatabase.getInstance(context).postDao().getById(postId).collect { data ->
title.value = TextFieldValue(data.post.title)
content.value = TextFieldValue(data.post.content)
val viewModel = hiltViewModel<EditPostViewModel>()
val post = viewModel.post.observeAsState().value
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(
modifier = Modifier.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(10.dp),
@ -57,7 +103,7 @@ fun EditPostScreen(postId: Int) {
)
Button(
{
viewModel.editPost(sharedPref, postId, title.value.text, content.value.text)
},
colors = ButtonDefaults.buttonColors(Color(0xFF388DF2)),
border = BorderStroke(1.dp, Color(0xFF0085FF)),

View File

@ -1,33 +1,77 @@
package com.example.dtf.screens
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.*
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
import androidx.compose.ui.unit.*
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.LoginViewModel
import com.example.dtf.widgets.MyTextField
@Composable
fun LoginScreen(navController: NavHostController) {
val sharedPref = PreferencesManager(LocalContext.current)
val login = 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(
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxSize()
@ -65,7 +109,7 @@ fun LoginScreen(navController: NavHostController) {
)
Button(
{
navController.navigate(ScreenPaths.Posts.route)
viewModel.login(sharedPref, login.value.text, password.value.text)
},
colors = ButtonDefaults.buttonColors(Color(0xFF388DF2)),
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.material.*
import androidx.compose.runtime.*
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.*
import androidx.compose.ui.graphics.Color
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.style.TextAlign
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.utils.ScreenPaths
import com.example.dtf.viewmodels.NewPostViewModel
import com.example.dtf.widgets.MyTextField
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@Composable
fun NewPostScreen() {
val categories = remember { mutableStateListOf<Category>() }
val context = LocalContext.current
fun NewPostScreen(navController: NavHostController) {
val sharedPref = PreferencesManager(LocalContext.current)
LaunchedEffect(Unit) {
withContext(Dispatchers.IO) {
AppDatabase.getInstance(context).categoryDao().getAll().collect {data ->
categories.clear()
categories.addAll(data)
}
}
}
val selectedCategory = remember { mutableStateOf("") }
val selectedCategory = remember { mutableStateOf(Category(null, "")) }
val expanded = remember { mutableStateOf(false) }
val title = 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(
modifier = Modifier.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(10.dp),
@ -56,7 +94,7 @@ fun NewPostScreen() {
modifier = Modifier.fillMaxWidth().padding(15.dp, 10.dp, 15.dp, 0.dp)
) {
Text(
text = selectedCategory.value,
text = selectedCategory.value.name,
style = TextStyle(
fontSize = 24.sp,
fontWeight = FontWeight(400),
@ -68,10 +106,10 @@ fun NewPostScreen() {
expanded = expanded.value,
onDismissRequest = {expanded.value = false}
) {
categories.forEach {category ->
categories?.forEach { category ->
DropdownMenuItem(
onClick = {
selectedCategory.value = category.name
selectedCategory.value = category
expanded.value = false
},
) {
@ -101,7 +139,9 @@ fun NewPostScreen() {
)
Button(
{
if (selectedCategory.value.id != null) {
viewModel.addPost(sharedPref, selectedCategory.value.id!!, title.value.text, content.value.text)
}
},
colors = ButtonDefaults.buttonColors(Color(0xFF388DF2)),
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.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
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.runtime.Composable
import androidx.compose.runtime.*
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.*
import androidx.compose.ui.draw.*
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.style.TextAlign
import androidx.compose.ui.unit.*
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.viewModelScope
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.models.Comment
import com.example.dtf.models.Post
import com.example.dtf.models.PostWithComments
import com.example.dtf.models.User
import com.example.dtf.utils.ScreenPaths
import com.example.dtf.viewmodels.PostViewModel
import com.example.dtf.widgets.MyTextField
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.Date
@Composable
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 context = LocalContext.current
val viewModel = hiltViewModel<PostViewModel>()
val post = viewModel.post.observeAsState().value
val comments = viewModel.getCommentsListUiState(postId).collectAsLazyPagingItems()
val likes = remember { mutableIntStateOf(0) }
val isLiked = remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
withContext(Dispatchers.IO) {
user.value = AppDatabase.getInstance(context).userDao().getById(1)
AppDatabase.getInstance(context).postDao().getById(postId).collect {data ->
post.value = data
}
viewModel.getLikes(postId).collect {
likes.intValue = it
}
}
LaunchedEffect(Unit) {
viewModel.isLiked(sharedPref, postId).collect {
isLiked.value = it
}
}
viewModel.retrievePost(postId)
val content = remember { mutableStateOf(TextFieldValue("")) }
Column(
LazyColumn(
modifier = Modifier
.fillMaxSize()
.background(Color.White)
.verticalScroll(rememberScrollState()),
.fillMaxWidth()
.background(Color.White),
verticalArrangement = Arrangement.spacedBy(5.dp),
horizontalAlignment = Alignment.Start
) {
Text(
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
) {
item {
Text(
text = "day.month.year".replace(
"day",
post.value.post.date.day.toString()
).replace(
"month",
post.value.post.date.month.toString()
).replace(
"year",
post.value.post.date.year.toString()
),
fontSize = 14.sp,
color = Color(0xFFCECCCC)
modifier = Modifier.padding(10.dp),
text = post?.title ?: "Loading...",
fontSize = 26.sp
)
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 {
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 = "Изменить",
fontSize = 18.sp,
color = Color(0xFFAFAFAF)
)
horizontalArrangement = Arrangement.End
) {
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 (
modifier = Modifier,
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
)
}
}
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
Divider()
Text(
modifier = Modifier.fillMaxWidth().padding(0.dp, 0.dp, 0.dp, 10.dp),
text = "Комментарии",
fontSize = 22.sp,
textAlign = TextAlign.Center
)
IconButton(
modifier = Modifier.fillMaxWidth().scale(1.5f),
onClick = {},
Row(
modifier = Modifier.fillMaxSize().padding(horizontal = 15.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Default.AddCircle,
contentDescription = null
MyTextField(
value = content,
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 ->
Comment(comment)
items(
comments.itemCount,
key = comments.itemKey()
) {
Comment(comments[it]!!)
}
item {
Spacer(modifier = Modifier.height(60.dp))
}
Spacer(modifier = Modifier.height(60.dp))
}
}
@Composable
private fun Comment(comment: Comment) {
val user = remember { mutableStateOf(User(-1, "", "", false)) }
val context = LocalContext.current
LaunchedEffect(Unit) {
withContext(Dispatchers.IO) {
user.value = AppDatabase.getInstance(context).userDao().getById(comment.userId)
}
}
val viewModel = hiltViewModel<PostViewModel>()
val user = viewModel.getCommentsAuthor(comment).collectAsState(null).value
Column(
modifier = Modifier
@ -168,7 +202,7 @@ private fun Comment(comment: Comment) {
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = user.value.username,
text = user?.username ?: "Loading...",
fontSize = 20.sp
)
Text(

View File

@ -2,6 +2,7 @@ package com.example.dtf.screens
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
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.text.style.TextOverflow
import androidx.compose.ui.unit.*
import androidx.hilt.navigation.compose.hiltViewModel
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.models.Category
import com.example.dtf.models.CategoryWithPosts
import com.example.dtf.models.Post
import com.example.dtf.utils.ScreenPaths
import com.example.dtf.viewmodels.PostsViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@Composable
fun PostsScreen(navController: NavHostController) {
val currentCategory = remember { mutableStateOf("") }
val categories = remember { mutableListOf<Category> () }
val categoryWithPosts = remember { mutableStateOf(CategoryWithPosts(Category(-1, ""), listOf()))}
val context = LocalContext.current
val scope = rememberCoroutineScope()
val sharedPref = PreferencesManager(LocalContext.current)
LaunchedEffect(Unit) {
withContext(Dispatchers.IO) {
AppDatabase.getInstance(context).categoryDao().getAll().collect {data ->
currentCategory.value = data.first().name
categories.clear()
categories.addAll(data)
AppDatabase.getInstance(context).categoryDao().getByName(currentCategory.value).collect {data ->
categoryWithPosts.value = data
}
}
}
}
val currentCategory = remember { mutableStateOf<Category?>(null) }
val viewModel = hiltViewModel<PostsViewModel>()
val posts = remember { mutableStateOf<Flow<PagingData<Post>>?>(null) }
Column(
modifier = Modifier.fillMaxSize(),
@ -60,42 +58,37 @@ fun PostsScreen(navController: NavHostController) {
horizontalArrangement = Arrangement.spacedBy(25.dp),
verticalAlignment = Alignment.CenterVertically
) {
Categories(scope, categoryWithPosts, currentCategory, categories)
Categories(viewModel, currentCategory, posts)
}
Row(
modifier = Modifier.fillMaxSize()
) {
Column(
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
if (currentCategory.value.isNotEmpty()) {
PostsByCategory(navController, categoryWithPosts)
}
Spacer(modifier = Modifier.height(60.dp))
if (currentCategory.value != null && posts.value != null) {
PostsByCategory(viewModel, navController, posts)
}
}
}
}
@Composable
private fun Categories(scope: CoroutineScope, categoryWithPosts: MutableState<CategoryWithPosts>, categoryState: MutableState<String>, categories: MutableList<Category>) {
val context = LocalContext.current
private fun Categories(viewModel: PostsViewModel, currentCategory: MutableState<Category?>, posts: MutableState<Flow<PagingData<Post>>?>) {
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))
categories.forEach {category ->
Text(
modifier = Modifier
.clickable {
categoryState.value = category.name
scope.launch {
AppDatabase.getInstance(context).categoryDao().getByName(categoryState.value).collect { data ->
categoryWithPosts.value = data
}
}
currentCategory.value = category
posts.value = viewModel.getPostsListUiState(category.id!!)
}
.drawBehind {
if (category.name == categoryState.value) {
if (category.name == currentCategory.value?.name) {
val strokeWidthPx = 2.dp.toPx()
val verticalOffset = size.height + 2.sp.toPx()
drawLine(
@ -113,65 +106,110 @@ private fun Categories(scope: CoroutineScope, categoryWithPosts: MutableState<Ca
}
@Composable
private fun PostsByCategory(navController: NavHostController, categoryWithPosts: MutableState<CategoryWithPosts>) {
categoryWithPosts.value.posts.forEach { post ->
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
private fun PostsByCategory(viewModel: PostsViewModel, navController: NavHostController, posts: MutableState<Flow<PagingData<Post>>?>) {
val postsItems = posts.value!!.collectAsLazyPagingItems()
LazyColumn(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
items(
count = postsItems.itemCount,
key = postsItems.itemKey()
) {
Post(viewModel, navController, postsItems[it]!!)
}
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(
modifier = Modifier.padding(10.dp),
text = post.title,
fontSize = 26.sp
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)
)
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
Row (
modifier = Modifier.fillMaxWidth().clickable {
if (isLiked.value) {
viewModel.unlikePost(sharedPref, post.id!!)
likes.intValue--
} else {
viewModel.likePost(sharedPref, post.id!!)
likes.intValue++
}
isLiked.value = !isLiked.value
},
horizontalArrangement = Arrangement.End
) {
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)
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 (
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.material.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.*
import androidx.compose.ui.graphics.*
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.*
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import com.example.dtf.db.AppDatabase
import com.example.dtf.models.User
import com.example.dtf.PreferencesManager
import com.example.dtf.utils.ScreenPaths
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import com.example.dtf.viewmodels.ProfileViewModel
@Composable
fun ProfileScreen(navController: NavHostController) {
val user = remember { mutableStateOf(User(-1, "", "", false)) }
val context = LocalContext.current
val sharedPref = PreferencesManager(LocalContext.current)
LaunchedEffect(Unit) {
withContext(Dispatchers.IO) {
user.value = AppDatabase.getInstance(context).userDao().getById(1)
}
}
val viewModel = hiltViewModel<ProfileViewModel>()
val user = viewModel.user.observeAsState().value
viewModel.retrieveUser(sharedPref)
Column(
modifier = Modifier.fillMaxSize()
@ -52,14 +47,14 @@ fun ProfileScreen(navController: NavHostController) {
contentAlignment = Alignment.Center
) {
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,
)
}
}
Spacer(modifier = Modifier.fillMaxHeight(0.1f))
Text(
text = user.value.username,
text = user?.username ?: "",
fontSize = 30.sp,
color = Color.White
)
@ -84,6 +79,7 @@ fun ProfileScreen(navController: NavHostController) {
Text(
modifier = Modifier.clickable {
viewModel.logout(sharedPref)
navController.navigate(ScreenPaths.Login.route)
},
text = "Выйти",

View File

@ -7,10 +7,13 @@ import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.AlertDialog
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
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.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavController
import androidx.navigation.NavHostController
import com.example.dtf.utils.ScreenPaths
import com.example.dtf.viewmodels.RegisterViewModel
import com.example.dtf.widgets.MyTextField
@Composable
@ -33,6 +38,49 @@ fun RegisterScreen(navController: NavHostController) {
val login = remember { mutableStateOf(TextFieldValue(""))}
val password = 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(
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxSize()
@ -78,7 +126,9 @@ fun RegisterScreen(navController: NavHostController) {
.heightIn(max=55.dp)
)
Button(
{},
{
viewModel.register(login.value.text, password.value.text, confirmPassword.value.text)
},
colors = ButtonDefaults.buttonColors(Color(0xFF388DF2)),
border = BorderStroke(1.dp, Color(0xFF0085FF)),
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.sp
import androidx.navigation.compose.currentBackStackEntryAsState
import com.example.dtf.PreferencesManager
import com.example.dtf.db.AppDatabase
import com.example.dtf.models.User
import com.example.dtf.utils.ScreenPaths
@ -37,28 +38,7 @@ import kotlinx.coroutines.withContext
@Composable
fun BottomNavBar(navController: NavController) {
val user = remember { mutableStateOf(User(-1, "", "", false)) }
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, "Профиль")
)
)
}
}
}
val sharedPref = PreferencesManager(LocalContext.current)
BottomNavigation(
modifier = Modifier.height(70.dp),
@ -69,7 +49,7 @@ fun BottomNavBar(navController: NavController) {
val currentRoute = navBackStackEntry?.destination?.route
listOfNotNull(
Triple(ScreenPaths.Posts.route, Icons.Default.Home, "Новости"),
if (user.value.isModerator) {
if (sharedPref.getData("isModerator", "false").toBooleanStrict()) {
Triple(ScreenPaths.NewPost.route, Icons.Default.Edit, "Создать")
} else { null },
Triple(ScreenPaths.Profile.route, Icons.Default.Person, "Профиль")

View File

@ -7,6 +7,7 @@ buildscript {
}
dependencies {
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.
plugins {