Compare commits

..

No commits in common. "master" and "1.0.0" have entirely different histories.

32 changed files with 1297 additions and 1628 deletions

View file

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>

View file

@ -1,50 +0,0 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="ComposePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewDeviceShouldUseNewSpec" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewParameterProviderOnFirstParameter" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
</profile>
</component>

View file

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="PlanningModeManager">
<option name="approvalStates">
<map>
<entry key="01c512e9-19ac-441f-9dda-3e55719b73bd" value="false" />
</map>
</option>
</component>
</project>

View file

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 449 KiB

File diff suppressed because it is too large Load diff

View file

@ -1,45 +0,0 @@
package com.sergay.hhu67.pw.app.data.models
import com.google.gson.annotations.SerializedName
data class IpInfo(
@SerializedName("ip") val ip: String?,
@SerializedName("country") val country: String?,
@SerializedName("city") val city: String?
)
data class AnswerGet(
@SerializedName("id") val id: Int,
@SerializedName("link") val link: String?,
@SerializedName("name") val name: String?,
@SerializedName("des") val des: String?
)
data class AnswerList(
@SerializedName("total") val total: Int,
@SerializedName("items") val items: List<AnswerGet>?
)
data class AnswerAdmin(
@SerializedName("id") val id: Int,
@SerializedName("username") val username: String,
@SerializedName("ip") val ip: String?,
@SerializedName("country") val country: String?,
@SerializedName("city") val city: String?,
@SerializedName("is_admin") val isAdmin: Boolean
)
data class AnswerAdminList(
@SerializedName("total") val total: Int,
@SerializedName("items") val items: List<AnswerAdmin>?
)
data class AnswerBot(
@SerializedName("id") val id: Int,
@SerializedName("phrase") val phrase: String
)
data class AnswerBotList(
@SerializedName("total") val total: Int,
@SerializedName("items") val items: List<AnswerBot>?
)

View file

@ -1,120 +0,0 @@
package com.sergay.hhu67.pw.app.data.network
import com.sergay.hhu67.pw.app.data.models.*
import okhttp3.MultipartBody
import okhttp3.OkHttpClient
import okhttp3.RequestBody
import okhttp3.ResponseBody
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.*
import java.util.concurrent.TimeUnit
lateinit var globalCookieJar: PersistentCookieJar
interface IpService {
@GET("/")
suspend fun getIpInfo(): IpInfo
}
interface ApiService {
@FormUrlEncoded
@POST("api/login")
suspend fun login(
@Field("username") user: String,
@Field("password") pass: String,
@Field("browser") browser: String = "Android Kotlin Client",
@Field("ip") ip: String,
@Field("country") country: String,
@Field("city") city: String
): Response<ResponseBody>
@GET("api/get/list")
suspend fun getImages(@Query("limit") limit: Int, @Query("offset") offset: Int): AnswerList
@GET("api/get/audio/list")
suspend fun getAudio(@Query("limit") limit: Int, @Query("offset") offset: Int): AnswerList
@GET("api/get/video/list")
suspend fun getVideo(@Query("limit") limit: Int, @Query("offset") offset: Int): AnswerList
@GET("api/get/bot/list/")
suspend fun getBotQuotes(@Query("limit") limit: Int, @Query("offset") offset: Int): AnswerBotList
@GET("api/get/admin/list/")
suspend fun getUsers(@Query("limit") limit: Int, @Query("offset") offset: Int): AnswerAdminList
@FormUrlEncoded
@POST("api/post/update/")
suspend fun updateImage(@Field("id") id: Int, @Field("name") name: String, @Field("des") des: String): Response<ResponseBody>
@FormUrlEncoded
@POST("api/post/update/audio/")
suspend fun updateAudio(@Field("id") id: Int, @Field("name") name: String, @Field("des") des: String): Response<ResponseBody>
@FormUrlEncoded
@POST("api/post/update/video/")
suspend fun updateVideo(@Field("id") id: Int, @Field("name") name: String, @Field("des") des: String): Response<ResponseBody>
@FormUrlEncoded
@POST("api/post/delete/")
suspend fun deleteImage(@Field("id") id: Int): Response<ResponseBody>
@FormUrlEncoded
@POST("api/post/delete/audio/")
suspend fun deleteAudio(@Field("id") id: Int): Response<ResponseBody>
@FormUrlEncoded
@POST("api/post/delete/video/")
suspend fun deleteVideo(@Field("id") id: Int): Response<ResponseBody>
@Multipart
@POST("api/post/user/")
suspend fun uploadImage(@Part image: MultipartBody.Part, @Part("name") name: RequestBody, @Part("des") des: RequestBody): Response<ResponseBody>
@Multipart
@POST("api/post/audio/user/")
suspend fun uploadAudio(@Part audio: MultipartBody.Part, @Part("name") name: RequestBody, @Part("des") des: RequestBody): Response<ResponseBody>
@Multipart
@POST("api/post/video/user/")
suspend fun uploadVideo(@Part video: MultipartBody.Part, @Part("name") name: RequestBody, @Part("des") des: RequestBody): Response<ResponseBody>
@FormUrlEncoded
@POST("api/post/delete/admin/user/")
suspend fun deleteUser(@Field("username") username: String): Response<ResponseBody>
@FormUrlEncoded
@POST("api/post/update/bot/")
suspend fun updateBotPhrase(@Field("id") id: Int, @Field("phrase") phrase: String): Response<ResponseBody>
@FormUrlEncoded
@POST("api/post/delete/bot/")
suspend fun deleteBotPhrase(@Field("id") id: Int): Response<ResponseBody>
companion object {
fun create(): ApiService {
val okHttp = OkHttpClient.Builder()
.cookieJar(globalCookieJar)
.connectTimeout(60, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.build()
return Retrofit.Builder()
.baseUrl("https://sergay.hhu67.pw/")
.client(okHttp)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(ApiService::class.java)
}
fun createIpService(): IpService {
return Retrofit.Builder()
.baseUrl("https://ip.hhu67.pw/")
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(IpService::class.java)
}
}
}

View file

@ -1,38 +0,0 @@
package com.sergay.hhu67.pw.app.data.network
import android.content.Context
import android.content.SharedPreferences
import androidx.core.content.edit
import okhttp3.Cookie
import okhttp3.CookieJar
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
class PersistentCookieJar(context: Context) : CookieJar {
private val prefs: SharedPreferences = context.getSharedPreferences("app_cookies", Context.MODE_PRIVATE)
private val cookieStore = mutableListOf<Cookie>()
init {
val savedToken = prefs.getString("token_cookie", null)
if (!savedToken.isNullOrEmpty()) {
Cookie.parse("https://sergay.hhu67.pw/".toHttpUrl(), savedToken)?.let {
cookieStore.add(it)
}
}
}
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
cookies.firstOrNull { it.name == "token" }?.let {
prefs.edit { putString("token_cookie", it.toString()) }
}
cookieStore.removeAll { existing -> cookies.any { it.name == existing.name } }
cookieStore.addAll(cookies)
}
override fun loadForRequest(url: HttpUrl): List<Cookie> = cookieStore
fun clear() {
cookieStore.clear()
prefs.edit { clear() }
}
}

