Compare commits
8 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dfc624431d | |||
| c896a6b1a8 | |||
| 6f3350b44c | |||
| cb3974bf6b | |||
| 9efbe25bcb | |||
| ba2a5d1928 | |||
| 0516419825 | |||
| 7387bedf9e |
|
|
@ -1,5 +1,6 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
|
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||||
<component name="GradleSettings">
|
<component name="GradleSettings">
|
||||||
<option name="linkedExternalProjectsSettings">
|
<option name="linkedExternalProjectsSettings">
|
||||||
<GradleProjectSettings>
|
<GradleProjectSettings>
|
||||||
|
|
|
||||||
50
.idea/inspectionProfiles/Project_Default.xml
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
<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>
|
||||||
10
.idea/planningMode.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?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>
|
||||||
6
.idea/vcs.xml
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
BIN
app/src/main/ic_launcher-playstore.png
Normal file
|
After Width: | Height: | Size: 449 KiB |
|
|
@ -0,0 +1,45 @@
|
||||||
|
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>?
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,120 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
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() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,368 @@
|
||||||
|
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("Отмена") } }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
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("Войти / Зарегистрироваться")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,489 @@
|
||||||
|
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 = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,344 @@
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,170 +1,74 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector
|
||||||
android:width="108dp"
|
|
||||||
android:height="108dp"
|
android:height="108dp"
|
||||||
|
android:width="108dp"
|
||||||
|
android:viewportHeight="108"
|
||||||
android:viewportWidth="108"
|
android:viewportWidth="108"
|
||||||
android:viewportHeight="108">
|
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<path
|
<path android:fillColor="#3DDC84"
|
||||||
android:fillColor="#3DDC84"
|
android:pathData="M0,0h108v108h-108z"/>
|
||||||
android:pathData="M0,0h108v108h-108z" />
|
<path android:fillColor="#00000000" android:pathData="M9,0L9,108"
|
||||||
<path
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:fillColor="#00000000"
|
<path android:fillColor="#00000000" android:pathData="M19,0L19,108"
|
||||||
android:pathData="M9,0L9,108"
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:strokeWidth="0.8"
|
<path android:fillColor="#00000000" android:pathData="M29,0L29,108"
|
||||||
android:strokeColor="#33FFFFFF" />
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
<path
|
<path android:fillColor="#00000000" android:pathData="M39,0L39,108"
|
||||||
android:fillColor="#00000000"
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:pathData="M19,0L19,108"
|
<path android:fillColor="#00000000" android:pathData="M49,0L49,108"
|
||||||
android:strokeWidth="0.8"
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:strokeColor="#33FFFFFF" />
|
<path android:fillColor="#00000000" android:pathData="M59,0L59,108"
|
||||||
<path
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:fillColor="#00000000"
|
<path android:fillColor="#00000000" android:pathData="M69,0L69,108"
|
||||||
android:pathData="M29,0L29,108"
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:strokeWidth="0.8"
|
<path android:fillColor="#00000000" android:pathData="M79,0L79,108"
|
||||||
android:strokeColor="#33FFFFFF" />
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
<path
|
<path android:fillColor="#00000000" android:pathData="M89,0L89,108"
|
||||||
android:fillColor="#00000000"
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:pathData="M39,0L39,108"
|
<path android:fillColor="#00000000" android:pathData="M99,0L99,108"
|
||||||
android:strokeWidth="0.8"
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:strokeColor="#33FFFFFF" />
|
<path android:fillColor="#00000000" android:pathData="M0,9L108,9"
|
||||||
<path
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:fillColor="#00000000"
|
<path android:fillColor="#00000000" android:pathData="M0,19L108,19"
|
||||||
android:pathData="M49,0L49,108"
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:strokeWidth="0.8"
|
<path android:fillColor="#00000000" android:pathData="M0,29L108,29"
|
||||||
android:strokeColor="#33FFFFFF" />
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
<path
|
<path android:fillColor="#00000000" android:pathData="M0,39L108,39"
|
||||||
android:fillColor="#00000000"
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:pathData="M59,0L59,108"
|
<path android:fillColor="#00000000" android:pathData="M0,49L108,49"
|
||||||
android:strokeWidth="0.8"
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:strokeColor="#33FFFFFF" />
|
<path android:fillColor="#00000000" android:pathData="M0,59L108,59"
|
||||||
<path
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:fillColor="#00000000"
|
<path android:fillColor="#00000000" android:pathData="M0,69L108,69"
|
||||||
android:pathData="M69,0L69,108"
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:strokeWidth="0.8"
|
<path android:fillColor="#00000000" android:pathData="M0,79L108,79"
|
||||||
android:strokeColor="#33FFFFFF" />
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
<path
|
<path android:fillColor="#00000000" android:pathData="M0,89L108,89"
|
||||||
android:fillColor="#00000000"
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:pathData="M79,0L79,108"
|
<path android:fillColor="#00000000" android:pathData="M0,99L108,99"
|
||||||
android:strokeWidth="0.8"
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:strokeColor="#33FFFFFF" />
|
<path android:fillColor="#00000000" android:pathData="M19,29L89,29"
|
||||||
<path
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:fillColor="#00000000"
|
<path android:fillColor="#00000000" android:pathData="M19,39L89,39"
|
||||||
android:pathData="M89,0L89,108"
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:strokeWidth="0.8"
|
<path android:fillColor="#00000000" android:pathData="M19,49L89,49"
|
||||||
android:strokeColor="#33FFFFFF" />
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
<path
|
<path android:fillColor="#00000000" android:pathData="M19,59L89,59"
|
||||||
android:fillColor="#00000000"
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:pathData="M99,0L99,108"
|
<path android:fillColor="#00000000" android:pathData="M19,69L89,69"
|
||||||
android:strokeWidth="0.8"
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:strokeColor="#33FFFFFF" />
|
<path android:fillColor="#00000000" android:pathData="M19,79L89,79"
|
||||||
<path
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:fillColor="#00000000"
|
<path android:fillColor="#00000000" android:pathData="M29,19L29,89"
|
||||||
android:pathData="M0,9L108,9"
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:strokeWidth="0.8"
|
<path android:fillColor="#00000000" android:pathData="M39,19L39,89"
|
||||||
android:strokeColor="#33FFFFFF" />
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
<path
|
<path android:fillColor="#00000000" android:pathData="M49,19L49,89"
|
||||||
android:fillColor="#00000000"
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:pathData="M0,19L108,19"
|
<path android:fillColor="#00000000" android:pathData="M59,19L59,89"
|
||||||
android:strokeWidth="0.8"
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:strokeColor="#33FFFFFF" />
|
<path android:fillColor="#00000000" android:pathData="M69,19L69,89"
|
||||||
<path
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:fillColor="#00000000"
|
<path android:fillColor="#00000000" android:pathData="M79,19L79,89"
|
||||||
android:pathData="M0,29L108,29"
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
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>
|
</vector>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@drawable/ic_launcher_background" />
|
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
<monochrome android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@drawable/ic_launcher_background" />
|
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
<monochrome android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
||||||
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 6.3 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 982 B After Width: | Height: | Size: 3.6 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 9.9 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 18 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 27 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 32 KiB |
|
|
@ -1,3 +1,3 @@
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">My Application</string>
|
<string name="app_name">Cергулек</string>
|
||||||
</resources>
|
</resources>
|
||||||