Compare commits

..

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

102 changed files with 4126 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

95
app/build.gradle Normal file
View File

@ -0,0 +1,95 @@
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'kotlin-kapt'
id 'org.jetbrains.kotlin.plugin.serialization'
}
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")
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.okhttp3:logging-interceptor:4.11.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1")
implementation("com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.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,32 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<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"
android:usesCleartextTraffic="true"
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,106 @@
package com.example.dtf
import android.app.Application
import androidx.room.*
import com.example.dtf.data.api.ServerService
import com.example.dtf.data.db.AppDatabase
import com.example.dtf.data.repositories.ICategoryRepository
import com.example.dtf.data.repositories.ICommentRepository
import com.example.dtf.data.repositories.ILikeRepository
import com.example.dtf.data.repositories.IPostRepository
import com.example.dtf.data.repositories.IUserRepository
import com.example.dtf.data.repositories.offline.OfflineCategoryRepository
import com.example.dtf.data.repositories.offline.OfflineCommentRepository
import com.example.dtf.data.repositories.offline.OfflineLikeRepository
import com.example.dtf.data.repositories.offline.OfflinePostRepository
import com.example.dtf.data.repositories.offline.OfflineUserRepository
import com.example.dtf.data.repositories.online.RestCommentRepository
import com.example.dtf.data.repositories.online.RestLikeRepository
import com.example.dtf.data.repositories.online.RestPostRepository
import com.example.dtf.data.repositories.online.RestUserRepository
import com.example.dtf.repositories.online.mediator.RestCategoryRepository
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 provideServerService(): ServerService = ServerService.getInstance()
@Provides
@Singleton
fun provideICategoryRepository(db: AppDatabase, serverService: ServerService): ICategoryRepository = RestCategoryRepository(db, serverService)
@Provides
@Singleton
fun provideICommentRepository(db: AppDatabase, serverService: ServerService): ICommentRepository = RestCommentRepository(db, serverService)
@Provides
@Singleton
fun provideILikeRepository(db: AppDatabase, serverService: ServerService): ILikeRepository = RestLikeRepository(db, serverService)
@Provides
@Singleton
fun provideIPostRepository(db: AppDatabase, serverService: ServerService): IPostRepository = RestPostRepository(db, serverService)
@Provides
@Singleton
fun provideIUserRepository(db: AppDatabase, serverService: ServerService): IUserRepository = RestUserRepository(db, serverService)
@Provides
@Singleton
fun provideOfflineCategoryRepository(db: AppDatabase): OfflineCategoryRepository = OfflineCategoryRepository(db.categoryDao())
@Provides
@Singleton
fun provideOfflineCommentRepository(db: AppDatabase): OfflineCommentRepository = OfflineCommentRepository(db.commentDao())
@Provides
@Singleton
fun provideOfflineLikeRepository(db: AppDatabase): OfflineLikeRepository = OfflineLikeRepository(db.likeDao())
@Provides
@Singleton
fun provideOfflinePostRepository(db: AppDatabase): OfflinePostRepository = OfflinePostRepository(db.postDao())
@Provides
@Singleton
fun provideOfflineUserRepository(db: AppDatabase): OfflineUserRepository = OfflineUserRepository(db.userDao())
@Provides
@Singleton
fun provideRestCategoryRepository(db: AppDatabase, serverService: ServerService): RestCategoryRepository = RestCategoryRepository(db, serverService)
@Provides
@Singleton
fun provideRestCommentRepository(db: AppDatabase, serverService: ServerService): RestCommentRepository = RestCommentRepository(db, serverService)
@Provides
@Singleton
fun provideRestLikeRepository(db: AppDatabase, serverService: ServerService): RestLikeRepository = RestLikeRepository(db, serverService)
@Provides
@Singleton
fun provideRestPostRepository(db: AppDatabase, serverService: ServerService): RestPostRepository = RestPostRepository(db, serverService)
@Provides
@Singleton
fun provideRestUserRepository(db: AppDatabase, serverService: ServerService): RestUserRepository = RestUserRepository(db, serverService)
}

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,138 @@
package com.example.dtf
import android.annotation.SuppressLint
import android.os.Build
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.annotation.RequiresApi
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.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.ui.screens.EditPostScreen
import com.example.dtf.ui.screens.LoginScreen
import com.example.dtf.ui.screens.NewPostScreen
import com.example.dtf.ui.screens.PostScreen
import com.example.dtf.ui.screens.PostsScreen
import com.example.dtf.ui.screens.ProfileScreen
import com.example.dtf.ui.screens.RegisterScreen
import com.example.dtf.ui.screens.TopPostsScreen
import com.example.dtf.ui.utils.ScreenPaths
import com.example.dtf.ui.widgets.BottomNavBar
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
@RequiresApi(Build.VERSION_CODES.O)
@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.TopPosts.route) {
includeBackButton.value = false
TopPostsScreen(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.data
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,157 @@
package com.example.dtf.data.api
import com.example.dtf.data.dto.Credentials
import com.example.dtf.data.dto.EditPostDto
import com.example.dtf.data.dto.MeUser
import com.example.dtf.data.dto.NewCommentDto
import com.example.dtf.data.dto.NewPostDto
import com.example.dtf.data.dto.Token
import com.example.dtf.data.dto.remote.CategoriesResponse
import com.example.dtf.data.dto.remote.CommentsResponse
import com.example.dtf.data.dto.remote.PostsResponse
import com.example.dtf.data.models.Comment
import com.example.dtf.data.models.Post
import com.example.dtf.data.models.User
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.Path
import retrofit2.http.Query
interface ServerService {
@POST("auth/login")
suspend fun login(
@Body credentials: Credentials
): Token
@POST("auth/register")
suspend fun register(
@Body credentials: Credentials
): String
@GET("user/me")
suspend fun getCurrentUser(): MeUser
@GET("category")
suspend fun getCategories(
@Query("offset") offset: Int?,
@Query("limit") limit: Int?
): CategoriesResponse
@GET("post")
suspend fun getPosts(
@Query("category") category: Int?,
@Query("offset") offset: Int?,
@Query("limit") limit: Int?
): PostsResponse
@GET("post/top")
suspend fun getTopPosts(
@Query("category") categoryId: Int,
@Query("from_date") fromDate: String?,
@Query("to_date") toDate: String?
): List<Post>
@GET("post/{postId}")
suspend fun getPost(
@Path("postId") postId: Int
): Post
@POST("post")
suspend fun createPost(
@Body post: NewPostDto
)
@POST("post/{postId}")
suspend fun updatePost(
@Path("postId") postId: Int,
@Body post: EditPostDto
)
@GET("comment")
suspend fun getComments(
@Query("postId") postId: Int?,
@Query("offset") offset: Int?,
@Query("limit") limit: Int?
): CommentsResponse
@GET("comment/{commentId}")
suspend fun getComment(
@Path("commentId") commentId: Int
): Comment
@POST("comment")
suspend fun createComment(
@Body comment: NewCommentDto
)
@POST("post/{postId}/like")
suspend fun likePost(
@Path("postId") postId: Int
)
@GET("post/{postId}/liked")
suspend fun postIsLiked(
@Path("postId") postId: Int
): Boolean
@GET("post/{postId}/likes")
suspend fun getPostLikes(
@Path("postId") postId: Int
): Int
@GET("user/{userId}")
suspend fun getUser(
@Path("userId") userId: Int
): User
companion object {
private const val BASE_URL = "http://192.168.43.115:8000/"
private var _token: String = ""
@Volatile
private var INSTANCE: ServerService? = null
fun getInstance(): ServerService {
return INSTANCE ?: synchronized(this) {
val logger = HttpLoggingInterceptor()
logger.level = HttpLoggingInterceptor.Level.BASIC
val client = OkHttpClient.Builder()
.addInterceptor(logger)
.addInterceptor {
val originalRequest = it.request()
if (_token.isEmpty()) {
it.proceed(originalRequest)
} else {
it.proceed(
originalRequest
.newBuilder()
.header("Authorization", "Bearer $_token")
.method(originalRequest.method, originalRequest.body)
.build()
)
}
}
.build()
return Retrofit.Builder()
.baseUrl(BASE_URL)
.client(client)
.addConverterFactory(Json.asConverterFactory("application/json".toMediaType()))
.build()
.create(ServerService::class.java)
.also { INSTANCE = it }
}
}
fun setToken(token: String) {
_token = token
}
}
}

View File

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

View File

@ -0,0 +1,35 @@
package com.example.dtf.data.dao
import androidx.paging.PagingSource
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import com.example.dtf.data.models.Comment
import kotlinx.coroutines.flow.Flow
@Dao
interface CommentDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(comment: Comment)
@Update
suspend fun update(comment: Comment)
@Delete
suspend fun delete(comment: 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 id DESC")
fun getByPost(postId: Int) : PagingSource<Int, Comment>
@Query("select * from comment where id >= :loadKey and post_id = :postId order by id desc limit :limit")
fun getByLoadKeyInitial(postId: Int, loadKey: Int, limit: Int) : Flow<List<Comment>>
@Query("select * from comment where id <= :loadKey and post_id = :postId order by id desc limit :limit")
fun getByLoadKey(postId: Int, loadKey: Int, limit: Int) : Flow<List<Comment>>
}

View File

@ -0,0 +1,24 @@
package com.example.dtf.data.dao
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.example.dtf.data.models.Like
import kotlinx.coroutines.flow.Flow
@Dao
interface LikeDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
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,32 @@
package com.example.dtf.data.dao
import androidx.paging.PagingSource
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import com.example.dtf.data.models.Post
import kotlinx.coroutines.flow.Flow
@Dao
interface PostDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(post: Post)
@Update
suspend fun update(post: Post)
@Delete
suspend fun delete(post: 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>
@Query("select * from post where category_id = :categoryId and id >= :loadKey ORDER BY date DESC limit :limit")
fun getByLoadKey(categoryId: Int, loadKey: Int, limit: Int) : Flow<List<Post>>
}

View File

@ -0,0 +1,25 @@
package com.example.dtf.data.dao
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import com.example.dtf.data.models.User
import kotlinx.coroutines.flow.Flow
@Dao
interface UserDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(user: User)
@Update
suspend fun update(user: User)
@Delete
suspend fun delete(user: User)
@Query("select * from user where user.id = :id")
fun getById(id: Int): Flow<User>
}

View File

@ -0,0 +1,38 @@
package com.example.dtf.data.db
import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import com.example.dtf.data.dao.CategoryDao
import com.example.dtf.data.dao.CommentDao
import com.example.dtf.data.dao.LikeDao
import com.example.dtf.data.dao.PostDao
import com.example.dtf.data.dao.UserDao
import com.example.dtf.data.models.Category
import com.example.dtf.data.models.Comment
import com.example.dtf.data.models.Like
import com.example.dtf.data.models.Post
import com.example.dtf.data.models.User
@Database(
entities = [
User::class,
Category::class,
Post::class,
Like::class,
Comment::class
],
version = 4,
exportSchema = false
)
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,9 @@
package com.example.dtf.data.dto
import kotlinx.serialization.Serializable
@Serializable
data class Credentials(
val username: String = "",
val password: String = ""
)

View File

@ -0,0 +1,9 @@
package com.example.dtf.data.dto
import kotlinx.serialization.Serializable
@Serializable
data class EditPostDto(
val title: String,
val content: String
)

View File

@ -0,0 +1,10 @@
package com.example.dtf.data.dto
import kotlinx.serialization.Serializable
@Serializable
data class MeUser(
val id: Int,
val username: String,
val is_moderator: Boolean
)

View File

@ -0,0 +1,9 @@
package com.example.dtf.data.dto
import kotlinx.serialization.Serializable
@Serializable
data class NewCommentDto(
val post_id: Int,
val content: String
)

View File

@ -0,0 +1,10 @@
package com.example.dtf.data.dto
import kotlinx.serialization.Serializable
@Serializable
data class NewPostDto(
val title: String,
val content: String,
val category_id: Int
)

View File

@ -0,0 +1,8 @@
package com.example.dtf.data.dto
import kotlinx.serialization.Serializable
@Serializable
data class Token(
val token: String
)

View File

@ -0,0 +1,10 @@
package com.example.dtf.data.dto.remote
import com.example.dtf.data.models.Category
import kotlinx.serialization.Serializable
@Serializable
data class CategoriesResponse(
val categories: List<Category>,
val nextKey: Int?
)

View File

@ -0,0 +1,10 @@
package com.example.dtf.data.dto.remote
import com.example.dtf.data.models.Comment
import kotlinx.serialization.Serializable
@Serializable
data class CommentsResponse(
val comments: List<Comment>,
val nextKey: Int?
)

View File

@ -0,0 +1,10 @@
package com.example.dtf.data.dto.remote
import com.example.dtf.data.models.Post
import kotlinx.serialization.Serializable
@Serializable
data class PostsResponse(
val posts: List<Post>,
val nextKey: Int?
)

View File

@ -0,0 +1,30 @@
package com.example.dtf.data.models
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import kotlinx.serialization.Serializable
@Entity(tableName = "category")
@Serializable
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,38 @@
package com.example.dtf.data.models
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import kotlinx.serialization.Serializable
@Entity(tableName = "comment")
@Serializable
data class Comment(
@PrimaryKey(autoGenerate = true)
@ColumnInfo
val id: Int?,
@ColumnInfo(name = "user_id")
val user_id: Int,
@ColumnInfo(name = "post_id")
val post_id: Int,
@ColumnInfo
val content: String,
@ColumnInfo
val date: String,
@ColumnInfo
val author: String = ""
) {
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,12 @@
package com.example.dtf.data.models
import androidx.room.ColumnInfo
import androidx.room.Entity
@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,43 @@
package com.example.dtf.data.models
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import kotlinx.serialization.Serializable
@Entity(
tableName = "post"
)
@Serializable
data class Post(
@PrimaryKey(autoGenerate = true)
@ColumnInfo
val id: Int?,
@ColumnInfo
val title: String,
@ColumnInfo
val content: String,
@ColumnInfo
val date: String,
@ColumnInfo(name = "user_id")
val user_id: Int,
@ColumnInfo(name = "category_id")
val category_id: Int,
@ColumnInfo
val likes: Int = 0,
) {
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,30 @@
package com.example.dtf.data.models
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import kotlinx.serialization.Serializable
@Entity(tableName = "user")
@Serializable
data class User(
@PrimaryKey(autoGenerate = true)
@ColumnInfo
val id: Int?,
@ColumnInfo
val username: String,
) {
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,12 @@
package com.example.dtf.data.repositories
import androidx.paging.PagingData
import com.example.dtf.data.models.Category
import kotlinx.coroutines.flow.Flow
interface ICategoryRepository {
fun getAll(): Flow<PagingData<Category>>
fun getAllCached(): Flow<List<Category>>
fun getFirst(): Flow<Category?>
}

View File

@ -0,0 +1,12 @@
package com.example.dtf.data.repositories
import androidx.paging.PagingData
import com.example.dtf.data.models.Comment
import kotlinx.coroutines.flow.Flow
interface ICommentRepository {
suspend fun insert(comment: Comment)
fun getById(id: Int): Flow<Comment>
fun getByPost(postId: Int): Flow<PagingData<Comment>>
}

View File

@ -0,0 +1,12 @@
package com.example.dtf.data.repositories
import com.example.dtf.data.models.Like
import kotlinx.coroutines.flow.Flow
interface ILikeRepository {
suspend fun insert(like: Like)
suspend fun delete(like: Like)
fun countByPost(postId: Int): Flow<Int>
fun isLikedByUser(userId: Int, postId: Int): Flow<Boolean>
}

View File

@ -0,0 +1,16 @@
package com.example.dtf.data.repositories
import androidx.paging.PagingData
import com.example.dtf.data.models.Post
import kotlinx.coroutines.flow.Flow
import java.time.LocalDate
interface IPostRepository {
suspend fun insert(post: Post)
suspend fun update(post: Post)
fun getById(id: Int): Flow<Post>
fun getByCategory(categoryId: Int): Flow<PagingData<Post>>
fun getTopTen(categoryId: Int, fromDate: LocalDate, toDate: LocalDate): Flow<List<Post>>
}

View File

@ -0,0 +1,8 @@
package com.example.dtf.data.repositories
import com.example.dtf.data.models.User
import kotlinx.coroutines.flow.Flow
interface IUserRepository {
fun getById(id: Int) : Flow<User>
}

View File

@ -0,0 +1,30 @@
package com.example.dtf.data.repositories.offline
import androidx.paging.Pager
import androidx.paging.PagingConfig
import com.example.dtf.data.dao.CategoryDao
import com.example.dtf.data.models.Category
import com.example.dtf.data.repositories.ICategoryRepository
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
class OfflineCategoryRepository @Inject constructor(
private val categoryDao: CategoryDao
) : ICategoryRepository {
override fun getAll() = Pager(
PagingConfig(
pageSize = 5,
enablePlaceholders = false
),
pagingSourceFactory = { categoryDao.getAll() }
).flow
override fun getAllCached(): Flow<List<Category>> {
return categoryDao.getAllCached()
}
override fun getFirst(): Flow<Category?> {
return categoryDao.getFirst()
}
}

View File

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

View File

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

View File

@ -0,0 +1,32 @@
package com.example.dtf.data.repositories.offline
import androidx.paging.Pager
import androidx.paging.PagingConfig
import com.example.dtf.data.dao.PostDao
import com.example.dtf.data.models.Post
import com.example.dtf.data.repositories.IPostRepository
import kotlinx.coroutines.flow.Flow
import java.time.LocalDate
import javax.inject.Inject
class OfflinePostRepository @Inject constructor(
private val postDao: PostDao
) : IPostRepository {
override suspend fun insert(post: Post) = postDao.insert(post)
override suspend fun update(post: Post) = postDao.update(post)
override fun getById(id: Int) = postDao.getById(id)
override fun getByCategory(categoryId: Int) = Pager(
PagingConfig(
pageSize = 3,
enablePlaceholders = false
),
pagingSourceFactory = { postDao.getByCategory(categoryId.toString()) }
).flow
override fun getTopTen(categoryId: Int, fromDate: LocalDate, toDate: LocalDate): Flow<List<Post>> {
throw NotImplementedError("Cannot access top without internet connection")
}
}

View File

@ -0,0 +1,11 @@
package com.example.dtf.data.repositories.offline
import com.example.dtf.data.dao.UserDao
import com.example.dtf.data.repositories.IUserRepository
import javax.inject.Inject
class OfflineUserRepository @Inject constructor(
private val userDao: UserDao
) : IUserRepository {
override fun getById(id: Int) = userDao.getById(id)
}

View File

@ -0,0 +1,37 @@
package com.example.dtf.repositories.online.mediator
import androidx.paging.ExperimentalPagingApi
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import com.example.dtf.data.api.ServerService
import com.example.dtf.data.db.AppDatabase
import com.example.dtf.data.models.Category
import com.example.dtf.data.repositories.ICategoryRepository
import com.example.dtf.data.repositories.online.mediator.CategoryMediator
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
class RestCategoryRepository @Inject constructor(
private val appDatabase: AppDatabase,
private val serverService: ServerService
) : ICategoryRepository {
@OptIn(ExperimentalPagingApi::class)
override fun getAll(): Flow<PagingData<Category>> = Pager(
PagingConfig(
pageSize = 3,
enablePlaceholders = false
),
remoteMediator = CategoryMediator(appDatabase, serverService),
pagingSourceFactory = { appDatabase.categoryDao().getAll() }
).flow
override fun getAllCached(): Flow<List<Category>> {
return appDatabase.categoryDao().getAllCached()
}
override fun getFirst(): Flow<Category?> {
return appDatabase.categoryDao().getFirst()
}
}

View File

@ -0,0 +1,41 @@
package com.example.dtf.data.repositories.online
import androidx.paging.ExperimentalPagingApi
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import com.example.dtf.data.api.ServerService
import com.example.dtf.data.db.AppDatabase
import com.example.dtf.data.dto.NewCommentDto
import com.example.dtf.data.models.Comment
import com.example.dtf.data.repositories.ICommentRepository
import com.example.dtf.data.repositories.online.mediator.CommentMediator
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import javax.inject.Inject
class RestCommentRepository @Inject constructor(
private val appDatabase: AppDatabase,
private val serverService: ServerService
) : ICommentRepository {
override suspend fun insert(comment: Comment) {
serverService.createComment(
comment = NewCommentDto(post_id = comment.post_id, content = comment.content)
)
appDatabase.commentDao().insert(comment)
}
override fun getById(id: Int): Flow<Comment> {
return flow { emit(serverService.getComment(id)) }
}
@OptIn(ExperimentalPagingApi::class)
override fun getByPost(postId: Int): Flow<PagingData<Comment>> = Pager(
PagingConfig(
pageSize = 5,
enablePlaceholders = false,
),
remoteMediator = CommentMediator(appDatabase, serverService, postId.toString()),
pagingSourceFactory = { appDatabase.commentDao().getByPost(postId) }
).flow
}

View File

@ -0,0 +1,30 @@
package com.example.dtf.data.repositories.online
import com.example.dtf.data.api.ServerService
import com.example.dtf.data.db.AppDatabase
import com.example.dtf.data.models.Like
import com.example.dtf.data.repositories.ILikeRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import javax.inject.Inject
class RestLikeRepository @Inject constructor(
private val appDatabase: AppDatabase,
private val serverService: ServerService
) : ILikeRepository {
override suspend fun insert(like: Like) {
serverService.likePost(like.postId)
}
override suspend fun delete(like: Like) {
serverService.likePost(like.postId)
}
override fun countByPost(postId: Int): Flow<Int> {
return flow { emit(serverService.getPostLikes(postId)) }
}
override fun isLikedByUser(userId: Int, postId: Int): Flow<Boolean> {
return flow { emit(serverService.postIsLiked(postId)) }
}
}

View File

@ -0,0 +1,65 @@
package com.example.dtf.data.repositories.online
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.paging.ExperimentalPagingApi
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import com.example.dtf.data.api.ServerService
import com.example.dtf.data.db.AppDatabase
import com.example.dtf.data.dto.EditPostDto
import com.example.dtf.data.dto.NewPostDto
import com.example.dtf.data.models.Post
import com.example.dtf.data.repositories.IPostRepository
import com.example.dtf.data.repositories.online.mediator.PostMediator
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import java.time.LocalDate
import javax.inject.Inject
class RestPostRepository @Inject constructor(
private val appDatabase: AppDatabase,
private val serverService: ServerService
) : IPostRepository {
override suspend fun insert(post: Post) {
serverService.createPost(
post = NewPostDto(post.title, post.content, post.category_id)
)
}
override suspend fun update(post: Post) {
serverService.updatePost(
postId = post.id!!,
post = EditPostDto(post.title, post.content)
)
appDatabase.postDao().update(post)
}
override fun getById(id: Int): Flow<Post> {
return flow { emit(serverService.getPost(id)) }
}
@OptIn(ExperimentalPagingApi::class)
override fun getByCategory(categoryId: Int): Flow<PagingData<Post>> = Pager(
PagingConfig(
pageSize = 4,
enablePlaceholders = false
),
remoteMediator = PostMediator(appDatabase, serverService, categoryId.toString()),
pagingSourceFactory = { appDatabase.postDao().getByCategory(categoryId.toString()) }
).flow
@RequiresApi(Build.VERSION_CODES.O)
override fun getTopTen(categoryId: Int, fromDate: LocalDate, toDate: LocalDate): Flow<List<Post>> {
return flow {
emit(
serverService.getTopPosts(
categoryId,
"${fromDate.year}-${fromDate.monthValue}-${fromDate.dayOfMonth}",
"${toDate.year}-${toDate.monthValue}-${toDate.dayOfMonth}"
)
)
}
}
}

View File

@ -0,0 +1,18 @@
package com.example.dtf.data.repositories.online
import com.example.dtf.data.api.ServerService
import com.example.dtf.data.db.AppDatabase
import com.example.dtf.data.models.User
import com.example.dtf.data.repositories.IUserRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import javax.inject.Inject
class RestUserRepository @Inject constructor(
private val appDatabase: AppDatabase,
private val serverService: ServerService
) : IUserRepository {
override fun getById(id: Int): Flow<User> {
return flow { emit(serverService.getUser(id)) }
}
}

View File

@ -0,0 +1,67 @@
package com.example.dtf.data.repositories.online.mediator
import androidx.paging.ExperimentalPagingApi
import androidx.paging.LoadType
import androidx.paging.PagingState
import androidx.paging.RemoteMediator
import androidx.room.withTransaction
import com.example.dtf.data.api.ServerService
import com.example.dtf.data.db.AppDatabase
import com.example.dtf.data.models.Category
import kotlinx.coroutines.flow.first
import java.io.IOException
@OptIn(ExperimentalPagingApi::class)
class CategoryMediator (
private val database: AppDatabase,
private val serverService: ServerService
) : RemoteMediator<Int, Category>(){
private val categoryDao = database.categoryDao()
override suspend fun load(
loadType: LoadType,
state: PagingState<Int, Category>
): MediatorResult {
return try {
var loadKey = when (loadType) {
LoadType.REFRESH -> null
LoadType.PREPEND -> return MediatorResult.Success(endOfPaginationReached = true)
LoadType.APPEND -> {
val lastItem = state.lastItemOrNull()
?: return MediatorResult.Success(endOfPaginationReached = true)
lastItem.id
}
}
if (loadKey == null) {
loadKey = 0
}
val response = serverService.getCategories(
offset = loadKey,
limit = state.config.pageSize
)
database.withTransaction {
val present = categoryDao.getByLoadKey(loadKey, state.config.pageSize).first()
if (loadType == LoadType.REFRESH) {
for (category in present) {
if (category !in response.categories){
categoryDao.delete(category)
}
}
}
for (category in response.categories) {
categoryDao.insert(category)
}
}
MediatorResult.Success(endOfPaginationReached = response.nextKey == null)
} catch (e: IOException) {
MediatorResult.Error(e)
}
}
}

View File

@ -0,0 +1,74 @@
package com.example.dtf.data.repositories.online.mediator
import androidx.paging.ExperimentalPagingApi
import androidx.paging.LoadType
import androidx.paging.PagingState
import androidx.paging.RemoteMediator
import androidx.room.withTransaction
import com.example.dtf.data.api.ServerService
import com.example.dtf.data.db.AppDatabase
import com.example.dtf.data.models.Comment
import kotlinx.coroutines.flow.first
import java.io.IOException
@OptIn(ExperimentalPagingApi::class)
class CommentMediator (
private val database: AppDatabase,
private val serverService: ServerService,
private val query: String
) : RemoteMediator<Int, Comment>(){
private val commentDao = database.commentDao()
override suspend fun load(
loadType: LoadType,
state: PagingState<Int, Comment>
): MediatorResult {
return try {
var loadKey = when (loadType) {
LoadType.REFRESH -> null
LoadType.PREPEND -> return MediatorResult.Success(endOfPaginationReached = true)
LoadType.APPEND -> {
val lastItem = state.lastItemOrNull()
?: return MediatorResult.Success(endOfPaginationReached = true)
lastItem.id
}
}
if (loadKey == null) {
loadKey = 0
}
val response = serverService.getComments(
postId = query.toInt(),
offset = loadKey,
limit = state.config.pageSize
)
database.withTransaction {
val present = if (loadKey == 0) {
commentDao.getByLoadKeyInitial(query.toInt(), loadKey, state.config.pageSize).first()
} else {
commentDao.getByLoadKey(query.toInt(), loadKey, state.config.pageSize).first()
}
if (loadType == LoadType.REFRESH) {
for (comment in present) {
if (comment !in response.comments) {
commentDao.delete(comment)
}
}
}
for (comment in response.comments) {
commentDao.insert(comment)
}
}
MediatorResult.Success(endOfPaginationReached = response.nextKey == null)
} catch (e: IOException) {
MediatorResult.Error(e)
}
}
}

View File

@ -0,0 +1,70 @@
package com.example.dtf.data.repositories.online.mediator
import androidx.paging.ExperimentalPagingApi
import androidx.paging.LoadType
import androidx.paging.PagingState
import androidx.paging.RemoteMediator
import androidx.room.withTransaction
import com.example.dtf.data.api.ServerService
import com.example.dtf.data.db.AppDatabase
import com.example.dtf.data.models.Post
import kotlinx.coroutines.flow.first
import java.io.IOException
@OptIn(ExperimentalPagingApi::class)
class PostMediator (
private val database: AppDatabase,
private val serverService: ServerService,
private val query: String
) : RemoteMediator<Int, Post>(){
private val postDao = database.postDao()
override suspend fun load(
loadType: LoadType,
state: PagingState<Int, Post>
): MediatorResult {
return try {
var loadKey = when (loadType) {
LoadType.REFRESH -> null
LoadType.PREPEND -> return MediatorResult.Success(endOfPaginationReached = true)
LoadType.APPEND -> {
val lastItem = state.lastItemOrNull()
?: return MediatorResult.Success(endOfPaginationReached = true)
lastItem.id
}
}
if (loadKey == null) {
loadKey = 0
}
val response = serverService.getPosts(
category = query.toInt(),
offset = loadKey,
limit = state.config.pageSize
)
database.withTransaction {
val present = postDao.getByLoadKey(query.toInt(), loadKey, state.config.pageSize).first()
if (loadType == LoadType.REFRESH) {
for (post in present) {
if (post !in response.posts) {
postDao.delete(post)
}
}
}
for (post in response.posts) {
postDao.insert(post)
}
}
MediatorResult.Success(endOfPaginationReached = response.nextKey == null)
} catch (e: IOException) {
MediatorResult.Error(e)
}
}
}

View File

@ -0,0 +1,138 @@
package com.example.dtf.ui.screens
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
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.Alignment
import androidx.compose.ui.Modifier
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.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import com.example.dtf.data.PreferencesManager
import com.example.dtf.ui.utils.ScreenPaths
import com.example.dtf.ui.viewmodels.EditPostViewModel
import com.example.dtf.ui.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,160 @@
package com.example.dtf.ui.screens
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
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.Alignment
import androidx.compose.ui.Modifier
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.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import com.example.dtf.data.PreferencesManager
import com.example.dtf.ui.utils.ScreenPaths
import com.example.dtf.ui.viewmodels.LoginViewModel
import com.example.dtf.ui.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,187 @@
package com.example.dtf.ui.screens
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.AlertDialog
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.DropdownMenu
import androidx.compose.material.DropdownMenuItem
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.livedata.observeAsState
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.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.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import com.example.dtf.data.PreferencesManager
import com.example.dtf.data.models.Category
import com.example.dtf.ui.utils.ScreenPaths
import com.example.dtf.ui.viewmodels.NewPostViewModel
import com.example.dtf.ui.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.getCategories().collectAsState(initial = listOf())
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.value.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 = 400.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,221 @@
package com.example.dtf.ui.screens
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Divider
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.Text
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.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color
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.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.itemKey
import com.example.dtf.data.PreferencesManager
import com.example.dtf.data.models.Comment
import com.example.dtf.ui.utils.ScreenPaths
import com.example.dtf.ui.viewmodels.PostViewModel
import com.example.dtf.ui.widgets.MyTextField
@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 = post.date,
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,
horizontalArrangement = Arrangement.End
) {
Text(
text = likes.intValue.toString(),
fontSize = 16.sp,
color = Color.Green
)
Icon(
modifier = Modifier.padding(start = 8.dp).clickable {
if (isLiked.value) {
viewModel.unlikePost(sharedPref, postId)
likes.intValue--
} else {
viewModel.likePost(sharedPref, postId)
likes.intValue++
}
isLiked.value = !isLiked.value
},
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 = comment.author.ifEmpty { user?.username ?: "Loading..." },
fontSize = 20.sp
)
Text(
text = comment.date,
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,228 @@
package com.example.dtf.ui.screens
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ThumbUp
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import androidx.paging.PagingData
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.itemKey
import com.example.dtf.data.PreferencesManager
import com.example.dtf.data.models.Category
import com.example.dtf.data.models.Post
import com.example.dtf.ui.utils.ScreenPaths
import com.example.dtf.ui.viewmodels.PostsViewModel
import kotlinx.coroutines.flow.Flow
@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)
) {
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.getCategoriesListUiState().collectAsLazyPagingItems()
LazyRow(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(0.1f)
.background(Color.White),
horizontalArrangement = Arrangement.spacedBy(25.dp),
verticalAlignment = Alignment.CenterVertically
) {
item {
Spacer(modifier = Modifier.width(5.dp))
}
items(
count = categories.itemCount,
key = categories.itemKey()
) {
if (currentCategory.value == null) {
currentCategory.value = categories[0]
posts.value = viewModel.getPostsListUiState(currentCategory.value!!.id!!)
}
Text(
modifier = Modifier
.clickable {
currentCategory.value = categories[it]!!
posts.value = viewModel.getPostsListUiState(categories[it]!!.id!!)
}
.drawBehind {
if (categories[it]!!.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 = categories[it]!!.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(post.likes) }
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
.heightIn(min=250.dp, max = 300.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 = post.date,
fontSize = 14.sp,
color = Color(0xFFCECCCC)
)
Row (
modifier = Modifier
.fillMaxWidth(),
horizontalArrangement = Arrangement.End
) {
Text(
text = likes.intValue.toString(),
fontSize = 16.sp,
color = Color.Green
)
Icon(
modifier = Modifier
.padding(start = 8.dp)
.clickable {
if (isLiked.value) {
viewModel.unlikePost(sharedPref, post.id!!)
likes.intValue--
} else {
viewModel.likePost(sharedPref, post.id!!)
likes.intValue++
}
isLiked.value = !isLiked.value
},
imageVector = Icons.Default.ThumbUp,
contentDescription = null,
tint = if (isLiked.value) { Color(40, 200, 40, 255) } else {Color.Black}
)
}
}
}
}

View File

@ -0,0 +1,106 @@
package com.example.dtf.ui.screens
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Card
import androidx.compose.material.Divider
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import com.example.dtf.data.PreferencesManager
import com.example.dtf.ui.utils.ScreenPaths
import com.example.dtf.ui.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,160 @@
package com.example.dtf.ui.screens
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
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.Alignment
import androidx.compose.ui.Modifier
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.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import com.example.dtf.ui.utils.ScreenPaths
import com.example.dtf.ui.viewmodels.RegisterViewModel
import com.example.dtf.ui.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,285 @@
package com.example.dtf.ui.screens
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ThumbUp
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.itemKey
import com.example.dtf.data.PreferencesManager
import com.example.dtf.data.models.Category
import com.example.dtf.data.models.Post
import com.example.dtf.ui.utils.ScreenPaths
import com.example.dtf.ui.viewmodels.TopPostsViewModel
import java.time.LocalDate
import java.time.Period
@RequiresApi(Build.VERSION_CODES.O)
@Composable
fun TopPostsScreen(navController: NavHostController) {
val viewModel = hiltViewModel<TopPostsViewModel>()
val currentCategory = remember { mutableStateOf<Category?>(null) }
val categories = viewModel.getCategoriesListUiState().collectAsLazyPagingItems()
val dateFrom = remember { mutableStateOf(LocalDate.now()) }
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(15.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Spacer(modifier = Modifier.height(15.dp))
Text(
text = "Топ 10 новостей",
fontSize = 36.sp
)
DateSelect(viewModel, dateFrom, currentCategory)
Categories(viewModel, currentCategory, categories, dateFrom)
Posts(viewModel, navController)
Spacer(modifier = Modifier.height(60.dp))
}
}
@RequiresApi(Build.VERSION_CODES.O)
@Composable
fun DateSelect(
viewModel: TopPostsViewModel,
dateFrom: MutableState<LocalDate>,
currentCategory: MutableState<Category?>
) {
val week = Pair(LocalDate.now() - Period.ofWeeks(1), "Неделя")
val month = Pair(LocalDate.now() - Period.ofMonths(1), "Месяц")
val year = Pair(LocalDate.now() - Period.ofYears(1), "Год")
dateFrom.value = week.first
Row(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(0.1f)
.background(Color.White),
horizontalArrangement = Arrangement.spacedBy(25.dp),
verticalAlignment = Alignment.CenterVertically
) {
Spacer(modifier = Modifier)
listOf(week, month, year).forEach { (date, name) ->
Text(
modifier = Modifier
.clickable {
dateFrom.value = date
if (currentCategory.value != null) {
viewModel.retrievePosts(currentCategory.value!!.id!!, dateFrom.value, LocalDate.now())
}
}
.drawBehind {
if (dateFrom.value == date) {
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 = name,
fontSize = 22.sp
)
}
}
}
@RequiresApi(Build.VERSION_CODES.O)
@Composable
fun Categories(
viewModel: TopPostsViewModel,
currentCategory: MutableState<Category?>,
categories: LazyPagingItems<Category>,
dateFrom: MutableState<LocalDate>) {
LazyRow(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(0.1f)
.background(Color.White),
horizontalArrangement = Arrangement.spacedBy(25.dp),
verticalAlignment = Alignment.CenterVertically
) {
item {
Spacer(modifier = Modifier)
}
items(
count = categories.itemCount,
key = categories.itemKey()
) {
if (currentCategory.value == null) {
currentCategory.value = categories[0]
viewModel.retrievePosts(currentCategory.value!!.id!!, dateFrom.value, LocalDate.now())
}
Text(
modifier = Modifier
.clickable {
currentCategory.value = categories[it]!!
viewModel.retrievePosts(
currentCategory.value!!.id!!,
dateFrom.value,
LocalDate.now()
)
}
.drawBehind {
if (categories[it]!!.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 = categories[it]!!.name,
fontSize = 22.sp
)
}
}
}
@Composable
fun Posts(viewModel: TopPostsViewModel, navController: NavHostController) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
for (post in viewModel.posts.observeAsState(listOf()).value) {
Post(viewModel, navController, post)
}
}
}
@Composable
fun Post(viewModel: TopPostsViewModel, navController: NavHostController, post: Post) {
val sharedPref = PreferencesManager(LocalContext.current)
val likes = remember { mutableIntStateOf(post.likes) }
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
.heightIn(min = 250.dp, max = 300.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 = post.date,
fontSize = 14.sp,
color = Color(0xFFCECCCC)
)
Row (
modifier = Modifier
.fillMaxWidth(),
horizontalArrangement = Arrangement.End
) {
Text(
text = likes.intValue.toString(),
fontSize = 16.sp,
color = Color.Green
)
Icon(
modifier = Modifier
.padding(start = 8.dp)
.clickable {
if (isLiked.value) {
viewModel.unlikePost(sharedPref, post.id!!)
likes.intValue--
} else {
viewModel.likePost(sharedPref, post.id!!)
likes.intValue++
}
isLiked.value = !isLiked.value
},
imageVector = Icons.Default.ThumbUp,
contentDescription = null,
tint = if (isLiked.value) { Color(40, 200, 40, 255) } else {
Color.Black}
)
}
}
}
}

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,13 @@
package com.example.dtf.ui.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 TopPosts: ScreenPaths("posts/top")
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.ui.viewmodels
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.dtf.data.PreferencesManager
import com.example.dtf.data.models.Post
import com.example.dtf.data.repositories.IPostRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class EditPostViewModel @Inject constructor(
private val postRepository: IPostRepository
) : 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.user_id, it.category_id))
_editingPostState.postValue(true)
}
} else {
_editingPostState.postValue(false)
}
}
}
}

