Лабораторная работа №5: Как будто бы готова

This commit is contained in:
Amon 2023-12-22 22:10:53 +04:00
parent 0f8abfbb99
commit 87332767e7
60 changed files with 1031 additions and 398 deletions

View File

@ -2,6 +2,7 @@ plugins {
id 'com.android.application' id 'com.android.application'
id 'org.jetbrains.kotlin.android' id 'org.jetbrains.kotlin.android'
id 'kotlin-kapt' id 'kotlin-kapt'
id 'org.jetbrains.kotlin.plugin.serialization'
} }
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
@ -86,4 +87,9 @@ dependencies {
implementation "androidx.paging:paging-runtime-ktx:$paging_version" implementation "androidx.paging:paging-runtime-ktx:$paging_version"
implementation "androidx.paging:paging-compose:$paging_version" implementation "androidx.paging:paging-compose:$paging_version"
implementation("androidx.room:room-paging:2.5.0") 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")
} }

View File

@ -1,6 +1,9 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application <application
android:name=".DTFApp" android:name=".DTFApp"
@ -11,6 +14,7 @@
android:label="@string/app_name" android:label="@string/app_name"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.DTF" android:theme="@style/Theme.DTF"
android:usesCleartextTraffic="true"
tools:targetApi="31"> tools:targetApi="31">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"

View File

