Compare commits

...

No commits in common. "main" and "Lab4" have entirely different histories.
main ... Lab4

80 changed files with 3021 additions and 37 deletions

35
.gitignore vendored
View File

@ -1,35 +0,0 @@
# ---> Android
# Gradle files
.gradle/
build/
# Local configuration file (sdk path, etc)
local.properties
# Log/OS Files
*.log
# Android Studio generated files and folders
captures/
.externalNativeBuild/
.cxx/
*.apk
output.json
# IntelliJ
*.iml
.idea/
misc.xml
deploymentTargetDropDown.xml
render.experimental.xml
# Keystore files
*.jks
*.keystore
# Google Services (e.g. APIs or Firebase)
google-services.json
# Android Profiling
*.hprof

View File

@ -1,2 +0,0 @@
# Polevoy_PMD

1
app/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

89
app/build.gradle Normal file
View File

@ -0,0 +1,89 @@
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'kotlin-kapt'
}
apply plugin: 'com.android.application'
apply plugin: 'com.google.dagger.hilt.android'
android {
namespace 'com.example.dtf'
compileSdk 34
defaultConfig {
applicationId "com.example.dtf"
minSdk 23
targetSdk 34
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary true
}
kapt {
arguments {arg("room.schemaLocation", "$projectDir/schemas")}
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion '1.2.0'
}
packagingOptions {
resources {
excludes += '/META-INF/{AL2.0,LGPL2.1}'
}
}
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.20"
def nav_version = "2.7.0"
implementation "androidx.navigation:navigation-compose:$nav_version"
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
implementation 'androidx.activity:activity-compose:1.3.1'
implementation "androidx.compose.ui:ui:$compose_ui_version"
implementation "androidx.compose.ui:ui-tooling-preview:$compose_ui_version"
implementation 'androidx.compose.material:material:1.2.0'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_ui_version"
debugImplementation "androidx.compose.ui:ui-tooling:$compose_ui_version"
debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_ui_version"
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")
}

21
app/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@ -0,0 +1,24 @@
package com.example.dtf
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.example.shawarma", appContext.packageName)
}
}

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
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"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/Theme.DTF"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:windowSoftInputMode="adjustResize"
android:exported="true"
android:theme="@style/Theme.DTF">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

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