View File

@ -0,0 +1,52 @@
package com.example.dtf.ui.viewmodels
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.dtf.data.PreferencesManager
import com.example.dtf.data.api.ServerService
import com.example.dtf.data.dto.Credentials
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class LoginViewModel @Inject constructor(
private val serverService: ServerService,
) : ViewModel() {
private val _successState = MutableLiveData<Boolean?>()
val successState: LiveData<Boolean?>
get() = _successState
fun calmSuccessState() {
_successState.postValue(null)
}
fun login(sharedPref: PreferencesManager, username: String, password: String) {
if (username.isEmpty() || password.isEmpty()) {
_successState.postValue(false)
return
}
viewModelScope.launch {
val token = serverService.login(Credentials(username, password))
if (token.token.isEmpty()) {
_successState.postValue(false)
return@launch
}
ServerService.setToken(token.token)
val user = serverService.getCurrentUser()
sharedPref.saveData("token", token.token)
sharedPref.saveData("username", user.username)
sharedPref.saveData("isModerator", user.is_moderator.toString())
sharedPref.saveData("userId", user.id.toString())
_successState.postValue(true)
}
}
}

View File

@ -0,0 +1,53 @@
package com.example.dtf.ui.viewmodels
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.dtf.data.PreferencesManager
import com.example.dtf.data.models.Post
import com.example.dtf.data.repositories.ICategoryRepository
import com.example.dtf.data.repositories.IPostRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import java.util.Date
import javax.inject.Inject
@HiltViewModel
class NewPostViewModel @Inject constructor(
private val postRepository: IPostRepository,
private val categoryRepository: ICategoryRepository
) : ViewModel() {
private val _addingPostState = MutableLiveData<Boolean?>(null)
val addingPostState: LiveData<Boolean?>
get() = _addingPostState
fun getCategories() = categoryRepository.getAllCached()
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().year + 1900}-${Date().month + 1}-${Date().date}",
sharedPref.getData("userId", "0").toInt(),
categoryId
)
)
_addingPostState.postValue(true)
} else {
_addingPostState.postValue(false)
}
}
}
}