View file

@ -1,368 +0,0 @@
package com.sergay.hhu67.pw.app.ui.components
import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.media3.common.MediaItem
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.ui.PlayerView
import coil.ImageLoader
import coil.compose.AsyncImage
import com.sergay.hhu67.pw.app.data.models.AnswerAdmin
import com.sergay.hhu67.pw.app.data.models.AnswerBot
import com.sergay.hhu67.pw.app.data.models.AnswerGet
import com.sergay.hhu67.pw.app.ui.viewmodel.AppTab
import com.sergay.hhu67.pw.app.ui.viewmodel.MainViewModel
import java.io.InputStream
import kotlin.math.ceil
@Composable
fun GridMediaItem(
item: AnswerGet,
isVideo: Boolean,
imageLoader: ImageLoader,
onClick: () -> Unit
) {
Card(
modifier = Modifier
.fillMaxWidth()
.clickable { onClick() }
) {
Column {
Box(
modifier = Modifier
.fillMaxWidth()
.height(150.dp)
) {
AsyncImage(
model = item.link,
imageLoader = imageLoader,
contentDescription = item.name,
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop
)
if (isVideo) {
Surface(
modifier = Modifier.align(Alignment.Center),
color = Color.Black.copy(alpha = 0.6f),
shape = MaterialTheme.shapes.extraLarge
) {
Icon(
Icons.Default.PlayArrow,
contentDescription = "Play",
tint = Color.White,
modifier = Modifier.size(36.dp).padding(6.dp)
)
}
}
}
Column(modifier = Modifier.padding(8.dp)) {
Text(
text = item.name ?: "Без названия",
style = MaterialTheme.typography.bodyMedium,
maxLines = 1
)
if (!item.des.isNullOrBlank()) {
Text(
text = item.des,
style = MaterialTheme.typography.labelSmall,
color = Color.LightGray,
maxLines = 2
)
}
}
}
}
}
@Composable
fun AudioListItem(
item: AnswerGet,
isAdmin: Boolean,
onPlayAudio: () -> Unit,
onEdit: () -> Unit,
onDelete: () -> Unit
) {
Card(modifier = Modifier.fillMaxWidth()) {
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
FilledIconButton(
onClick = onPlayAudio,
modifier = Modifier.size(42.dp),
colors = IconButtonDefaults.filledIconButtonColors(
containerColor = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.onPrimary
)
) {
Icon(
Icons.Default.PlayArrow,
contentDescription = "Воспроизвести",
modifier = Modifier.size(24.dp)
)
}
Spacer(Modifier.width(12.dp))
Column(modifier = Modifier.weight(1f)) {
Text(text = item.name ?: "Без названия", style = MaterialTheme.typography.titleMedium)
if (!item.des.isNullOrBlank()) {
Spacer(Modifier.height(4.dp))
Text(text = item.des, style = MaterialTheme.typography.bodyMedium, color = Color.LightGray)
}
}
if (isAdmin) {
Spacer(Modifier.width(4.dp))
IconButton(onClick = onEdit) {
Icon(Icons.Default.Edit, contentDescription = "Редактировать")
}
IconButton(onClick = onDelete) {
Icon(Icons.Default.Delete, contentDescription = "Удалить", tint = MaterialTheme.colorScheme.error)
}
}
}
}
}
@Composable
fun VideoPlayerView(url: String) {
val context = LocalContext.current
val player = remember(url) {
ExoPlayer.Builder(context).build().apply {
setMediaItem(MediaItem.fromUri(Uri.parse(url)))
prepare()
playWhenReady = true
}
}
DisposableEffect(Unit) {
onDispose { player.release() }
}
AndroidView(
factory = {
PlayerView(it).apply {
this.player = player
useController = true
}
},
modifier = Modifier
.fillMaxWidth()
.height(550.dp)
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AdminDashboard(vm: MainViewModel) {
var adminTab by remember { mutableIntStateOf(0) }
var editQuoteItem by remember { mutableStateOf<AnswerBot?>(null) }
Column(modifier = Modifier.fillMaxSize()) {
SecondaryTabRow(selectedTabIndex = adminTab) {
Tab(selected = adminTab == 0, onClick = { adminTab = 0 }, text = { Text("Юзеры (${vm.usersTotalCount})") })
Tab(selected = adminTab == 1, onClick = { adminTab = 1 }, text = { Text("Фразы бота (${vm.quotesTotalCount})") })
}
if (adminTab == 0) {
val totalPages = maxOf(1, ceil(vm.usersTotalCount / 20.0).toInt())
Column(modifier = Modifier.fillMaxSize()) {
LazyColumn(
modifier = Modifier.weight(1f),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(vm.adminUsers, key = { it.id }) { u ->
Card(modifier = Modifier.fillMaxWidth()) {
Row(modifier = Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically) {
Column(modifier = Modifier.weight(1f)) {
Text("${u.username} ${if (u.isAdmin) "(Админ)" else ""}", style = MaterialTheme.typography.titleMedium)
Text("IP: ${u.ip ?: "-"} | ${u.country ?: ""} ${u.city ?: ""}", style = MaterialTheme.typography.labelSmall, color = Color.Gray)
}
IconButton(onClick = { vm.deleteUser(u.username) }) {
Icon(Icons.Default.Delete, contentDescription = "Удалить", tint = MaterialTheme.colorScheme.error)
}
}
}
}
item {
PaginationControls(
currentPage = vm.usersCurrentPage,
totalPages = totalPages,
onPageSelected = { vm.loadAdminUsers(it) }
)
}
}
}
} else {
val totalPages = maxOf(1, ceil(vm.quotesTotalCount / 20.0).toInt())
Column(modifier = Modifier.fillMaxSize()) {
LazyColumn(
modifier = Modifier.weight(1f),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(vm.adminQuotes, key = { it.id }) { q ->
Card(modifier = Modifier.fillMaxWidth()) {
Row(modifier = Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically) {
Text(text = q.phrase, modifier = Modifier.weight(1f))
IconButton(onClick = { editQuoteItem = q }) {
Icon(Icons.Default.Edit, contentDescription = "Редактировать")
}
IconButton(onClick = { vm.deleteBotPhrase(q.id) }) {
Icon(Icons.Default.Delete, contentDescription = "Удалить", tint = MaterialTheme.colorScheme.error)
}
}
}
}
item {
PaginationControls(
currentPage = vm.quotesCurrentPage,
totalPages = totalPages,
onPageSelected = { vm.loadAdminQuotes(it) }
)
}
}
}
}
}
editQuoteItem?.let { q ->
var newPhrase by remember { mutableStateOf(q.phrase) }
AlertDialog(
onDismissRequest = { editQuoteItem = null },
title = { Text("Редактировать фразу") },
text = {
OutlinedTextField(
value = newPhrase,
onValueChange = { newPhrase = it },
label = { Text("Фраза") },
modifier = Modifier.fillMaxWidth()
)
},
confirmButton = {
Button(onClick = {
vm.updateBotPhrase(q.id, newPhrase)
editQuoteItem = null
}) { Text("Сохранить") }
},
dismissButton = { TextButton(onClick = { editQuoteItem = null }) { Text("Отмена") } }
)
}
}
@Composable
fun PaginationControls(currentPage: Int, totalPages: Int, onPageSelected: (Int) -> Unit) {
if (totalPages <= 1) return
Surface(
color = MaterialTheme.colorScheme.surface,
modifier = Modifier.fillMaxWidth(),
tonalElevation = 2.dp
) {
LazyRow(
modifier = Modifier
.padding(vertical = 8.dp, horizontal = 16.dp)
.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally),
verticalAlignment = Alignment.CenterVertically
) {
items(totalPages) { page ->
val isSelected = page == currentPage
FilterChip(
selected = isSelected,
onClick = { onPageSelected(page) },
label = { Text("${page + 1}") },
colors = FilterChipDefaults.filterChipColors(
selectedContainerColor = MaterialTheme.colorScheme.primary,
selectedLabelColor = MaterialTheme.colorScheme.onPrimary
)
)
}
}
}
}
@Composable
fun EditDialog(item: AnswerGet, onDismiss: () -> Unit, onSave: (Int, String, String) -> Unit) {
var name by remember { mutableStateOf(item.name ?: "") }
var des by remember { mutableStateOf(item.des ?: "") }
AlertDialog(
onDismissRequest = onDismiss,
title = { Text("Редактировать") },
text = {
Column {
OutlinedTextField(value = name, onValueChange = { name = it }, label = { Text("Название") })
Spacer(Modifier.height(8.dp))
OutlinedTextField(value = des, onValueChange = { des = it }, label = { Text("Описание") })
}
},
confirmButton = { Button(onClick = { onSave(item.id, name, des) }) { Text("Сохранить") } },
dismissButton = { TextButton(onClick = onDismiss) { Text("Отмена") } }
)
}
@Composable
fun UploadDialog(tab: AppTab, onDismiss: () -> Unit, onUpload: (ByteArray, String, String, String) -> Unit) {
val context = LocalContext.current
var name by remember { mutableStateOf("") }
var des by remember { mutableStateOf("") }
var fileBytes by remember { mutableStateOf<ByteArray?>(null) }
var fileName by remember { mutableStateOf("") }
val mime = when (tab) {
AppTab.IMAGES -> "image/*"
AppTab.AUDIO -> "audio/*"
AppTab.VIDEO -> "video/*"
else -> "*/*"
}
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? ->
uri?.let {
val stream: InputStream? = context.contentResolver.openInputStream(it)
fileBytes = stream?.readBytes()
fileName = "file_${System.currentTimeMillis()}"
}
}
AlertDialog(
onDismissRequest = onDismiss,
title = { Text("Загрузка: ${tab.title}") },
text = {
Column {
OutlinedTextField(value = name, onValueChange = { name = it }, label = { Text("Название") })
Spacer(Modifier.height(8.dp))
OutlinedTextField(value = des, onValueChange = { des = it }, label = { Text("Описание") })
Spacer(Modifier.height(8.dp))
Button(onClick = { launcher.launch(mime) }) {
Text(if (fileBytes != null) "Файл выбран" else "Выбрать файл")
}
}
},
confirmButton = {
Button(
enabled = fileBytes != null,
onClick = { fileBytes?.let { onUpload(it, fileName, name, des) } }
) { Text("Загрузить") }
},
dismissButton = { TextButton(onClick = onDismiss) { Text("Отмена") } }
)
}