@ -0,0 +1,131 @@
package com.example.dtf
import android.annotation.SuppressLint
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.Scaffold
import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navigation
import com.example.dtf.screens.EditPostScreen
import com.example.dtf.screens.LoginScreen
import com.example.dtf.screens.NewPostScreen
import com.example.dtf.screens.PostScreen
import com.example.dtf.screens.PostsScreen
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"
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val navController = rememberNavController()
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
val includeBackButton = remember { mutableStateOf(false) }
Scaffold(
modifier = Modifier.fillMaxSize(),
backgroundColor = Color.Transparent,
topBar = {
if (currentRoute != ScreenPaths.Login.route && currentRoute != ScreenPaths.Register.route && includeBackButton.value) {
TopAppBar(
modifier = Modifier.height(60.dp),
title = {
},
navigationIcon = {
IconButton(
onClick = {
includeBackButton.value = false
navController.popBackStack()
}
) {
Icon(
imageVector = Icons.Default.ArrowBack,
contentDescription = "Назад",
)
}
},
)
}
},
bottomBar = {
if (currentRoute != ScreenPaths.Login.route && currentRoute != ScreenPaths.Register.route) {
BottomNavBar(navController)
}
}
) {
NavHost(navController, ScreenPaths.Login.route) {
composable(ScreenPaths.Login.route) {
LoginScreen(navController)
}
composable(ScreenPaths.Register.route) {
RegisterScreen(navController)
}
navigation(ScreenPaths.Profile.route + "/show", ScreenPaths.Profile.route) {
composable(ScreenPaths.Profile.route + "/show") {
includeBackButton.value = false
ProfileScreen(navController)
}
}
navigation(ScreenPaths.Posts.route + "/all", ScreenPaths.Posts.route) {
composable(ScreenPaths.Posts.route + "/all") {
includeBackButton.value = false
PostsScreen(navController)
}
composable(ScreenPaths.NewPost.route) {
includeBackButton.value = true
NewPostScreen(navController)
}
composable(ScreenPaths.Post.route) { navBackStackEntry ->
includeBackButton.value = true
navBackStackEntry.arguments?.getString("post")?.let { postId ->
PostScreen(
postId.toInt(),
navController
)
}
}
composable(ScreenPaths.EditPost.route) { navBackStackEntry ->
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

@ -0,0 +1,23 @@
package com.example.dtf.dao
import androidx.room.*
import com.example.dtf.models.*
import kotlinx.coroutines.flow.Flow
@Dao
interface CategoryDao {
@Insert
suspend fun insert(category: Category)
@Update
suspend fun update(category: Category)
@Delete
suspend fun delete(category: Category)
@Query("select * from category")
fun getAll() : Flow<List<Category>>
@Query("select * from category where category.id = :id")
fun getById(id: Int) : Flow<Category>
}

View File

@ -0,0 +1,27 @@
package com.example.dtf.dao
import androidx.paging.PagingSource
import androidx.room.*
import com.example.dtf.models.*
import kotlinx.coroutines.flow.Flow
@Dao
interface CommentDao {
@Insert
suspend fun insert(comment: Comment)
@Update
suspend fun update(comment: Comment)
@Delete
suspend fun delete(comment: Comment)
@Query("select * from comment")
fun getAll() : Flow<List<Comment>>
@Query("select * from comment where comment.id = :id")
fun getById(id: Int) : Flow<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

@ -0,0 +1,27 @@
package com.example.dtf.dao
import androidx.paging.PagingSource
import androidx.room.*
import com.example.dtf.models.*
import kotlinx.coroutines.flow.Flow
@Dao
interface PostDao {
@Insert
suspend fun insert(post: Post)
@Update
suspend fun update(post: Post)
@Delete
suspend fun delete(post: Post)
@Query("select * from post")
fun getAll() : Flow<List<Post>>
@Query("select * from post where post.id = :id")
fun getById(id: Int) : Flow<Post>
@Query("select * from post where post.category_id = :categoryId ORDER BY date DESC")
fun getByCategory(categoryId: String) : PagingSource<Int, Post>
}

View File

@ -0,0 +1,26 @@
package com.example.dtf.dao
import androidx.room.*
import com.example.dtf.models.User
import kotlinx.coroutines.flow.Flow
@Dao
interface UserDao {
@Insert
suspend fun insert(user: User)
@Update
suspend fun update(user: User)
@Delete
suspend fun delete(user: User)
@Query("select * from user order by username collate nocase asc")
fun getAll() : Flow<List<User>>
@Query("select * from user where user.id = :id")
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

@ -0,0 +1,39 @@
package com.example.dtf.db
import android.content.Context
import androidx.room.*
import com.example.dtf.models.Category
import com.example.dtf.models.Comment
import com.example.dtf.models.Post
import com.example.dtf.models.User
import kotlinx.coroutines.CoroutineScope
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(
entities = [
User::class,
Category::class,
Post::class,
Like::class,
Comment::class
],
version = 2,
exportSchema = false
)
@TypeConverters(Converter::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao() : UserDao
abstract fun categoryDao() : CategoryDao
abstract fun commentDao() : CommentDao
abstract fun postDao() : PostDao
abstract fun likeDao() : LikeDao
companion object {
public const val DB_NAME: String = "news-db"
}
}

View File

@ -0,0 +1,16 @@
package com.example.dtf.db
import androidx.room.TypeConverter
import java.util.Date
class Converter {
@TypeConverter
fun fromTimestamp(value: Long?): Date? {
return value?.let { Date(it) }
}
@TypeConverter
fun dateToTimestamp(date: Date?): Long? {
return date?.time?.toLong()
}
}

View File

@ -0,0 +1,26 @@
package com.example.dtf.models
import androidx.room.*
@Entity(tableName = "category")
data class Category(
@PrimaryKey(autoGenerate = true)
@ColumnInfo
val id: Int?,
@ColumnInfo
val name: String
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Category
return id == other.id
}
override fun hashCode(): Int {
return id ?: -1
}
}

View File

@ -0,0 +1,10 @@
package com.example.dtf.models
import androidx.room.Embedded
import androidx.room.Relation
data class CategoryWithPosts(
@Embedded val category: Category,
@Relation(parentColumn = "id", entityColumn = "category_id")
val posts: List<Post>
)

View File

@ -0,0 +1,33 @@
package com.example.dtf.models
import androidx.room.*
import java.util.Date
@Entity(tableName = "comment")
data class Comment(
@PrimaryKey(autoGenerate = true)
@ColumnInfo
val id: Int?,
@ColumnInfo(name = "user_id")
val userId: Int,
@ColumnInfo(name = "post_id")
val postId: Int,
@ColumnInfo
val content: String,
@ColumnInfo
val date: Date
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Comment
return id == other.id
}
override fun hashCode(): Int {
return id ?: -1
}
}

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,96 @@
package com.example.dtf.models
import androidx.compose.runtime.collection.MutableVector
import androidx.compose.runtime.collection.mutableVectorOf
import androidx.room.*
import java.util.Date
//private val posts = listOf(
// Post(
// 1,
// "Что не так с half-life 2",
// "Да всё не так",
// 1,
// mutableVectorOf(
// Comment(1, 2, "Пост бред. Начнём с того, что вот эта твоя манера речи клоунская...", Date(2023, 10, 20)),
// Comment(2, 1, "Да какой бред, чел, я всё по факту сказал", Date(2023, 10, 20))
// ),
// Date(2023, 10, 22)
// ),
// Post(
// 2,
// "Я действительно ненавижу фильм про титаник",
// "Пруфов не будет",
// 2,
// mutableVectorOf(Comment(1, 2, "Очередной бред от автора", Date(2023, 9, 20))),
// Date(2023, 9, 22)
// ),
// Post(
// 3,
// "\"Госпожа Кагуя\" это переоценённый тайтл",
// "Я, конечно, не смотрел, но мне так кажется. А всё потому, что там происходит такое, что даже Аристотелю не снилось. А может даже Платону. Об этом можно рассуждать тысячи лет, но я смогу уложиться всего в пару слов. И первое слово - этот тайтл полное днище. Ладно, не слово",
// 3,
// mutableVectorOf(Comment(1, 2, "Автора на увольнение", Date(2023, 9, 20))),
// Date(2023, 9, 22)
// ),
// Post(
// 4,
// "\"Восхождение в тени\" это переоценённый тайтл",
// "Я, конечно, не смотрел, но мне так кажется. А всё потому, что там происходит такое, что даже Аристотелю не снилось. А может даже Платону. Об этом можно рассуждать тысячи лет, но я смогу уложиться всего в пару слов. И первое слово - этот тайтл полное днище. Ладно, не слово",
// 3,
// mutableVectorOf(Comment(1, 2, "Автора на увольнение", Date(2023, 9, 20))),
// Date(2023, 9, 22)
// ),
// Post(
// 5,
// "\"Тетрадь смерти\" это переоценённый тайтл",
// "Я, конечно, не смотрел, но мне так кажется. А всё потому, что там происходит такое, что даже Аристотелю не снилось. А может даже Платону. Об этом можно рассуждать тысячи лет, но я смогу уложиться всего в пару слов. И первое слово - этот тайтл полное днище. Ладно, не слово",
// 3,
// mutableVectorOf(Comment(1, 2, "Автора на увольнение", Date(2023, 9, 20))),
// Date(2023, 9, 22)
// ),
// Post(
// 6,
// "\"Бакуман\" это переоценённый тайтл",
// "Я, конечно, не смотрел, но мне так кажется. А всё потому, что там происходит такое, что даже Аристотелю не снилось. А может даже Платону. Об этом можно рассуждать тысячи лет, но я смогу уложиться всего в пару слов. И первое слово - этот тайтл полное днище. Ладно, не слово",
// 3,
// mutableVectorOf(Comment(1, 2, "Автора на увольнение", Date(2023, 9, 20))),
// Date(2023, 9, 22)
// )
//)
@Entity(
tableName = "post",
foreignKeys = [
ForeignKey(User::class, ["id"], ["user_id"])
]
)
data class Post(
@PrimaryKey(autoGenerate = true)
@ColumnInfo
val id: Int?,
@ColumnInfo
val title: String,
@ColumnInfo
val content: String,
@ColumnInfo
val date: Date,
@ColumnInfo(name = "user_id")
val userId: Int,
@ColumnInfo(name = "category_id")
val categoryId: Int
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Post
return id == other.id
}
override fun hashCode(): Int {
return id ?: -1
}
}

View File

@ -0,0 +1,10 @@
package com.example.dtf.models
import androidx.room.Embedded
import androidx.room.Relation
data class PostWithComments(
@Embedded val post: Post,
@Relation(parentColumn = "id", entityColumn = "post_id")
val comments: List<Comment>
)

View File

@ -0,0 +1,30 @@
package com.example.dtf.models
import androidx.room.*
@Entity(tableName = "user")
data class User(
@PrimaryKey(autoGenerate = true)
@ColumnInfo
val id: Int?,
@ColumnInfo
val username: String,
@ColumnInfo
val password: String,
@ColumnInfo
val isModerator: Boolean
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as User
return id == other.id
}
override fun hashCode(): Int {
return id ?: -1
}
}

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

@ -0,0 +1,124 @@
package com.example.dtf.screens
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
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
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 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
@Composable
fun EditPostScreen(navController: NavHostController, postId: Int) {
val sharedPref = PreferencesManager(LocalContext.current)
val title = remember { mutableStateOf(TextFieldValue("")) }
val content = remember { mutableStateOf(TextFieldValue("")) }
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),
horizontalAlignment = Alignment.CenterHorizontally
) {
MyTextField(
value = title,
"Заголовок",
onChanged = {title.value = it},
modifier = Modifier.fillMaxWidth().padding(15.dp, 10.dp, 15.dp, 0.dp),
backgroundColor = Color(0xFFFEFEFE)
)
MyTextField(
value = content,
"Содержимое",
onChanged = {content.value = it},
modifier = Modifier.fillMaxWidth().heightIn(min = 500.dp).padding(horizontal = 15.dp),
backgroundColor = Color(0xFFFEFEFE),
false
)
Button(
{
viewModel.editPost(sharedPref, postId, title.value.text, content.value.text)
},
colors = ButtonDefaults.buttonColors(Color(0xFF388DF2)),
border = BorderStroke(1.dp, Color(0xFF0085FF)),
shape = RoundedCornerShape(15.dp),
modifier = Modifier.fillMaxWidth(0.5f)
) {
Text(
text = "Сохранить",
style = TextStyle(
fontSize = 24.sp,
fontWeight = FontWeight(400),
color = Color(0xFFFFFFFF),
)
)
}
Spacer(modifier = Modifier.height(70.dp))
}
}