View File

@ -0,0 +1,91 @@
package com.example.dtf.ui.viewmodels
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.PagingData
import com.example.dtf.data.PreferencesManager
import com.example.dtf.data.models.Comment
import com.example.dtf.data.models.Like
import com.example.dtf.data.models.Post
import com.example.dtf.data.repositories.ICommentRepository
import com.example.dtf.data.repositories.ILikeRepository
import com.example.dtf.data.repositories.IPostRepository
import com.example.dtf.data.repositories.IUserRepository
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: IPostRepository,
private val commentRepository: ICommentRepository,
private val likeRepository: ILikeRepository,
private val userRepository: IUserRepository
) : 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().year + 1900}-${Date().month + 1}-${Date().date}",
sharedPref.getData("username", "Unknown")
)
)
}
}
fun getCommentsAuthor(comment: Comment) = userRepository.getById(comment.user_id)
fun getCommentsListUiState(postId: Int): Flow<PagingData<Comment>> = commentRepository.getByPost(postId)
}

View File

@ -0,0 +1,51 @@
package com.example.dtf.ui.viewmodels
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.dtf.data.PreferencesManager
import com.example.dtf.data.models.Like
import com.example.dtf.data.repositories.ICategoryRepository
import com.example.dtf.data.repositories.ILikeRepository
import com.example.dtf.data.repositories.IPostRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class PostsViewModel @Inject constructor(
private val postRepository: IPostRepository,
private val categoryRepository: ICategoryRepository,
private val likeRepository: ILikeRepository
) : 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 getCategoriesListUiState() = categoryRepository.getAll()
fun getPostsListUiState(categoryId: Int) = postRepository.getByCategory(categoryId)
}

