diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000..8011611 Binary files /dev/null and b/app/src/main/ic_launcher-playstore.png differ diff --git a/app/src/main/java/com/sergay/hhu67/pw/app/MainActivity.kt b/app/src/main/java/com/sergay/hhu67/pw/app/MainActivity.kt index 4bf0d55..e7c674c 100644 --- a/app/src/main/java/com/sergay/hhu67/pw/app/MainActivity.kt +++ b/app/src/main/java/com/sergay/hhu67/pw/app/MainActivity.kt @@ -1,5 +1,7 @@ package com.sergay.hhu67.pw.app +import android.content.Context +import android.content.SharedPreferences import android.net.Uri import android.os.Bundle 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.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.* @@ -28,7 +31,10 @@ import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import androidx.core.content.edit import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.compose.viewModel import androidx.media3.common.MediaItem @@ -40,6 +46,7 @@ import coil.decode.VideoFrameDecoder import com.google.gson.annotations.SerializedName import kotlinx.coroutines.launch import okhttp3.* +import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.RequestBody.Companion.toRequestBody import retrofit2.Response @@ -96,17 +103,36 @@ data class AnswerBotList( @SerializedName("items") val items: List? ) -class AppCookieJar : CookieJar { +class PersistentCookieJar(context: Context) : CookieJar { + private val prefs: SharedPreferences = context.getSharedPreferences("app_cookies", Context.MODE_PRIVATE) private val cookieStore = mutableListOf() + + 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) { + 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 = cookieStore - fun clear() = cookieStore.clear() + + fun clear() { + cookieStore.clear() + prefs.edit { clear() } + } } -val cookieJar = AppCookieJar() +lateinit var globalCookieJar: PersistentCookieJar interface IpService { @GET("/") @@ -191,7 +217,7 @@ interface ApiService { companion object { fun create(): ApiService { val okHttp = OkHttpClient.Builder() - .cookieJar(cookieJar) + .cookieJar(globalCookieJar) .connectTimeout(60, TimeUnit.SECONDS) .readTimeout(60, TimeUnit.SECONDS) .build() @@ -219,7 +245,7 @@ interface ApiService { // ========================================== enum class AppTab(val title: String) { - IMAGES("Картинки"), + IMAGES("Фото"), AUDIO("Аудио"), VIDEO("Видео"), ADMIN("Админка") @@ -231,39 +257,57 @@ sealed interface 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 ipApi = ApiService.createIpService() - var isAuthenticated by mutableStateOf(false) - var isAdmin by mutableStateOf(false) + 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.Loading) - // Пагинация для медиа (Картинки: 20, Аудио: 20, Видео: 10) - var currentPage by mutableStateOf(0) - var totalItemsCount by mutableStateOf(0) + var currentPage by mutableIntStateOf(0) + var totalItemsCount by mutableIntStateOf(0) - // Модальное окно просмотра Фото / Видео var previewMediaItem by mutableStateOf(null) var isPreviewVideo by mutableStateOf(false) - // Аудиоплеер (Нижняя строка) var audioPlaylist by mutableStateOf>(emptyList()) - var currentAudioIndex by mutableStateOf(-1) + var currentAudioIndex by mutableIntStateOf(-1) var isAudioPlaying by mutableStateOf(false) - // Админка: юзеры var adminUsers by mutableStateOf>(emptyList()) - var usersTotalCount by mutableStateOf(0) - var usersCurrentPage by mutableStateOf(0) + var usersTotalCount by mutableIntStateOf(0) + var usersCurrentPage by mutableIntStateOf(0) - // Админка: фразы бота var adminQuotes by mutableStateOf>(emptyList()) - var quotesTotalCount by mutableStateOf(0) - var quotesCurrentPage by mutableStateOf(0) + var quotesTotalCount by mutableIntStateOf(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) { viewModelScope.launch { @@ -285,12 +329,16 @@ class MainViewModel : ViewModel() { 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) @@ -306,12 +354,12 @@ class MainViewModel : ViewModel() { } fun logout() { - cookieJar.clear() + globalCookieJar.clear() + authPrefs.edit { clear() } isAuthenticated = false isAdmin = false currentTab = AppTab.IMAGES - currentAudioIndex = -1 - isAudioPlaying = false + dismissAudio() currentPage = 0 } @@ -334,15 +382,13 @@ class MainViewModel : ViewModel() { when (currentTab) { AppTab.IMAGES -> { val limit = 20 - val offset = page * limit - val res = api.getImages(limit = limit, offset = offset) + val res = api.getImages(limit = limit, offset = page * limit) totalItemsCount = res.total state = ContentState.SuccessItems(res.items ?: emptyList()) } AppTab.AUDIO -> { val limit = 20 - val offset = page * limit - val res = api.getAudio(limit = limit, offset = offset) + val res = api.getAudio(limit = limit, offset = page * limit) totalItemsCount = res.total val list = res.items ?: emptyList() audioPlaylist = list @@ -350,8 +396,7 @@ class MainViewModel : ViewModel() { } AppTab.VIDEO -> { val limit = 10 - val offset = page * limit - val res = api.getVideo(limit = limit, offset = offset) + val res = api.getVideo(limit = limit, offset = page * limit) totalItemsCount = res.total state = ContentState.SuccessItems(res.items ?: emptyList()) } @@ -366,8 +411,7 @@ class MainViewModel : ViewModel() { fun loadAdminUsers(page: Int) { viewModelScope.launch { try { - val offset = page * 20 - val res = api.getUsers(limit = 20, offset = offset) + val res = api.getUsers(limit = 20, offset = page * 20) adminUsers = res.items ?: emptyList() usersTotalCount = res.total usersCurrentPage = page @@ -378,8 +422,7 @@ class MainViewModel : ViewModel() { fun loadAdminQuotes(page: Int) { viewModelScope.launch { try { - val offset = page * 20 - val res = api.getBotQuotes(limit = 20, offset = offset) + val res = api.getBotQuotes(limit = 20, offset = page * 20) adminQuotes = res.items ?: emptyList() quotesTotalCount = res.total quotesCurrentPage = page @@ -394,6 +437,11 @@ class MainViewModel : ViewModel() { } } + fun dismissAudio() { + currentAudioIndex = -1 + isAudioPlaying = false + } + fun nextAudio() { if (audioPlaylist.isNotEmpty()) { currentAudioIndex = (currentAudioIndex + 1) % audioPlaylist.size @@ -487,13 +535,25 @@ class MainViewModel : ViewModel() { } } +class MainViewModelFactory(private val context: Context) : ViewModelProvider.Factory { + override fun create(modelClass: Class): 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() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + globalCookieJar = PersistentCookieJar(applicationContext) + setContent { MaterialTheme(colorScheme = darkColorScheme( primary = Color(0xFFBB86FC), @@ -501,7 +561,7 @@ class MainActivity : ComponentActivity() { background = Color(0xFF121212) )) { Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) { - val vm: MainViewModel = viewModel() + val vm: MainViewModel = viewModel(factory = MainViewModelFactory(applicationContext)) if (!vm.isAuthenticated) { AuthScreen(vm = vm) } else { @@ -522,7 +582,7 @@ fun AuthScreen(vm: MainViewModel) { 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) + Text("Авторизация", style = MaterialTheme.typography.titleLarge) Spacer(Modifier.height(16.dp)) OutlinedTextField( @@ -576,7 +636,6 @@ fun MainAppScreen(vm: MainViewModel) { val exoPlayer = remember { ExoPlayer.Builder(ctx).build() } - // Настройка Coil для превью кадров из видео val imageLoader = remember { ImageLoader.Builder(ctx) .components { @@ -596,6 +655,8 @@ fun MainAppScreen(vm: MainViewModel) { } if (vm.isAudioPlaying) exoPlayer.play() else exoPlayer.pause() } + } else { + exoPlayer.stop() } } @@ -609,7 +670,7 @@ fun MainAppScreen(vm: MainViewModel) { title = { Text(vm.currentTab.title) }, actions = { IconButton(onClick = { vm.logout() }) { - Icon(Icons.Default.Logout, contentDescription = "Выйти") + Icon(Icons.AutoMirrored.Filled.Logout, contentDescription = "Выйти") } IconButton(onClick = { if (vm.currentTab == AppTab.ADMIN) { @@ -635,62 +696,92 @@ fun MainAppScreen(vm: MainViewModel) { Column { if (vm.currentAudioIndex in vm.audioPlaylist.indices) { val currentTrack = vm.audioPlaylist[vm.currentAudioIndex] - Surface(color = MaterialTheme.colorScheme.surfaceVariant, modifier = Modifier.fillMaxWidth()) { - Row( - modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.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 - ) + 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) } - IconButton(onClick = { vm.prevAudio() }) { - Icon(Icons.Default.SkipPrevious, contentDescription = "Назад") - } - IconButton(onClick = { vm.isAudioPlaying = !vm.isAudioPlaying }) { - Icon(if (vm.isAudioPlaying) Icons.Default.Pause else Icons.Default.PlayArrow, null) - } - IconButton(onClick = { vm.nextAudio() }) { - Icon(Icons.Default.SkipNext, contentDescription = "Вперед") + } + ) { + 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 = "Закрыть плеер") + } } } } } - NavigationBar { + NavigationBar( + modifier = Modifier.height(64.dp), + tonalElevation = 4.dp + ) { NavigationBarItem( selected = vm.currentTab == AppTab.IMAGES, onClick = { vm.selectTab(AppTab.IMAGES) }, - icon = { Icon(Icons.Default.Image, null) }, - label = { Text("Фото") } + icon = { Icon(Icons.Default.Image, null, modifier = Modifier.size(20.dp)) }, + label = { Text("Фото", style = MaterialTheme.typography.labelSmall) } ) NavigationBarItem( selected = vm.currentTab == AppTab.AUDIO, onClick = { vm.selectTab(AppTab.AUDIO) }, - icon = { Icon(Icons.Default.Audiotrack, null) }, - label = { Text("Аудио") } + icon = { Icon(Icons.Default.Audiotrack, null, modifier = Modifier.size(20.dp)) }, + label = { Text("Аудио", style = MaterialTheme.typography.labelSmall) } ) NavigationBarItem( selected = vm.currentTab == AppTab.VIDEO, onClick = { vm.selectTab(AppTab.VIDEO) }, - icon = { Icon(Icons.Default.Videocam, null) }, - label = { Text("Видео") } + icon = { Icon(Icons.Default.Videocam, null, modifier = Modifier.size(20.dp)) }, + label = { Text("Видео", style = MaterialTheme.typography.labelSmall) } ) if (vm.isAdmin) { NavigationBarItem( selected = vm.currentTab == AppTab.ADMIN, onClick = { vm.selectTab(AppTab.ADMIN) }, - icon = { Icon(Icons.Default.AdminPanelSettings, null) }, - label = { Text("Админка") } + icon = { Icon(Icons.Default.AdminPanelSettings, null, modifier = Modifier.size(20.dp)) }, + label = { Text("Админка", style = MaterialTheme.typography.labelSmall) } ) } } @@ -717,43 +808,65 @@ fun MainAppScreen(vm: MainViewModel) { is ContentState.SuccessItems -> { Column(modifier = Modifier.fillMaxSize()) { Box(modifier = Modifier.weight(1f)) { - if (vm.currentTab == AppTab.IMAGES) { - LazyVerticalGrid( - columns = GridCells.Fixed(2), - contentPadding = PaddingValues(8.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - items(s.items, key = { it.id }) { item -> - ImageGridItem(item = item, onClick = { - vm.previewMediaItem = item - vm.isPreviewVideo = false - }) + when (vm.currentTab) { + AppTab.IMAGES -> { + LazyVerticalGrid( + columns = GridCells.Fixed(2), + contentPadding = PaddingValues(8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + items(s.items, key = { it.id }) { item -> + GridMediaItem( + item = item, + isVideo = false, + imageLoader = imageLoader, + onClick = { + vm.previewMediaItem = item + vm.isPreviewVideo = false + } + ) + } } } - } else { - LazyColumn( - contentPadding = PaddingValues(16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - items(s.items.size) { index -> - val item = s.items[index] - MediaListItem( - item = item, - tab = vm.currentTab, - imageLoader = imageLoader, - onPlayAudio = { vm.playAudioTrack(index) }, - onOpenVideo = { - vm.previewMediaItem = item - vm.isPreviewVideo = true - } - ) + AppTab.VIDEO -> { + LazyVerticalGrid( + columns = GridCells.Fixed(2), + contentPadding = PaddingValues(8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + items(s.items, key = { it.id }) { item -> + GridMediaItem( + item = item, + isVideo = true, + imageLoader = imageLoader, + onClick = { + vm.previewMediaItem = item + 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( currentPage = vm.currentPage, totalPages = totalPages, @@ -766,10 +879,17 @@ fun MainAppScreen(vm: MainViewModel) { } } - // Модальное окно полного просмотра vm.previewMediaItem?.let { item -> - Dialog(onDismissRequest = { vm.previewMediaItem = null }) { - Card(modifier = Modifier.fillMaxWidth().wrapContentHeight()) { + Dialog( + onDismissRequest = { vm.previewMediaItem = null }, + properties = DialogProperties(usePlatformDefaultWidth = false) + ) { + Card( + modifier = Modifier + .fillMaxWidth(0.95f) + .wrapContentHeight() + .padding(8.dp) + ) { Column(modifier = Modifier.padding(16.dp)) { if (vm.isPreviewVideo && !item.link.isNullOrEmpty()) { VideoPlayerView(url = item.link) @@ -777,10 +897,13 @@ fun MainAppScreen(vm: MainViewModel) { AsyncImage( model = item.link, contentDescription = null, - modifier = Modifier.fillMaxWidth().height(260.dp), + modifier = Modifier + .fillMaxWidth() + .height(320.dp), contentScale = ContentScale.Fit ) } + Spacer(Modifier.height(12.dp)) Text(text = item.name ?: "Без названия", style = MaterialTheme.typography.titleLarge) if (!item.des.isNullOrBlank()) { @@ -836,25 +959,49 @@ fun MainAppScreen(vm: MainViewModel) { } // ========================================== -// 4. ЭЛЕМЕНТЫ СПИСКОВ И СЕТКИ +// 4. КОМПОНЕНТЫ // ========================================== @Composable -fun ImageGridItem(item: AnswerGet, onClick: () -> Unit) { +fun GridMediaItem( + item: AnswerGet, + isVideo: Boolean, + imageLoader: ImageLoader, + onClick: () -> Unit +) { Card( modifier = Modifier .fillMaxWidth() .clickable { onClick() } ) { Column { - AsyncImage( - model = item.link, - contentDescription = item.name, + Box( modifier = Modifier .fillMaxWidth() - .height(160.dp), - contentScale = ContentScale.Crop - ) + .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 ?: "Без названия", @@ -875,64 +1022,26 @@ fun ImageGridItem(item: AnswerGet, onClick: () -> Unit) { } @Composable -fun MediaListItem( +fun AudioListItem( item: AnswerGet, - tab: AppTab, - imageLoader: ImageLoader, - onPlayAudio: () -> Unit, - onOpenVideo: () -> Unit + onPlayAudio: () -> Unit ) { - Card( - modifier = Modifier - .fillMaxWidth() - .clickable { - if (tab == AppTab.VIDEO) onOpenVideo() - } - ) { - 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)) { + Card(modifier = Modifier.fillMaxWidth()) { + Row( + modifier = Modifier.padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + 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) } - Spacer(Modifier.height(8.dp)) - if (tab == AppTab.AUDIO) { - Button(onClick = onPlayAudio) { - Icon(Icons.Default.PlayArrow, null) - Spacer(Modifier.width(4.dp)) - Text("Включить") - } - } + } + Button(onClick = onPlayAudio) { + Icon(Icons.Default.PlayArrow, null) + Spacer(Modifier.width(4.dp)) + Text("Включить") } } } @@ -960,7 +1069,9 @@ fun VideoPlayerView(url: String) { useController = true } }, - modifier = Modifier.fillMaxWidth().height(230.dp) + modifier = Modifier + .fillMaxWidth() + .height(420.dp) ) } @@ -970,11 +1081,11 @@ fun VideoPlayerView(url: String) { @Composable fun AdminDashboard(vm: MainViewModel) { - var adminTab by remember { mutableStateOf(0) } + var adminTab by remember { mutableIntStateOf(0) } var editQuoteItem by remember { mutableStateOf(null) } Column(modifier = Modifier.fillMaxSize()) { - TabRow(selectedTabIndex = adminTab) { + 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})") }) } diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml index 07d5da9..ca3826a 100644 --- a/app/src/main/res/drawable/ic_launcher_background.xml +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -1,170 +1,74 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + xmlns:android="http://schemas.android.com/apk/res/android"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index 6f3b755..82cd4a2 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -1,6 +1,6 @@ - - - + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml index 6f3b755..82cd4a2 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -1,6 +1,6 @@ - - - + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp index c209e78..3aa8e4f 100644 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.webp and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..b7b41b2 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp index b2dfe3d..d60a965 100644 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp index 4f0f1d6..9e69f27 100644 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.webp and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..ba20232 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp index 62b611d..e91f01d 100644 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp index 948a307..5258d40 100644 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..2e2da35 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp index 1b9a695..9e5ddef 100644 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp index 28d4b77..93a762b 100644 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..d1acf65 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp index 9287f50..2f1cffd 100644 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp index aa7d642..ee3559a 100644 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..9c927d4 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp index 9126ae3..e108156 100644 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3de1a39..b4786d8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,3 @@ - My Application - \ No newline at end of file + Cергулек +