View file

@ -1,66 +0,0 @@
package com.sergay.hhu67.pw.app.ui.screens
import android.widget.Toast
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.unit.dp
import com.sergay.hhu67.pw.app.ui.viewmodel.MainViewModel
@Composable
fun AuthScreen(vm: MainViewModel) {
val ctx = LocalContext.current
var username by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
Box(modifier = Modifier.fillMaxSize().padding(24.dp), contentAlignment = Alignment.Center) {
Card(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(24.dp), horizontalAlignment = Alignment.CenterHorizontally) {
Text("Авторизация", style = MaterialTheme.typography.titleLarge)
Spacer(Modifier.height(16.dp))
OutlinedTextField(
value = username,
onValueChange = { username = it },
label = { Text("Имя пользователя") },
singleLine = true,
modifier = Modifier.fillMaxWidth()
)
Spacer(Modifier.height(8.dp))
OutlinedTextField(
value = password,
onValueChange = { password = it },
label = { Text("Пароль") },
visualTransformation = PasswordVisualTransformation(),
singleLine = true,
modifier = Modifier.fillMaxWidth()
)
Spacer(Modifier.height(20.dp))
if (vm.isAuthLoading) {
CircularProgressIndicator()
} else {
Button(
onClick = {
if (username.isBlank() || password.isBlank()) {
Toast.makeText(ctx, "Заполните все поля", Toast.LENGTH_SHORT).show()
} else {
vm.login(username, password) { _, msg ->
Toast.makeText(ctx, msg, Toast.LENGTH_SHORT).show()
}
}
},
modifier = Modifier.fillMaxWidth()
) {
Text("Войти / Зарегистрироваться")
}
}
}
}
}
}

