Уже страшновато.

This commit is contained in:
ElEgEv 2023-12-10 15:02:49 +04:00
parent 3860540d9c
commit a530e3b2fe
36 changed files with 601 additions and 119 deletions

Binary file not shown.

9
compose/.idea/modules.xml generated Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/modules/app/pmu-demo.app.androidTest.iml" filepath="$PROJECT_DIR$/.idea/modules/app/pmu-demo.app.androidTest.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/app/pmu-demo.app.main.iml" filepath="$PROJECT_DIR$/.idea/modules/app/pmu-demo.app.main.iml" />
</modules>
</component>
</project>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$/../../../app/src/androidTest">
<sourceFolder url="file://$MODULE_DIR$/../../../app/src/androidTest/assets" type="java-test-resource" />
</content>
</component>
</module>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module version="4">
<component name="ModuleClassLoaderOverlays">
<paths>
<option value="C:\Users\egore\AppData\Local\Temp\overlay12619265447938240062" />
</paths>
</component>
</module>

View File

@ -17,6 +17,7 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.ExposedDropdownMenuBox
@ -54,7 +55,6 @@ import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument import androidx.navigation.navArgument
import ru.ulstu.`is`.pmu.R import ru.ulstu.`is`.pmu.R
import ru.ulstu.`is`.pmu.tank.composeui.StudentView
import ru.ulstu.`is`.pmu.ui.theme.CustomDark import ru.ulstu.`is`.pmu.ui.theme.CustomDark
import ru.ulstu.`is`.pmu.ui.theme.CustomOrange import ru.ulstu.`is`.pmu.ui.theme.CustomOrange
import ru.ulstu.`is`.pmu.ui.theme.PmudemoTheme import ru.ulstu.`is`.pmu.ui.theme.PmudemoTheme
@ -66,6 +66,7 @@ import ru.ulstu.`is`.pmu.tank.composeui.TankList
import ru.ulstu.`is`.pmu.tanks.composeui.Account import ru.ulstu.`is`.pmu.tanks.composeui.Account
import ru.ulstu.`is`.pmu.tanks.composeui.Constructor import ru.ulstu.`is`.pmu.tanks.composeui.Constructor
import ru.ulstu.`is`.pmu.tanks.composeui.Hangar import ru.ulstu.`is`.pmu.tanks.composeui.Hangar
import ru.ulstu.`is`.pmu.ui.theme.CustomYellow
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@ -149,12 +150,6 @@ fun Navhost(
) { Constructor(navController) } ) { Constructor(navController) }
composable(Screen.Hangar.route) { Hangar(navController) } composable(Screen.Hangar.route) { Hangar(navController) }
composable(Screen.Account.route) { Account(navController) } composable(Screen.Account.route) { Account(navController) }
composable(
Screen.StudentView.route,
arguments = listOf(navArgument("id") { type = NavType.IntType })
) { backStackEntry ->
backStackEntry.arguments?.let { StudentView(it.getInt("id")) }
}
} }
} }
@ -251,6 +246,12 @@ fun MainNavbar(navController: NavController) {
if(currentScreen?.route == Screen.TankList.route){ if(currentScreen?.route == Screen.TankList.route){
DropDownList(navController) DropDownList(navController)
Button( Button(
modifier = Modifier
.width(200.dp)
.height(50.dp),
colors = ButtonDefaults.buttonColors(
containerColor = CustomOrange,
contentColor = CustomDark),
onClick = { onClick = {
val route = Screen.Constructor.route.replace("{id}", 0.toString()) val route = Screen.Constructor.route.replace("{id}", 0.toString())
navController.navigate(route) navController.navigate(route)

View File

@ -41,7 +41,6 @@ enum class Screen(
companion object { companion object {
val bottomBarItems = listOf( val bottomBarItems = listOf(
TankList, TankList,
Constructor,
Hangar, Hangar,
Account Account
) )

View File

@ -1,74 +0,0 @@
package ru.ulstu.`is`.pmu.tank.composeui
import android.content.res.Configuration
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import ru.ulstu.`is`.pmu.R
import ru.ulstu.`is`.pmu.tank.model.getStudents
import ru.ulstu.`is`.pmu.ui.theme.PmudemoTheme
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun StudentView(id: Int) {
val student = getStudents()[id]
Column(
Modifier
.fillMaxWidth()
.padding(all = 10.dp)
) {
OutlinedTextField(modifier = Modifier.fillMaxWidth(),
value = student.firstName, onValueChange = {}, readOnly = true,
label = {
Text(stringResource(id = R.string.student_firstname))
}
)
OutlinedTextField(modifier = Modifier.fillMaxWidth(),
value = student.lastName, onValueChange = {}, readOnly = true,
label = {
Text(stringResource(id = R.string.student_lastname))
}
)
OutlinedTextField(modifier = Modifier.fillMaxWidth(),
value = student.group, onValueChange = {}, readOnly = true,
label = {
Text(stringResource(id = R.string.student_group))
}
)
OutlinedTextField(modifier = Modifier.fillMaxWidth(),
value = student.phone, onValueChange = {}, readOnly = true,
label = {
Text(stringResource(id = R.string.student_phone))
}
)
OutlinedTextField(modifier = Modifier.fillMaxWidth(),
value = student.email, onValueChange = {}, readOnly = true,
label = {
Text(stringResource(id = R.string.student_email))
}
)
}
}
@Preview(name = "Light Mode", showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_NO)
@Preview(name = "Dark Mode", showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
fun StudentViewPreview() {
PmudemoTheme {
Surface(
color = MaterialTheme.colorScheme.background
) {
StudentView(id = 0)
}
}
}

View File

@ -1,5 +1,6 @@
package ru.ulstu.`is`.pmu.tank.composeui.edit package ru.ulstu.`is`.pmu.tank.composeui.edit
import android.graphics.Bitmap
import android.util.Log import android.util.Log
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@ -7,6 +8,7 @@ import androidx.compose.runtime.setValue
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.application.ui.getEmptyBitmap
import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -44,10 +46,11 @@ class TankEditViewModel(
suspend fun saveTank() { suspend fun saveTank() {
if (validateInput()) { if (validateInput()) {
val image: Bitmap = tankUiState.tankDetails.image!!
if (tankUid > 0) { if (tankUid > 0) {
tankRepository.updateTank(tankUiState.tankDetails.toTank(tankUid)) tankRepository.updateTank(tankUiState.tankDetails.toTank(tankUid), image = image)
} else { } else {
tankRepository.insertTank(tankUiState.tankDetails.toTank()) tankRepository.insertTank(tankUiState.tankDetails.toTank(), image = image)
} }
} }
} }
@ -56,9 +59,9 @@ class TankEditViewModel(
return with(uiState) { return with(uiState) {
name.isNotBlank() name.isNotBlank()
&& price > 0 && price > 0
&& image > 0
&& levelId!! > 0 && levelId!! > 0
&& nationId!! > 0 && nationId!! > 0
&& image != null
} }
} }
} }
@ -71,26 +74,28 @@ data class TankUiState(
data class TankDetails( data class TankDetails(
val name: String = "", val name: String = "",
val price: Int = 0, val price: Int = 0,
val image: Int = 0,
val levelId: Long? = 0, val levelId: Long? = 0,
val nationId: Long? = 0, val nationId: Long? = 0,
val miniature: Bitmap = getEmptyBitmap(),
val image: Bitmap? = null,
val imageId: Long = -1
) )
fun TankDetails.toTank(uid: Long = 0): Tank = Tank( fun TankDetails.toTank(uid: Long = 0): Tank = Tank(
tankId = uid, tankId = uid,
name = name, name = name,
price = price, price = price,
image = image,
levelId = levelId, levelId = levelId,
nationId = nationId nationId = nationId,
imageId = imageId
) )
fun Tank.toDetails(): TankDetails = TankDetails( fun Tank.toDetails(): TankDetails = TankDetails(
name = name, name = name,
price = price, price = price,
image = image,
levelId = levelId, levelId = levelId,
nationId = nationId nationId = nationId,
imageId = imageId
) )
fun Tank.toUiState(isEntryValid: Boolean = false): TankUiState = TankUiState( fun Tank.toUiState(isEntryValid: Boolean = false): TankUiState = TankUiState(

View File

@ -23,8 +23,9 @@ interface TankDao {
//получаем все танки пользователя по его Id //получаем все танки пользователя по его Id
@Query( @Query(
"SELECT t.tankId, t.name, t.price, t.image, l.level, n.nationName FROM UserTankCrossRef AS ut " + "SELECT t.tankId, t.name, t.price, t.image_id, l.level, n.nationName FROM UserTankCrossRef AS ut " +
"LEFT JOIN tanks as t on ut.tankId = t.tankId " + "LEFT JOIN tanks as t on ut.tankId = t.tankId " +
"LEFT JOIN tank_images as ti on t.image_id = ti.image_id " +
"LEFT JOIN levels as l on t.levelId = l.uid " + "LEFT JOIN levels as l on t.levelId = l.uid " +
"LEFT JOIN nations as n on t.nationId = n.uid " + "LEFT JOIN nations as n on t.nationId = n.uid " +
"WHERE ut.userId = :uid GROUP BY t.nationId, t.levelId ORDER BY t.nationId" "WHERE ut.userId = :uid GROUP BY t.nationId, t.levelId ORDER BY t.nationId"
@ -39,4 +40,7 @@ interface TankDao {
@Delete @Delete
suspend fun delete(tank: Tank) suspend fun delete(tank: Tank)
@Query("DELETE FROM tanks WHERE tankId = :id")
suspend fun delete(id: Long)
} }

View File

@ -0,0 +1,26 @@
package ru.ulstu.`is`.pmu.tank.dao
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Update
import ru.ulstu.`is`.pmu.tank.model.TankImage
@Dao
interface TankImageDao {
@Insert
suspend fun insert(productImage: TankImage): Long
@Insert
suspend fun insertMany(productImages: List<TankImage>)
@Update
suspend fun update(productImage: TankImage)
@Delete
suspend fun delete(productImage: TankImage)
@Query("DELETE FROM tank_images WHERE image_id = :id")
suspend fun delete(id: Long)
}

View File

@ -25,7 +25,9 @@ class AppDataContainer(private val context: Context) : AppContainer {
OfflineNationRepository(AppDatabase.getInstance(context).nationDao()) OfflineNationRepository(AppDatabase.getInstance(context).nationDao())
} }
override val tankRepository: TankRepository by lazy { override val tankRepository: TankRepository by lazy {
OfflineTankRepository(AppDatabase.getInstance(context).tankDao()) OfflineTankRepository(
AppDatabase.getInstance(context).tankDao(),
tankImageDao = AppDatabase.getInstance(context).tankImageDao())
} }
override val userRepository: UserRepository by lazy { override val userRepository: UserRepository by lazy {
OfflineUserRepository(AppDatabase.getInstance(context).userDao()) OfflineUserRepository(AppDatabase.getInstance(context).userDao())

View File

@ -12,6 +12,7 @@ import ru.ulstu.`is`.pmu.R
import ru.ulstu.`is`.pmu.tank.dao.LevelDao import ru.ulstu.`is`.pmu.tank.dao.LevelDao
import ru.ulstu.`is`.pmu.tank.dao.NationDao import ru.ulstu.`is`.pmu.tank.dao.NationDao
import ru.ulstu.`is`.pmu.tank.dao.TankDao import ru.ulstu.`is`.pmu.tank.dao.TankDao
import ru.ulstu.`is`.pmu.tank.dao.TankImageDao
import ru.ulstu.`is`.pmu.tank.dao.UserDao import ru.ulstu.`is`.pmu.tank.dao.UserDao
import ru.ulstu.`is`.pmu.tank.model.Level import ru.ulstu.`is`.pmu.tank.model.Level
import ru.ulstu.`is`.pmu.tank.model.Nation import ru.ulstu.`is`.pmu.tank.model.Nation
@ -27,14 +28,15 @@ abstract class AppDatabase : RoomDatabase() {
abstract fun levelDao(): LevelDao abstract fun levelDao(): LevelDao
abstract fun tankDao() : TankDao abstract fun tankDao() : TankDao
abstract fun userDao() : UserDao abstract fun userDao() : UserDao
abstract fun tankImageDao() : TankImageDao
companion object { companion object {
private const val DB_NAME: String = "12-db" private const val DB_NAME: String = "13-db"
@Volatile @Volatile
private var INSTANCE: AppDatabase? = null private var INSTANCE: AppDatabase? = null
private suspend fun populateDatabase() { private suspend fun populateDatabase(context: Context) {
INSTANCE?.let { database -> INSTANCE?.let { database ->
// Nations // Nations
val nationDao = database.nationDao() val nationDao = database.nationDao()
@ -84,19 +86,23 @@ abstract class AppDatabase : RoomDatabase() {
levelDao.insert(level9) levelDao.insert(level9)
levelDao.insert(level10) levelDao.insert(level10)
//images
val tankImageDao = database.tankImageDao()
tankImageDao.insertMany(PrepopulateStore.getProductImages(context))
//Tanks //Tanks
val tankDao = database.tankDao() val tankDao = database.tankDao()
val tank1 = Tank(20L,"МС-1", 1000, R.drawable.t_34_85, level1.uid, nation1.uid) val tank1 = Tank(20L,"МС-1", 1000, 1L, level1.uid, nation1.uid)
val tank2 = Tank(21L, "Т-34-85", 960000, R.drawable.t_34_85, level6.uid, nation1.uid) val tank2 = Tank(21L, "Т-34-85", 960000, 1L, level6.uid, nation1.uid)
val tank10 = Tank(22L, "Pershing", 1260000, R.drawable.sherman, level8.uid, nation3.uid) val tank10 = Tank(22L, "Pershing", 1260000,3L, level8.uid, nation3.uid)
val tank6 = Tank(23L, "Ferdinand", 2500000, R.drawable.tiger_1, level8.uid, nation2.uid) val tank6 = Tank(23L, "Ferdinand", 2500000, 2L, level8.uid, nation2.uid)
val tank3 = Tank(24L, "ИС-2", 1230000, R.drawable.t_34_85, level7.uid, nation1.uid) val tank3 = Tank(24L, "ИС-2", 1230000, 1L, level7.uid, nation1.uid)
val tank4 = Tank(25L, "ИСУ-152", 2350000, R.drawable.t_34_85, level8.uid, nation1.uid) val tank4 = Tank(25L, "ИСУ-152", 2350000, 1L, level8.uid, nation1.uid)
val tank5 = Tank(26L, "Tiger 1", 1430000, R.drawable.tiger_1, level7.uid, nation2.uid) val tank5 = Tank(26L, "Tiger 1", 1430000,2L, level7.uid, nation2.uid)
val tank7 = Tank(27L, "Tiger 2", 2500000, R.drawable.tiger_1, level8.uid, nation2.uid) val tank7 = Tank(27L, "Tiger 2", 2500000, 2L, level8.uid, nation2.uid)
val tank8 = Tank(28L, "Panther", 1350000, R.drawable.tiger_1, level7.uid, nation2.uid) val tank8 = Tank(28L, "Panther", 1350000, 2L, level7.uid, nation2.uid)
val tank9 = Tank(29L, "M4A2E3", 990000, R.drawable.sherman, level6.uid, nation3.uid) val tank9 = Tank(29L, "M4A2E3", 990000, 3L, level6.uid, nation3.uid)
val tank11 = Tank(30L, "Hellcat", 940000, R.drawable.sherman, level7.uid, nation3.uid) val tank11 = Tank(30L, "Hellcat", 940000, 3L, level7.uid, nation3.uid)
tankDao.insert(tank1) tankDao.insert(tank1)
tankDao.insert(tank2) tankDao.insert(tank2)
@ -135,7 +141,7 @@ abstract class AppDatabase : RoomDatabase() {
override fun onCreate(db: SupportSQLiteDatabase) { override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db) super.onCreate(db)
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
populateDatabase() populateDatabase(appContext)
} }
} }
}) })

View File

@ -0,0 +1,44 @@
package ru.ulstu.`is`.pmu.tank.database
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import androidx.room.TypeConverter
import java.io.ByteArrayOutputStream
import java.util.Date
class Converters {
/*
@TypeConverter
fun toUserRole(value: Int): UserRole {
return enumValues<UserRole>()[value]
}
@TypeConverter
fun fromUserRole(value: UserRole): Int {
return value.ordinal
}
*/
@TypeConverter
fun toDate(value: Long?): Date? {
return value?.let { Date(it) }
}
@TypeConverter
fun fromDate(date: Date?): Long? {
return date?.time
}
@TypeConverter
fun fromBitmap(bitmap: Bitmap): ByteArray {
val outputStream = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.PNG, 1, outputStream)
return outputStream.toByteArray()
}
@TypeConverter
fun toBitmap(byteArray: ByteArray): Bitmap {
return BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size)
}
}

View File

@ -0,0 +1,33 @@
package ru.ulstu.`is`.pmu.tank.database
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import com.application.ui.miniatureBound
import com.application.ui.resize
import com.application.ui.tankImageBound
import ru.ulstu.`is`.pmu.tank.model.TankImage
class PrepopulateStore {
companion object {
fun getProductImages(context: Context): List<TankImage> {
return listOf(
TankImage(id = 1, data = getProductImage(context, 1)),
TankImage(id = 2, data = getProductImage(context, 2)),
TankImage(id = 3, data = getProductImage(context, 3))
)
}
private fun getProductMiniature(context: Context, imageId: Int): Bitmap {
val inputStream = context.assets.open("${imageId}.jpg")
val bitmap = BitmapFactory.decodeStream(inputStream)
return bitmap.resize(miniatureBound)
}
private fun getProductImage(context: Context, imageId: Int): Bitmap {
val inputStream = context.assets.open("${imageId}.jpg")
val bitmap = BitmapFactory.decodeStream(inputStream)
return bitmap.resize(tankImageBound)
}
}
}

View File

@ -2,12 +2,22 @@ package ru.ulstu.`is`.pmu.tank.model
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Ignore import androidx.room.Ignore
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import ru.ulstu.`is`.pmu.R import ru.ulstu.`is`.pmu.R
@Entity( @Entity(
tableName = "tanks" tableName = "tanks",
foreignKeys = [
ForeignKey(
entity = TankImage::class,
parentColumns = ["image_id"],
childColumns = ["image_id"],
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.RESTRICT
)
]
) )
data class Tank( data class Tank(
@PrimaryKey(autoGenerate = true) @PrimaryKey(autoGenerate = true)
@ -16,21 +26,21 @@ data class Tank(
val name: String, val name: String,
@ColumnInfo(name = "price") @ColumnInfo(name = "price")
val price: Int, val price: Int,
@ColumnInfo(name="image") @ColumnInfo(name = "image_id", index = true)
val image: Int, val imageId: Long,
@ColumnInfo(name = "levelId", index = true) @ColumnInfo(name = "levelId", index = true)
val levelId: Long?, val levelId: Long?,
@ColumnInfo(name = "nationId", index = true) @ColumnInfo(name = "nationId", index = true)
val nationId: Long? val nationId: Long?,
) { ) {
@Ignore @Ignore
constructor( constructor(
name: String, name: String,
price: Int, price: Int,
image: Int, imageId: Long,
level: Level, level: Level,
nation: Nation nation: Nation
) : this(null, name, price, image, level.uid, nation.uid) ) : this(null, name, price, imageId, level.uid, nation.uid)
companion object { companion object {
fun getTank(index: Long = 0): Tank { fun getTank(index: Long = 0): Tank {
@ -38,7 +48,7 @@ data class Tank(
index, index,
"Первый танк", "Первый танк",
100000, 100000,
R.drawable.t_34_85, 1L,
1, 1,
1 1
) )

View File

@ -0,0 +1,36 @@
package ru.ulstu.`is`.pmu.tank.model
import android.graphics.Bitmap
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "tank_images")
data class TankImage(
@ColumnInfo(name = "image_id")
@PrimaryKey(autoGenerate = true)
val id: Long?,
val data: Bitmap
) {
override fun equals(other: Any?): Boolean {
if (this === other) {
return true
}
if (javaClass != other?.javaClass) {
return false
}
other as TankImage
if (id != other.id) {
return false
}
return true
}
override fun hashCode(): Int {
return (id ?: -1) as Int
}
}

View File

@ -1,5 +1,6 @@
package ru.ulstu.`is`.pmu.tank.model package ru.ulstu.`is`.pmu.tank.model
import android.graphics.Bitmap
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Embedded import androidx.room.Embedded
import androidx.room.Entity import androidx.room.Entity
@ -13,7 +14,7 @@ data class TankWithNationAndLevel (
@ColumnInfo(name = "price") @ColumnInfo(name = "price")
val price: Int, val price: Int,
@ColumnInfo(name="image") @ColumnInfo(name="image")
val image: Int, val image: Bitmap,
val level: Int, val level: Int,
val nationName: String val nationName: String
) { ) {

View File

@ -1,20 +1,49 @@
package ru.ulstu.`is`.pmu.tank.repository package ru.ulstu.`is`.pmu.tank.repository
import android.graphics.Bitmap
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import ru.ulstu.`is`.pmu.tank.dao.TankDao import ru.ulstu.`is`.pmu.tank.dao.TankDao
import ru.ulstu.`is`.pmu.tank.dao.TankImageDao
import ru.ulstu.`is`.pmu.tank.model.Tank import ru.ulstu.`is`.pmu.tank.model.Tank
import ru.ulstu.`is`.pmu.tank.model.TankImage
import ru.ulstu.`is`.pmu.tank.model.TankWithNationAndLevel import ru.ulstu.`is`.pmu.tank.model.TankWithNationAndLevel
class OfflineTankRepository(private val tankDao: TankDao) : TankRepository { class OfflineTankRepository(
private val tankDao: TankDao,
private val tankImageDao: TankImageDao
) : TankRepository {
override fun getAll(): Flow<List<Tank>> = tankDao.getAll() override fun getAll(): Flow<List<Tank>> = tankDao.getAll()
override fun getTank(uid: Long): Flow<Tank?> = tankDao.getTankUid(uid) override fun getTank(uid: Long): Flow<Tank?> = tankDao.getTankUid(uid)
override fun getUserTanks(uid: Long): Flow<List<TankWithNationAndLevel>> = tankDao.getUserTanks(uid) override fun getUserTanks(uid: Long): Flow<List<TankWithNationAndLevel>> = tankDao.getUserTanks(uid)
override suspend fun insertTank(tank: Tank) = tankDao.insert(tank) override suspend fun insertTank(tank: Tank, image: Bitmap) {
val imageId = tankImageDao.insert(
TankImage(
id = null,
data = image
)
)
override suspend fun updateTank(tank: Tank) = tankDao.update(tank) tankDao.insert(tank.copy(imageId = imageId))
}
override suspend fun updateTank(tank: Tank, image: Bitmap) {
val imageId = tankImageDao.insert(
TankImage(
id = null,
data = image
)
)
tankDao.update(tank.copy(imageId = imageId))
tankImageDao.delete(tank.imageId)
}
override suspend fun deleteTank(tank: Tank) = tankDao.delete(tank) override suspend fun deleteTank(tank: Tank) = tankDao.delete(tank)
override suspend fun delete(tankId: Long) {
tankDao.delete(tankId)
}
} }

View File

@ -1,5 +1,6 @@
package ru.ulstu.`is`.pmu.tank.repository package ru.ulstu.`is`.pmu.tank.repository
import android.graphics.Bitmap
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import ru.ulstu.`is`.pmu.tank.model.Tank import ru.ulstu.`is`.pmu.tank.model.Tank
import ru.ulstu.`is`.pmu.tank.model.TankWithNationAndLevel import ru.ulstu.`is`.pmu.tank.model.TankWithNationAndLevel
@ -8,7 +9,8 @@ interface TankRepository {
fun getAll(): Flow<List<Tank>> fun getAll(): Flow<List<Tank>>
fun getTank(uid: Long): Flow<Tank?> fun getTank(uid: Long): Flow<Tank?>
fun getUserTanks(uid: Long): Flow<List<TankWithNationAndLevel>> fun getUserTanks(uid: Long): Flow<List<TankWithNationAndLevel>>
suspend fun insertTank(tank: Tank) suspend fun insertTank(tank: Tank, image: Bitmap)
suspend fun updateTank(tank: Tank) suspend fun updateTank(tank: Tank, image: Bitmap)
suspend fun deleteTank(tank: Tank) suspend fun deleteTank(tank: Tank)
suspend fun delete(tankId: Long)
} }

View File

@ -1,6 +1,7 @@
package ru.ulstu.`is`.pmu.tanks.composeui package ru.ulstu.`is`.pmu.tanks.composeui
import android.content.res.Configuration import android.content.res.Configuration
import android.graphics.Bitmap
import android.util.Log import android.util.Log
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@ -39,6 +40,9 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController import androidx.navigation.NavController
import com.application.ui.miniatureBound
import com.application.ui.resize
import com.application.ui.tankImageBound
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import ru.ulstu.`is`.pmu.R import ru.ulstu.`is`.pmu.R
import ru.ulstu.`is`.pmu.tank.composeui.edit.LevelDropDownViewModel import ru.ulstu.`is`.pmu.tank.composeui.edit.LevelDropDownViewModel
@ -54,6 +58,7 @@ import ru.ulstu.`is`.pmu.tank.composeui.edit.toUiState
import ru.ulstu.`is`.pmu.tank.model.Level import ru.ulstu.`is`.pmu.tank.model.Level
import ru.ulstu.`is`.pmu.tank.model.Nation import ru.ulstu.`is`.pmu.tank.model.Nation
import ru.ulstu.`is`.pmu.tank.model.Tank import ru.ulstu.`is`.pmu.tank.model.Tank
import ru.ulstu.`is`.pmu.tanks.composeui.image.CuteImageUploader
import ru.ulstu.`is`.pmu.ui.AppViewModelProvider import ru.ulstu.`is`.pmu.ui.AppViewModelProvider
import ru.ulstu.`is`.pmu.ui.theme.CustomDark import ru.ulstu.`is`.pmu.ui.theme.CustomDark
import ru.ulstu.`is`.pmu.ui.theme.CustomOrange import ru.ulstu.`is`.pmu.ui.theme.CustomOrange
@ -73,6 +78,7 @@ fun Constructor(
nationDropDownViewModel.setCurrentNation(tankEditViewModel.tankUiState.tankDetails.nationId ?: 1) nationDropDownViewModel.setCurrentNation(tankEditViewModel.tankUiState.tankDetails.nationId ?: 1)
Constructor( Constructor(
tankViewModel = tankEditViewModel,
tankUiState = tankEditViewModel.tankUiState, tankUiState = tankEditViewModel.tankUiState,
levelUiState = levelDropDownViewModel.levelUiState, levelUiState = levelDropDownViewModel.levelUiState,
levelsListUiState = levelDropDownViewModel.levelsListUiState, levelsListUiState = levelDropDownViewModel.levelsListUiState,
@ -192,6 +198,7 @@ private fun NationDropDown(
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun Constructor( private fun Constructor(
tankViewModel: TankEditViewModel,
tankUiState: TankUiState, tankUiState: TankUiState,
levelUiState: LevelUiState, levelUiState: LevelUiState,
levelsListUiState: LevelsListUiState, levelsListUiState: LevelsListUiState,
@ -208,6 +215,15 @@ private fun Constructor(
//для работы выпадающего списка наций //для работы выпадающего списка наций
var expandedNation by remember { mutableStateOf(false) } var expandedNation by remember { mutableStateOf(false) }
fun handleImageUpload(bitmap: Bitmap) {
tankViewModel.updateUiState(
tankUiState.tankDetails.copy(
image = bitmap.resize(tankImageBound),
miniature = bitmap.resize(miniatureBound)
)
)
}
Column( Column(
verticalArrangement = Arrangement.spacedBy(35.dp), verticalArrangement = Arrangement.spacedBy(35.dp),
modifier = Modifier.padding(0.dp, 15.dp) modifier = Modifier.padding(0.dp, 15.dp)
@ -234,6 +250,8 @@ private fun Constructor(
modifier = Modifier.padding(0.dp, 5.dp) modifier = Modifier.padding(0.dp, 5.dp)
) { ) {
Text(text="Название:", fontSize = 30.sp, color = Color.Black, fontWeight = FontWeight.Bold) Text(text="Название:", fontSize = 30.sp, color = Color.Black, fontWeight = FontWeight.Bold)
Spacer(Modifier.height(25.dp))
Text(text="Изображение:", fontSize = 30.sp, color = Color.Black, fontWeight = FontWeight.Bold)
Spacer(Modifier.height(30.dp)) Spacer(Modifier.height(30.dp))
Text(text="Уровень:", fontSize = 30.sp, color = Color.Black, fontWeight = FontWeight.Bold) Text(text="Уровень:", fontSize = 30.sp, color = Color.Black, fontWeight = FontWeight.Bold)
Spacer(Modifier.height(30.dp)) Spacer(Modifier.height(30.dp))
@ -250,6 +268,11 @@ private fun Constructor(
.width(200.dp), .width(200.dp),
) )
Spacer(Modifier.height(10.dp)) Spacer(Modifier.height(10.dp))
CuteImageUploader(
bitmap = tankUiState.tankDetails.image,
onResult = { bitmap: Bitmap -> handleImageUpload(bitmap) }
)
Spacer(Modifier.height(10.dp))
// Выпадающий список уровней // Выпадающий список уровней
LevelDropDown( LevelDropDown(
levelUiState = levelUiState, levelUiState = levelUiState,
@ -421,6 +444,7 @@ fun ConstructorEditPreview() {
color = CustomDark color = CustomDark
) { ) {
Constructor( Constructor(
tankViewModel = viewModel(factory = AppViewModelProvider.Factory),
tankUiState = Tank.getTank().toUiState(true), tankUiState = Tank.getTank().toUiState(true),
levelUiState = Level.DEMO_LEVEL.toUiState(), levelUiState = Level.DEMO_LEVEL.toUiState(),
levelsListUiState = LevelsListUiState(listOf()), levelsListUiState = LevelsListUiState(listOf()),

View File

@ -0,0 +1,25 @@
package ru.ulstu.`is`.pmu.tanks.composeui.image
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.layout.ContentScale
@Composable
fun CuteImage(
imageBitmap: ImageBitmap,
modifier: Modifier = Modifier
) {
Image(
bitmap = imageBitmap,
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = modifier
.aspectRatio(1F)
.clip(RoundedCornerShape(Dimensions.cornerRadius))
)
}

View File

@ -0,0 +1,31 @@
package ru.ulstu.`is`.pmu.tanks.composeui.image
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.unit.dp
@Composable
fun CuteImageButton(
imageBitmap: ImageBitmap,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
val cornerShape = RoundedCornerShape(Dimensions.cornerRadius)
Button(
onClick = { onClick() },
modifier = modifier,
shape = cornerShape,
contentPadding = PaddingValues(0.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color.Transparent
)
) {
CuteImage(imageBitmap)
}
}

View File

@ -0,0 +1,132 @@
package ru.ulstu.`is`.pmu.tanks.composeui.image
import android.content.res.Configuration
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import ru.ulstu.`is`.pmu.R
import ru.ulstu.`is`.pmu.ui.theme.CustomDark
import ru.ulstu.`is`.pmu.ui.theme.CustomOrange
import ru.ulstu.`is`.pmu.ui.theme.PmudemoTheme
@Composable
fun CuteImageUploader(
bitmap: Bitmap?,
onResult: (Bitmap) -> Unit
) {
val context = LocalContext.current
val title: String = if (bitmap == null) {
stringResource(R.string.not_uploaded)
} else {
stringResource(R.string.size, bitmap.width, bitmap.height)
}
val launcher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.GetContent(),
onResult = { uri: Uri? ->
uri?.let {
val inputStream = context.contentResolver.openInputStream(uri)
val newBitmap: Bitmap = BitmapFactory.decodeStream(inputStream)
onResult(newBitmap)
}
}
)
fun handleUploadButtonClick() {
launcher.launch("image/*")
}
Row(
modifier = Modifier.height(IntrinsicSize.Min)
) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.background(
color = MaterialTheme.colorScheme.surface,
shape = RoundedCornerShape(Dimensions.cornerRadius)
)
.weight(0.25F)
.aspectRatio(1F)
) {
if (bitmap != null) {
CuteImage(
imageBitmap = bitmap.asImageBitmap(),
modifier = Modifier.width(100.dp)
)
} else {
Icon(
painter = painterResource(R.drawable.ic_camera),
contentDescription = null,
tint = MaterialTheme.colorScheme.onBackground
)
}
}
Spacer(modifier = Modifier.width(10.dp))
Column(
modifier = Modifier
.background(
color = MaterialTheme.colorScheme.surface,
shape = RoundedCornerShape(Dimensions.cornerRadius)
)
.fillMaxHeight()
.padding(10.dp)
.weight(0.75F)
) {
Text(
text = title,
color = MaterialTheme.colorScheme.onBackground
)
Spacer(modifier = Modifier.weight(1F))
Button(
modifier = Modifier
.width(200.dp)
.height(50.dp),
colors = ButtonDefaults.buttonColors(
containerColor = CustomOrange,
contentColor = CustomDark
),
onClick = { handleUploadButtonClick() },
){
Text(text = "Загрузить изображение")
}
}
}
}
@Preview(name = "Light Mode", uiMode = Configuration.UI_MODE_NIGHT_NO)
@Preview(name = "Dark Mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun CuteImageLoaderPreview() {
PmudemoTheme {
CuteImageUploader(null) {}
}
}

View File

@ -0,0 +1,61 @@
package ru.ulstu.`is`.pmu.tanks.composeui.image
import androidx.compose.foundation.Image
import androidx.compose.foundation.gestures.detectTransformGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableFloatStateOf
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.clip
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.unit.IntSize
@Composable
fun CuteZoomableImage(
imageBitmap: ImageBitmap
) {
val size = remember { mutableStateOf(IntSize.Zero) }
val scale = remember { mutableFloatStateOf(1f) }
val offsetX = remember { mutableFloatStateOf(0f) }
val offsetY = remember { mutableFloatStateOf(0f) }
Box(
modifier = Modifier
.clip(RectangleShape)
.fillMaxSize()
.pointerInput(Unit) {
detectTransformGestures { _, pan, zoom, _ ->
scale.floatValue = maxOf(1f, minOf(3f, scale.floatValue * zoom))
val maxX = (size.value.width * (scale.floatValue - 1)) / 2
val minX = -maxX
offsetX.floatValue = maxOf(minX, minOf(maxX, offsetX.floatValue + pan.x))
val maxY = (size.value.height * (scale.floatValue - 1)) / 2
val minY = -maxY
offsetY.floatValue = maxOf(minY, minOf(maxY, offsetY.floatValue + pan.y))
}
}.onSizeChanged {
size.value = it
}
) {
Image(
bitmap = imageBitmap,
contentDescription = null,
modifier = Modifier
.align(Alignment.Center)
.graphicsLayer(
scaleX = scale.floatValue,
scaleY = scale.floatValue,
translationX = offsetX.floatValue,
translationY = offsetY.floatValue
)
)
}
}

View File

@ -0,0 +1,7 @@
package ru.ulstu.`is`.pmu.tanks.composeui.image
import androidx.compose.ui.unit.dp
object Dimensions {
val cornerRadius = 10.dp
}

View File

@ -0,0 +1,26 @@
package com.application.ui
import android.graphics.Bitmap
import kotlin.math.sqrt
const val miniatureBound: Int = 250
const val tankImageBound: Int = 800
fun getEmptyBitmap(): Bitmap {
return Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)
}
fun Bitmap.resize(bound: Int): Bitmap {
val width = this.width
val height = this.height
val size: Int = width * height
val newSize: Int = bound * bound
val factor: Double = if (size > newSize) {
sqrt(size.toDouble() / newSize)
} else {
1.0
}
val newWidth: Int = (width / factor).toInt()
val newHeight: Int = (height / factor).toInt()
return Bitmap.createScaledBitmap(this, newWidth, newHeight, false)
}

View File

@ -0,0 +1,21 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="42dp"
android:height="36dp"
android:viewportWidth="42"
android:viewportHeight="36">
<path
android:pathData="M35,34H7c-2.76,0 -5,-2.24 -5,-5V12c0,-2.76 2.24,-5 5,-5h6l1.25,-3.11C14.7,2.75 15.8,2 17.03,2h7.94c1.23,0 2.33,0.75 2.79,1.89L29,7h6c2.76,0 5,2.24 5,5v17C40,31.76 37.76,34 35,34z"
android:strokeLineJoin="round"
android:strokeWidth="2.5"
android:fillColor="#00000000"
android:strokeColor="#FFFFFF"/>
<path
android:pathData="M21,21m-8,0a8,8 0,1 1,16 0a8,8 0,1 1,-16 0"
android:strokeLineJoin="round"
android:strokeWidth="2.5"
android:fillColor="#00000000"
android:strokeColor="#FFFFFF"/>
<path
android:fillColor="#FFFFFFFF"
android:pathData="M33,16L33,16c-1.1,0 -2,-0.9 -2,-2v0c0,-1.1 0.9,-2 2,-2h0c1.1,0 2,0.9 2,2v0C35,15.1 34.1,16 33,16z"/>
</vector>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

View File

@ -31,4 +31,10 @@
<p>Здесь могла быть Ваша реклама!</p>\n\n <p>Здесь могла быть Ваша реклама!</p>\n\n
<p>Наш сайт <a href="https://ulstu.ru">ulstu.ru</a></p> <p>Наш сайт <a href="https://ulstu.ru">ulstu.ru</a></p>
</string> </string>
<!-- IMAGE -->
<string name="image">Image</string>
<string name="not_uploaded">Not uploaded</string>
<string name="size">Size: %1$dx%2$d</string>
<string name="upload_image">Upload image</string>
</resources> </resources>