View File

@ -0,0 +1,29 @@
package com.example.dtf.ui.viewmodels
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.example.dtf.data.PreferencesManager
import com.example.dtf.data.models.User
import com.example.dtf.data.repositories.offline.OfflineUserRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class ProfileViewModel @Inject constructor(
private val userRepository: OfflineUserRepository
) : ViewModel() {
private val _user = MutableLiveData<User>()
val user: LiveData<User>
get() = _user
fun retrieveUser(sharedPref: PreferencesManager) {
_user.postValue(User(null, sharedPref.getData("username", "Nickname")))
}
fun logout(sharedPref: PreferencesManager) {
sharedPref.deleteData("token")
sharedPref.deleteData("username")
sharedPref.deleteData("isModerator")
}
}

View File

@ -0,0 +1,48 @@
package com.example.dtf.ui.viewmodels
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.dtf.data.api.ServerService
import com.example.dtf.data.dto.Credentials
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class RegisterViewModel @Inject constructor(
private val serverService: ServerService
) : 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 {
if (serverService.register(Credentials(username, password)) == "NOT OK") {
_successState.postValue(false)
} else {
_successState.postValue(true)
}
}
}
}

View File

@ -0,0 +1,65 @@
package com.example.dtf.ui.viewmodels
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.dtf.data.PreferencesManager
import com.example.dtf.data.models.Like
import com.example.dtf.data.models.Post
import com.example.dtf.data.repositories.ICategoryRepository
import com.example.dtf.data.repositories.ILikeRepository
import com.example.dtf.data.repositories.IPostRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import java.time.LocalDate
import javax.inject.Inject
@HiltViewModel
class TopPostsViewModel @Inject constructor(
private val postRepository: IPostRepository,
private val categoryRepository: ICategoryRepository,
private val likeRepository: ILikeRepository
) : ViewModel() {
private val _posts = MutableLiveData<List<Post>>()
val posts: LiveData<List<Post>>
get() = _posts
fun getCategoriesListUiState() = categoryRepository.getAll()
fun retrievePosts(categoryId: Int, fromDate: LocalDate, toDate: LocalDate) {
viewModelScope.launch {
postRepository.getTopTen(categoryId, fromDate, toDate).collect {
_posts.value = 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
)
}

View File

@ -0,0 +1,70 @@
package com.example.dtf.ui.widgets
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.material.icons.filled.Star
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import androidx.navigation.compose.currentBackStackEntryAsState
import com.example.dtf.data.PreferencesManager
import com.example.dtf.ui.utils.ScreenPaths
@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, "Новости"),
Triple(ScreenPaths.TopPosts.route, Icons.Default.Star, "Топ"),
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.ui.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)
}
}

19
build.gradle Normal file
View File

@ -0,0 +1,19 @@
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
id 'org.jetbrains.kotlin.plugin.serialization' version '1.7.0' apply false
id 'org.jetbrains.kotlin.jvm' 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

Some files were not shown because too many files have changed in this diff Show More