View file

@ -1,489 +0,0 @@
package com.sergay.hhu67.pw.app.ui.screens
import androidx.compose.foundation.background
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Logout
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.media3.common.MediaItem
import androidx.media3.exoplayer.ExoPlayer
import coil.ImageLoader
import coil.compose.AsyncImage
import coil.decode.VideoFrameDecoder
import com.sergay.hhu67.pw.app.data.models.AnswerGet
import com.sergay.hhu67.pw.app.ui.components.*
import com.sergay.hhu67.pw.app.ui.viewmodel.AppTab
import com.sergay.hhu67.pw.app.ui.viewmodel.ContentState
import com.sergay.hhu67.pw.app.ui.viewmodel.MainViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlin.math.ceil
import kotlin.system.exitProcess
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainAppScreen(vm: MainViewModel) {
val ctx = LocalContext.current
val scope = rememberCoroutineScope()
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
var showAddDialog by remember { mutableStateOf(false) }
var editItem by remember { mutableStateOf<AnswerGet?>(null) }
val exoPlayer = remember { ExoPlayer.Builder(ctx).build() }
LaunchedEffect(vm.patriotModeActive) {
if (vm.patriotModeActive) {
vm.isAudioPlaying = false
exoPlayer.stop()
val patriotUrl = "https://sergay.hhu67.pw/api/get/audio/5e4fa0b0b66f9dcff9af7c9952e0939c.mp3"
val mediaItem = MediaItem.fromUri(patriotUrl)
repeat(2) {
exoPlayer.setMediaItem(mediaItem)
exoPlayer.prepare()
exoPlayer.play()
while (exoPlayer.isPlaying || exoPlayer.playbackState != ExoPlayer.STATE_ENDED) {
delay(500)
}
}
vm.showPatriotDialog = true
delay(2000)
exitProcess(0)
}
}
LaunchedEffect(vm.inoagentModeActive) {
if (vm.inoagentModeActive) {
vm.showInoagentDialog = true
delay(2000)
exitProcess(0)
}
}
val imageLoader = remember {
ImageLoader.Builder(ctx)
.components {
add(VideoFrameDecoder.Factory())
}
.build()
}
LaunchedEffect(vm.currentAudioIndex, vm.isAudioPlaying) {
if (vm.currentAudioIndex in vm.audioPlaylist.indices) {
val track = vm.audioPlaylist[vm.currentAudioIndex]
if (!track.link.isNullOrEmpty()) {
val currentMedia = exoPlayer.currentMediaItem?.mediaId
if (currentMedia != track.link) {
exoPlayer.setMediaItem(MediaItem.Builder().setUri(track.link).setMediaId(track.link).build())
exoPlayer.prepare()
}
if (vm.isAudioPlaying) exoPlayer.play() else exoPlayer.pause()
}
} else {
exoPlayer.stop()
}
}
DisposableEffect(Unit) {
onDispose { exoPlayer.release() }
}
ModalNavigationDrawer(
drawerState = drawerState,
drawerContent = {
ModalDrawerSheet(modifier = Modifier.width(280.dp)) {
Spacer(Modifier.height(12.dp))
Text("Меню", modifier = Modifier.padding(16.dp), style = MaterialTheme.typography.titleMedium)
HorizontalDivider()
NavigationDrawerItem(
label = { Text("Фото") },
selected = vm.currentTab == AppTab.IMAGES,
onClick = {
vm.selectTab(AppTab.IMAGES)
scope.launch { drawerState.close() }
},
icon = { Icon(Icons.Default.Image, null) },
modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
)
NavigationDrawerItem(
label = { Text("Аудио") },
selected = vm.currentTab == AppTab.AUDIO,
onClick = {
vm.selectTab(AppTab.AUDIO)
scope.launch { drawerState.close() }
},
icon = { Icon(Icons.Default.Audiotrack, null) },
modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
)
NavigationDrawerItem(
label = { Text("Видео") },
selected = vm.currentTab == AppTab.VIDEO,
onClick = {
vm.selectTab(AppTab.VIDEO)
scope.launch { drawerState.close() }
},
icon = { Icon(Icons.Default.Videocam, null) },
modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
)
if (vm.isAdmin) {
NavigationDrawerItem(
label = { Text("Админка") },
selected = vm.currentTab == AppTab.ADMIN,
onClick = {
vm.selectTab(AppTab.ADMIN)
scope.launch { drawerState.close() }
},
icon = { Icon(Icons.Default.AdminPanelSettings, null) },
modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
)
}
HorizontalDivider()
NavigationDrawerItem(
label = { Text("ПАТРИОТ🇷🇺", color = Color.Red, style = MaterialTheme.typography.titleMedium) },
selected = false,
onClick = {
scope.launch { drawerState.close() }
vm.startPatriotMode()
},
icon = { Icon(Icons.Default.Star, contentDescription = null, tint = Color.Red) },
modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
)
NavigationDrawerItem(
label = { Text("ПОТУЖНОСТІ🇺🇦", color = Color.Blue, style = MaterialTheme.typography.titleMedium) },
selected = false,
onClick = {
scope.launch { drawerState.close() }
vm.startInoagentMode()
},
icon = { Icon(Icons.Default.BrightnessLow, contentDescription = null, tint = Color.Blue) },
modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
)
}
}
) {
Scaffold(
topBar = {
TopAppBar(
title = { Text(vm.currentTab.title) },
navigationIcon = {
IconButton(onClick = { scope.launch { drawerState.open() } }) {
Icon(Icons.Default.Menu, contentDescription = "Меню")
}
},
actions = {
IconButton(onClick = { vm.logout() }) {
Icon(Icons.AutoMirrored.Filled.Logout, contentDescription = "Выйти")
}
IconButton(onClick = {
if (vm.currentTab == AppTab.ADMIN) {
vm.loadAdminUsers(vm.usersCurrentPage)
vm.loadAdminQuotes(vm.quotesCurrentPage)
} else {
vm.loadMediaData(vm.currentPage)
}
}) {
Icon(Icons.Default.Refresh, contentDescription = "Обновить")
}
}
)
},
floatingActionButton = {
if (vm.isAdmin && vm.currentTab != AppTab.ADMIN) {
FloatingActionButton(onClick = { showAddDialog = true }) {
Icon(Icons.Default.Add, contentDescription = "Добавить")
}
}
},
bottomBar = {
if (vm.currentAudioIndex in vm.audioPlaylist.indices) {
val currentTrack = vm.audioPlaylist[vm.currentAudioIndex]
val dismissState = rememberSwipeToDismissBoxState(
confirmValueChange = { value ->
if (value != SwipeToDismissBoxValue.Settled) {
vm.dismissAudio()
true
} else false
}
)
SwipeToDismissBox(
state = dismissState,
backgroundContent = {
Box(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.errorContainer)
.padding(horizontal = 20.dp),
contentAlignment = Alignment.CenterEnd
) {
Icon(Icons.Default.Close, contentDescription = "Закрыть", tint = MaterialTheme.colorScheme.onErrorContainer)
}
}
) {
Surface(color = MaterialTheme.colorScheme.surfaceVariant, modifier = Modifier.fillMaxWidth()) {
Row(
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = currentTrack.name ?: "Аудиозапись",
style = MaterialTheme.typography.bodyMedium,
maxLines = 1
)
Text(
text = currentTrack.des ?: "Смахните для закрытия",
style = MaterialTheme.typography.labelSmall,
color = Color.LightGray,
maxLines = 1
)
}
IconButton(onClick = { vm.prevAudio() }, modifier = Modifier.size(36.dp)) {
Icon(Icons.Default.SkipPrevious, contentDescription = "Назад")
}
IconButton(onClick = { vm.isAudioPlaying = !vm.isAudioPlaying }, modifier = Modifier.size(36.dp)) {
Icon(if (vm.isAudioPlaying) Icons.Default.Pause else Icons.Default.PlayArrow, null)
}
IconButton(onClick = { vm.nextAudio() }, modifier = Modifier.size(36.dp)) {
Icon(Icons.Default.SkipNext, contentDescription = "Вперед")
}
IconButton(onClick = { vm.dismissAudio() }, modifier = Modifier.size(36.dp)) {
Icon(Icons.Default.Close, contentDescription = "Закрыть плеер")
}
}
}
}
}
}
) { padding ->
Box(modifier = Modifier.fillMaxSize().padding(padding)) {
if (vm.currentTab == AppTab.ADMIN) {
AdminDashboard(vm = vm)
} else {
val pageSize = if (vm.currentTab == AppTab.VIDEO) 10 else 20
val totalPages = maxOf(1, ceil(vm.totalItemsCount.toDouble() / pageSize).toInt())
when (val s = vm.state) {
is ContentState.Loading -> CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
is ContentState.Error -> Column(
modifier = Modifier.align(Alignment.Center),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(s.message, color = MaterialTheme.colorScheme.error)
Spacer(Modifier.height(8.dp))
Button(onClick = { vm.loadMediaData(vm.currentPage) }) { Text("Повторить") }
}
is ContentState.SuccessItems -> {
val pageSize = if (vm.currentTab == AppTab.VIDEO) 10 else 20
val totalPages = maxOf(1, ceil(vm.totalItemsCount.toDouble() / pageSize).toInt())
when (vm.currentTab) {
AppTab.IMAGES -> {
LazyVerticalGrid(
columns = GridCells.Fixed(2),
contentPadding = PaddingValues(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.fillMaxSize()
) {
items(s.items, key = { it.id }) { item ->
GridMediaItem(
item = item,
isVideo = false,
imageLoader = imageLoader,
onClick = {
vm.previewMediaItem = item
vm.isPreviewVideo = false
}
)
}
item(span = { GridItemSpan(maxLineSpan) }) {
PaginationControls(
currentPage = vm.currentPage,
totalPages = totalPages,
onPageSelected = { vm.loadMediaData(it) }
)
}
}
}
AppTab.VIDEO -> {
LazyVerticalGrid(
columns = GridCells.Fixed(2),
contentPadding = PaddingValues(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.fillMaxSize()
) {
items(s.items, key = { it.id }) { item ->
GridMediaItem(
item = item,
isVideo = true,
imageLoader = imageLoader,
onClick = {
vm.previewMediaItem = item
vm.isPreviewVideo = true
}
)
}
item(span = { GridItemSpan(maxLineSpan) }) {
PaginationControls(
currentPage = vm.currentPage,
totalPages = totalPages,
onPageSelected = { vm.loadMediaData(it) }
)
}
}
}
AppTab.AUDIO -> {
LazyColumn(
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
modifier = Modifier.fillMaxSize()
) {
items(s.items.size) { index ->
val item = s.items[index]
AudioListItem(
item = item,
isAdmin = vm.isAdmin,
onPlayAudio = { vm.playAudioTrack(index) },
onEdit = { editItem = item },
onDelete = { vm.deleteItem(item.id) }
)
}
item {
PaginationControls(
currentPage = vm.currentPage,
totalPages = totalPages,
onPageSelected = { vm.loadMediaData(it) }
)
}
}
}
AppTab.ADMIN -> {}
}
}
}
}
}
}
}
vm.previewMediaItem?.let { item ->
Dialog(
onDismissRequest = { vm.previewMediaItem = null },
properties = DialogProperties(usePlatformDefaultWidth = false)
) {
Card(
modifier = Modifier
.fillMaxWidth(0.95f)
.fillMaxHeight(0.85f)
.padding(8.dp)
) {
Column(
modifier = Modifier
.padding(16.dp)
.verticalScroll(rememberScrollState())
) {
if (vm.isPreviewVideo && !item.link.isNullOrEmpty()) {
VideoPlayerView(url = item.link)
} else if (!item.link.isNullOrEmpty()) {
AsyncImage(
model = item.link,
contentDescription = null,
modifier = Modifier
.fillMaxWidth()
.height(500.dp),
contentScale = ContentScale.Fit
)
}
Spacer(Modifier.height(12.dp))
Text(text = item.name ?: "Без названия", style = MaterialTheme.typography.titleLarge)
if (!item.des.isNullOrBlank()) {
Spacer(Modifier.height(4.dp))
Text(text = item.des, style = MaterialTheme.typography.bodyMedium, color = Color.LightGray)
}
Spacer(Modifier.height(16.dp))
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
if (vm.isAdmin) {
IconButton(onClick = {
editItem = item
vm.previewMediaItem = null
}) {
Icon(Icons.Default.Edit, contentDescription = "Редактировать")
}
IconButton(onClick = { vm.deleteItem(item.id) }) {
Icon(Icons.Default.Delete, contentDescription = "Удалить", tint = MaterialTheme.colorScheme.error)
}
}
Spacer(Modifier.weight(1f))
Button(onClick = { vm.previewMediaItem = null }) {
Text("Закрыть")
}
}
}
}
}
}
if (showAddDialog) {
UploadDialog(
tab = vm.currentTab,
onDismiss = { showAddDialog = false },
onUpload = { bytes, fileName, name, des ->
vm.uploadMedia(bytes, fileName, name, des)
showAddDialog = false
}
)
}
editItem?.let { item ->
EditDialog(
item = item,
onDismiss = { editItem = null },
onSave = { id, name, des ->
vm.updateItem(id, name, des)
editItem = null
}
)
}
if (vm.showPatriotDialog) {
AlertDialog(
onDismissRequest = {},
title = { Text("ПОТОУЖНОСТЬ ЗАШКАЛИВАЕТ", style = MaterialTheme.typography.headlineLarge, color = Color.Red) },
confirmButton = {}
)
}
if (vm.showInoagentDialog) {
AlertDialog(
onDismissRequest = {},
title = { Text("ИНОАГЕНТ ОБНАРУЖЕН БЫСТРО В БУСИК", style = MaterialTheme.typography.headlineLarge, color = Color.Blue) },
confirmButton = {}
)
}
}

