Compare commits

..

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

63 changed files with 2238 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

74
app/build.gradle Normal file
View File

@ -0,0 +1,74 @@
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'kotlin-kapt'
}
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'
}

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,27 @@
<?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: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="com.example.dtf.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,128 @@
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
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()
}
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(
postId.toInt()
)
}
}
}
}
}
}
}
}

View File

@ -0,0 +1,26 @@
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>
@Query("select * from category where category.name = :name")
fun getByName(name: String) : Flow<CategoryWithPosts>
}

View File

@ -0,0 +1,26 @@
package com.example.dtf.dao
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")
fun getByPost(postId: Int) : Flow<List<Comment>>
}

View File

@ -0,0 +1,26 @@
package com.example.dtf.dao
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<PostWithComments>
@Query("select * from post where post.category_id = :categoryId")
fun getByCategory(categoryId: String) : Flow<Post>
}

View File

@ -0,0 +1,23 @@
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): User
}

View File

@ -0,0 +1,147 @@
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 java.util.Date
@Database(
entities = [
User::class,
Category::class,
Post::class,
Comment::class
],
version = 1,
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
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 }
}
}
}
}

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,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,78 @@
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.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 com.example.dtf.db.AppDatabase
import com.example.dtf.widgets.MyTextField
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@Composable
fun EditPostScreen(postId: Int) {
val context = 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)
}
}
}
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(
{
},
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,101 @@
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.ui.*
import androidx.compose.ui.graphics.Color
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.navigation.NavHostController
import com.example.dtf.utils.ScreenPaths
import com.example.dtf.widgets.MyTextField
@Composable
fun LoginScreen(navController: NavHostController) {
val login = remember { mutableStateOf(TextFieldValue(""))}
val password = remember { mutableStateOf(TextFieldValue(""))}
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(
{
navController.navigate(ScreenPaths.Posts.route)
},
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,122 @@
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.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 com.example.dtf.db.AppDatabase
import com.example.dtf.models.Category
import com.example.dtf.widgets.MyTextField
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@Composable
fun NewPostScreen() {
val categories = remember { mutableStateListOf<Category>() }
val context = LocalContext.current
LaunchedEffect(Unit) {
withContext(Dispatchers.IO) {
AppDatabase.getInstance(context).categoryDao().getAll().collect {data ->
categories.clear()
categories.addAll(data)
}
}
}
val selectedCategory = remember { mutableStateOf("") }
val expanded = remember { mutableStateOf(false) }
val title = remember { mutableStateOf(TextFieldValue()) }
val content = remember { mutableStateOf(TextFieldValue()) }
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,
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.name
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(
{
},
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,195 @@
package com.example.dtf.screens
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.*
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.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.navigation.NavHostController
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.widgets.MyTextField
import kotlinx.coroutines.Dispatchers
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 user = remember { mutableStateOf(User(-1, "", "", false)) }
val context = LocalContext.current
LaunchedEffect(Unit) {
withContext(Dispatchers.IO) {
user.value = AppDatabase.getInstance(context).userDao().getById(1)
AppDatabase.getInstance(context).postDao().getById(postId).collect {data ->
post.value = data
}
}
}
val content = remember { mutableStateOf(TextFieldValue("")) }
Column(
modifier = Modifier
.fillMaxSize()
.background(Color.White)
.verticalScroll(rememberScrollState()),
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
) {
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)
)
if (user.value.isModerator) {
Text(
modifier = Modifier.clickable {
navController.navigate(ScreenPaths.EditPost.route.replace("{post}", postId.toString()))
},
text = "Изменить",
fontSize = 18.sp,
color = Color(0xFFAFAFAF)
)
}
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
)
IconButton(
modifier = Modifier.fillMaxWidth().scale(1.5f),
onClick = {},
) {
Icon(
imageVector = Icons.Default.AddCircle,
contentDescription = null
)
}
}
post.value.comments.reversed().forEach() {comment ->
Comment(comment)
}
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)
}
}
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.value.username,
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,178 @@
package com.example.dtf.screens
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.*
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.navigation.NavHostController
import com.example.dtf.db.AppDatabase
import com.example.dtf.models.Category
import com.example.dtf.models.CategoryWithPosts
import com.example.dtf.utils.ScreenPaths
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@Composable
fun PostsScreen(navController: NavHostController) {
val currentCategory = remember { mutableStateOf("") }
val categories = remember { mutableListOf<Category> () }
val categoryWithPosts = remember { mutableStateOf(CategoryWithPosts(Category(-1, ""), listOf()))}
val context = LocalContext.current
val scope = rememberCoroutineScope()
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
}
}
}
}
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(scope, categoryWithPosts, currentCategory, categories)
}
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))
}
}
}
}
@Composable
private fun Categories(scope: CoroutineScope, categoryWithPosts: MutableState<CategoryWithPosts>, categoryState: MutableState<String>, categories: MutableList<Category>) {
val context = LocalContext.current
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
}
}
}
.drawBehind {
if (category.name == categoryState.value) {
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(navController: NavHostController, categoryWithPosts: MutableState<CategoryWithPosts>) {
categoryWithPosts.value.posts.forEach { post ->
Column(
modifier = Modifier
.fillMaxHeight(0.3f)
.heightIn(max = 250.dp)
.fillMaxWidth()
.background(Color.White)
.clickable {
navController.navigate(ScreenPaths.Post.route.replace("{post}", post.id.toString()))
},
verticalArrangement = Arrangement.spacedBy(5.dp),
horizontalAlignment = Alignment.Start
) {
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(),
horizontalArrangement = Arrangement.End
) {
Text(
text = "0",
fontSize = 16.sp,
color = Color.Green
)
Icon(
modifier = Modifier.padding(start = 8.dp),
imageVector = Icons.Default.ThumbUp,
contentDescription = null
)
}
}
}
}
}

View File

@ -0,0 +1,97 @@
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.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.*
import androidx.compose.ui.graphics.*
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.*
import androidx.navigation.NavHostController
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 ProfileScreen(navController: NavHostController) {
val user = remember { mutableStateOf(User(-1, "", "", false)) }
val context = LocalContext.current
LaunchedEffect(Unit) {
withContext(Dispatchers.IO) {
user.value = AppDatabase.getInstance(context).userDao().getById(1)
}
}
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.value.username.isNotEmpty()) { user.value.username[0].toString() } else {""},
fontSize = 30.sp,
)
}
}
Spacer(modifier = Modifier.fillMaxHeight(0.1f))
Text(
text = user.value.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 {
navController.navigate(ScreenPaths.Login.route)
},
text = "Выйти",
fontSize = 20.sp,
color = Color.Red
)
}
}
}
}
}

View File

@ -0,0 +1,109 @@
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.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.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.navigation.NavController
import androidx.navigation.NavHostController
import com.example.dtf.utils.ScreenPaths
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(""))}
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(
{},
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,103 @@
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.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 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, "Профиль")
)
)
}
}
}
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 (user.value.isModerator) {
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)
}
}

16
build.gradle Normal file
View File

@ -0,0 +1,16 @@
buildscript {
ext {
compose_ui_version = '1.2.0'
}
repositories {
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.0.0'
}
}// 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'