View File

@ -0,0 +1,145 @@
package com.example.dtf.screens
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
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.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()
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth(0.7f).fillMaxHeight().padding(vertical=100.dp)
) {
Text(
text = "Авторизация",
style = TextStyle(
fontSize = 40.sp,
fontWeight = FontWeight(400),
color = Color(0xFF000000),
),
modifier = Modifier.fillMaxHeight(0.3f)
)
MyTextField(
login,
"Логин",
{login.value = it},
modifier = Modifier
.padding(bottom=30.dp)
.fillMaxWidth()
.heightIn(max=55.dp)
)
MyTextField(
password,
"Пароль",
{password.value = it},
modifier = Modifier
.padding(bottom=60.dp)
.fillMaxWidth()
.heightIn(max=55.dp)
)
Button(
{
viewModel.login(sharedPref, login.value.text, password.value.text)
},
colors = ButtonDefaults.buttonColors(Color(0xFF388DF2)),
border = BorderStroke(1.dp, Color(0xFF0085FF)),
shape = RoundedCornerShape(15.dp),
modifier = Modifier.padding(bottom=20.dp).fillMaxWidth(0.5f)
) {
Text(
text = "Войти",
style = TextStyle(
fontSize = 24.sp,
fontWeight = FontWeight(400),
color = Color(0xFFFFFFFF),
)
)
}
Text(
text = "Нет аккаунта?",
style = TextStyle(
fontSize = 16.sp,
fontWeight = FontWeight(400),
color = Color(0xFFB6B3B3),
),
modifier = Modifier.clickable {
navController.navigate(ScreenPaths.Register.route) {
popUpTo(ScreenPaths.Register.route) {
inclusive = true
}
}
}
)
}
}
}