View file

@ -1,344 +0,0 @@
package com.sergay.hhu67.pw.app.ui.viewmodel
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.net.Uri
import android.os.Build
import android.provider.Settings
import androidx.compose.runtime.*
import androidx.core.content.FileProvider
import androidx.core.content.edit
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.sergay.hhu67.pw.app.data.models.*
import com.sergay.hhu67.pw.app.data.network.ApiService
import com.sergay.hhu67.pw.app.data.network.globalCookieJar
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.File
import java.io.FileOutputStream
import kotlin.math.ceil
enum class AppTab(val title: String) {
IMAGES("Фото"),
AUDIO("Аудио"),
VIDEO("Видео"),
ADMIN("Админка")
}
sealed interface ContentState {
object Loading : ContentState
data class SuccessItems(val items: List<AnswerGet>) : ContentState
data class Error(val message: String) : ContentState
}
class MainViewModel(appContext: Context) : ViewModel() {
private val authPrefs: SharedPreferences = appContext.getSharedPreferences("auth_session", Context.MODE_PRIVATE)
private val api = ApiService.create()
private val ipApi = ApiService.createIpService()
var isAuthenticated by mutableStateOf(authPrefs.getBoolean("is_auth", false))
var isAdmin by mutableStateOf(authPrefs.getBoolean("is_admin", false))
var isAuthLoading by mutableStateOf(false)
var currentTab by mutableStateOf(AppTab.IMAGES)
var state by mutableStateOf<ContentState>(ContentState.Loading)
var currentPage by mutableIntStateOf(0)
var totalItemsCount by mutableIntStateOf(0)
var previewMediaItem by mutableStateOf<AnswerGet?>(null)
var isPreviewVideo by mutableStateOf(false)
var audioPlaylist by mutableStateOf<List<AnswerGet>>(emptyList())
var currentAudioIndex by mutableIntStateOf(-1)
var isAudioPlaying by mutableStateOf(false)
var adminUsers by mutableStateOf<List<AnswerAdmin>>(emptyList())
var usersTotalCount by mutableIntStateOf(0)
var usersCurrentPage by mutableIntStateOf(0)
var adminQuotes by mutableStateOf<List<AnswerBot>>(emptyList())
var quotesTotalCount by mutableIntStateOf(0)
var quotesCurrentPage by mutableIntStateOf(0)
var patriotModeActive by mutableStateOf(false)
var showPatriotDialog by mutableStateOf(false)
var inoagentModeActive by mutableStateOf(false)
var showInoagentDialog by mutableStateOf(false)
init {
if (isAuthenticated) {
checkRoleAndRestore()
}
}
private fun checkRoleAndRestore() {
viewModelScope.launch {
try {
api.getUsers(limit = 1, offset = 0)
isAdmin = true
authPrefs.edit { putBoolean("is_admin", true) }
} catch (e: retrofit2.HttpException) {
if (e.code() == 403) {
isAdmin = false
authPrefs.edit { putBoolean("is_admin", false) }
}
} catch (_: Exception) {}
selectTab(AppTab.IMAGES)
}
}
fun login(user: String, pass: String, onResult: (Boolean, String) -> Unit) {
viewModelScope.launch {
isAuthLoading = true
try {
val ipInfo = try {
ipApi.getIpInfo()
} catch (_: Exception) {
IpInfo("127.0.0.1", "Unknown", "Unknown")
}
val resp = api.login(
user = user,
pass = pass,
ip = ipInfo.ip ?: "127.0.0.1",
country = ipInfo.country ?: "Unknown",
city = ipInfo.city ?: "Unknown"
)
if (resp.isSuccessful) {
isAuthenticated = true
authPrefs.edit { putBoolean("is_auth", true) }
try {
api.getUsers(limit = 1, offset = 0)
isAdmin = true
authPrefs.edit { putBoolean("is_admin", true) }
onResult(true, "Вход выполнен: Администратор")
} catch (e: retrofit2.HttpException) {
isAdmin = false
authPrefs.edit { putBoolean("is_admin", false) }
onResult(true, "Вход выполнен: Пользователь")
}
selectTab(AppTab.IMAGES)
} else {
onResult(false, "Неверный логин или пароль")
}
} catch (e: Exception) {
onResult(false, e.localizedMessage ?: "Ошибка сети")
} finally {
isAuthLoading = false
}
}
}
fun logout() {
globalCookieJar.clear()
authPrefs.edit { clear() }
isAuthenticated = false
isAdmin = false
currentTab = AppTab.IMAGES
dismissAudio()
currentPage = 0
}
fun selectTab(tab: AppTab) {
currentTab = tab
currentPage = 0
if (tab == AppTab.ADMIN) {
loadAdminUsers(0)
loadAdminQuotes(0)
} else {
loadMediaData(0)
}
}
fun loadMediaData(page: Int = currentPage) {
state = ContentState.Loading
currentPage = page
viewModelScope.launch {
try {
when (currentTab) {
AppTab.IMAGES -> {
val limit = 20
val res = api.getImages(limit = limit, offset = page * limit)
totalItemsCount = res.total
state = ContentState.SuccessItems(res.items ?: emptyList())
}
AppTab.AUDIO -> {
val limit = 20
val res = api.getAudio(limit = limit, offset = page * limit)
totalItemsCount = res.total
val list = res.items ?: emptyList()
audioPlaylist = list
state = ContentState.SuccessItems(list)
}
AppTab.VIDEO -> {
val limit = 10
val res = api.getVideo(limit = limit, offset = page * limit)
totalItemsCount = res.total
state = ContentState.SuccessItems(res.items ?: emptyList())
}
AppTab.ADMIN -> {}
}
} catch (e: Exception) {
state = ContentState.Error(e.localizedMessage ?: "Ошибка загрузки")
}
}
}
fun loadAdminUsers(page: Int) {
viewModelScope.launch {
try {
val res = api.getUsers(limit = 20, offset = page * 20)
adminUsers = res.items ?: emptyList()
usersTotalCount = res.total
usersCurrentPage = page
} catch (_: Exception) {}
}
}
fun loadAdminQuotes(page: Int) {
viewModelScope.launch {
try {
val res = api.getBotQuotes(limit = 20, offset = page * 20)
adminQuotes = res.items ?: emptyList()
quotesTotalCount = res.total
quotesCurrentPage = page
} catch (_: Exception) {}
}
}
fun playAudioTrack(index: Int) {
if (index in audioPlaylist.indices) {
currentAudioIndex = index
isAudioPlaying = true
}
}
fun dismissAudio() {
currentAudioIndex = -1
isAudioPlaying = false
}
fun nextAudio() {
if (audioPlaylist.isNotEmpty()) {
currentAudioIndex = (currentAudioIndex + 1) % audioPlaylist.size
isAudioPlaying = true
}
}
fun prevAudio() {
if (audioPlaylist.isNotEmpty()) {
currentAudioIndex = if (currentAudioIndex - 1 < 0) audioPlaylist.size - 1 else currentAudioIndex - 1
isAudioPlaying = true
}
}
fun deleteItem(id: Int) {
viewModelScope.launch {
try {
when (currentTab) {
AppTab.IMAGES -> api.deleteImage(id)
AppTab.AUDIO -> api.deleteAudio(id)
AppTab.VIDEO -> api.deleteVideo(id)
else -> {}
}
previewMediaItem = null
loadMediaData(currentPage)
} catch (_: Exception) {}
}
}
fun updateItem(id: Int, name: String, des: String) {
viewModelScope.launch {
try {
when (currentTab) {
AppTab.IMAGES -> api.updateImage(id, name, des)
AppTab.AUDIO -> api.updateAudio(id, name, des)
AppTab.VIDEO -> api.updateVideo(id, name, des)
else -> {}
}
previewMediaItem = null
loadMediaData(currentPage)
} catch (_: Exception) {}
}
}
fun deleteUser(username: String) {
viewModelScope.launch {
try {
api.deleteUser(username)
loadAdminUsers(usersCurrentPage)
} catch (_: Exception) {}
}
}
fun updateBotPhrase(id: Int, phrase: String) {
viewModelScope.launch {
try {
api.updateBotPhrase(id, phrase)
loadAdminQuotes(quotesCurrentPage)
} catch (_: Exception) {}
}
}
fun startPatriotMode() {
patriotModeActive = true
}
fun startInoagentMode() {
inoagentModeActive = true
}
fun deleteBotPhrase(id: Int) {
viewModelScope.launch {
try {
api.deleteBotPhrase(id)
loadAdminQuotes(quotesCurrentPage)
} catch (_: Exception) {}
}
}
fun uploadMedia(bytes: ByteArray, fileName: String, name: String, des: String) {
viewModelScope.launch {
state = ContentState.Loading
try {
val reqFile = bytes.toRequestBody("multipart/form-data".toMediaTypeOrNull())
val namePart = name.toRequestBody("text/plain".toMediaTypeOrNull())
val desPart = des.toRequestBody("text/plain".toMediaTypeOrNull())
when (currentTab) {
AppTab.IMAGES -> api.uploadImage(MultipartBody.Part.createFormData("image", fileName, reqFile), namePart, desPart)
AppTab.AUDIO -> api.uploadAudio(MultipartBody.Part.createFormData("audio", fileName, reqFile), namePart, desPart)
AppTab.VIDEO -> api.uploadVideo(MultipartBody.Part.createFormData("video", fileName, reqFile), namePart, desPart)
else -> {}
}
loadMediaData(0)
} catch (e: Exception) {
state = ContentState.Error("Ошибка загрузки: ${e.localizedMessage}")
}
}
}
}
class MainViewModelFactory(private val context: Context) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(MainViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
return MainViewModel(context) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}

View file

@ -1,74 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector
android:height="108dp"
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:viewportHeight="108"
android:height="108dp"
android:viewportWidth="108"
xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z"/>
<path android:fillColor="#00000000" android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<monochrome android:drawable="@mipmap/ic_launcher_foreground"/>
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<monochrome android:drawable="@mipmap/ic_launcher_foreground"/>
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

View file

@ -1,3 +1,3 @@
<resources>
<string name="app_name">Cергулек</string>
<string name="app_name">My Application</string>
</resources>