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ергулек
+