minor changes
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 |
|
|
@ -1,5 +1,7 @@
|
||||||
package com.sergay.hhu67.pw.app
|
package com.sergay.hhu67.pw.app
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
|
@ -16,6 +18,7 @@ import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
import androidx.compose.foundation.lazy.grid.items
|
import androidx.compose.foundation.lazy.grid.items
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.Logout
|
||||||
import androidx.compose.material.icons.filled.*
|
import androidx.compose.material.icons.filled.*
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
|
|
@ -28,7 +31,10 @@ import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
import androidx.core.content.edit
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import androidx.media3.common.MediaItem
|
import androidx.media3.common.MediaItem
|
||||||
|
|
@ -40,6 +46,7 @@ import coil.decode.VideoFrameDecoder
|
||||||
import com.google.gson.annotations.SerializedName
|
import com.google.gson.annotations.SerializedName
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import okhttp3.*
|
import okhttp3.*
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import retrofit2.Response
|
import retrofit2.Response
|
||||||
|
|
@ -96,17 +103,36 @@ data class AnswerBotList(
|
||||||
@SerializedName("items") val items: List<AnswerBot>?
|
@SerializedName("items") val items: List<AnswerBot>?
|
||||||
)
|
)
|
||||||
|
|
||||||
class AppCookieJar : CookieJar {
|
class PersistentCookieJar(context: Context) : CookieJar {
|
||||||
|
private val prefs: SharedPreferences = context.getSharedPreferences("app_cookies", Context.MODE_PRIVATE)
|
||||||
private val cookieStore = mutableListOf<Cookie>()
|
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>) {
|
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.removeAll { existing -> cookies.any { it.name == existing.name } }
|
||||||
cookieStore.addAll(cookies)
|
cookieStore.addAll(cookies)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun loadForRequest(url: HttpUrl): List<Cookie> = cookieStore
|
override fun loadForRequest(url: HttpUrl): List<Cookie> = cookieStore
|
||||||
fun clear() = cookieStore.clear()
|
|
||||||
|
fun clear() {
|
||||||
|
cookieStore.clear()
|
||||||
|
prefs.edit { clear() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val cookieJar = AppCookieJar()
|
lateinit var globalCookieJar: PersistentCookieJar
|
||||||
|
|
||||||
interface IpService {
|
interface IpService {
|
||||||
@GET("/")
|
@GET("/")
|
||||||
|
|
@ -191,7 +217,7 @@ interface ApiService {
|
||||||
companion object {
|
companion object {
|
||||||
fun create(): ApiService {
|
fun create(): ApiService {
|
||||||
val okHttp = OkHttpClient.Builder()
|
val okHttp = OkHttpClient.Builder()
|
||||||
.cookieJar(cookieJar)
|
.cookieJar(globalCookieJar)
|
||||||
.connectTimeout(60, TimeUnit.SECONDS)
|
.connectTimeout(60, TimeUnit.SECONDS)
|
||||||
.readTimeout(60, TimeUnit.SECONDS)
|
.readTimeout(60, TimeUnit.SECONDS)
|
||||||
.build()
|
.build()
|
||||||
|
|
@ -219,7 +245,7 @@ interface ApiService {
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|
||||||
enum class AppTab(val title: String) {
|
enum class AppTab(val title: String) {
|
||||||
IMAGES("Картинки"),
|
IMAGES("Фото"),
|
||||||
AUDIO("Аудио"),
|
AUDIO("Аудио"),
|
||||||
VIDEO("Видео"),
|
VIDEO("Видео"),
|
||||||
ADMIN("Админка")
|
ADMIN("Админка")
|
||||||
|
|
@ -231,39 +257,57 @@ sealed interface ContentState {
|
||||||
data class Error(val message: String) : ContentState
|
data class Error(val message: String) : ContentState
|
||||||
}
|
}
|
||||||
|
|
||||||
class MainViewModel : ViewModel() {
|
class MainViewModel(appContext: Context) : ViewModel() {
|
||||||
|
private val authPrefs: SharedPreferences = appContext.getSharedPreferences("auth_session", Context.MODE_PRIVATE)
|
||||||
private val api = ApiService.create()
|
private val api = ApiService.create()
|
||||||
private val ipApi = ApiService.createIpService()
|
private val ipApi = ApiService.createIpService()
|
||||||
|
|
||||||
var isAuthenticated by mutableStateOf(false)
|
var isAuthenticated by mutableStateOf(authPrefs.getBoolean("is_auth", false))
|
||||||
var isAdmin by mutableStateOf(false)
|
var isAdmin by mutableStateOf(authPrefs.getBoolean("is_admin", false))
|
||||||
var isAuthLoading by mutableStateOf(false)
|
var isAuthLoading by mutableStateOf(false)
|
||||||
|
|
||||||
var currentTab by mutableStateOf(AppTab.IMAGES)
|
var currentTab by mutableStateOf(AppTab.IMAGES)
|
||||||
var state by mutableStateOf<ContentState>(ContentState.Loading)
|
var state by mutableStateOf<ContentState>(ContentState.Loading)
|
||||||
|
|
||||||
// Пагинация для медиа (Картинки: 20, Аудио: 20, Видео: 10)
|
var currentPage by mutableIntStateOf(0)
|
||||||
var currentPage by mutableStateOf(0)
|
var totalItemsCount by mutableIntStateOf(0)
|
||||||
var totalItemsCount by mutableStateOf(0)
|
|
||||||
|
|
||||||
// Модальное окно просмотра Фото / Видео
|
|
||||||
var previewMediaItem by mutableStateOf<AnswerGet?>(null)
|
var previewMediaItem by mutableStateOf<AnswerGet?>(null)
|
||||||
var isPreviewVideo by mutableStateOf(false)
|
var isPreviewVideo by mutableStateOf(false)
|
||||||
|
|
||||||
// Аудиоплеер (Нижняя строка)
|
|
||||||
var audioPlaylist by mutableStateOf<List<AnswerGet>>(emptyList())
|
var audioPlaylist by mutableStateOf<List<AnswerGet>>(emptyList())
|
||||||
var currentAudioIndex by mutableStateOf(-1)
|
var currentAudioIndex by mutableIntStateOf(-1)
|
||||||
var isAudioPlaying by mutableStateOf(false)
|
var isAudioPlaying by mutableStateOf(false)
|
||||||
|
|
||||||
// Админка: юзеры
|
|
||||||
var adminUsers by mutableStateOf<List<AnswerAdmin>>(emptyList())
|
var adminUsers by mutableStateOf<List<AnswerAdmin>>(emptyList())
|
||||||
var usersTotalCount by mutableStateOf(0)
|
var usersTotalCount by mutableIntStateOf(0)
|
||||||
var usersCurrentPage by mutableStateOf(0)
|
var usersCurrentPage by mutableIntStateOf(0)
|
||||||
|
|
||||||
// Админка: фразы бота
|
|
||||||
var adminQuotes by mutableStateOf<List<AnswerBot>>(emptyList())
|
var adminQuotes by mutableStateOf<List<AnswerBot>>(emptyList())
|
||||||
var quotesTotalCount by mutableStateOf(0)
|
var quotesTotalCount by mutableIntStateOf(0)
|
||||||
var quotesCurrentPage by mutableStateOf(0)
|
var quotesCurrentPage by mutableIntStateOf(0)
|
||||||
|
|
||||||
|
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) {
|
fun login(user: String, pass: String, onResult: (Boolean, String) -> Unit) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
|
@ -285,12 +329,16 @@ class MainViewModel : ViewModel() {
|
||||||
|
|
||||||
if (resp.isSuccessful) {
|
if (resp.isSuccessful) {
|
||||||
isAuthenticated = true
|
isAuthenticated = true
|
||||||
|
authPrefs.edit { putBoolean("is_auth", true) }
|
||||||
|
|
||||||
try {
|
try {
|
||||||
api.getUsers(limit = 1, offset = 0)
|
api.getUsers(limit = 1, offset = 0)
|
||||||
isAdmin = true
|
isAdmin = true
|
||||||
|
authPrefs.edit { putBoolean("is_admin", true) }
|
||||||
onResult(true, "Вход выполнен: Администратор")
|
onResult(true, "Вход выполнен: Администратор")
|
||||||
} catch (e: retrofit2.HttpException) {
|
} catch (e: retrofit2.HttpException) {
|
||||||
isAdmin = false
|
isAdmin = false
|
||||||
|
authPrefs.edit { putBoolean("is_admin", false) }
|
||||||
onResult(true, "Вход выполнен: Пользователь")
|
onResult(true, "Вход выполнен: Пользователь")
|
||||||
}
|
}
|
||||||
selectTab(AppTab.IMAGES)
|
selectTab(AppTab.IMAGES)
|
||||||
|
|
@ -306,12 +354,12 @@ class MainViewModel : ViewModel() {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun logout() {
|
fun logout() {
|
||||||
cookieJar.clear()
|
globalCookieJar.clear()
|
||||||
|
authPrefs.edit { clear() }
|
||||||
isAuthenticated = false
|
isAuthenticated = false
|
||||||
isAdmin = false
|
isAdmin = false
|
||||||
currentTab = AppTab.IMAGES
|
currentTab = AppTab.IMAGES
|
||||||
currentAudioIndex = -1
|
dismissAudio()
|
||||||
isAudioPlaying = false
|
|
||||||
currentPage = 0
|
currentPage = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -334,15 +382,13 @@ class MainViewModel : ViewModel() {
|
||||||
when (currentTab) {
|
when (currentTab) {
|
||||||
AppTab.IMAGES -> {
|
AppTab.IMAGES -> {
|
||||||
val limit = 20
|
val limit = 20
|
||||||
val offset = page * limit
|
val res = api.getImages(limit = limit, offset = page * limit)
|
||||||
val res = api.getImages(limit = limit, offset = offset)
|
|
||||||
totalItemsCount = res.total
|
totalItemsCount = res.total
|
||||||
state = ContentState.SuccessItems(res.items ?: emptyList())
|
state = ContentState.SuccessItems(res.items ?: emptyList())
|
||||||
}
|
}
|
||||||
AppTab.AUDIO -> {
|
AppTab.AUDIO -> {
|
||||||
val limit = 20
|
val limit = 20
|
||||||
val offset = page * limit
|
val res = api.getAudio(limit = limit, offset = page * limit)
|
||||||
val res = api.getAudio(limit = limit, offset = offset)
|
|
||||||
totalItemsCount = res.total
|
totalItemsCount = res.total
|
||||||
val list = res.items ?: emptyList()
|
val list = res.items ?: emptyList()
|
||||||
audioPlaylist = list
|
audioPlaylist = list
|
||||||
|
|
@ -350,8 +396,7 @@ class MainViewModel : ViewModel() {
|
||||||
}
|
}
|
||||||
AppTab.VIDEO -> {
|
AppTab.VIDEO -> {
|
||||||
val limit = 10
|
val limit = 10
|
||||||
val offset = page * limit
|
val res = api.getVideo(limit = limit, offset = page * limit)
|
||||||
val res = api.getVideo(limit = limit, offset = offset)
|
|
||||||
totalItemsCount = res.total
|
totalItemsCount = res.total
|
||||||
state = ContentState.SuccessItems(res.items ?: emptyList())
|
state = ContentState.SuccessItems(res.items ?: emptyList())
|
||||||
}
|
}
|
||||||
|
|
@ -366,8 +411,7 @@ class MainViewModel : ViewModel() {
|
||||||
fun loadAdminUsers(page: Int) {
|
fun loadAdminUsers(page: Int) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
val offset = page * 20
|
val res = api.getUsers(limit = 20, offset = page * 20)
|
||||||
val res = api.getUsers(limit = 20, offset = offset)
|
|
||||||
adminUsers = res.items ?: emptyList()
|
adminUsers = res.items ?: emptyList()
|
||||||
usersTotalCount = res.total
|
usersTotalCount = res.total
|
||||||
usersCurrentPage = page
|
usersCurrentPage = page
|
||||||
|
|
@ -378,8 +422,7 @@ class MainViewModel : ViewModel() {
|
||||||
fun loadAdminQuotes(page: Int) {
|
fun loadAdminQuotes(page: Int) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
val offset = page * 20
|
val res = api.getBotQuotes(limit = 20, offset = page * 20)
|
||||||
val res = api.getBotQuotes(limit = 20, offset = offset)
|
|
||||||
adminQuotes = res.items ?: emptyList()
|
adminQuotes = res.items ?: emptyList()
|
||||||
quotesTotalCount = res.total
|
quotesTotalCount = res.total
|
||||||
quotesCurrentPage = page
|
quotesCurrentPage = page
|
||||||
|
|
@ -394,6 +437,11 @@ class MainViewModel : ViewModel() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun dismissAudio() {
|
||||||
|
currentAudioIndex = -1
|
||||||
|
isAudioPlaying = false
|
||||||
|
}
|
||||||
|
|
||||||
fun nextAudio() {
|
fun nextAudio() {
|
||||||
if (audioPlaylist.isNotEmpty()) {
|
if (audioPlaylist.isNotEmpty()) {
|
||||||
currentAudioIndex = (currentAudioIndex + 1) % audioPlaylist.size
|
currentAudioIndex = (currentAudioIndex + 1) % audioPlaylist.size
|
||||||
|
|
@ -487,13 +535,25 @@ class MainViewModel : ViewModel() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// 3. ЭКРАНЫ
|
// 3. UI И ЭКРАНЫ
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
globalCookieJar = PersistentCookieJar(applicationContext)
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
MaterialTheme(colorScheme = darkColorScheme(
|
MaterialTheme(colorScheme = darkColorScheme(
|
||||||
primary = Color(0xFFBB86FC),
|
primary = Color(0xFFBB86FC),
|
||||||
|
|
@ -501,7 +561,7 @@ class MainActivity : ComponentActivity() {
|
||||||
background = Color(0xFF121212)
|
background = Color(0xFF121212)
|
||||||
)) {
|
)) {
|
||||||
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
|
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
|
||||||
val vm: MainViewModel = viewModel()
|
val vm: MainViewModel = viewModel(factory = MainViewModelFactory(applicationContext))
|
||||||
if (!vm.isAuthenticated) {
|
if (!vm.isAuthenticated) {
|
||||||
AuthScreen(vm = vm)
|
AuthScreen(vm = vm)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -522,7 +582,7 @@ fun AuthScreen(vm: MainViewModel) {
|
||||||
Box(modifier = Modifier.fillMaxSize().padding(24.dp), contentAlignment = Alignment.Center) {
|
Box(modifier = Modifier.fillMaxSize().padding(24.dp), contentAlignment = Alignment.Center) {
|
||||||
Card(modifier = Modifier.fillMaxWidth()) {
|
Card(modifier = Modifier.fillMaxWidth()) {
|
||||||
Column(modifier = Modifier.padding(24.dp), horizontalAlignment = Alignment.CenterHorizontally) {
|
Column(modifier = Modifier.padding(24.dp), horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
Text("Авторизация / Регистрация", style = MaterialTheme.typography.titleLarge)
|
Text("Авторизация", style = MaterialTheme.typography.titleLarge)
|
||||||
Spacer(Modifier.height(16.dp))
|
Spacer(Modifier.height(16.dp))
|
||||||
|
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
|
|
@ -576,7 +636,6 @@ fun MainAppScreen(vm: MainViewModel) {
|
||||||
|
|
||||||
val exoPlayer = remember { ExoPlayer.Builder(ctx).build() }
|
val exoPlayer = remember { ExoPlayer.Builder(ctx).build() }
|
||||||
|
|
||||||
// Настройка Coil для превью кадров из видео
|
|
||||||
val imageLoader = remember {
|
val imageLoader = remember {
|
||||||
ImageLoader.Builder(ctx)
|
ImageLoader.Builder(ctx)
|
||||||
.components {
|
.components {
|
||||||
|
|
@ -596,6 +655,8 @@ fun MainAppScreen(vm: MainViewModel) {
|
||||||
}
|
}
|
||||||
if (vm.isAudioPlaying) exoPlayer.play() else exoPlayer.pause()
|
if (vm.isAudioPlaying) exoPlayer.play() else exoPlayer.pause()
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
exoPlayer.stop()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -609,7 +670,7 @@ fun MainAppScreen(vm: MainViewModel) {
|
||||||
title = { Text(vm.currentTab.title) },
|
title = { Text(vm.currentTab.title) },
|
||||||
actions = {
|
actions = {
|
||||||
IconButton(onClick = { vm.logout() }) {
|
IconButton(onClick = { vm.logout() }) {
|
||||||
Icon(Icons.Default.Logout, contentDescription = "Выйти")
|
Icon(Icons.AutoMirrored.Filled.Logout, contentDescription = "Выйти")
|
||||||
}
|
}
|
||||||
IconButton(onClick = {
|
IconButton(onClick = {
|
||||||
if (vm.currentTab == AppTab.ADMIN) {
|
if (vm.currentTab == AppTab.ADMIN) {
|
||||||
|
|
@ -635,62 +696,92 @@ fun MainAppScreen(vm: MainViewModel) {
|
||||||
Column {
|
Column {
|
||||||
if (vm.currentAudioIndex in vm.audioPlaylist.indices) {
|
if (vm.currentAudioIndex in vm.audioPlaylist.indices) {
|
||||||
val currentTrack = vm.audioPlaylist[vm.currentAudioIndex]
|
val currentTrack = vm.audioPlaylist[vm.currentAudioIndex]
|
||||||
Surface(color = MaterialTheme.colorScheme.surfaceVariant, modifier = Modifier.fillMaxWidth()) {
|
val dismissState = rememberSwipeToDismissBoxState(
|
||||||
Row(
|
confirmValueChange = { value ->
|
||||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
|
if (value != SwipeToDismissBoxValue.Settled) {
|
||||||
verticalAlignment = Alignment.CenterVertically
|
vm.dismissAudio()
|
||||||
) {
|
true
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
} else false
|
||||||
Text(
|
}
|
||||||
text = currentTrack.name ?: "Аудиозапись",
|
)
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
maxLines = 1
|
SwipeToDismissBox(
|
||||||
)
|
state = dismissState,
|
||||||
Text(
|
backgroundContent = {
|
||||||
text = currentTrack.des ?: "",
|
Box(
|
||||||
style = MaterialTheme.typography.labelSmall,
|
modifier = Modifier
|
||||||
color = Color.LightGray,
|
.fillMaxSize()
|
||||||
maxLines = 1
|
.background(MaterialTheme.colorScheme.errorContainer)
|
||||||
)
|
.padding(horizontal = 20.dp),
|
||||||
|
contentAlignment = Alignment.CenterEnd
|
||||||
|
) {
|
||||||
|
Icon(Icons.Default.Close, contentDescription = "Закрыть", tint = MaterialTheme.colorScheme.onErrorContainer)
|
||||||
}
|
}
|
||||||
IconButton(onClick = { vm.prevAudio() }) {
|
}
|
||||||
Icon(Icons.Default.SkipPrevious, contentDescription = "Назад")
|
) {
|
||||||
}
|
Surface(color = MaterialTheme.colorScheme.surfaceVariant, modifier = Modifier.fillMaxWidth()) {
|
||||||
IconButton(onClick = { vm.isAudioPlaying = !vm.isAudioPlaying }) {
|
Row(
|
||||||
Icon(if (vm.isAudioPlaying) Icons.Default.Pause else Icons.Default.PlayArrow, null)
|
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
|
||||||
}
|
verticalAlignment = Alignment.CenterVertically
|
||||||
IconButton(onClick = { vm.nextAudio() }) {
|
) {
|
||||||
Icon(Icons.Default.SkipNext, contentDescription = "Вперед")
|
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 = "Закрыть плеер")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
NavigationBar {
|
NavigationBar(
|
||||||
|
modifier = Modifier.height(64.dp),
|
||||||
|
tonalElevation = 4.dp
|
||||||
|
) {
|
||||||
NavigationBarItem(
|
NavigationBarItem(
|
||||||
selected = vm.currentTab == AppTab.IMAGES,
|
selected = vm.currentTab == AppTab.IMAGES,
|
||||||
onClick = { vm.selectTab(AppTab.IMAGES) },
|
onClick = { vm.selectTab(AppTab.IMAGES) },
|
||||||
icon = { Icon(Icons.Default.Image, null) },
|
icon = { Icon(Icons.Default.Image, null, modifier = Modifier.size(20.dp)) },
|
||||||
label = { Text("Фото") }
|
label = { Text("Фото", style = MaterialTheme.typography.labelSmall) }
|
||||||
)
|
)
|
||||||
NavigationBarItem(
|
NavigationBarItem(
|
||||||
selected = vm.currentTab == AppTab.AUDIO,
|
selected = vm.currentTab == AppTab.AUDIO,
|
||||||
onClick = { vm.selectTab(AppTab.AUDIO) },
|
onClick = { vm.selectTab(AppTab.AUDIO) },
|
||||||
icon = { Icon(Icons.Default.Audiotrack, null) },
|
icon = { Icon(Icons.Default.Audiotrack, null, modifier = Modifier.size(20.dp)) },
|
||||||
label = { Text("Аудио") }
|
label = { Text("Аудио", style = MaterialTheme.typography.labelSmall) }
|
||||||
)
|
)
|
||||||
NavigationBarItem(
|
NavigationBarItem(
|
||||||
selected = vm.currentTab == AppTab.VIDEO,
|
selected = vm.currentTab == AppTab.VIDEO,
|
||||||
onClick = { vm.selectTab(AppTab.VIDEO) },
|
onClick = { vm.selectTab(AppTab.VIDEO) },
|
||||||
icon = { Icon(Icons.Default.Videocam, null) },
|
icon = { Icon(Icons.Default.Videocam, null, modifier = Modifier.size(20.dp)) },
|
||||||
label = { Text("Видео") }
|
label = { Text("Видео", style = MaterialTheme.typography.labelSmall) }
|
||||||
)
|
)
|
||||||
if (vm.isAdmin) {
|
if (vm.isAdmin) {
|
||||||
NavigationBarItem(
|
NavigationBarItem(
|
||||||
selected = vm.currentTab == AppTab.ADMIN,
|
selected = vm.currentTab == AppTab.ADMIN,
|
||||||
onClick = { vm.selectTab(AppTab.ADMIN) },
|
onClick = { vm.selectTab(AppTab.ADMIN) },
|
||||||
icon = { Icon(Icons.Default.AdminPanelSettings, null) },
|
icon = { Icon(Icons.Default.AdminPanelSettings, null, modifier = Modifier.size(20.dp)) },
|
||||||
label = { Text("Админка") }
|
label = { Text("Админка", style = MaterialTheme.typography.labelSmall) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -717,43 +808,65 @@ fun MainAppScreen(vm: MainViewModel) {
|
||||||
is ContentState.SuccessItems -> {
|
is ContentState.SuccessItems -> {
|
||||||
Column(modifier = Modifier.fillMaxSize()) {
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
Box(modifier = Modifier.weight(1f)) {
|
Box(modifier = Modifier.weight(1f)) {
|
||||||
if (vm.currentTab == AppTab.IMAGES) {
|
when (vm.currentTab) {
|
||||||
LazyVerticalGrid(
|
AppTab.IMAGES -> {
|
||||||
columns = GridCells.Fixed(2),
|
LazyVerticalGrid(
|
||||||
contentPadding = PaddingValues(8.dp),
|
columns = GridCells.Fixed(2),
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
contentPadding = PaddingValues(8.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
) {
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
items(s.items, key = { it.id }) { item ->
|
) {
|
||||||
ImageGridItem(item = item, onClick = {
|
items(s.items, key = { it.id }) { item ->
|
||||||
vm.previewMediaItem = item
|
GridMediaItem(
|
||||||
vm.isPreviewVideo = false
|
item = item,
|
||||||
})
|
isVideo = false,
|
||||||
|
imageLoader = imageLoader,
|
||||||
|
onClick = {
|
||||||
|
vm.previewMediaItem = item
|
||||||
|
vm.isPreviewVideo = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
AppTab.VIDEO -> {
|
||||||
LazyColumn(
|
LazyVerticalGrid(
|
||||||
contentPadding = PaddingValues(16.dp),
|
columns = GridCells.Fixed(2),
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
contentPadding = PaddingValues(8.dp),
|
||||||
) {
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
items(s.items.size) { index ->
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
val item = s.items[index]
|
) {
|
||||||
MediaListItem(
|
items(s.items, key = { it.id }) { item ->
|
||||||
item = item,
|
GridMediaItem(
|
||||||
tab = vm.currentTab,
|
item = item,
|
||||||
imageLoader = imageLoader,
|
isVideo = true,
|
||||||
onPlayAudio = { vm.playAudioTrack(index) },
|
imageLoader = imageLoader,
|
||||||
onOpenVideo = {
|
onClick = {
|
||||||
vm.previewMediaItem = item
|
vm.previewMediaItem = item
|
||||||
vm.isPreviewVideo = true
|
vm.isPreviewVideo = true
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
AppTab.AUDIO -> {
|
||||||
|
LazyColumn(
|
||||||
|
contentPadding = PaddingValues(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
items(s.items.size) { index ->
|
||||||
|
val item = s.items[index]
|
||||||
|
AudioListItem(
|
||||||
|
item = item,
|
||||||
|
onPlayAudio = { vm.playAudioTrack(index) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AppTab.ADMIN -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Пагинатор для медиа
|
|
||||||
PaginationControls(
|
PaginationControls(
|
||||||
currentPage = vm.currentPage,
|
currentPage = vm.currentPage,
|
||||||
totalPages = totalPages,
|
totalPages = totalPages,
|
||||||
|
|
@ -766,10 +879,17 @@ fun MainAppScreen(vm: MainViewModel) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Модальное окно полного просмотра
|
|
||||||
vm.previewMediaItem?.let { item ->
|
vm.previewMediaItem?.let { item ->
|
||||||
Dialog(onDismissRequest = { vm.previewMediaItem = null }) {
|
Dialog(
|
||||||
Card(modifier = Modifier.fillMaxWidth().wrapContentHeight()) {
|
onDismissRequest = { vm.previewMediaItem = null },
|
||||||
|
properties = DialogProperties(usePlatformDefaultWidth = false)
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(0.95f)
|
||||||
|
.wrapContentHeight()
|
||||||
|
.padding(8.dp)
|
||||||
|
) {
|
||||||
Column(modifier = Modifier.padding(16.dp)) {
|
Column(modifier = Modifier.padding(16.dp)) {
|
||||||
if (vm.isPreviewVideo && !item.link.isNullOrEmpty()) {
|
if (vm.isPreviewVideo && !item.link.isNullOrEmpty()) {
|
||||||
VideoPlayerView(url = item.link)
|
VideoPlayerView(url = item.link)
|
||||||
|
|
@ -777,10 +897,13 @@ fun MainAppScreen(vm: MainViewModel) {
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = item.link,
|
model = item.link,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.fillMaxWidth().height(260.dp),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(320.dp),
|
||||||
contentScale = ContentScale.Fit
|
contentScale = ContentScale.Fit
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(Modifier.height(12.dp))
|
Spacer(Modifier.height(12.dp))
|
||||||
Text(text = item.name ?: "Без названия", style = MaterialTheme.typography.titleLarge)
|
Text(text = item.name ?: "Без названия", style = MaterialTheme.typography.titleLarge)
|
||||||
if (!item.des.isNullOrBlank()) {
|
if (!item.des.isNullOrBlank()) {
|
||||||
|
|
@ -836,25 +959,49 @@ fun MainAppScreen(vm: MainViewModel) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// 4. ЭЛЕМЕНТЫ СПИСКОВ И СЕТКИ
|
// 4. КОМПОНЕНТЫ
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ImageGridItem(item: AnswerGet, onClick: () -> Unit) {
|
fun GridMediaItem(
|
||||||
|
item: AnswerGet,
|
||||||
|
isVideo: Boolean,
|
||||||
|
imageLoader: ImageLoader,
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable { onClick() }
|
.clickable { onClick() }
|
||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
AsyncImage(
|
Box(
|
||||||
model = item.link,
|
|
||||||
contentDescription = item.name,
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(160.dp),
|
.height(150.dp)
|
||||||
contentScale = ContentScale.Crop
|
) {
|
||||||
)
|
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)) {
|
Column(modifier = Modifier.padding(8.dp)) {
|
||||||
Text(
|
Text(
|
||||||
text = item.name ?: "Без названия",
|
text = item.name ?: "Без названия",
|
||||||
|
|
@ -875,64 +1022,26 @@ fun ImageGridItem(item: AnswerGet, onClick: () -> Unit) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MediaListItem(
|
fun AudioListItem(
|
||||||
item: AnswerGet,
|
item: AnswerGet,
|
||||||
tab: AppTab,
|
onPlayAudio: () -> Unit
|
||||||
imageLoader: ImageLoader,
|
|
||||||
onPlayAudio: () -> Unit,
|
|
||||||
onOpenVideo: () -> Unit
|
|
||||||
) {
|
) {
|
||||||
Card(
|
Card(modifier = Modifier.fillMaxWidth()) {
|
||||||
modifier = Modifier
|
Row(
|
||||||
.fillMaxWidth()
|
modifier = Modifier.padding(16.dp),
|
||||||
.clickable {
|
verticalAlignment = Alignment.CenterVertically
|
||||||
if (tab == AppTab.VIDEO) onOpenVideo()
|
) {
|
||||||
}
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
) {
|
|
||||||
Column {
|
|
||||||
// Превью-обложка для видео
|
|
||||||
if (tab == AppTab.VIDEO && !item.link.isNullOrEmpty()) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(180.dp)
|
|
||||||
) {
|
|
||||||
AsyncImage(
|
|
||||||
model = item.link,
|
|
||||||
imageLoader = imageLoader,
|
|
||||||
contentDescription = "Video preview",
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
contentScale = ContentScale.Crop
|
|
||||||
)
|
|
||||||
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(48.dp).padding(8.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(modifier = Modifier.padding(16.dp)) {
|
|
||||||
Text(text = item.name ?: "Без названия", style = MaterialTheme.typography.titleMedium)
|
Text(text = item.name ?: "Без названия", style = MaterialTheme.typography.titleMedium)
|
||||||
if (!item.des.isNullOrBlank()) {
|
if (!item.des.isNullOrBlank()) {
|
||||||
Spacer(Modifier.height(4.dp))
|
Spacer(Modifier.height(4.dp))
|
||||||
Text(text = item.des, style = MaterialTheme.typography.bodyMedium, color = Color.LightGray)
|
Text(text = item.des, style = MaterialTheme.typography.bodyMedium, color = Color.LightGray)
|
||||||
}
|
}
|
||||||
Spacer(Modifier.height(8.dp))
|
}
|
||||||
if (tab == AppTab.AUDIO) {
|
Button(onClick = onPlayAudio) {
|
||||||
Button(onClick = onPlayAudio) {
|
Icon(Icons.Default.PlayArrow, null)
|
||||||
Icon(Icons.Default.PlayArrow, null)
|
Spacer(Modifier.width(4.dp))
|
||||||
Spacer(Modifier.width(4.dp))
|
Text("Включить")
|
||||||
Text("Включить")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -960,7 +1069,9 @@ fun VideoPlayerView(url: String) {
|
||||||
useController = true
|
useController = true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
modifier = Modifier.fillMaxWidth().height(230.dp)
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(420.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -970,11 +1081,11 @@ fun VideoPlayerView(url: String) {
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AdminDashboard(vm: MainViewModel) {
|
fun AdminDashboard(vm: MainViewModel) {
|
||||||
var adminTab by remember { mutableStateOf(0) }
|
var adminTab by remember { mutableIntStateOf(0) }
|
||||||
var editQuoteItem by remember { mutableStateOf<AnswerBot?>(null) }
|
var editQuoteItem by remember { mutableStateOf<AnswerBot?>(null) }
|
||||||
|
|
||||||
Column(modifier = Modifier.fillMaxSize()) {
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
TabRow(selectedTabIndex = adminTab) {
|
SecondaryTabRow(selectedTabIndex = adminTab) {
|
||||||
Tab(selected = adminTab == 0, onClick = { adminTab = 0 }, text = { Text("Юзеры (${vm.usersTotalCount})") })
|
Tab(selected = adminTab == 0, onClick = { adminTab = 0 }, text = { Text("Юзеры (${vm.usersTotalCount})") })
|
||||||
Tab(selected = adminTab == 1, onClick = { adminTab = 1 }, text = { Text("Фразы бота (${vm.quotesTotalCount})") })
|
Tab(selected = adminTab == 1, onClick = { adminTab = 1 }, text = { Text("Фразы бота (${vm.quotesTotalCount})") })
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||