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

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.filled.ArrowBack
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExposedDropdownMenuBox
@ -54,7 +55,6 @@ import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
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.CustomOrange
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.Constructor
import ru.ulstu.`is`.pmu.tanks.composeui.Hangar
import ru.ulstu.`is`.pmu.ui.theme.CustomYellow
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@ -149,12 +150,6 @@ fun Navhost(
) { Constructor(navController) }
composable(Screen.Hangar.route) { Hangar(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){
DropDownList(navController)
Button(
modifier = Modifier
.width(200.dp)
.height(50.dp),
colors = ButtonDefaults.buttonColors(
containerColor = CustomOrange,
contentColor = CustomDark),
onClick = {
val route = Screen.Constructor.route.replace("{id}", 0.toString())
navController.navigate(route)

View File

@ -41,7 +41,6 @@ enum class Screen(
companion object {
val bottomBarItems = listOf(
TankList,
Constructor,
Hangar,
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
import android.graphics.Bitmap
import android.util.Log
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@ -7,6 +8,7 @@ import androidx.compose.runtime.setValue
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.application.ui.getEmptyBitmap
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
@ -44,10 +46,11 @@ class TankEditViewModel(
suspend fun saveTank() {
if (validateInput()) {
val image: Bitmap = tankUiState.tankDetails.image!!
if (tankUid > 0) {
tankRepository.updateTank(tankUiState.tankDetails.toTank(tankUid))
tankRepository.updateTank(tankUiState.tankDetails.toTank(tankUid), image = image)
} else {
tankRepository.insertTank(tankUiState.tankDetails.toTank())
tankRepository.insertTank(tankUiState.tankDetails.toTank(), image = image)
}
}
}
@ -56,9 +59,9 @@ class TankEditViewModel(
return with(uiState) {
name.isNotBlank()
&& price > 0
&& image > 0
&& levelId!! > 0
&& nationId!! > 0
&& image != null
}
}
}
@ -71,26 +74,28 @@ data class TankUiState(
data class TankDetails(
val name: String = "",
val price: Int = 0,
val image: Int = 0,
val levelId: 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(
tankId = uid,
name = name,
price = price,
image = image,
levelId = levelId,
nationId = nationId
nationId = nationId,
imageId = imageId
)
fun Tank.toDetails(): TankDetails = TankDetails(
name = name,
price = price,
image = image,
levelId = levelId,
nationId = nationId
nationId = nationId,
imageId = imageId
)
fun Tank.toUiState(isEntryValid: Boolean = false): TankUiState = TankUiState(

View File

@ -23,8 +23,9 @@ interface TankDao {
//получаем все танки пользователя по его Id
@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 tank_images as ti on t.image_id = ti.image_id " +
"LEFT JOIN levels as l on t.levelId = l.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"
@ -39,4 +40,7 @@ interface TankDao {
@Delete
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())
}
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 {
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.NationDao
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.model.Level
import ru.ulstu.`is`.pmu.tank.model.Nation
@ -27,14 +28,15 @@ abstract class AppDatabase : RoomDatabase() {
abstract fun levelDao(): LevelDao
abstract fun tankDao() : TankDao
abstract fun userDao() : UserDao
abstract fun tankImageDao() : TankImageDao
companion object {
private const val DB_NAME: String = "12-db"
private const val DB_NAME: String = "13-db"
@Volatile
private var INSTANCE: AppDatabase? = null
private suspend fun populateDatabase() {
private suspend fun populateDatabase(context: Context) {
INSTANCE?.let { database ->
// Nations
val nationDao = database.nationDao()
@ -84,19 +86,23 @@ abstract class AppDatabase : RoomDatabase() {
levelDao.insert(level9)
levelDao.insert(level10)
//images
val tankImageDao = database.tankImageDao()
tankImageDao.insertMany(PrepopulateStore.getProductImages(context))
//Tanks
val tankDao = database.tankDao()
val tank1 = Tank(20L,"МС-1", 1000, R.drawable.t_34_85, level1.uid, nation1.uid)
val tank2 = Tank(21L, "Т-34-85", 960000, R.drawable.t_34_85, level6.uid, nation1.uid)
val tank10 = Tank(22L, "Pershing", 1260000, R.drawable.sherman, level8.uid, nation3.uid)
val tank6 = Tank(23L, "Ferdinand", 2500000, R.drawable.tiger_1, level8.uid, nation2.uid)
val tank3 = Tank(24L, "ИС-2", 1230000, R.drawable.t_34_85, level7.uid, nation1.uid)
val tank4 = Tank(25L, "ИСУ-152", 2350000, R.drawable.t_34_85, level8.uid, nation1.uid)
val tank5 = Tank(26L, "Tiger 1", 1430000, R.drawable.tiger_1, level7.uid, nation2.uid)
val tank7 = Tank(27L, "Tiger 2", 2500000, R.drawable.tiger_1, level8.uid, nation2.uid)
val tank8 = Tank(28L, "Panther", 1350000, R.drawable.tiger_1, level7.uid, nation2.uid)
val tank9 = Tank(29L, "M4A2E3", 990000, R.drawable.sherman, level6.uid, nation3.uid)
val tank11 = Tank(30L, "Hellcat", 940000, R.drawable.sherman, level7.uid, nation3.uid)
val tank1 = Tank(20L,"МС-1", 1000, 1L, level1.uid, nation1.uid)
val tank2 = Tank(21L, "Т-34-85", 960000, 1L, level6.uid, nation1.uid)
val tank10 = Tank(22L, "Pershing", 1260000,3L, level8.uid, nation3.uid)
val tank6 = Tank(23L, "Ferdinand", 2500000, 2L, level8.uid, nation2.uid)
val tank3 = Tank(24L, "ИС-2", 1230000, 1L, level7.uid, nation1.uid)
val tank4 = Tank(25L, "ИСУ-152", 2350000, 1L, level8.uid, nation1.uid)
val tank5 = Tank(26L, "Tiger 1", 1430000,2L, level7.uid, nation2.uid)
val tank7 = Tank(27L, "Tiger 2", 2500000, 2L, level8.uid, nation2.uid)
val tank8 = Tank(28L, "Panther", 1350000, 2L, level7.uid, nation2.uid)
val tank9 = Tank(29L, "M4A2E3", 990000, 3L, level6.uid, nation3.uid)
val tank11 = Tank(30L, "Hellcat", 940000, 3L, level7.uid, nation3.uid)
tankDao.insert(tank1)
tankDao.insert(tank2)
@ -135,7 +141,7 @@ abstract class AppDatabase : RoomDatabase() {
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
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.Entity
import androidx.room.ForeignKey
import androidx.room.Ignore
import androidx.room.PrimaryKey
import ru.ulstu.`is`.pmu.R
@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(
@PrimaryKey(autoGenerate = true)
@ -16,21 +26,21 @@ data class Tank(
val name: String,
@ColumnInfo(name = "price")
val price: Int,
@ColumnInfo(name="image")
val image: Int,
@ColumnInfo(name = "image_id", index = true)
val imageId: Long,
@ColumnInfo(name = "levelId", index = true)
val levelId: Long?,
@ColumnInfo(name = "nationId", index = true)
val nationId: Long?
val nationId: Long?,
) {
@Ignore
constructor(
name: String,
price: Int,
image: Int,
imageId: Long,
level: Level,
nation: Nation
) : this(null, name, price, image, level.uid, nation.uid)
) : this(null, name, price, imageId, level.uid, nation.uid)
companion object {
fun getTank(index: Long = 0): Tank {
@ -38,7 +48,7 @@ data class Tank(
index,
"Первый танк",
100000,
R.drawable.t_34_85,
1L,
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
import android.graphics.Bitmap
import androidx.room.ColumnInfo
import androidx.room.Embedded
import androidx.room.Entity
@ -13,7 +14,7 @@ data class TankWithNationAndLevel (
@ColumnInfo(name = "price")
val price: Int,
@ColumnInfo(name="image")
val image: Int,
val image: Bitmap,
val level: Int,
val nationName: String
) {

View File

@ -1,20 +1,49 @@
package ru.ulstu.`is`.pmu.tank.repository
import android.graphics.Bitmap
import kotlinx.coroutines.flow.Flow
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.TankImage
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 getTank(uid: Long): Flow<Tank?> = tankDao.getTankUid(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 delete(tankId: Long) {
tankDao.delete(tankId)
}
}

View File

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

View File

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