View File

@ -0,0 +1,162 @@
package com.example.dtf.screens
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
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
import androidx.compose.ui.text.TextStyle
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 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
@Composable
fun NewPostScreen(navController: NavHostController) {
val sharedPref = PreferencesManager(LocalContext.current)
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),
horizontalAlignment = Alignment.CenterHorizontally
) {
Button(
{
expanded.value = true
},
colors = ButtonDefaults.buttonColors(Color(0xFFFFFFFF)),
border = BorderStroke(1.dp, Color(0xFF0085FF)),
shape = RoundedCornerShape(15.dp),
modifier = Modifier.fillMaxWidth().padding(15.dp, 10.dp, 15.dp, 0.dp)
) {
Text(
text = selectedCategory.value.name,
style = TextStyle(
fontSize = 24.sp,
fontWeight = FontWeight(400),
color = Color(0xFF000000),
)
)
DropdownMenu(
modifier = Modifier.fillMaxWidth().padding(horizontal = 15.dp),
expanded = expanded.value,
onDismissRequest = {expanded.value = false}
) {
categories?.forEach { category ->
DropdownMenuItem(
onClick = {
selectedCategory.value = category
expanded.value = false
},
) {
Text(
text = category.name,
fontSize = 16.sp,
textAlign = TextAlign.Center
)
}
}
}
}
MyTextField(
value = title,
"Заголовок",
onChanged = {title.value = it},
modifier = Modifier.fillMaxWidth().padding(horizontal = 15.dp),
backgroundColor = Color(0xFFFEFEFE)
)
MyTextField(
value = content,
"Содержимое",
onChanged = {content.value = it},
modifier = Modifier.fillMaxWidth().heightIn(min = 500.dp).padding(horizontal = 15.dp),
backgroundColor = Color(0xFFFEFEFE),
false
)
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)),
shape = RoundedCornerShape(15.dp),
modifier = Modifier.fillMaxWidth(0.5f)
) {
Text(
text = "Опубликовать",
style = TextStyle(
fontSize = 24.sp,
fontWeight = FontWeight(400),
color = Color(0xFFFFFFFF),
)
)
}
Spacer(modifier = Modifier.height(70.dp))
}
}