@ -2,13 +2,12 @@ package com.example.dtf
import android.app.Application import android.app.Application
import androidx.room.* import androidx.room.*
import kotlinx.coroutines.CoroutineScope import com.example.dtf.api.ServerService
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import androidx.sqlite.db.SupportSQLiteDatabase
import com.example.dtf.db.AppDatabase import com.example.dtf.db.AppDatabase
import com.example.dtf.models.User
import com.example.dtf.repositories.* import com.example.dtf.repositories.*
import com.example.dtf.repositories.offline.*
import com.example.dtf.repositories.online.*
import com.example.dtf.repositories.online.mediator.RestCategoryRepository
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
@ -32,21 +31,65 @@ object AppModule {
@Provides @Provides
@Singleton @Singleton
fun provideCategoryRepository(db: AppDatabase) = CategoryRepository(db.categoryDao()) fun provideServerService(): ServerService = ServerService.getInstance()
@Provides @Provides
@Singleton @Singleton
fun provideCommentRepository(db: AppDatabase) = CommentRepository(db.commentDao()) fun provideICategoryRepository(db: AppDatabase, serverService: ServerService): ICategoryRepository = RestCategoryRepository(db, serverService)
@Provides @Provides
@Singleton @Singleton
fun provideLikeRepository(db: AppDatabase) = LikeRepository(db.likeDao()) fun provideICommentRepository(db: AppDatabase, serverService: ServerService): ICommentRepository = RestCommentRepository(db, serverService)
@Provides @Provides
@Singleton @Singleton
fun providePostRepository(db: AppDatabase) = PostRepository(db.postDao()) fun provideILikeRepository(db: AppDatabase, serverService: ServerService): ILikeRepository = RestLikeRepository(db, serverService)
@Provides @Provides
@Singleton @Singleton
fun proviveUserRepository(db: AppDatabase) = UserRepository(db.userDao()) 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,152 @@
package com.example.dtf.api
import com.example.dtf.dto.Credentials
import com.example.dtf.dto.EditPostDto
import com.example.dtf.dto.MeUser
import com.example.dtf.dto.NewCommentDto
import com.example.dtf.dto.NewPostDto
import com.example.dtf.dto.Token
import com.example.dtf.dto.remote.CategoriesResponse
import com.example.dtf.dto.remote.CommentsResponse
import com.example.dtf.dto.remote.PostsResponse
import com.example.dtf.models.Category
import com.example.dtf.models.Comment
import com.example.dtf.models.Post
import com.example.dtf.models.User
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.http.POST
import retrofit2.Retrofit
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Path
import retrofit2.http.Query
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType
import retrofit2.http.Header
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/{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.1.9: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

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

View File

@ -7,7 +7,7 @@ import kotlinx.coroutines.flow.Flow
@Dao @Dao
interface CommentDao { interface CommentDao {
@Insert @Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(comment: Comment) suspend fun insert(comment: Comment)
@Update @Update
@ -16,12 +16,12 @@ interface CommentDao {
@Delete @Delete
suspend fun delete(comment: Comment) suspend fun delete(comment: Comment)
@Query("select * from comment")
fun getAll() : Flow<List<Comment>>
@Query("select * from comment where comment.id = :id") @Query("select * from comment where comment.id = :id")
fun getById(id: Int) : Flow<Comment> fun getById(id: Int) : Flow<Comment>
@Query("select * from comment where comment.post_id = :postId ORDER BY date DESC") @Query("select * from comment where comment.post_id = :postId ORDER BY id DESC")
fun getByPost(postId: Int) : PagingSource<Int, Comment> fun getByPost(postId: Int) : PagingSource<Int, Comment>
@Query("delete from comment where post_id = :query")
fun deleteByQuery(query: String)
} }

View File

@ -6,7 +6,7 @@ import kotlinx.coroutines.flow.Flow
@Dao @Dao
interface LikeDao { interface LikeDao {
@Insert @Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(like: Like) suspend fun insert(like: Like)
@Delete @Delete

View File

@ -7,7 +7,7 @@ import kotlinx.coroutines.flow.Flow
@Dao @Dao
interface PostDao { interface PostDao {
@Insert @Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(post: Post) suspend fun insert(post: Post)
@Update @Update
@ -16,12 +16,12 @@ interface PostDao {
@Delete @Delete
suspend fun delete(post: Post) suspend fun delete(post: Post)
@Query("select * from post")
fun getAll() : Flow<List<Post>>
@Query("select * from post where post.id = :id") @Query("select * from post where post.id = :id")
fun getById(id: Int) : Flow<Post> fun getById(id: Int) : Flow<Post>
@Query("select * from post where post.category_id = :categoryId ORDER BY date DESC") @Query("select * from post where post.category_id = :categoryId ORDER BY date DESC")
fun getByCategory(categoryId: String) : PagingSource<Int, Post> fun getByCategory(categoryId: String) : PagingSource<Int, Post>
@Query("delete from post where category_id = :query")
fun deleteByQuery(query: String)
} }

View File

@ -6,7 +6,7 @@ import kotlinx.coroutines.flow.Flow
@Dao @Dao
interface UserDao { interface UserDao {
@Insert @Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(user: User) suspend fun insert(user: User)
@Update @Update
@ -15,12 +15,6 @@ interface UserDao {
@Delete @Delete
suspend fun delete(user: User) suspend fun delete(user: User)
@Query("select * from user order by username collate nocase asc")
fun getAll() : Flow<List<User>>
@Query("select * from user where user.id = :id") @Query("select * from user where user.id = :id")
fun getById(id: Int): Flow<User> fun getById(id: Int): Flow<User>
@Query("select * from user where user.username = :username and user.password = :password")
fun getByUsernameAndPassword(username: String, password: String): Flow<User?>
} }

View File

@ -22,7 +22,7 @@ import java.util.Date
Like::class, Like::class,
Comment::class Comment::class
], ],
version = 2, version = 4,
exportSchema = false exportSchema = false
) )
@TypeConverters(Converter::class) @TypeConverters(Converter::class)

View File

@ -0,0 +1,9 @@
package com.example.dtf.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.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.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.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.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.dto
import kotlinx.serialization.Serializable
@Serializable
data class Token(
val token: String
)

View File

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

View File

@ -1,8 +1,10 @@
package com.example.dtf.models package com.example.dtf.models
import androidx.room.* import androidx.room.*
import kotlinx.serialization.Serializable
@Entity(tableName = "category") @Entity(tableName = "category")
@Serializable
data class Category( data class Category(
@PrimaryKey(autoGenerate = true) @PrimaryKey(autoGenerate = true)
@ColumnInfo @ColumnInfo

View File

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

View File

@ -1,21 +1,24 @@
package com.example.dtf.models package com.example.dtf.models
import androidx.room.* import androidx.room.*
import java.util.Date import kotlinx.serialization.Serializable
@Entity(tableName = "comment") @Entity(tableName = "comment")
@Serializable
data class Comment( data class Comment(
@PrimaryKey(autoGenerate = true) @PrimaryKey(autoGenerate = true)
@ColumnInfo @ColumnInfo
val id: Int?, val id: Int?,
@ColumnInfo(name = "user_id") @ColumnInfo(name = "user_id")
val userId: Int, val user_id: Int,
@ColumnInfo(name = "post_id") @ColumnInfo(name = "post_id")
val postId: Int, val post_id: Int,
@ColumnInfo @ColumnInfo
val content: String, val content: String,
@ColumnInfo @ColumnInfo
val date: Date val date: String,
@ColumnInfo
val author: String = ""
) { ) {
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true

View File

@ -1,9 +1,7 @@
package com.example.dtf.models package com.example.dtf.models
import androidx.compose.runtime.collection.MutableVector
import androidx.compose.runtime.collection.mutableVectorOf
import androidx.room.* import androidx.room.*
import java.util.Date import kotlinx.serialization.Serializable
//private val posts = listOf( //private val posts = listOf(
// Post( // Post(
@ -60,11 +58,9 @@ import java.util.Date
//) //)
@Entity( @Entity(
tableName = "post", tableName = "post"
foreignKeys = [
ForeignKey(User::class, ["id"], ["user_id"])
]
) )
@Serializable
data class Post( data class Post(
@PrimaryKey(autoGenerate = true) @PrimaryKey(autoGenerate = true)
@ColumnInfo @ColumnInfo
@ -74,11 +70,13 @@ data class Post(
@ColumnInfo @ColumnInfo
val content: String, val content: String,
@ColumnInfo @ColumnInfo
val date: Date, val date: String,
@ColumnInfo(name = "user_id") @ColumnInfo(name = "user_id")
val userId: Int, val user_id: Int,
@ColumnInfo(name = "category_id") @ColumnInfo(name = "category_id")
val categoryId: Int val category_id: Int,
@ColumnInfo
val likes: Int = 0,
) { ) {
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true

View File

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

View File

@ -1,18 +1,16 @@
package com.example.dtf.models package com.example.dtf.models
import androidx.room.* import androidx.room.*
import kotlinx.serialization.Serializable
@Entity(tableName = "user") @Entity(tableName = "user")
@Serializable
data class User( data class User(
@PrimaryKey(autoGenerate = true) @PrimaryKey(autoGenerate = true)
@ColumnInfo @ColumnInfo
val id: Int?, val id: Int?,
@ColumnInfo @ColumnInfo
val username: String, val username: String,
@ColumnInfo
val password: String,
@ColumnInfo
val isModerator: Boolean
) { ) {
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true

View File

@ -1,19 +0,0 @@
package com.example.dtf.repositories
import com.example.dtf.dao.CategoryDao
import com.example.dtf.models.Category
import javax.inject.Inject
class CategoryRepository @Inject constructor(
private val categoryDao: CategoryDao
) {
suspend fun insert(category: Category) = categoryDao.insert(category)
suspend fun update(category: Category) = categoryDao.update(category)
suspend fun delete(category: Category) = categoryDao.delete(category)
fun getAll() = categoryDao.getAll()
fun getById(id: Int) = categoryDao.getById(id)
}

View File

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

View File

@ -0,0 +1,12 @@
package com.example.dtf.repositories
import androidx.paging.PagingData
import com.example.dtf.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.repositories
import androidx.paging.PagingData
import com.example.dtf.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.repositories
import com.example.dtf.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,13 @@
package com.example.dtf.repositories
import androidx.paging.PagingData
import com.example.dtf.models.Post
import kotlinx.coroutines.flow.Flow
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>>
}

View File

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

View File

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

View File

@ -1,29 +0,0 @@
package com.example.dtf.repositories
import androidx.paging.*
import com.example.dtf.dao.PostDao
import com.example.dtf.models.Post
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
class PostRepository @Inject constructor(
private val postDao: PostDao
) {
suspend fun insert(post: Post) = postDao.insert(post)
suspend fun update(post: Post) = postDao.update(post)
suspend fun delete(post: Post) = postDao.delete(post)
fun getAll() = postDao.getAll()
fun getById(id: Int) = postDao.getById(id)
fun getByCategory(categoryId: Int) = Pager(
PagingConfig(
pageSize = 3,
enablePlaceholders = false
),
pagingSourceFactory = { postDao.getByCategory(categoryId.toString()) }
).flow
}

View File

@ -1,22 +0,0 @@
package com.example.dtf.repositories
import com.example.dtf.dao.UserDao
import com.example.dtf.models.User
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
class UserRepository @Inject constructor(
private val userDao: UserDao
) {
suspend fun insert(user: User) = userDao.insert(user)
suspend fun update(user: User) = userDao.update(user)
suspend fun delete(user: User) = userDao.delete(user)
fun getAll() = userDao.getAll()
fun getById(id: Int) = userDao.getById(id)
fun getByUsernameAndPassword(username: String, password: String) = userDao.getByUsernameAndPassword(username, password)
}

View File

@ -0,0 +1,30 @@
package com.example.dtf.repositories.offline
import androidx.paging.Pager
import androidx.paging.PagingConfig
import com.example.dtf.dao.CategoryDao
import com.example.dtf.models.Category
import com.example.dtf.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,23 @@
package com.example.dtf.repositories.offline
import androidx.paging.*
import com.example.dtf.dao.CommentDao
import com.example.dtf.models.Comment
import com.example.dtf.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.repositories.offline
import com.example.dtf.dao.LikeDao
import com.example.dtf.models.Like
import com.example.dtf.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,25 @@
package com.example.dtf.repositories.offline
import androidx.paging.*
import com.example.dtf.dao.PostDao
import com.example.dtf.models.Post
import com.example.dtf.repositories.IPostRepository
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
}

View File

@ -0,0 +1,12 @@
package com.example.dtf.repositories.offline
import com.example.dtf.dao.UserDao
import com.example.dtf.models.User
import com.example.dtf.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.api.ServerService
import com.example.dtf.db.AppDatabase
import com.example.dtf.models.Category
import com.example.dtf.repositories.ICategoryRepository
import kotlinx.coroutines.flow.Flow
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.repositories.online
import androidx.paging.ExperimentalPagingApi
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import com.example.dtf.api.ServerService
import com.example.dtf.db.AppDatabase
import com.example.dtf.dto.NewCommentDto
import com.example.dtf.models.Comment
import com.example.dtf.repositories.ICommentRepository
import com.example.dtf.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.repositories.online
import com.example.dtf.api.ServerService
import com.example.dtf.db.AppDatabase
import com.example.dtf.models.Like
import com.example.dtf.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,49 @@
package com.example.dtf.repositories.online
import androidx.paging.ExperimentalPagingApi
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import com.example.dtf.api.ServerService
import com.example.dtf.db.AppDatabase
import com.example.dtf.dto.EditPostDto
import com.example.dtf.dto.NewPostDto
import com.example.dtf.models.Post
import com.example.dtf.repositories.IPostRepository
import com.example.dtf.repositories.online.mediator.PostMediator
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
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
}

View File

@ -0,0 +1,18 @@
package com.example.dtf.repositories.online
import com.example.dtf.api.ServerService
import com.example.dtf.db.AppDatabase
import com.example.dtf.models.User
import com.example.dtf.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,63 @@
package com.example.dtf.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.api.ServerService
import com.example.dtf.db.AppDatabase
import com.example.dtf.models.Category
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 {
if (loadType == LoadType.REFRESH) {
for (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,63 @@
package com.example.dtf.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.api.ServerService
import com.example.dtf.db.AppDatabase
import com.example.dtf.models.Comment
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 {
if (loadType == LoadType.REFRESH) {
commentDao.deleteByQuery(query)
}
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,63 @@
package com.example.dtf.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.api.ServerService
import com.example.dtf.db.AppDatabase
import com.example.dtf.models.Post
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 {
if (loadType == LoadType.REFRESH) {
postDao.deleteByQuery(query)
}
for (comment in response.posts) {
postDao.insert(comment)
}
}
MediatorResult.Success(endOfPaginationReached = response.nextKey == null)
} catch (e: IOException) {
MediatorResult.Error(e)
}
}
}

View File

@ -18,6 +18,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.* import androidx.compose.ui.unit.*
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.paging.compose.collectAsLazyPagingItems
import com.example.dtf.PreferencesManager import com.example.dtf.PreferencesManager
import com.example.dtf.models.Category import com.example.dtf.models.Category
import com.example.dtf.utils.ScreenPaths import com.example.dtf.utils.ScreenPaths
@ -36,9 +37,7 @@ fun NewPostScreen(navController: NavHostController) {
val viewModel = hiltViewModel<NewPostViewModel>() val viewModel = hiltViewModel<NewPostViewModel>()
val success = viewModel.addingPostState.observeAsState().value val success = viewModel.addingPostState.observeAsState().value
val categories = viewModel.categories.observeAsState().value val categories = viewModel.getCategories().collectAsState(initial = listOf())
viewModel.retrieveCategories()
if (success == true) { if (success == true) {
navController.navigate(ScreenPaths.Posts.route) { navController.navigate(ScreenPaths.Posts.route) {
@ -91,7 +90,9 @@ fun NewPostScreen(navController: NavHostController) {
colors = ButtonDefaults.buttonColors(Color(0xFFFFFFFF)), colors = ButtonDefaults.buttonColors(Color(0xFFFFFFFF)),
border = BorderStroke(1.dp, Color(0xFF0085FF)), border = BorderStroke(1.dp, Color(0xFF0085FF)),
shape = RoundedCornerShape(15.dp), shape = RoundedCornerShape(15.dp),
modifier = Modifier.fillMaxWidth().padding(15.dp, 10.dp, 15.dp, 0.dp) modifier = Modifier
.fillMaxWidth()
.padding(15.dp, 10.dp, 15.dp, 0.dp)
) { ) {
Text( Text(
text = selectedCategory.value.name, text = selectedCategory.value.name,
@ -102,11 +103,13 @@ fun NewPostScreen(navController: NavHostController) {
) )
) )
DropdownMenu( DropdownMenu(
modifier = Modifier.fillMaxWidth().padding(horizontal = 15.dp), modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 15.dp),
expanded = expanded.value, expanded = expanded.value,
onDismissRequest = {expanded.value = false} onDismissRequest = {expanded.value = false}
) { ) {
categories?.forEach { category -> categories.value.forEach { category ->
DropdownMenuItem( DropdownMenuItem(
onClick = { onClick = {
selectedCategory.value = category selectedCategory.value = category
@ -126,17 +129,23 @@ fun NewPostScreen(navController: NavHostController) {
value = title, value = title,
"Заголовок", "Заголовок",
onChanged = {title.value = it}, onChanged = {title.value = it},
modifier = Modifier.fillMaxWidth().padding(horizontal = 15.dp), modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 15.dp),
backgroundColor = Color(0xFFFEFEFE) backgroundColor = Color(0xFFFEFEFE)
) )
MyTextField( MyTextField(
value = content, value = content,
"Содержимое", "Содержимое",
onChanged = {content.value = it}, onChanged = {content.value = it},
modifier = Modifier.fillMaxWidth().heightIn(min = 500.dp).padding(horizontal = 15.dp), modifier = Modifier
.fillMaxWidth()
.heightIn(min = 400.dp)
.padding(horizontal = 15.dp),
backgroundColor = Color(0xFFFEFEFE), backgroundColor = Color(0xFFFEFEFE),
false false
) )
Button( Button(
{ {
if (selectedCategory.value.id != null) { if (selectedCategory.value.id != null) {

View File

@ -19,23 +19,14 @@ import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.* import androidx.compose.ui.unit.*
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.viewModelScope
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.paging.compose.collectAsLazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.itemKey import androidx.paging.compose.itemKey
import com.example.dtf.PreferencesManager import com.example.dtf.PreferencesManager
import com.example.dtf.db.AppDatabase
import com.example.dtf.models.Comment import com.example.dtf.models.Comment
import com.example.dtf.models.Post
import com.example.dtf.models.PostWithComments
import com.example.dtf.models.User
import com.example.dtf.utils.ScreenPaths import com.example.dtf.utils.ScreenPaths
import com.example.dtf.viewmodels.PostViewModel import com.example.dtf.viewmodels.PostViewModel
import com.example.dtf.widgets.MyTextField import com.example.dtf.widgets.MyTextField
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.Date
@Composable @Composable
fun PostScreen(postId: Int, navController: NavHostController) { fun PostScreen(postId: Int, navController: NavHostController) {
@ -89,16 +80,7 @@ fun PostScreen(postId: Int, navController: NavHostController) {
) { ) {
if (post != null) { if (post != null) {
Text( Text(
text = "day.month.year".replace( text = post.date,
"day",
post.date.day.toString()
).replace(
"month",
post.date.month.toString()
).replace(
"year",
post.date.year.toString()
),
fontSize = 14.sp, fontSize = 14.sp,
color = Color(0xFFCECCCC) color = Color(0xFFCECCCC)
) )
@ -115,16 +97,7 @@ fun PostScreen(postId: Int, navController: NavHostController) {
) )
} }
Row ( Row (
modifier = Modifier.clickable { modifier = Modifier,
if (isLiked.value) {
viewModel.unlikePost(sharedPref, postId)
likes.intValue--
} else {
viewModel.likePost(sharedPref, postId)
likes.intValue++
}
isLiked.value = !isLiked.value
},
horizontalArrangement = Arrangement.End horizontalArrangement = Arrangement.End
) { ) {
Text( Text(
@ -133,7 +106,16 @@ fun PostScreen(postId: Int, navController: NavHostController) {
color = Color.Green color = Color.Green
) )
Icon( Icon(
modifier = Modifier.padding(start = 8.dp), 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, imageVector = Icons.Default.ThumbUp,
contentDescription = null, contentDescription = null,
tint = if (isLiked.value) { Color(40, 200, 40, 255) } else {Color.Black} tint = if (isLiked.value) { Color(40, 200, 40, 255) } else {Color.Black}
@ -202,20 +184,11 @@ private fun Comment(comment: Comment) {
horizontalArrangement = Arrangement.SpaceBetween horizontalArrangement = Arrangement.SpaceBetween
) { ) {
Text( Text(
text = user?.username ?: "Loading...", text = comment.author.ifEmpty { user?.username ?: "Loading..." },
fontSize = 20.sp fontSize = 20.sp
) )
Text( Text(
text = "day.month.year".replace( text = comment.date,
"day",
comment.date.day.toString()
).replace(
"month",
comment.date.month.toString()
).replace(
"year",
comment.date.year.toString()
),
fontSize = 14.sp, fontSize = 14.sp,
color = Color(0xFFCECCCC) color = Color(0xFFCECCCC)
) )

View File

@ -3,6 +3,7 @@ package com.example.dtf.screens
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.* import androidx.compose.foundation.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ThumbUp import androidx.compose.material.icons.filled.ThumbUp
@ -18,21 +19,14 @@ import androidx.compose.ui.unit.*
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.paging.PagingData import androidx.paging.PagingData
import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.collectAsLazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.itemKey import androidx.paging.compose.itemKey
import com.example.dtf.PreferencesManager import com.example.dtf.PreferencesManager
import com.example.dtf.db.AppDatabase
import com.example.dtf.models.Category import com.example.dtf.models.Category
import com.example.dtf.models.CategoryWithPosts
import com.example.dtf.models.Post import com.example.dtf.models.Post
import com.example.dtf.utils.ScreenPaths import com.example.dtf.utils.ScreenPaths
import com.example.dtf.viewmodels.PostsViewModel import com.example.dtf.viewmodels.PostsViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@Composable @Composable
fun PostsScreen(navController: NavHostController) { fun PostsScreen(navController: NavHostController) {
@ -47,19 +41,8 @@ fun PostsScreen(navController: NavHostController) {
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(10.dp) verticalArrangement = Arrangement.spacedBy(10.dp)
) { ) {
Row(
modifier = Modifier Categories(viewModel, currentCategory, posts)
.fillMaxWidth()
.fillMaxHeight(0.1f)
.horizontalScroll(
rememberScrollState()
)
.background(Color.White),
horizontalArrangement = Arrangement.spacedBy(25.dp),
verticalAlignment = Alignment.CenterVertically
) {
Categories(viewModel, currentCategory, posts)
}
Row( Row(
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) { ) {
@ -72,36 +55,50 @@ fun PostsScreen(navController: NavHostController) {
@Composable @Composable
private fun Categories(viewModel: PostsViewModel, currentCategory: MutableState<Category?>, posts: MutableState<Flow<PagingData<Post>>?>) { private fun Categories(viewModel: PostsViewModel, currentCategory: MutableState<Category?>, posts: MutableState<Flow<PagingData<Post>>?>) {
val categories = viewModel.getCategories().collectAsState(listOf()).value val categories = viewModel.getCategoriesListUiState().collectAsLazyPagingItems()
if (categories.isNotEmpty() && currentCategory.value == null) {
currentCategory.value = categories[0]
posts.value = viewModel.getPostsListUiState(currentCategory.value!!.id!!)
}
Spacer(modifier = Modifier.width(5.dp)) LazyRow(
categories.forEach {category -> modifier = Modifier
Text( .fillMaxWidth()
modifier = Modifier .fillMaxHeight(0.1f)
.clickable { .background(Color.White),
currentCategory.value = category horizontalArrangement = Arrangement.spacedBy(25.dp),
posts.value = viewModel.getPostsListUiState(category.id!!) verticalAlignment = Alignment.CenterVertically
} ) {
.drawBehind { item {
if (category.name == currentCategory.value?.name) { Spacer(modifier = Modifier.width(5.dp))
val strokeWidthPx = 2.dp.toPx() }
val verticalOffset = size.height + 2.sp.toPx() items(
drawLine( count = categories.itemCount,
color = Color(0xFF319CFF), key = categories.itemKey()
strokeWidth = strokeWidthPx, ) {
start = Offset(0f, verticalOffset), if (currentCategory.value == null) {
end = Offset(size.width, verticalOffset) 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 {
text = category.name, if (categories[it]!!.name == currentCategory.value?.name) {
fontSize = 22.sp 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
)
}
} }
} }
@ -129,7 +126,7 @@ private fun PostsByCategory(viewModel: PostsViewModel, navController: NavHostCon
private fun Post(viewModel: PostsViewModel, navController: NavHostController, post: Post) { private fun Post(viewModel: PostsViewModel, navController: NavHostController, post: Post) {
val sharedPref = PreferencesManager(LocalContext.current) val sharedPref = PreferencesManager(LocalContext.current)
val likes = remember { mutableIntStateOf(0) } val likes = remember { mutableIntStateOf(post.likes) }
val isLiked = remember { mutableStateOf(false) } val isLiked = remember { mutableStateOf(false) }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
@ -162,41 +159,28 @@ private fun Post(viewModel: PostsViewModel, navController: NavHostController, po
fontSize = 26.sp fontSize = 26.sp
) )
Text( Text(
modifier = Modifier.fillMaxHeight(0.6f).padding(10.dp, 0.dp, 10.dp, 0.dp), modifier = Modifier
.fillMaxHeight(0.6f)
.padding(10.dp, 0.dp, 10.dp, 0.dp),
text = post.content, text = post.content,
fontSize = 20.sp, fontSize = 20.sp,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
) )
Row( Row(
modifier = Modifier.fillMaxWidth().padding(10.dp), modifier = Modifier
.fillMaxWidth()
.padding(10.dp),
verticalAlignment = Alignment.Bottom, verticalAlignment = Alignment.Bottom,
horizontalArrangement = Arrangement.SpaceBetween horizontalArrangement = Arrangement.SpaceBetween
) { ) {
Text( Text(
text = "day.month.year".replace( text = post.date,
"day",
post.date.day.toString()
).replace(
"month",
post.date.month.toString()
).replace(
"year",
post.date.year.toString()
),
fontSize = 14.sp, fontSize = 14.sp,
color = Color(0xFFCECCCC) color = Color(0xFFCECCCC)
) )
Row ( Row (
modifier = Modifier.fillMaxWidth().clickable { modifier = Modifier
if (isLiked.value) { .fillMaxWidth(),
viewModel.unlikePost(sharedPref, post.id!!)
likes.intValue--
} else {
viewModel.likePost(sharedPref, post.id!!)
likes.intValue++
}
isLiked.value = !isLiked.value
},
horizontalArrangement = Arrangement.End horizontalArrangement = Arrangement.End
) { ) {
Text( Text(
@ -205,7 +189,18 @@ private fun Post(viewModel: PostsViewModel, navController: NavHostController, po
color = Color.Green color = Color.Green
) )
Icon( Icon(
modifier = Modifier.padding(start = 8.dp), 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, imageVector = Icons.Default.ThumbUp,
contentDescription = null, contentDescription = null,
tint = if (isLiked.value) { Color(40, 200, 40, 255) } else {Color.Black} tint = if (isLiked.value) { Color(40, 200, 40, 255) } else {Color.Black}

View File

@ -6,14 +6,14 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.example.dtf.PreferencesManager import com.example.dtf.PreferencesManager
import com.example.dtf.models.Post import com.example.dtf.models.Post
import com.example.dtf.repositories.PostRepository import com.example.dtf.repositories.IPostRepository
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class EditPostViewModel @Inject constructor( class EditPostViewModel @Inject constructor(
private val postRepository: PostRepository private val postRepository: IPostRepository
) : ViewModel() { ) : ViewModel() {
private val _editingPostState = MutableLiveData<Boolean?>(null) private val _editingPostState = MutableLiveData<Boolean?>(null)
val editingPostState: LiveData<Boolean?> val editingPostState: LiveData<Boolean?>
@ -41,7 +41,7 @@ class EditPostViewModel @Inject constructor(
viewModelScope.launch { viewModelScope.launch {
if (title.isNotEmpty() && content.isNotEmpty()) { if (title.isNotEmpty() && content.isNotEmpty()) {
postRepository.getById(postId).collect { postRepository.getById(postId).collect {
postRepository.update(Post(postId, title, content, it.date, it.userId, it.categoryId)) postRepository.update(Post(postId, title, content, it.date, it.user_id, it.category_id))
_editingPostState.postValue(true) _editingPostState.postValue(true)
} }
} else { } else {

View File

@ -5,55 +5,44 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.example.dtf.PreferencesManager import com.example.dtf.PreferencesManager
import com.example.dtf.models.Category import com.example.dtf.api.ServerService
import com.example.dtf.models.User import com.example.dtf.dto.Credentials
import com.example.dtf.repositories.CategoryRepository import com.example.dtf.repositories.IUserRepository
import com.example.dtf.repositories.UserRepository
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class LoginViewModel @Inject constructor( class LoginViewModel @Inject constructor(
private val userRepository: UserRepository, private val serverService: ServerService,
private val categoryRepository: CategoryRepository
) : ViewModel() { ) : ViewModel() {
private val _successState = MutableLiveData<Boolean?>() private val _successState = MutableLiveData<Boolean?>()
val successState: LiveData<Boolean?> val successState: LiveData<Boolean?>
get() = _successState get() = _successState
init {
addAdmin()
}
fun calmSuccessState() { fun calmSuccessState() {
_successState.postValue(null) _successState.postValue(null)
} }
fun login(sharedPref: PreferencesManager, username: String, password: String) { fun login(sharedPref: PreferencesManager, username: String, password: String) {
viewModelScope.launch { viewModelScope.launch {
userRepository.getByUsernameAndPassword(username, password).collect { val token = serverService.login(Credentials(username, password))
if (it == null) {
_successState.postValue(false)
} else {
sharedPref.saveData("userId", it.id.toString())
sharedPref.saveData("isModerator", it.isModerator.toString())
_successState.postValue(true)
}
}
}
}
private fun addAdmin() { if (token.token.isEmpty()) {
viewModelScope.launch { _successState.postValue(false)
userRepository.getAll().collect { return@launch
if (it.size == 0) {
userRepository.insert(User(1, "admin", "admin", true))
categoryRepository.insert(Category(1, "Аниме"))
categoryRepository.insert(Category(2, "Игры"))
categoryRepository.insert(Category(3, "Фильмы"))
}
} }
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

@ -5,36 +5,24 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.example.dtf.PreferencesManager import com.example.dtf.PreferencesManager
import com.example.dtf.models.Category
import com.example.dtf.models.Post import com.example.dtf.models.Post
import com.example.dtf.repositories.CategoryRepository import com.example.dtf.repositories.ICategoryRepository
import com.example.dtf.repositories.PostRepository import com.example.dtf.repositories.IPostRepository
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.time.LocalDate
import java.util.Date import java.util.Date
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class NewPostViewModel @Inject constructor( class NewPostViewModel @Inject constructor(
private val postRepository: PostRepository, private val postRepository: IPostRepository,
private val categoryRepository: CategoryRepository private val categoryRepository: ICategoryRepository
) : ViewModel() { ) : ViewModel() {
private val _addingPostState = MutableLiveData<Boolean?>(null) private val _addingPostState = MutableLiveData<Boolean?>(null)
val addingPostState: LiveData<Boolean?> val addingPostState: LiveData<Boolean?>
get() = _addingPostState get() = _addingPostState
private val _categories = MutableLiveData<List<Category>>(listOf()) fun getCategories() = categoryRepository.getAllCached()
val categories: LiveData<List<Category>>
get() = _categories
fun retrieveCategories() {
viewModelScope.launch {
categoryRepository.getAll().collect {
_categories.postValue(it)
}
}
}
fun calmAddingState() { fun calmAddingState() {
_addingPostState.value = null _addingPostState.value = null
@ -50,7 +38,7 @@ class NewPostViewModel @Inject constructor(
null, null,
title, title,
content, content,
Date(), "${Date().year + 1900}-${Date().month + 1}-${Date().date}",
sharedPref.getData("userId", "0").toInt(), sharedPref.getData("userId", "0").toInt(),
categoryId categoryId
) )

View File

@ -4,16 +4,15 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.paging.PagingData import androidx.paging.PagingData
import com.example.dtf.PreferencesManager import com.example.dtf.PreferencesManager
import com.example.dtf.models.Comment import com.example.dtf.models.Comment
import com.example.dtf.models.Like import com.example.dtf.models.Like
import com.example.dtf.models.Post import com.example.dtf.models.Post
import com.example.dtf.repositories.CommentRepository import com.example.dtf.repositories.ICommentRepository
import com.example.dtf.repositories.LikeRepository import com.example.dtf.repositories.ILikeRepository
import com.example.dtf.repositories.PostRepository import com.example.dtf.repositories.IPostRepository
import com.example.dtf.repositories.UserRepository import com.example.dtf.repositories.IUserRepository
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -22,10 +21,10 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
class PostViewModel @Inject constructor( class PostViewModel @Inject constructor(
private val postRepository: PostRepository, private val postRepository: IPostRepository,
private val commentRepository: CommentRepository, private val commentRepository: ICommentRepository,
private val likeRepository: LikeRepository, private val likeRepository: ILikeRepository,
private val userRepository: UserRepository private val userRepository: IUserRepository
) : ViewModel() { ) : ViewModel() {
private val _post = MutableLiveData<Post>() private val _post = MutableLiveData<Post>()
val post: LiveData<Post> val post: LiveData<Post>
@ -79,13 +78,14 @@ class PostViewModel @Inject constructor(
userId, userId,
postId, postId,
content, content,
Date() "${Date().year + 1900}-${Date().month + 1}-${Date().date}",
sharedPref.getData("username", "Unknown")
) )
) )
} }
} }
fun getCommentsAuthor(comment: Comment) = userRepository.getById(comment.userId) fun getCommentsAuthor(comment: Comment) = userRepository.getById(comment.user_id)
fun getCommentsListUiState(postId: Int): Flow<PagingData<Comment>> = commentRepository.getByPost(postId) fun getCommentsListUiState(postId: Int): Flow<PagingData<Comment>> = commentRepository.getByPost(postId)
} }

View File

@ -4,18 +4,18 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.example.dtf.PreferencesManager import com.example.dtf.PreferencesManager
import com.example.dtf.models.Like import com.example.dtf.models.Like
import com.example.dtf.repositories.CategoryRepository import com.example.dtf.repositories.ICategoryRepository
import com.example.dtf.repositories.LikeRepository import com.example.dtf.repositories.ILikeRepository
import com.example.dtf.repositories.PostRepository import com.example.dtf.repositories.IPostRepository
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class PostsViewModel @Inject constructor( class PostsViewModel @Inject constructor(
private val postRepository: PostRepository, private val postRepository: IPostRepository,
private val categoryRepository: CategoryRepository, private val categoryRepository: ICategoryRepository,
private val likeRepository: LikeRepository private val likeRepository: ILikeRepository
) : ViewModel() { ) : ViewModel() {
fun getLikes(postId: Int) = likeRepository.countByPost(postId) fun getLikes(postId: Int) = likeRepository.countByPost(postId)
@ -45,7 +45,9 @@ class PostsViewModel @Inject constructor(
postId postId
) )
fun getCategories() = categoryRepository.getAll() fun getCategoriesListUiState() = categoryRepository.getAll()
fun getInitialCategory() = categoryRepository.getFirst()
fun getPostsListUiState(categoryId: Int) = postRepository.getByCategory(categoryId) fun getPostsListUiState(categoryId: Int) = postRepository.getByCategory(categoryId)
} }

View File

@ -6,33 +6,26 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.example.dtf.PreferencesManager import com.example.dtf.PreferencesManager
import com.example.dtf.models.User import com.example.dtf.models.User
import com.example.dtf.repositories.UserRepository import com.example.dtf.repositories.offline.OfflineUserRepository
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class ProfileViewModel @Inject constructor( class ProfileViewModel @Inject constructor(
private val userRepository: UserRepository private val userRepository: OfflineUserRepository
) : ViewModel() { ) : ViewModel() {
private val _user = MutableLiveData<User>() private val _user = MutableLiveData<User>()
val user: LiveData<User> val user: LiveData<User>
get() = _user get() = _user
fun retrieveUser(sharedPref: PreferencesManager) { fun retrieveUser(sharedPref: PreferencesManager) {
val userId = sharedPref.getData("userId", "-1").toInt() _user.postValue(User(null, sharedPref.getData("username", "Nickname")))
if (userId == -1) return
viewModelScope.launch {
userRepository.getById(userId).collect {
_user.postValue(it)
}
}
} }
fun logout(sharedPref: PreferencesManager) { fun logout(sharedPref: PreferencesManager) {
sharedPref.deleteData("userId") sharedPref.deleteData("token")
sharedPref.deleteData("username")
sharedPref.deleteData("isModerator") sharedPref.deleteData("isModerator")
} }
} }

View File

@ -4,16 +4,16 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.example.dtf.PreferencesManager import com.example.dtf.api.ServerService
import com.example.dtf.models.User import com.example.dtf.dto.Credentials
import com.example.dtf.repositories.UserRepository import com.example.dtf.repositories.offline.OfflineUserRepository
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class RegisterViewModel @Inject constructor( class RegisterViewModel @Inject constructor(
private val userRepository: UserRepository private val serverService: ServerService
) : ViewModel() { ) : ViewModel() {
private val _successState = MutableLiveData<Boolean?>() private val _successState = MutableLiveData<Boolean?>()
val successState: LiveData<Boolean?> val successState: LiveData<Boolean?>
@ -38,15 +38,11 @@ class RegisterViewModel @Inject constructor(
_successState.postValue(false) _successState.postValue(false)
return return
} }
viewModelScope.launch { viewModelScope.launch {
userRepository.getByUsernameAndPassword(username, password).collect { if (serverService.register(Credentials(username, password)) == "NOT OK") {
if (it != null) { _successState.postValue(false)
_successState.postValue(false) } else {
} else { _successState.postValue(true)
userRepository.insert(User(null, username, password, false))
_successState.postValue(true)
}
} }
} }
} }

View File

@ -14,4 +14,6 @@ plugins {
id 'com.android.application' version '7.4.2' apply false id 'com.android.application' version '7.4.2' apply false
id 'com.android.library' 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.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
} }

View File

@ -4,5 +4,5 @@
# Location of the SDK. This is only used by Gradle. # Location of the SDK. This is only used by Gradle.
# For customization when using a Version Control System, please read the # For customization when using a Version Control System, please read the
# header note. # header note.
#Tue Nov 07 09:36:13 SAMT 2023 #Fri Dec 22 01:26:17 GMT+04:00 2023
sdk.dir=C\:\\Users\\Aqua\\AppData\\Local\\Android\\Sdk sdk.dir=D\:\\Programs\\Android