feat(files): нативный медиа-слой — PDF (PdfRenderer), видео/аудио (Media3/ExoPlayer)
Открытие популярных медиа нативно (директива владельца): OpenableFiles детектит image/video/audio/pdf; openItem скачивает в кэш (WebDAV Basic-auth) → нативный вьюер. PdfViewerActivity (PdfRenderer→страницы в LazyColumn), MediaPlayerActivity (ExoPlayer PlayerView, видео+аудио). +media3-exoplayer/ui, coil-gif в каталоге.
This commit is contained in:
@@ -43,4 +43,7 @@ dependencies {
|
|||||||
implementation libs.documentfile
|
implementation libs.documentfile
|
||||||
implementation libs.coil.compose
|
implementation libs.coil.compose
|
||||||
implementation libs.coil.svg
|
implementation libs.coil.svg
|
||||||
|
implementation libs.coil.gif
|
||||||
|
implementation libs.media3.exoplayer
|
||||||
|
implementation libs.media3.ui
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,15 @@
|
|||||||
android:name=".ImageViewerActivity"
|
android:name=".ImageViewerActivity"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:theme="@android:style/Theme.Material.Light.NoActionBar" />
|
android:theme="@android:style/Theme.Material.Light.NoActionBar" />
|
||||||
|
<activity
|
||||||
|
android:name=".MediaPlayerActivity"
|
||||||
|
android:exported="false"
|
||||||
|
android:configChanges="orientation|screenSize|keyboardHidden"
|
||||||
|
android:theme="@android:style/Theme.Material.NoActionBar" />
|
||||||
|
<activity
|
||||||
|
android:name=".PdfViewerActivity"
|
||||||
|
android:exported="false"
|
||||||
|
android:theme="@android:style/Theme.Material.NoActionBar" />
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name=".F7FileProvider"
|
android:name=".F7FileProvider"
|
||||||
|
|||||||
@@ -2,5 +2,8 @@ package ru.forbion.f7cloud.feature.files
|
|||||||
|
|
||||||
sealed class FileOpenAction {
|
sealed class FileOpenAction {
|
||||||
data class Image(val path: String, val title: String) : FileOpenAction()
|
data class Image(val path: String, val title: String) : FileOpenAction()
|
||||||
|
data class Pdf(val path: String, val title: String) : FileOpenAction()
|
||||||
|
/** Видео/аудио — нативный плеер Media3 (isVideo=false → аудио-режим). */
|
||||||
|
data class Media(val path: String, val title: String, val isVideo: Boolean) : FileOpenAction()
|
||||||
data class External(val path: String, val mimeType: String, val title: String) : FileOpenAction()
|
data class External(val path: String, val mimeType: String, val title: String) : FileOpenAction()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,6 +144,14 @@ fun FilesScreen(
|
|||||||
context.startActivity(ImageViewerActivity.intent(context, action.path, action.title))
|
context.startActivity(ImageViewerActivity.intent(context, action.path, action.title))
|
||||||
vm.clearOpenAction()
|
vm.clearOpenAction()
|
||||||
}
|
}
|
||||||
|
is FileOpenAction.Pdf -> {
|
||||||
|
context.startActivity(PdfViewerActivity.intent(context, action.path, action.title))
|
||||||
|
vm.clearOpenAction()
|
||||||
|
}
|
||||||
|
is FileOpenAction.Media -> {
|
||||||
|
context.startActivity(MediaPlayerActivity.intent(context, action.path, action.title, action.isVideo))
|
||||||
|
vm.clearOpenAction()
|
||||||
|
}
|
||||||
is FileOpenAction.External -> {
|
is FileOpenAction.External -> {
|
||||||
LocalFileOpener.openExternal(context, File(action.path), action.mimeType)
|
LocalFileOpener.openExternal(context, File(action.path), action.mimeType)
|
||||||
vm.clearOpenAction()
|
vm.clearOpenAction()
|
||||||
|
|||||||
@@ -491,6 +491,9 @@ class FilesViewModel(
|
|||||||
WebOpenableFiles.isWebOpenable(item.name) -> openWebFile(session, item)
|
WebOpenableFiles.isWebOpenable(item.name) -> openWebFile(session, item)
|
||||||
ArchiveFiles.isArchive(item.name) -> openArchiveFile(session, item)
|
ArchiveFiles.isArchive(item.name) -> openArchiveFile(session, item)
|
||||||
OpenableFiles.isImage(item.name) -> openImageFile(session, item)
|
OpenableFiles.isImage(item.name) -> openImageFile(session, item)
|
||||||
|
OpenableFiles.isPdf(item.name) -> openMediaFile(session, item) { p -> FileOpenAction.Pdf(p, item.name) }
|
||||||
|
OpenableFiles.isVideo(item.name) -> openMediaFile(session, item) { p -> FileOpenAction.Media(p, item.name, isVideo = true) }
|
||||||
|
OpenableFiles.isAudio(item.name) -> openMediaFile(session, item) { p -> FileOpenAction.Media(p, item.name, isVideo = false) }
|
||||||
else -> _state.value = _state.value.copy(
|
else -> _state.value = _state.value.copy(
|
||||||
error = "Этот тип файла пока не поддерживается",
|
error = "Этот тип файла пока не поддерживается",
|
||||||
)
|
)
|
||||||
@@ -573,6 +576,24 @@ class FilesViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Медиа (PDF/видео/аудио) — скачиваем в кэш (WebDAV Basic-auth) и открываем нативным вьюером. */
|
||||||
|
private fun openMediaFile(session: AuthSession, item: FileItem, toAction: (String) -> FileOpenAction) {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
_state.value = _state.value.copy(openingFile = item.name, error = null)
|
||||||
|
runCatching { downloadRepository.download(session, item.relativePath, item.name) }
|
||||||
|
.onSuccess { file ->
|
||||||
|
_state.value = _state.value.copy(openingFile = null, openAction = toAction(file.absolutePath))
|
||||||
|
}
|
||||||
|
.onFailure { t ->
|
||||||
|
_state.value = _state.value.copy(
|
||||||
|
openingFile = null,
|
||||||
|
error = t.message ?: "Не удалось открыть файл",
|
||||||
|
unauthorized = t is UnauthorizedException,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun openImageFile(session: AuthSession, item: FileItem) {
|
private fun openImageFile(session: AuthSession, item: FileItem) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
_state.value = _state.value.copy(openingFile = item.name, error = null)
|
_state.value = _state.value.copy(openingFile = item.name, error = null)
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
package ru.forbion.f7cloud.feature.files
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
|
import androidx.media3.common.MediaItem
|
||||||
|
import androidx.media3.exoplayer.ExoPlayer
|
||||||
|
import androidx.media3.ui.PlayerView
|
||||||
|
import ru.forbion.f7cloud.core.designsystem.F7Theme
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
/** Нативный проигрыватель видео/аудио (Media3/ExoPlayer). Файл уже скачан в кэш. */
|
||||||
|
class MediaPlayerActivity : ComponentActivity() {
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
val path = intent.getStringExtra(EXTRA_PATH).orEmpty()
|
||||||
|
val title = intent.getStringExtra(EXTRA_TITLE).orEmpty()
|
||||||
|
val isVideo = intent.getBooleanExtra(EXTRA_IS_VIDEO, true)
|
||||||
|
if (path.isBlank()) {
|
||||||
|
finish()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setContent {
|
||||||
|
F7Theme {
|
||||||
|
MediaPlayerScreen(path = path, title = title, isVideo = isVideo, onClose = { finish() })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val EXTRA_PATH = "path"
|
||||||
|
private const val EXTRA_TITLE = "title"
|
||||||
|
private const val EXTRA_IS_VIDEO = "is_video"
|
||||||
|
|
||||||
|
fun intent(context: Context, path: String, title: String, isVideo: Boolean): Intent =
|
||||||
|
Intent(context, MediaPlayerActivity::class.java).apply {
|
||||||
|
putExtra(EXTRA_PATH, path)
|
||||||
|
putExtra(EXTRA_TITLE, title)
|
||||||
|
putExtra(EXTRA_IS_VIDEO, isVideo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun MediaPlayerScreen(
|
||||||
|
path: String,
|
||||||
|
title: String,
|
||||||
|
isVideo: Boolean,
|
||||||
|
onClose: () -> Unit,
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val player = remember {
|
||||||
|
ExoPlayer.Builder(context).build().apply {
|
||||||
|
setMediaItem(MediaItem.fromUri(Uri.fromFile(File(path))))
|
||||||
|
prepare()
|
||||||
|
playWhenReady = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
onDispose { player.release() }
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color.Black),
|
||||||
|
) {
|
||||||
|
AndroidView(
|
||||||
|
factory = { ctx ->
|
||||||
|
PlayerView(ctx).apply {
|
||||||
|
this.player = player
|
||||||
|
useController = true
|
||||||
|
setShowNextButton(false)
|
||||||
|
setShowPreviousButton(false)
|
||||||
|
// Аудио: PlayerView без видео-поверхности показывает только контролы.
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
)
|
||||||
|
TopAppBar(
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
color = Color.White,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = onClose) {
|
||||||
|
Text("←", color = Color.White, modifier = Modifier.padding(8.dp))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier.align(Alignment.TopCenter),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,23 +4,40 @@ import java.util.Locale
|
|||||||
|
|
||||||
enum class OpenableKind {
|
enum class OpenableKind {
|
||||||
IMAGE,
|
IMAGE,
|
||||||
|
VIDEO,
|
||||||
|
AUDIO,
|
||||||
|
PDF,
|
||||||
}
|
}
|
||||||
|
|
||||||
object OpenableFiles {
|
object OpenableFiles {
|
||||||
private val IMAGE_EXT = setOf("jpg", "jpeg", "png", "gif", "webp", "bmp", "heic", "heif")
|
private val IMAGE_EXT = setOf("jpg", "jpeg", "png", "gif", "webp", "bmp", "heic", "heif", "svg")
|
||||||
|
private val VIDEO_EXT = setOf("mp4", "webm", "mkv", "mov", "3gp", "m4v", "avi", "ts")
|
||||||
|
private val AUDIO_EXT = setOf("mp3", "aac", "ogg", "oga", "opus", "wav", "flac", "m4a", "weba")
|
||||||
|
private val PDF_EXT = setOf("pdf")
|
||||||
|
|
||||||
fun kind(name: String): OpenableKind? {
|
fun kind(name: String): OpenableKind? {
|
||||||
val ext = name.lowercase(Locale.ROOT).substringAfterLast('.', missingDelimiterValue = "")
|
val ext = name.lowercase(Locale.ROOT).substringAfterLast('.', missingDelimiterValue = "")
|
||||||
return if (ext in IMAGE_EXT) OpenableKind.IMAGE else null
|
return when (ext) {
|
||||||
|
in IMAGE_EXT -> OpenableKind.IMAGE
|
||||||
|
in VIDEO_EXT -> OpenableKind.VIDEO
|
||||||
|
in AUDIO_EXT -> OpenableKind.AUDIO
|
||||||
|
in PDF_EXT -> OpenableKind.PDF
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isOpenable(name: String): Boolean = kind(name) != null || ArchiveFiles.isArchive(name)
|
fun isOpenable(name: String): Boolean = kind(name) != null || ArchiveFiles.isArchive(name)
|
||||||
|
|
||||||
fun isImage(name: String): Boolean = kind(name) == OpenableKind.IMAGE
|
fun isImage(name: String): Boolean = kind(name) == OpenableKind.IMAGE
|
||||||
|
fun isVideo(name: String): Boolean = kind(name) == OpenableKind.VIDEO
|
||||||
|
fun isAudio(name: String): Boolean = kind(name) == OpenableKind.AUDIO
|
||||||
|
fun isPdf(name: String): Boolean = kind(name) == OpenableKind.PDF
|
||||||
|
|
||||||
fun hint(name: String): String? = when {
|
fun hint(name: String): String? = when (kind(name)) {
|
||||||
isImage(name) -> "Просмотр"
|
OpenableKind.IMAGE -> "Просмотр"
|
||||||
ArchiveFiles.isArchive(name) -> "Открыть с помощью…"
|
OpenableKind.VIDEO -> "Видео"
|
||||||
else -> null
|
OpenableKind.AUDIO -> "Аудио"
|
||||||
|
OpenableKind.PDF -> "PDF"
|
||||||
|
null -> if (ArchiveFiles.isArchive(name)) "Открыть с помощью…" else null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,149 @@
|
|||||||
|
package ru.forbion.f7cloud.feature.files
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Color as AndroidColor
|
||||||
|
import android.graphics.pdf.PdfRenderer
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.ParcelFileDescriptor
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.produceState
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import ru.forbion.f7cloud.core.designsystem.F7Theme
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
/** Нативный просмотр PDF через android.graphics.pdf.PdfRenderer (без внешних зависимостей). */
|
||||||
|
class PdfViewerActivity : ComponentActivity() {
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
val path = intent.getStringExtra(EXTRA_PATH).orEmpty()
|
||||||
|
val title = intent.getStringExtra(EXTRA_TITLE).orEmpty()
|
||||||
|
if (path.isBlank() || !File(path).exists()) {
|
||||||
|
finish()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setContent {
|
||||||
|
F7Theme {
|
||||||
|
PdfViewerScreen(path = path, title = title, onClose = { finish() })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val EXTRA_PATH = "path"
|
||||||
|
private const val EXTRA_TITLE = "title"
|
||||||
|
|
||||||
|
fun intent(context: Context, path: String, title: String): Intent =
|
||||||
|
Intent(context, PdfViewerActivity::class.java).apply {
|
||||||
|
putExtra(EXTRA_PATH, path)
|
||||||
|
putExtra(EXTRA_TITLE, title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun PdfViewerScreen(
|
||||||
|
path: String,
|
||||||
|
title: String,
|
||||||
|
onClose: () -> Unit,
|
||||||
|
) {
|
||||||
|
val pages by produceState(initialValue = emptyList<Bitmap>(), path) {
|
||||||
|
value = withContext(Dispatchers.IO) { renderPdf(File(path)) }
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color(0xFF303030)),
|
||||||
|
) {
|
||||||
|
if (pages.isEmpty()) {
|
||||||
|
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center), color = Color.White)
|
||||||
|
} else {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentPadding = PaddingValues(top = 64.dp, bottom = 16.dp, start = 8.dp, end = 8.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
) {
|
||||||
|
items(pages) { bmp ->
|
||||||
|
Image(
|
||||||
|
bitmap = bmp.asImageBitmap(),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.fillMaxWidth().background(Color.White),
|
||||||
|
contentScale = ContentScale.FillWidth,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TopAppBar(
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
color = Color.White,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = onClose) {
|
||||||
|
Text("←", color = Color.White, modifier = Modifier.padding(8.dp))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier.align(Alignment.TopCenter),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Рендер всех страниц PDF в bitmap'ы (ширина ~1080px, высота по соотношению сторон). */
|
||||||
|
private fun renderPdf(file: File): List<Bitmap> {
|
||||||
|
val out = mutableListOf<Bitmap>()
|
||||||
|
runCatching {
|
||||||
|
ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY).use { pfd ->
|
||||||
|
PdfRenderer(pfd).use { renderer ->
|
||||||
|
val targetWidth = 1080
|
||||||
|
for (i in 0 until renderer.pageCount) {
|
||||||
|
renderer.openPage(i).use { page ->
|
||||||
|
val scale = targetWidth.toFloat() / page.width
|
||||||
|
val height = (page.height * scale).toInt().coerceAtLeast(1)
|
||||||
|
val bmp = Bitmap.createBitmap(targetWidth, height, Bitmap.Config.ARGB_8888)
|
||||||
|
bmp.eraseColor(AndroidColor.WHITE)
|
||||||
|
page.render(bmp, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY)
|
||||||
|
out += bmp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ composeBom = "2025.02.00"
|
|||||||
coreKtx = "1.15.0" # был дрейф 1.13.1 / 1.15.0
|
coreKtx = "1.15.0" # был дрейф 1.13.1 / 1.15.0
|
||||||
lifecycle = "2.8.7" # был дрейф 2.8.1 / 2.8.7
|
lifecycle = "2.8.7" # был дрейф 2.8.1 / 2.8.7
|
||||||
coil = "2.7.0" # был дрейф 2.6.0 / 2.7.0
|
coil = "2.7.0" # был дрейф 2.6.0 / 2.7.0
|
||||||
|
media3 = "1.4.1" # нативный медиа-слой (видео/аудио) — ExoPlayer
|
||||||
coroutines = "1.10.1" # был дрейф 1.8.1 / 1.10.1
|
coroutines = "1.10.1" # был дрейф 1.8.1 / 1.10.1
|
||||||
navigationCompose = "2.8.9"
|
navigationCompose = "2.8.9"
|
||||||
room = "2.7.2"
|
room = "2.7.2"
|
||||||
@@ -38,6 +39,9 @@ lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-ru
|
|||||||
lifecycle-process = { group = "androidx.lifecycle", name = "lifecycle-process", version.ref = "lifecycle" }
|
lifecycle-process = { group = "androidx.lifecycle", name = "lifecycle-process", version.ref = "lifecycle" }
|
||||||
coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" }
|
coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" }
|
||||||
coil-svg = { group = "io.coil-kt", name = "coil-svg", version.ref = "coil" }
|
coil-svg = { group = "io.coil-kt", name = "coil-svg", version.ref = "coil" }
|
||||||
|
coil-gif = { group = "io.coil-kt", name = "coil-gif", version.ref = "coil" }
|
||||||
|
media3-exoplayer = { group = "androidx.media3", name = "media3-exoplayer", version.ref = "media3" }
|
||||||
|
media3-ui = { group = "androidx.media3", name = "media3-ui", version.ref = "media3" }
|
||||||
coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutines" }
|
coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutines" }
|
||||||
coroutines-play-services = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-play-services", version.ref = "coroutines" }
|
coroutines-play-services = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-play-services", version.ref = "coroutines" }
|
||||||
okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }
|
okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }
|
||||||
|
|||||||
Reference in New Issue
Block a user