diff --git a/app/build.gradle b/app/build.gradle index a8c82a9..db00a26 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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") } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9015826..7b80306 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ xmlns:tools="http://schemas.android.com/tools"> diff --git a/app/src/main/java/com/example/dtf/AppModule.kt b/app/src/main/java/com/example/dtf/AppModule.kt new file mode 100644 index 0000000..dc785fb --- /dev/null +++ b/app/src/main/java/com/example/dtf/AppModule.kt @@ -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()) +} \ No newline at end of file diff --git a/app/src/main/java/com/example/dtf/DTFApp.kt b/app/src/main/java/com/example/dtf/DTFApp.kt new file mode 100644 index 0000000..4415c81 --- /dev/null +++ b/app/src/main/java/com/example/dtf/DTFApp.kt @@ -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() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/dtf/MainActivity.kt b/app/src/main/java/com/example/dtf/MainActivity.kt index 1341af0..97f4de9 100644 --- a/app/src/main/java/com/example/dtf/MainActivity.kt +++ b/app/src/main/java/com/example/dtf/MainActivity.kt @@ -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() ) } diff --git a/app/src/main/java/com/example/dtf/PreferencesManager.kt b/app/src/main/java/com/example/dtf/PreferencesManager.kt new file mode 100644 index 0000000..4412218 --- /dev/null +++ b/app/src/main/java/com/example/dtf/PreferencesManager.kt @@ -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() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/dtf/dao/CategoryDao.kt b/app/src/main/java/com/example/dtf/dao/CategoryDao.kt index b749de7..2463790 100644 --- a/app/src/main/java/com/example/dtf/dao/CategoryDao.kt +++ b/app/src/main/java/com/example/dtf/dao/CategoryDao.kt @@ -20,7 +20,4 @@ interface CategoryDao { @Query("select * from category where category.id = :id") fun getById(id: Int) : Flow - - @Query("select * from category where category.name = :name") - fun getByName(name: String) : Flow } diff --git a/app/src/main/java/com/example/dtf/dao/CommentDao.kt b/app/src/main/java/com/example/dtf/dao/CommentDao.kt index 6913a96..36a7eb1 100644 --- a/app/src/main/java/com/example/dtf/dao/CommentDao.kt +++ b/app/src/main/java/com/example/dtf/dao/CommentDao.kt @@ -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 - @Query("select * from comment where comment.post_id = :postId") - fun getByPost(postId: Int) : Flow> + @Query("select * from comment where comment.post_id = :postId ORDER BY date DESC") + fun getByPost(postId: Int) : PagingSource } diff --git a/app/src/main/java/com/example/dtf/dao/LikeDao.kt b/app/src/main/java/com/example/dtf/dao/LikeDao.kt new file mode 100644 index 0000000..20e8d00 --- /dev/null +++ b/app/src/main/java/com/example/dtf/dao/LikeDao.kt @@ -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 + + @Query("SELECT COUNT(*) = 1 FROM likes WHERE post_id = :postId AND user_id = :userId") + fun isLikedByUser(userId: Int, postId: Int) : Flow +} \ No newline at end of file diff --git a/app/src/main/java/com/example/dtf/dao/PostDao.kt b/app/src/main/java/com/example/dtf/dao/PostDao.kt index 037156b..d7edb53 100644 --- a/app/src/main/java/com/example/dtf/dao/PostDao.kt +++ b/app/src/main/java/com/example/dtf/dao/PostDao.kt @@ -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> @Query("select * from post where post.id = :id") - fun getById(id: Int) : Flow + fun getById(id: Int) : Flow - @Query("select * from post where post.category_id = :categoryId") - fun getByCategory(categoryId: String) : Flow + @Query("select * from post where post.category_id = :categoryId ORDER BY date DESC") + fun getByCategory(categoryId: String) : PagingSource } diff --git a/app/src/main/java/com/example/dtf/dao/UserDao.kt b/app/src/main/java/com/example/dtf/dao/UserDao.kt index 436b2f5..60ecc10 100644 --- a/app/src/main/java/com/example/dtf/dao/UserDao.kt +++ b/app/src/main/java/com/example/dtf/dao/UserDao.kt @@ -19,5 +19,8 @@ interface UserDao { fun getAll() : Flow> @Query("select * from user where user.id = :id") - fun getById(id: Int): User + fun getById(id: Int): Flow + + @Query("select * from user where user.username = :username and user.password = :password") + fun getByUsernameAndPassword(username: String, password: String): Flow } \ No newline at end of file diff --git a/app/src/main/java/com/example/dtf/db/AppDatabase.kt b/app/src/main/java/com/example/dtf/db/AppDatabase.kt index 7359ca3..161f1f8 100644 --- a/app/src/main/java/com/example/dtf/db/AppDatabase.kt +++ b/app/src/main/java/com/example/dtf/db/AppDatabase.kt @@ -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" } - } \ No newline at end of file diff --git a/app/src/main/java/com/example/dtf/models/Like.kt b/app/src/main/java/com/example/dtf/models/Like.kt new file mode 100644 index 0000000..3889518 --- /dev/null +++ b/app/src/main/java/com/example/dtf/models/Like.kt @@ -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 +) \ No newline at end of file diff --git a/app/src/main/java/com/example/dtf/repositories/CategoryRepository.kt b/app/src/main/java/com/example/dtf/repositories/CategoryRepository.kt new file mode 100644 index 0000000..02dac8f --- /dev/null +++ b/app/src/main/java/com/example/dtf/repositories/CategoryRepository.kt @@ -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) +} \ No newline at end of file diff --git a/app/src/main/java/com/example/dtf/repositories/CommentRepository.kt b/app/src/main/java/com/example/dtf/repositories/CommentRepository.kt new file mode 100644 index 0000000..5852b51 --- /dev/null +++ b/app/src/main/java/com/example/dtf/repositories/CommentRepository.kt @@ -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 +} \ No newline at end of file diff --git a/app/src/main/java/com/example/dtf/repositories/LikeRepository.kt b/app/src/main/java/com/example/dtf/repositories/LikeRepository.kt new file mode 100644 index 0000000..24f107f --- /dev/null +++ b/app/src/main/java/com/example/dtf/repositories/LikeRepository.kt @@ -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) +} \ No newline at end of file diff --git a/app/src/main/java/com/example/dtf/repositories/PostRepository.kt b/app/src/main/java/com/example/dtf/repositories/PostRepository.kt new file mode 100644 index 0000000..f12ee02 --- /dev/null +++ b/app/src/main/java/com/example/dtf/repositories/PostRepository.kt @@ -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 +} \ No newline at end of file diff --git a/app/src/main/java/com/example/dtf/repositories/UserRepository.kt b/app/src/main/java/com/example/dtf/repositories/UserRepository.kt new file mode 100644 index 0000000..028a762 --- /dev/null +++ b/app/src/main/java/com/example/dtf/repositories/UserRepository.kt @@ -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) +} \ No newline at end of file diff --git a/app/src/main/java/com/example/dtf/screens/EditPostScreen.kt b/app/src/main/java/com/example/dtf/screens/EditPostScreen.kt index 377aa2b..029e739 100644 --- a/app/src/main/java/com/example/dtf/screens/EditPostScreen.kt +++ b/app/src/main/java/com/example/dtf/screens/EditPostScreen.kt @@ -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() + + 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)), diff --git a/app/src/main/java/com/example/dtf/screens/LoginScreen.kt b/app/src/main/java/com/example/dtf/screens/LoginScreen.kt index cd4de29..65f6046 100644 --- a/app/src/main/java/com/example/dtf/screens/LoginScreen.kt +++ b/app/src/main/java/com/example/dtf/screens/LoginScreen.kt @@ -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() + + 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)), diff --git a/app/src/main/java/com/example/dtf/screens/NewPostScreen.kt b/app/src/main/java/com/example/dtf/screens/NewPostScreen.kt index 78a1753..6db9b89 100644 --- a/app/src/main/java/com/example/dtf/screens/NewPostScreen.kt +++ b/app/src/main/java/com/example/dtf/screens/NewPostScreen.kt @@ -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() } - 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() + 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)), diff --git a/app/src/main/java/com/example/dtf/screens/PostScreen.kt b/app/src/main/java/com/example/dtf/screens/PostScreen.kt index 98fa7c7..e42b0be 100644 --- a/app/src/main/java/com/example/dtf/screens/PostScreen.kt +++ b/app/src/main/java/com/example/dtf/screens/PostScreen.kt @@ -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() + 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() + 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( diff --git a/app/src/main/java/com/example/dtf/screens/PostsScreen.kt b/app/src/main/java/com/example/dtf/screens/PostsScreen.kt index 6fa8247..7866bec 100644 --- a/app/src/main/java/com/example/dtf/screens/PostsScreen.kt +++ b/app/src/main/java/com/example/dtf/screens/PostsScreen.kt @@ -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 () } - 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(null) } + + val viewModel = hiltViewModel() + val posts = remember { mutableStateOf>?>(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, categoryState: MutableState, categories: MutableList) { - val context = LocalContext.current +private fun Categories(viewModel: PostsViewModel, currentCategory: MutableState, posts: MutableState>?>) { + 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) { - 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>?>) { + 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 - ) - } } } } diff --git a/app/src/main/java/com/example/dtf/screens/ProfileScreen.kt b/app/src/main/java/com/example/dtf/screens/ProfileScreen.kt index 25af8c9..feb4aca 100644 --- a/app/src/main/java/com/example/dtf/screens/ProfileScreen.kt +++ b/app/src/main/java/com/example/dtf/screens/ProfileScreen.kt @@ -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() + 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 = "Выйти", diff --git a/app/src/main/java/com/example/dtf/screens/RegisterScreen.kt b/app/src/main/java/com/example/dtf/screens/RegisterScreen.kt index 7fcad36..49dc1b6 100644 --- a/app/src/main/java/com/example/dtf/screens/RegisterScreen.kt +++ b/app/src/main/java/com/example/dtf/screens/RegisterScreen.kt @@ -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() + 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), diff --git a/app/src/main/java/com/example/dtf/viewmodels/EditPostViewModel.kt b/app/src/main/java/com/example/dtf/viewmodels/EditPostViewModel.kt new file mode 100644 index 0000000..9b42b6b --- /dev/null +++ b/app/src/main/java/com/example/dtf/viewmodels/EditPostViewModel.kt @@ -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(null) + val editingPostState: LiveData + get() = _editingPostState + + private val _post = MutableLiveData() + val post: LiveData + 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) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/dtf/viewmodels/LoginViewModel.kt b/app/src/main/java/com/example/dtf/viewmodels/LoginViewModel.kt new file mode 100644 index 0000000..2da272f --- /dev/null +++ b/app/src/main/java/com/example/dtf/viewmodels/LoginViewModel.kt @@ -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() + val successState: LiveData + 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, "Фильмы")) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/dtf/viewmodels/NewPostViewModel.kt b/app/src/main/java/com/example/dtf/viewmodels/NewPostViewModel.kt new file mode 100644 index 0000000..6ffca76 --- /dev/null +++ b/app/src/main/java/com/example/dtf/viewmodels/NewPostViewModel.kt @@ -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(null) + val addingPostState: LiveData + get() = _addingPostState + + private val _categories = MutableLiveData>(listOf()) + val categories: LiveData> + 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) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/dtf/viewmodels/PostViewModel.kt b/app/src/main/java/com/example/dtf/viewmodels/PostViewModel.kt new file mode 100644 index 0000000..0ffa1c7 --- /dev/null +++ b/app/src/main/java/com/example/dtf/viewmodels/PostViewModel.kt @@ -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() + val post: LiveData + 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> = commentRepository.getByPost(postId) +} \ No newline at end of file diff --git a/app/src/main/java/com/example/dtf/viewmodels/PostsViewModel.kt b/app/src/main/java/com/example/dtf/viewmodels/PostsViewModel.kt new file mode 100644 index 0000000..70cace9 --- /dev/null +++ b/app/src/main/java/com/example/dtf/viewmodels/PostsViewModel.kt @@ -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) +} \ No newline at end of file diff --git a/app/src/main/java/com/example/dtf/viewmodels/ProfileViewModel.kt b/app/src/main/java/com/example/dtf/viewmodels/ProfileViewModel.kt new file mode 100644 index 0000000..f1568cc --- /dev/null +++ b/app/src/main/java/com/example/dtf/viewmodels/ProfileViewModel.kt @@ -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() + val user: LiveData + 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") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/dtf/viewmodels/RegisterViewModel.kt b/app/src/main/java/com/example/dtf/viewmodels/RegisterViewModel.kt new file mode 100644 index 0000000..8bf8b60 --- /dev/null +++ b/app/src/main/java/com/example/dtf/viewmodels/RegisterViewModel.kt @@ -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() + val successState: LiveData + 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) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/dtf/widgets/BottomNavBar.kt b/app/src/main/java/com/example/dtf/widgets/BottomNavBar.kt index 6de2910..1735628 100644 --- a/app/src/main/java/com/example/dtf/widgets/BottomNavBar.kt +++ b/app/src/main/java/com/example/dtf/widgets/BottomNavBar.kt @@ -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, "Профиль") diff --git a/build.gradle b/build.gradle index 44023d2..c4b0614 100644 --- a/build.gradle +++ b/build.gradle @@ -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 {