Compare commits
7 Commits
0.5.4.1
...
feature-ya
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20cdcddd36 | ||
| c79fa89c1d | |||
|
|
2a995364a2 | ||
|
|
0f017a781f | ||
|
|
3e251a9b7a | ||
|
|
5c7257de3f | ||
|
|
873505776b |
@@ -53,8 +53,7 @@ Muza - это удобное и простое в использовании м
|
||||
|
||||
## Скачать
|
||||
|
||||
[<img src="https://help.rustore.ru/pic/e/d/edc2a045e17e4971c7cca77c7fde4b66.png" alt="Скачать из RuStore">](https://apps.rustore.ru/app/it.hamy.muza)
|
||||
|
||||
[<img src="https://i.ibb.co/jMwfXFd/rustore-light.png" alt="Скачать из RuStore" height="60">](https://apps.rustore.ru/app/it.hamy.muza)
|
||||
[<img src="https://github.com/machiav3lli/oandbackupx/blob/034b226cea5c1b30eb4f6a6f313e4dadcbb0ece4/badge_github.png"
|
||||
alt="Скачать из Гитхаба"
|
||||
height="80">](https://github.com/hammsterr/muza/releases/latest)
|
||||
|
||||
@@ -75,6 +75,7 @@ dependencies {
|
||||
implementation(projects.composeReordering)
|
||||
|
||||
|
||||
|
||||
implementation(libs.compose.activity)
|
||||
implementation(libs.compose.foundation)
|
||||
implementation(libs.compose.ui)
|
||||
@@ -90,6 +91,10 @@ dependencies {
|
||||
|
||||
implementation(libs.room)
|
||||
implementation("androidx.media3:media3-datasource-okhttp:1.0.0-alpha03")
|
||||
|
||||
implementation ("com.yandex.android:mobileads:6.4.0")
|
||||
implementation("com.google.android.gms:play-services-ads-identifier:18.0.1")
|
||||
|
||||
kapt(libs.room.compiler)
|
||||
|
||||
implementation(projects.innertube)
|
||||
|
||||
4
app/proguard-rules.pro
vendored
4
app/proguard-rules.pro
vendored
@@ -31,4 +31,6 @@
|
||||
-dontwarn org.openjsse.javax.net.ssl.SSLParameters
|
||||
-dontwarn org.openjsse.javax.net.ssl.SSLSocket
|
||||
-dontwarn org.openjsse.net.ssl.OpenJSSE
|
||||
-dontwarn org.slf4j.impl.StaticLoggerBinder
|
||||
-dontwarn org.slf4j.impl.StaticLoggerBinder
|
||||
|
||||
-keep class com.yandex** { *; }
|
||||
@@ -1,11 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||
<uses-permission android:name="com.google.android.gms.permission.AD_ID" tools:node="remove"/>
|
||||
|
||||
<queries>
|
||||
<intent>
|
||||
|
||||
@@ -128,6 +128,7 @@ class MainActivity : ComponentActivity(), PersistMapOwner {
|
||||
|
||||
override lateinit var persistMap: PersistMap
|
||||
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
bindService(intent<PlayerService>(), serviceConnection, Context.BIND_AUTO_CREATE)
|
||||
@@ -168,7 +169,7 @@ class MainActivity : ComponentActivity(), PersistMapOwner {
|
||||
val colorPaletteName = getEnum(colorPaletteNameKey, ColorPaletteName.Dynamic)
|
||||
val colorPaletteMode = getEnum(colorPaletteModeKey, ColorPaletteMode.System)
|
||||
val thumbnailRoundness =
|
||||
getEnum(thumbnailRoundnessKey, ThumbnailRoundness.Light)
|
||||
getEnum(thumbnailRoundnessKey, ThumbnailRoundness.Слабое)
|
||||
|
||||
val useSystemFont = getBoolean(useSystemFontKey, false)
|
||||
val applyFontPadding = getBoolean(applyFontPaddingKey, false)
|
||||
@@ -267,7 +268,7 @@ class MainActivity : ComponentActivity(), PersistMapOwner {
|
||||
|
||||
thumbnailRoundnessKey -> {
|
||||
val thumbnailRoundness =
|
||||
sharedPreferences.getEnum(key, ThumbnailRoundness.Light)
|
||||
sharedPreferences.getEnum(key, ThumbnailRoundness.Слабое)
|
||||
|
||||
appearance = appearance.copy(
|
||||
thumbnailShape = thumbnailRoundness.shape()
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.app.Application
|
||||
import coil.ImageLoader
|
||||
import coil.ImageLoaderFactory
|
||||
import coil.disk.DiskCache
|
||||
import com.yandex.mobile.ads.common.MobileAds
|
||||
import it.hamy.muza.enums.CoilDiskCacheMaxSize
|
||||
import it.hamy.muza.utils.coilDiskCacheMaxSizeKey
|
||||
import it.hamy.muza.utils.getEnum
|
||||
@@ -13,6 +14,11 @@ class MainApplication : Application(), ImageLoaderFactory {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
DatabaseInitializer()
|
||||
MobileAds.initialize(this) {
|
||||
/**
|
||||
* Инициализация либы яндекса
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
override fun newImageLoader(): ImageLoader {
|
||||
|
||||
@@ -6,17 +6,19 @@ import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
enum class ThumbnailRoundness {
|
||||
None,
|
||||
Light,
|
||||
Medium,
|
||||
Heavy;
|
||||
Отключено,
|
||||
Слабое,
|
||||
Среднее,
|
||||
Сильное,
|
||||
Максимальное;
|
||||
|
||||
fun shape(): Shape {
|
||||
return when (this) {
|
||||
None -> RectangleShape
|
||||
Light -> RoundedCornerShape(2.dp)
|
||||
Medium -> RoundedCornerShape(4.dp)
|
||||
Heavy -> RoundedCornerShape(8.dp)
|
||||
Отключено -> RectangleShape
|
||||
Слабое -> RoundedCornerShape(2.dp)
|
||||
Среднее -> RoundedCornerShape(4.dp)
|
||||
Сильное -> RoundedCornerShape(8.dp)
|
||||
Максимальное -> RoundedCornerShape(14.dp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package it.hamy.muza.ui.components
|
||||
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import com.yandex.mobile.ads.banner.BannerAdEventListener
|
||||
import com.yandex.mobile.ads.banner.BannerAdSize
|
||||
import com.yandex.mobile.ads.banner.BannerAdView
|
||||
import com.yandex.mobile.ads.common.AdRequest
|
||||
import com.yandex.mobile.ads.common.AdRequestError
|
||||
import com.yandex.mobile.ads.common.ImpressionData
|
||||
|
||||
@Composable
|
||||
fun YandexAdsBanner(id: String) {
|
||||
AndroidView(modifier = Modifier.fillMaxSize(), factory = { context ->
|
||||
BannerAdView(context).apply {
|
||||
/**
|
||||
* ID блока рекламы
|
||||
*/
|
||||
setAdUnitId(id)
|
||||
/**
|
||||
* Размер блока рекламы
|
||||
*/
|
||||
setAdSize(BannerAdSize.inlineSize(context, 140, 60))
|
||||
/**
|
||||
* Билдер запроса
|
||||
*/
|
||||
val adRequest = AdRequest.Builder().build()
|
||||
/**
|
||||
* Слушатель экшнов
|
||||
*/
|
||||
setBannerAdEventListener(object : BannerAdEventListener {
|
||||
override fun onAdLoaded() {
|
||||
|
||||
}
|
||||
|
||||
override fun onAdFailedToLoad(p0: AdRequestError) {
|
||||
/**
|
||||
* Тут дебажим ошибки
|
||||
*/
|
||||
}
|
||||
|
||||
override fun onAdClicked() {
|
||||
|
||||
}
|
||||
|
||||
override fun onLeftApplication() {
|
||||
|
||||
}
|
||||
|
||||
override fun onReturnedToApplication() {
|
||||
|
||||
}
|
||||
|
||||
override fun onImpression(p0: ImpressionData?) {
|
||||
|
||||
}
|
||||
|
||||
})
|
||||
/**
|
||||
* Запуск баннера
|
||||
*/
|
||||
loadAd(adRequest)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -176,11 +176,11 @@ fun ArtistScreen(browseId: String) {
|
||||
tabIndex = tabIndex,
|
||||
onTabChanged = { tabIndex = it },
|
||||
tabColumnContent = { Item ->
|
||||
Item(0, "Overview", R.drawable.sparkles)
|
||||
Item(1, "Songs", R.drawable.musical_notes)
|
||||
Item(2, "Albums", R.drawable.disc)
|
||||
Item(3, "Singles", R.drawable.disc)
|
||||
Item(4, "Library", R.drawable.library)
|
||||
Item(0, "Обзор", R.drawable.sparkles)
|
||||
Item(1, "Песни", R.drawable.musical_notes)
|
||||
Item(2, "Альбомы", R.drawable.disc)
|
||||
Item(3, "Синглы", R.drawable.disc)
|
||||
Item(4, "Библиотека", R.drawable.library)
|
||||
},
|
||||
) { currentTabIndex ->
|
||||
saveableStateHolder.SaveableStateProvider(key = currentTabIndex) {
|
||||
@@ -267,7 +267,7 @@ fun ArtistScreen(browseId: String) {
|
||||
ItemsPage(
|
||||
tag = "artist/$browseId/albums",
|
||||
headerContent = headerContent,
|
||||
emptyItemsText = "This artist didn't release any album",
|
||||
emptyItemsText = "Исполнитель не выпустил ни одного альбома",
|
||||
itemsPageProvider = artistPage?.let {
|
||||
({ continuation ->
|
||||
continuation?.let {
|
||||
@@ -317,7 +317,7 @@ fun ArtistScreen(browseId: String) {
|
||||
ItemsPage(
|
||||
tag = "artist/$browseId/singles",
|
||||
headerContent = headerContent,
|
||||
emptyItemsText = "This artist didn't release any single",
|
||||
emptyItemsText = "Исполнитель не выпустил ни одного сингла",
|
||||
itemsPageProvider = artistPage?.let {
|
||||
({ continuation ->
|
||||
continuation?.let {
|
||||
|
||||
@@ -9,17 +9,22 @@ import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
import androidx.compose.foundation.layout.asPaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.only
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.GridItemSpan
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -40,6 +45,7 @@ import it.hamy.muza.enums.SortOrder
|
||||
import it.hamy.muza.models.Playlist
|
||||
import it.hamy.muza.models.PlaylistPreview
|
||||
import it.hamy.muza.query
|
||||
import it.hamy.muza.ui.components.YandexAdsBanner
|
||||
import it.hamy.muza.ui.components.themed.FloatingActionsContainerWithScrollToTop
|
||||
import it.hamy.muza.ui.components.themed.Header
|
||||
import it.hamy.muza.ui.components.themed.HeaderIconButton
|
||||
@@ -53,9 +59,11 @@ import it.hamy.muza.utils.playlistSortByKey
|
||||
import it.hamy.muza.utils.playlistSortOrderKey
|
||||
import it.hamy.muza.utils.rememberPreference
|
||||
|
||||
|
||||
@ExperimentalAnimationApi
|
||||
@ExperimentalFoundationApi
|
||||
@Composable
|
||||
|
||||
fun HomePlaylists(
|
||||
onBuiltInPlaylist: (BuiltInPlaylist) -> Unit,
|
||||
onPlaylistClick: (Playlist) -> Unit,
|
||||
@@ -67,6 +75,8 @@ fun HomePlaylists(
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (isCreatingANewPlaylist) {
|
||||
TextFieldDialog(
|
||||
hintText = "Введите название плейлиста",
|
||||
@@ -100,107 +110,122 @@ fun HomePlaylists(
|
||||
|
||||
val lazyGridState = rememberLazyGridState()
|
||||
|
||||
Box {
|
||||
LazyVerticalGrid(
|
||||
state = lazyGridState,
|
||||
columns = GridCells.Adaptive(Dimensions.thumbnails.song * 2 + Dimensions.itemsVerticalPadding * 2),
|
||||
contentPadding = LocalPlayerAwareWindowInsets.current
|
||||
.only(WindowInsetsSides.Vertical + WindowInsetsSides.End).asPaddingValues(),
|
||||
verticalArrangement = Arrangement.spacedBy(Dimensions.itemsVerticalPadding * 2),
|
||||
horizontalArrangement = Arrangement.spacedBy(
|
||||
space = Dimensions.itemsVerticalPadding * 2,
|
||||
alignment = Alignment.CenterHorizontally
|
||||
),
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(colorPalette.background0)
|
||||
) {
|
||||
item(key = "header", contentType = 0, span = { GridItemSpan(maxLineSpan) }) {
|
||||
Header(title = "Плейлисты") {
|
||||
SecondaryTextButton(
|
||||
text = "Новый плейлист",
|
||||
onClick = { isCreatingANewPlaylist = true }
|
||||
)
|
||||
LazyVerticalGrid(
|
||||
state = lazyGridState,
|
||||
columns = GridCells.Adaptive(Dimensions.thumbnails.song * 2 + Dimensions.itemsVerticalPadding * 2),
|
||||
contentPadding = LocalPlayerAwareWindowInsets.current
|
||||
.only(WindowInsetsSides.Vertical + WindowInsetsSides.End).asPaddingValues(),
|
||||
verticalArrangement = Arrangement.spacedBy(Dimensions.itemsVerticalPadding * 2),
|
||||
horizontalArrangement = Arrangement.spacedBy(
|
||||
space = Dimensions.itemsVerticalPadding * 2,
|
||||
alignment = Alignment.CenterHorizontally
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(colorPalette.background0)
|
||||
) {
|
||||
item(key = "header", contentType = 0, span = { GridItemSpan(maxLineSpan) }) {
|
||||
Header(title = "Плейлисты") {
|
||||
SecondaryTextButton(
|
||||
text = "Новый плейлист",
|
||||
onClick = { isCreatingANewPlaylist = true }
|
||||
)
|
||||
|
||||
Spacer(
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
)
|
||||
|
||||
HeaderIconButton(
|
||||
icon = R.drawable.medical,
|
||||
color = if (sortBy == PlaylistSortBy.SongCount) colorPalette.text else colorPalette.textDisabled,
|
||||
onClick = { sortBy = PlaylistSortBy.SongCount }
|
||||
)
|
||||
|
||||
HeaderIconButton(
|
||||
icon = R.drawable.text,
|
||||
color = if (sortBy == PlaylistSortBy.Name) colorPalette.text else colorPalette.textDisabled,
|
||||
onClick = { sortBy = PlaylistSortBy.Name }
|
||||
)
|
||||
|
||||
HeaderIconButton(
|
||||
icon = R.drawable.time,
|
||||
color = if (sortBy == PlaylistSortBy.DateAdded) colorPalette.text else colorPalette.textDisabled,
|
||||
onClick = { sortBy = PlaylistSortBy.DateAdded }
|
||||
)
|
||||
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.width(2.dp)
|
||||
)
|
||||
|
||||
HeaderIconButton(
|
||||
icon = R.drawable.arrow_up,
|
||||
color = colorPalette.text,
|
||||
onClick = { sortOrder = !sortOrder },
|
||||
modifier = Modifier
|
||||
.graphicsLayer { rotationZ = sortOrderIconRotation }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item(key = "favorites") {
|
||||
PlaylistItem(
|
||||
icon = R.drawable.heart,
|
||||
colorTint = colorPalette.red,
|
||||
name = "Любимые",
|
||||
songCount = null,
|
||||
thumbnailSizeDp = thumbnailSizeDp,
|
||||
alternative = true,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
)
|
||||
|
||||
HeaderIconButton(
|
||||
icon = R.drawable.medical,
|
||||
color = if (sortBy == PlaylistSortBy.SongCount) colorPalette.text else colorPalette.textDisabled,
|
||||
onClick = { sortBy = PlaylistSortBy.SongCount }
|
||||
)
|
||||
|
||||
HeaderIconButton(
|
||||
icon = R.drawable.text,
|
||||
color = if (sortBy == PlaylistSortBy.Name) colorPalette.text else colorPalette.textDisabled,
|
||||
onClick = { sortBy = PlaylistSortBy.Name }
|
||||
)
|
||||
|
||||
HeaderIconButton(
|
||||
icon = R.drawable.time,
|
||||
color = if (sortBy == PlaylistSortBy.DateAdded) colorPalette.text else colorPalette.textDisabled,
|
||||
onClick = { sortBy = PlaylistSortBy.DateAdded }
|
||||
)
|
||||
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.width(2.dp)
|
||||
)
|
||||
|
||||
HeaderIconButton(
|
||||
icon = R.drawable.arrow_up,
|
||||
color = colorPalette.text,
|
||||
onClick = { sortOrder = !sortOrder },
|
||||
modifier = Modifier
|
||||
.graphicsLayer { rotationZ = sortOrderIconRotation }
|
||||
.clickable(onClick = { onBuiltInPlaylist(BuiltInPlaylist.Favorites) })
|
||||
.animateItemPlacement()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item(key = "favorites") {
|
||||
PlaylistItem(
|
||||
icon = R.drawable.heart,
|
||||
colorTint = colorPalette.red,
|
||||
name = "Любимые",
|
||||
songCount = null,
|
||||
thumbnailSizeDp = thumbnailSizeDp,
|
||||
alternative = true,
|
||||
modifier = Modifier
|
||||
.clickable(onClick = { onBuiltInPlaylist(BuiltInPlaylist.Favorites) })
|
||||
.animateItemPlacement()
|
||||
)
|
||||
}
|
||||
item(key = "offline") {
|
||||
PlaylistItem(
|
||||
icon = R.drawable.airplane,
|
||||
colorTint = colorPalette.blue,
|
||||
name = "Сохранённые",
|
||||
songCount = null,
|
||||
thumbnailSizeDp = thumbnailSizeDp,
|
||||
alternative = true,
|
||||
modifier = Modifier
|
||||
.clickable(onClick = { onBuiltInPlaylist(BuiltInPlaylist.Offline) })
|
||||
.animateItemPlacement()
|
||||
)
|
||||
}
|
||||
|
||||
item(key = "offline") {
|
||||
PlaylistItem(
|
||||
icon = R.drawable.airplane,
|
||||
colorTint = colorPalette.blue,
|
||||
name = "Сохранённые",
|
||||
songCount = null,
|
||||
thumbnailSizeDp = thumbnailSizeDp,
|
||||
alternative = true,
|
||||
modifier = Modifier
|
||||
.clickable(onClick = { onBuiltInPlaylist(BuiltInPlaylist.Offline) })
|
||||
.animateItemPlacement()
|
||||
)
|
||||
}
|
||||
|
||||
items(items = items, key = { it.playlist.id }) { playlistPreview ->
|
||||
PlaylistItem(
|
||||
playlist = playlistPreview,
|
||||
thumbnailSizeDp = thumbnailSizeDp,
|
||||
thumbnailSizePx = thumbnailSizePx,
|
||||
alternative = true,
|
||||
modifier = Modifier
|
||||
.clickable(onClick = { onPlaylistClick(playlistPreview.playlist) })
|
||||
.animateItemPlacement()
|
||||
)
|
||||
items(items = items, key = { it.playlist.id }) { playlistPreview ->
|
||||
PlaylistItem(
|
||||
playlist = playlistPreview,
|
||||
thumbnailSizeDp = thumbnailSizeDp,
|
||||
thumbnailSizePx = thumbnailSizePx,
|
||||
alternative = true,
|
||||
modifier = Modifier
|
||||
.clickable(onClick = { onPlaylistClick(playlistPreview.playlist) })
|
||||
.animateItemPlacement()
|
||||
)
|
||||
}
|
||||
item {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(start = 14.dp, end = 10.dp, top = 30.dp)
|
||||
.align(Alignment.CenterHorizontally),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
YandexAdsBanner(id = "R-M-5961316-1")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FloatingActionsContainerWithScrollToTop(
|
||||
lazyGridState = lazyGridState,
|
||||
iconId = R.drawable.search,
|
||||
|
||||
@@ -53,6 +53,10 @@ import it.hamy.muza.utils.secondary
|
||||
import it.hamy.muza.utils.semiBold
|
||||
import it.hamy.muza.utils.trackLoopEnabledKey
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import it.hamy.muza.models.Info
|
||||
import it.hamy.muza.ui.screens.artistRoute
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@Composable
|
||||
fun Controls(
|
||||
@@ -79,8 +83,14 @@ fun Controls(
|
||||
mutableStateOf<Long?>(null)
|
||||
}
|
||||
|
||||
LaunchedEffect(mediaId) {
|
||||
Database.likedAt(mediaId).distinctUntilChanged().collect { likedAt = it }
|
||||
var artistsInfo: List<Info>? by remember { mutableStateOf(null) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
withContext(Dispatchers.IO) {
|
||||
if (artistsInfo == null) artistsInfo = Database.songArtistInfo(mediaId)
|
||||
|
||||
Database.likedAt(mediaId).distinctUntilChanged().collect { likedAt = it }
|
||||
}
|
||||
}
|
||||
|
||||
val shouldBePlayingTransition = updateTransition(shouldBePlaying, label = "shouldBePlaying")
|
||||
@@ -113,7 +123,11 @@ fun Controls(
|
||||
text = artist ?: "",
|
||||
style = typography.s.semiBold.secondary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.clickable {
|
||||
val goTo = artistRoute::global
|
||||
goTo(artistsInfo?.get(0)?.id)
|
||||
}
|
||||
)
|
||||
|
||||
Spacer(
|
||||
|
||||
@@ -79,6 +79,11 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.withContext
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.ContextCompat.getSystemService
|
||||
|
||||
@Composable
|
||||
fun Lyrics(
|
||||
@@ -91,6 +96,10 @@ fun Lyrics(
|
||||
ensureSongInserted: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
|
||||
|
||||
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = isDisplayed,
|
||||
enter = fadeIn(),
|
||||
@@ -117,11 +126,15 @@ fun Lyrics(
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
|
||||
val clipboardManager = ContextCompat.getSystemService(context, ClipboardManager::class.java)
|
||||
|
||||
LaunchedEffect(mediaId, isShowingSynchronizedLyrics) {
|
||||
withContext(Dispatchers.IO) {
|
||||
Database.lyrics(mediaId).collect {
|
||||
if (isShowingSynchronizedLyrics && it?.synced == null) {
|
||||
val mediaMetadata = mediaMetadataProvider()
|
||||
|
||||
var duration = withContext(Dispatchers.Main) {
|
||||
durationProvider()
|
||||
}
|
||||
@@ -352,6 +365,15 @@ fun Lyrics(
|
||||
}
|
||||
)
|
||||
|
||||
MenuEntry(
|
||||
icon = R.drawable.text,
|
||||
text = "Скопировать текст",
|
||||
onClick = {
|
||||
menuState.hide()
|
||||
clipboardManager?.setPrimaryClip(ClipData.newPlainText("Lyrics", lyrics?.fixed))
|
||||
}
|
||||
)
|
||||
|
||||
MenuEntry(
|
||||
icon = R.drawable.search,
|
||||
text = "Искать текст в интернете",
|
||||
|
||||
@@ -6,6 +6,8 @@ import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.ContentTransform
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.VectorConverter
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.core.updateTransition
|
||||
import androidx.compose.animation.fadeIn
|
||||
@@ -15,6 +17,9 @@ import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.gestures.draggable
|
||||
import androidx.compose.foundation.gestures.rememberDraggableState
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -25,6 +30,7 @@ import androidx.compose.foundation.layout.asPaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.only
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
@@ -36,6 +42,7 @@ import androidx.compose.foundation.text.BasicText
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
@@ -44,10 +51,12 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.Player
|
||||
@@ -82,12 +91,7 @@ import it.hamy.muza.utils.shuffleQueue
|
||||
import it.hamy.muza.utils.smoothScrollToTop
|
||||
import it.hamy.muza.utils.windows
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.gestures.draggable
|
||||
import androidx.compose.foundation.gestures.rememberDraggableState
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@ExperimentalFoundationApi
|
||||
@@ -188,6 +192,10 @@ fun Queue(
|
||||
|
||||
val musicBarsTransition = updateTransition(targetState = mediaItemIndex, label = "")
|
||||
|
||||
val deleteHistory = remember {
|
||||
mutableStateListOf<String>()
|
||||
}
|
||||
|
||||
Column {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
@@ -209,8 +217,7 @@ fun Queue(
|
||||
key = { it.uid.hashCode() }
|
||||
) { window ->
|
||||
val isPlayingThisMediaItem = mediaItemIndex == window.firstPeriodIndex
|
||||
var offsetX by remember { mutableStateOf(0f) }
|
||||
|
||||
val offsetX = remember { Animatable(Offset(0f, 0f), Offset.VectorConverter) }
|
||||
SongItem(
|
||||
song = window.mediaItem,
|
||||
thumbnailSizePx = thumbnailSizePx,
|
||||
@@ -291,23 +298,43 @@ fun Queue(
|
||||
reorderingState = reorderingState,
|
||||
index = window.firstPeriodIndex
|
||||
)
|
||||
|
||||
.draggable(
|
||||
orientation = Orientation.Horizontal,
|
||||
state = rememberDraggableState(onDelta = { delta ->
|
||||
if (isPlayingThisMediaItem) return@rememberDraggableState
|
||||
offsetX += delta
|
||||
runBlocking {
|
||||
offsetX.snapTo(offsetX.value + Offset(delta, 0f))
|
||||
}
|
||||
}),
|
||||
onDragStopped = { velocity ->
|
||||
if ((offsetX <= -300.0f && velocity <= -3000.0f) || (offsetX >= 300.0f && velocity >= 3000.0f)) {
|
||||
binder.player.removeMediaItem(window.firstPeriodIndex)
|
||||
onDragStopped = { _ ->
|
||||
if (offsetX.value.x >= 200.0f || offsetX.value.x <= -200.0f) {
|
||||
val currentIndex = window.firstPeriodIndex
|
||||
val mediaId = window.mediaItem.mediaId
|
||||
|
||||
if (deleteHistory.indexOf(mediaId) != -1) return@draggable
|
||||
deleteHistory.add(mediaId)
|
||||
|
||||
var indexToDelete = currentIndex
|
||||
for (i in 0 until currentIndex) {
|
||||
if (deleteHistory.indexOf(windows.elementAt(i).mediaItem.mediaId) != -1) {
|
||||
indexToDelete--
|
||||
}
|
||||
}
|
||||
|
||||
if (offsetX.value.x < 0) {
|
||||
offsetX.animateTo(Offset(-1500.0f, offsetX.value.y))
|
||||
} else {
|
||||
offsetX.animateTo(Offset(1500.0f, offsetX.value.y))
|
||||
}
|
||||
|
||||
binder.player.removeMediaItem(indexToDelete)
|
||||
deleteHistory.removeFirst()
|
||||
} else {
|
||||
offsetX = 0f
|
||||
offsetX.animateTo(Offset(0f, offsetX.value.y))
|
||||
}
|
||||
}
|
||||
)
|
||||
.offset{ IntOffset(offsetX.roundToInt(), 0) }
|
||||
|
||||
.offset { IntOffset(offsetX.value.x.roundToInt(), 0) }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -356,7 +383,7 @@ fun Queue(
|
||||
.height(64.dp)
|
||||
) {
|
||||
BasicText(
|
||||
text = "${windows.size} песни",
|
||||
text = "${windows.size}",
|
||||
style = typography.xxs.medium,
|
||||
modifier = Modifier
|
||||
.background(
|
||||
@@ -386,7 +413,7 @@ fun Queue(
|
||||
.animateContentSize()
|
||||
) {
|
||||
BasicText(
|
||||
text = "Повтор очереди ",
|
||||
text = "Повтор ",
|
||||
style = typography.xxs.medium,
|
||||
)
|
||||
|
||||
@@ -402,7 +429,7 @@ fun Queue(
|
||||
}
|
||||
) {
|
||||
BasicText(
|
||||
text = if (it) "ВКЛ." else "ВЫКЛ.",
|
||||
text = if (it) "вкл." else "откл.",
|
||||
style = typography.xxs.medium,
|
||||
)
|
||||
}
|
||||
@@ -410,4 +437,4 @@ fun Queue(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -48,7 +48,7 @@ fun AppearanceSettings() {
|
||||
var colorPaletteMode by rememberPreference(colorPaletteModeKey, ColorPaletteMode.System)
|
||||
var thumbnailRoundness by rememberPreference(
|
||||
thumbnailRoundnessKey,
|
||||
ThumbnailRoundness.Light
|
||||
ThumbnailRoundness.Слабое
|
||||
)
|
||||
var useSystemFont by rememberPreference(useSystemFontKey, false)
|
||||
var applyFontPadding by rememberPreference(applyFontPaddingKey, false)
|
||||
|
||||
@@ -190,7 +190,7 @@ fun OtherSettings() {
|
||||
|
||||
SwitchSettingEntry(
|
||||
title = "Invincible service",
|
||||
text = "When turning off battery optimizations is not enough",
|
||||
text = "Обход экономии батареи",
|
||||
isChecked = isInvincibilityEnabled,
|
||||
onCheckedChange = { isInvincibilityEnabled = it }
|
||||
)
|
||||
@@ -199,8 +199,8 @@ fun OtherSettings() {
|
||||
SettingsEntryGroupText(title = "PROXY")
|
||||
|
||||
SwitchSettingEntry(
|
||||
title = "HTTP Proxy",
|
||||
text = "Enable HTTP Proxy",
|
||||
title = "Proxy",
|
||||
text = "Включить proxy",
|
||||
isChecked = isProxyEnabled,
|
||||
onCheckedChange = { isProxyEnabled = it }
|
||||
)
|
||||
@@ -211,7 +211,7 @@ fun OtherSettings() {
|
||||
selectedValue = proxyMode, onValueSelected = {proxyMode = it})
|
||||
TextDialogSettingEntry(
|
||||
title = "Хост",
|
||||
text = "Введите http хост",
|
||||
text = "Введите хост",
|
||||
currentText = proxyHost,
|
||||
onTextSave = { proxyHost = it })
|
||||
TextDialogSettingEntry(
|
||||
|
||||
@@ -23,7 +23,7 @@ data class MusicShelfRenderer(
|
||||
?: emptyList()) to
|
||||
(musicResponsiveListItemRenderer
|
||||
?.flexColumns
|
||||
?.lastOrNull()
|
||||
?.getOrNull(1)
|
||||
?.musicResponsiveListItemFlexColumnRenderer
|
||||
?.text
|
||||
?.splitBySeparator()
|
||||
|
||||
Reference in New Issue
Block a user