View File

@ -0,0 +1,229 @@
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
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.*
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 sharedPref = PreferencesManager(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) {
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("")) }
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.background(Color.White),
verticalArrangement = Arrangement.spacedBy(5.dp),
horizontalAlignment = Alignment.Start
) {
item {
Text(
modifier = Modifier.padding(10.dp),
text = post?.title ?: "Loading...",
fontSize = 26.sp
)
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 {
if (isLiked.value) {
viewModel.unlikePost(sharedPref, postId)
likes.intValue--
} else {
viewModel.likePost(sharedPref, postId)
likes.intValue++
}
isLiked.value = !isLiked.value
},
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}
)
}
}
Divider()
Text(
modifier = Modifier.fillMaxWidth().padding(0.dp, 0.dp, 0.dp, 10.dp),
text = "Комментарии",
fontSize = 22.sp,
textAlign = TextAlign.Center
)
Row(
modifier = Modifier.fillMaxSize().padding(horizontal = 15.dp),
verticalAlignment = Alignment.CenterVertically
) {
MyTextField(
value = content,
placeholder = "Комментарий",
onChanged = {content.value = it},
modifier = Modifier.fillMaxWidth(0.9f),
backgroundColor = Color.White
)
IconButton(
modifier = Modifier.fillMaxWidth().scale(1.5f),
onClick = {
viewModel.addComment(sharedPref, postId, content.value.text)
content.value = TextFieldValue("")
},
) {
Icon(
imageVector = Icons.Default.AddCircle,
contentDescription = null
)
}
}
}
items(
comments.itemCount,
key = comments.itemKey()
) {
Comment(comments[it]!!)
}
item {
Spacer(modifier = Modifier.height(60.dp))
}
}
}
@Composable
private fun Comment(comment: Comment) {
val viewModel = hiltViewModel<PostViewModel>()
val user = viewModel.getCommentsAuthor(comment).collectAsState(null).value
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 15.dp, vertical = 5.dp)
.border(width = 1.dp, color = Color(0xFFADADAD), shape = RoundedCornerShape(size = 10.dp))
.background(color = Color(0xFFFFFFFF), shape = RoundedCornerShape(size = 10.dp))
) {
Row(
modifier = Modifier.fillMaxWidth().padding(10.dp),
verticalAlignment = Alignment.Top,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = user?.username ?: "Loading...",
fontSize = 20.sp
)
Text(
text = "day.month.year".replace(
"day",
comment.date.day.toString()
).replace(
"month",
comment.date.month.toString()
).replace(
"year",
comment.date.year.toString()
),
fontSize = 14.sp,
color = Color(0xFFCECCCC)
)
}
Text(
modifier = Modifier.padding(start = 10.dp, bottom = 5.dp),
text = comment.content,
fontSize = 16.sp
)
}
}

View File

@ -0,0 +1,216 @@
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
import androidx.compose.runtime.Composable
import androidx.compose.runtime.*
import androidx.compose.ui.*
import androidx.compose.ui.draw.*
import androidx.compose.ui.geometry.*
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 sharedPref = PreferencesManager(LocalContext.current)
val currentCategory = remember { mutableStateOf<Category?>(null) }
val viewModel = hiltViewModel<PostsViewModel>()
val posts = remember { mutableStateOf<Flow<PagingData<Post>>?>(null) }
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(0.1f)
.horizontalScroll(
rememberScrollState()
)
.background(Color.White),
horizontalArrangement = Arrangement.spacedBy(25.dp),
verticalAlignment = Alignment.CenterVertically
) {
Categories(viewModel, currentCategory, posts)
}
Row(
modifier = Modifier.fillMaxSize()
) {
if (currentCategory.value != null && posts.value != null) {
PostsByCategory(viewModel, navController, posts)
}
}
}
}
@Composable
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 {
currentCategory.value = category
posts.value = viewModel.getPostsListUiState(category.id!!)
}
.drawBehind {
if (category.name == currentCategory.value?.name) {
val strokeWidthPx = 2.dp.toPx()
val verticalOffset = size.height + 2.sp.toPx()
drawLine(
color = Color(0xFF319CFF),
strokeWidth = strokeWidthPx,
start = Offset(0f, verticalOffset),
end = Offset(size.width, verticalOffset)
)
}
},
text = category.name,
fontSize = 22.sp
)
}
}
@Composable
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(
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)
)
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 = 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}
)
}
}
}
}

View File

@ -0,0 +1,93 @@
package com.example.dtf.screens
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.runtime.Composable
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.PreferencesManager
import com.example.dtf.utils.ScreenPaths
import com.example.dtf.viewmodels.ProfileViewModel
@Composable
fun ProfileScreen(navController: NavHostController) {
val sharedPref = PreferencesManager(LocalContext.current)
val viewModel = hiltViewModel<ProfileViewModel>()
val user = viewModel.user.observeAsState().value
viewModel.retrieveUser(sharedPref)
Column(
modifier = Modifier.fillMaxSize()
) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.fillMaxHeight(0.3f)
.fillMaxWidth()
.background(Color(0xFF388DF2))
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
Card(
modifier = Modifier.size(90.dp),
shape = RoundedCornerShape(45.dp),
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(
text = if (user != null) { user.username[0].toString() } else {""},
fontSize = 30.sp,
)
}
}
Spacer(modifier = Modifier.fillMaxHeight(0.1f))
Text(
text = user?.username ?: "",
fontSize = 30.sp,
color = Color.White
)
}
}
Divider()
Row(
modifier = Modifier
.fillMaxHeight(0.9f)
.fillMaxWidth()
.background(Color.White),
) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Bottom,
horizontalAlignment = Alignment.CenterHorizontally
) {
Box(
modifier = Modifier.fillMaxHeight(0.2f),
contentAlignment = Alignment.Center,
) {
Text(
modifier = Modifier.clickable {
viewModel.logout(sharedPref)
navController.navigate(ScreenPaths.Login.route)
},
text = "Выйти",
fontSize = 20.sp,
color = Color.Red
)
}
}
}
}
}

View File

@ -0,0 +1,159 @@
package com.example.dtf.screens
import android.graphics.drawable.shapes.Shape
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
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.*
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
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.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
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()
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth(0.7f).fillMaxHeight().padding(vertical=100.dp)
) {
Text(
text = "Регистрация",
style = TextStyle(
fontSize = 40.sp,
fontWeight = FontWeight(400),
color = Color(0xFF000000),
),
modifier = Modifier.fillMaxHeight(0.2f)
)
MyTextField(
login,
"Логин",
{login.value = it},
modifier = Modifier
.padding(bottom=30.dp)
.fillMaxWidth()
.heightIn(max=55.dp)
)
MyTextField(
password,
"Пароль",
{password.value = it},
modifier = Modifier
.padding(bottom=30.dp)
.fillMaxWidth()
.heightIn(max=55.dp)
)
MyTextField(
confirmPassword,
"Пароль повторно",
{confirmPassword.value = it},
modifier = Modifier
.padding(bottom=60.dp)
.fillMaxWidth()
.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),
modifier = Modifier.padding(bottom=20.dp).fillMaxWidth()
) {
Text(
text = "Зарегистрироваться",
style = TextStyle(
fontSize = 24.sp,
fontWeight = FontWeight(400),
color = Color(0xFFFFFFFF),
)
)
}
Text(
text = "Уже есть аккаунт?",
style = TextStyle(
fontSize = 16.sp,
fontWeight = FontWeight(400),
color = Color(0xFFB6B3B3),
),
modifier = Modifier.clickable {
navController.navigate(ScreenPaths.Login.route)
}
)
}
}
}

View File

@ -0,0 +1,11 @@
package com.example.dtf.ui.theme
import androidx.compose.ui.graphics.Color
val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC)
val Pink80 = Color(0xFFEFB8C8)
val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260)

View File

@ -0,0 +1,11 @@
package com.example.dtf.ui.theme
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Shapes
import androidx.compose.ui.unit.dp
val Shapes = Shapes(
small = RoundedCornerShape(4.dp),
medium = RoundedCornerShape(4.dp),
large = RoundedCornerShape(0.dp)
)

View File

@ -0,0 +1,41 @@
package com.example.dtf.ui.theme
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material.MaterialTheme
import androidx.compose.material.darkColors
import androidx.compose.material.lightColors
import androidx.compose.runtime.Composable
private val DarkColorPalette = darkColors(
)
private val LightColorPalette = lightColors(
/* Other default colors to override
background = Color.White,
surface = Color.White,
onPrimary = Color.White,
onSecondary = Color.Black,
onBackground = Color.Black,
onSurface = Color.Black,
*/
)
@Composable
fun DTFTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val colors = if (darkTheme) {
DarkColorPalette
} else {
LightColorPalette
}
MaterialTheme(
colors = colors,
typography = Typography,
shapes = Shapes,
content = content
)
}

View File

@ -0,0 +1,34 @@
package com.example.dtf.ui.theme
import androidx.compose.material.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
val Typography = Typography(
body1 = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)
/* Other default text styles to override
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
*/
)

View File

@ -0,0 +1,12 @@
package com.example.dtf.utils
sealed class ScreenPaths (val route: String){
object Auth: ScreenPaths("auth")
object Login: ScreenPaths("auth/login")
object Register: ScreenPaths("auth/register")
object Post: ScreenPaths("posts/{post}")
object Posts: ScreenPaths("posts")
object NewPost: ScreenPaths("posts/new")
object EditPost: ScreenPaths("posts/{post}/edit")
object Profile: ScreenPaths("profile")
}

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

@ -0,0 +1,83 @@
package com.example.dtf.widgets
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.material.BottomNavigation
import androidx.compose.material.BottomNavigationItem
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Person
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.navigation.NavController
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
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
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@Composable
fun BottomNavBar(navController: NavController) {
val sharedPref = PreferencesManager(LocalContext.current)
BottomNavigation(
modifier = Modifier.height(70.dp),
backgroundColor = Color.White,
contentColor = Color.Black
) {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
listOfNotNull(
Triple(ScreenPaths.Posts.route, Icons.Default.Home, "Новости"),
if (sharedPref.getData("isModerator", "false").toBooleanStrict()) {
Triple(ScreenPaths.NewPost.route, Icons.Default.Edit, "Создать")
} else { null },
Triple(ScreenPaths.Profile.route, Icons.Default.Person, "Профиль")
).forEach { x ->
BottomNavigationItem(
icon = {
Icon(
imageVector = x.second,
contentDescription = x.third,
modifier = Modifier.size(42.dp)
) },
selectedContentColor = Color.Black,
unselectedContentColor = Color.Black.copy(0.4f),
alwaysShowLabel = true,
selected = currentRoute == x.first,
onClick = {
navController.navigate(x.first) {
navController.graph.startDestinationRoute?.let { route ->
popUpTo(route) {
saveState = true
}
}
launchSingleTop = true
restoreState = true
}
},
label = { Text(x.third) }
)
}
}
}

View File

@ -0,0 +1,45 @@
package com.example.dtf.widgets
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Text
import androidx.compose.material.TextField
import androidx.compose.material.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
fun MyTextField(value: MutableState<TextFieldValue>, placeholder: String, onChanged: (TextFieldValue) -> Unit, modifier: Modifier, backgroundColor: Color = Color(0xFFF2F2F2), singleLine: Boolean = true) {
TextField(
value.value, onChanged,
shape = RoundedCornerShape(size = 15.dp),
colors = TextFieldDefaults.textFieldColors(
backgroundColor = backgroundColor,
disabledTextColor = Color.Transparent,
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
disabledIndicatorColor = Color.Transparent,
),
placeholder = {
Text(
text=placeholder,
fontSize = 16.sp,
color = Color(0xFFAAAAAA)
)
},
singleLine = singleLine,
textStyle = TextStyle(
fontSize = 16.sp
),
modifier = modifier
.background(color = backgroundColor, shape = RoundedCornerShape(size = 15.dp))
.border(width = 1.dp, color = Color(0xFFC7C7C7), shape = RoundedCornerShape(size = 15.dp))
)
}

View File

@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

View File

@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>

View File

@ -0,0 +1,3 @@
<resources>
<string name="app_name">DTF</string>
</resources>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.DTF" parent="android:Theme.Material.Light.NoActionBar">
<item name="android:statusBarColor">@color/purple_700</item>
</style>
</resources>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older that API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>

View File

@ -0,0 +1,17 @@
package com.example.dtf
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}

17
build.gradle Normal file
View File

@ -0,0 +1,17 @@
buildscript {
ext {
compose_ui_version = '1.2.0'
}
repositories {
google()
}
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 {
id 'com.android.application' version '7.4.2' apply false
id 'com.android.library' version '7.4.2' apply false
id 'org.jetbrains.kotlin.android' version '1.7.0' apply false
}

26
gradle.properties Normal file
View File

@ -0,0 +1,26 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true
android.defaults.buildfeatures.buildconfig=true
android.nonFinalResIds=false
android.suppressUnsupportedCompileSdk=34

View File

@ -0,0 +1,6 @@
#Fri Oct 06 03:21:29 GMT+04:00 2023
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME

8
local.properties Normal file
View File

@ -0,0 +1,8 @@
## This file must *NOT* be checked into Version Control Systems,
# as it contains information specific to your local configuration.
#
# Location of the SDK. This is only used by Gradle.
# For customization when using a Version Control System, please read the
# header note.
#Tue Nov 07 09:36:13 SAMT 2023
sdk.dir=C\:\\Users\\Aqua\\AppData\\Local\\Android\\Sdk

16
settings.gradle Normal file
View File

@ -0,0 +1,16 @@
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "DTF"
include ':app'