7 Commits

Author SHA1 Message Date
Dmitriy Chugunov
20cdcddd36 add yandex ads banner 2024-02-11 23:37:22 +03:00
c79fa89c1d Обновление
0.5.4.2
2024-01-30 15:14:45 +05:00
Hamy
2a995364a2 Update README.md 2023-09-20 15:27:25 +05:00
Hamy
0f017a781f Update README.md 2023-09-20 15:26:43 +05:00
Hamy
3e251a9b7a Update README.md 2023-09-20 15:26:22 +05:00
Hamy
5c7257de3f Update README.md 2023-09-20 15:25:54 +05:00
Hamy
873505776b Update README.md 2023-09-20 15:20:21 +05:00
16 changed files with 315 additions and 140 deletions

View File

@@ -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" [<img src="https://github.com/machiav3lli/oandbackupx/blob/034b226cea5c1b30eb4f6a6f313e4dadcbb0ece4/badge_github.png"
alt="Скачать из Гитхаба" alt="Скачать из Гитхаба"
height="80">](https://github.com/hammsterr/muza/releases/latest) height="80">](https://github.com/hammsterr/muza/releases/latest)

View File

@@ -75,6 +75,7 @@ dependencies {
implementation(projects.composeReordering) implementation(projects.composeReordering)
implementation(libs.compose.activity) implementation(libs.compose.activity)
implementation(libs.compose.foundation) implementation(libs.compose.foundation)
implementation(libs.compose.ui) implementation(libs.compose.ui)
@@ -90,6 +91,10 @@ dependencies {
implementation(libs.room) implementation(libs.room)
implementation("androidx.media3:media3-datasource-okhttp:1.0.0-alpha03") 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) kapt(libs.room.compiler)
implementation(projects.innertube) implementation(projects.innertube)

View File

@@ -31,4 +31,6 @@
-dontwarn org.openjsse.javax.net.ssl.SSLParameters -dontwarn org.openjsse.javax.net.ssl.SSLParameters
-dontwarn org.openjsse.javax.net.ssl.SSLSocket -dontwarn org.openjsse.javax.net.ssl.SSLSocket
-dontwarn org.openjsse.net.ssl.OpenJSSE -dontwarn org.openjsse.net.ssl.OpenJSSE
-dontwarn org.slf4j.impl.StaticLoggerBinder -dontwarn org.slf4j.impl.StaticLoggerBinder
-keep class com.yandex** { *; }

View File

@@ -1,11 +1,14 @@
<?xml version="1.0" encoding="utf-8"?> <?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.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" /> <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> <queries>
<intent> <intent>

View File

@@ -128,6 +128,7 @@ class MainActivity : ComponentActivity(), PersistMapOwner {
override lateinit var persistMap: PersistMap override lateinit var persistMap: PersistMap
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
bindService(intent<PlayerService>(), serviceConnection, Context.BIND_AUTO_CREATE) bindService(intent<PlayerService>(), serviceConnection, Context.BIND_AUTO_CREATE)
@@ -168,7 +169,7 @@ class MainActivity : ComponentActivity(), PersistMapOwner {
val colorPaletteName = getEnum(colorPaletteNameKey, ColorPaletteName.Dynamic) val colorPaletteName = getEnum(colorPaletteNameKey, ColorPaletteName.Dynamic)
val colorPaletteMode = getEnum(colorPaletteModeKey, ColorPaletteMode.System) val colorPaletteMode = getEnum(colorPaletteModeKey, ColorPaletteMode.System)
val thumbnailRoundness = val thumbnailRoundness =
getEnum(thumbnailRoundnessKey, ThumbnailRoundness.Light) getEnum(thumbnailRoundnessKey, ThumbnailRoundness.Слабое)
val useSystemFont = getBoolean(useSystemFontKey, false) val useSystemFont = getBoolean(useSystemFontKey, false)
val applyFontPadding = getBoolean(applyFontPaddingKey, false) val applyFontPadding = getBoolean(applyFontPaddingKey, false)
@@ -267,7 +268,7 @@ class MainActivity : ComponentActivity(), PersistMapOwner {
thumbnailRoundnessKey -> { thumbnailRoundnessKey -> {
val thumbnailRoundness = val thumbnailRoundness =
sharedPreferences.getEnum(key, ThumbnailRoundness.Light) sharedPreferences.getEnum(key, ThumbnailRoundness.Слабое)
appearance = appearance.copy( appearance = appearance.copy(
thumbnailShape = thumbnailRoundness.shape() thumbnailShape = thumbnailRoundness.shape()

View File

@@ -4,6 +4,7 @@ import android.app.Application
import coil.ImageLoader import coil.ImageLoader
import coil.ImageLoaderFactory import coil.ImageLoaderFactory
import coil.disk.DiskCache import coil.disk.DiskCache
import com.yandex.mobile.ads.common.MobileAds
import it.hamy.muza.enums.CoilDiskCacheMaxSize import it.hamy.muza.enums.CoilDiskCacheMaxSize
import it.hamy.muza.utils.coilDiskCacheMaxSizeKey import it.hamy.muza.utils.coilDiskCacheMaxSizeKey
import it.hamy.muza.utils.getEnum import it.hamy.muza.utils.getEnum
@@ -13,6 +14,11 @@ class MainApplication : Application(), ImageLoaderFactory {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
DatabaseInitializer() DatabaseInitializer()
MobileAds.initialize(this) {
/**
* Инициализация либы яндекса
*/
}
} }
override fun newImageLoader(): ImageLoader { override fun newImageLoader(): ImageLoader {

View File

@@ -6,17 +6,19 @@ import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
enum class ThumbnailRoundness { enum class ThumbnailRoundness {
None, Отключено,
Light, Слабое,
Medium, Среднее,
Heavy; Сильное,
Максимальное;
fun shape(): Shape { fun shape(): Shape {
return when (this) { return when (this) {
None -> RectangleShape Отключено -> RectangleShape
Light -> RoundedCornerShape(2.dp) Слабое -> RoundedCornerShape(2.dp)
Medium -> RoundedCornerShape(4.dp) Среднее -> RoundedCornerShape(4.dp)
Heavy -> RoundedCornerShape(8.dp) Сильное -> RoundedCornerShape(8.dp)
Максимальное -> RoundedCornerShape(14.dp)
} }
} }
} }

View File

@@ -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)
}
})
}

View File

@@ -176,11 +176,11 @@ fun ArtistScreen(browseId: String) {
tabIndex = tabIndex, tabIndex = tabIndex,
onTabChanged = { tabIndex = it }, onTabChanged = { tabIndex = it },
tabColumnContent = { Item -> tabColumnContent = { Item ->
Item(0, "Overview", R.drawable.sparkles) Item(0, "Обзор", R.drawable.sparkles)
Item(1, "Songs", R.drawable.musical_notes) Item(1, "Песни", R.drawable.musical_notes)
Item(2, "Albums", R.drawable.disc) Item(2, "Альбомы", R.drawable.disc)
Item(3, "Singles", R.drawable.disc) Item(3, "Синглы", R.drawable.disc)
Item(4, "Library", R.drawable.library) Item(4, "Библиотека", R.drawable.library)
}, },
) { currentTabIndex -> ) { currentTabIndex ->
saveableStateHolder.SaveableStateProvider(key = currentTabIndex) { saveableStateHolder.SaveableStateProvider(key = currentTabIndex) {
@@ -267,7 +267,7 @@ fun ArtistScreen(browseId: String) {
ItemsPage( ItemsPage(
tag = "artist/$browseId/albums", tag = "artist/$browseId/albums",
headerContent = headerContent, headerContent = headerContent,
emptyItemsText = "This artist didn't release any album", emptyItemsText = "Исполнитель не выпустил ни одного альбома",
itemsPageProvider = artistPage?.let { itemsPageProvider = artistPage?.let {
({ continuation -> ({ continuation ->
continuation?.let { continuation?.let {
@@ -317,7 +317,7 @@ fun ArtistScreen(browseId: String) {
ItemsPage( ItemsPage(
tag = "artist/$browseId/singles", tag = "artist/$browseId/singles",
headerContent = headerContent, headerContent = headerContent,
emptyItemsText = "This artist didn't release any single", emptyItemsText = "Исполнитель не выпустил ни одного сингла",
itemsPageProvider = artistPage?.let { itemsPageProvider = artistPage?.let {
({ continuation -> ({ continuation ->
continuation?.let { continuation?.let {

View File

@@ -9,17 +9,22 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.GridItemSpan import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.grid.rememberLazyGridState 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.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue 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.Playlist
import it.hamy.muza.models.PlaylistPreview import it.hamy.muza.models.PlaylistPreview
import it.hamy.muza.query 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.FloatingActionsContainerWithScrollToTop
import it.hamy.muza.ui.components.themed.Header import it.hamy.muza.ui.components.themed.Header
import it.hamy.muza.ui.components.themed.HeaderIconButton 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.playlistSortOrderKey
import it.hamy.muza.utils.rememberPreference import it.hamy.muza.utils.rememberPreference
@ExperimentalAnimationApi @ExperimentalAnimationApi
@ExperimentalFoundationApi @ExperimentalFoundationApi
@Composable @Composable
fun HomePlaylists( fun HomePlaylists(
onBuiltInPlaylist: (BuiltInPlaylist) -> Unit, onBuiltInPlaylist: (BuiltInPlaylist) -> Unit,
onPlaylistClick: (Playlist) -> Unit, onPlaylistClick: (Playlist) -> Unit,
@@ -67,6 +75,8 @@ fun HomePlaylists(
mutableStateOf(false) mutableStateOf(false)
} }
if (isCreatingANewPlaylist) { if (isCreatingANewPlaylist) {
TextFieldDialog( TextFieldDialog(
hintText = "Введите название плейлиста", hintText = "Введите название плейлиста",
@@ -100,107 +110,122 @@ fun HomePlaylists(
val lazyGridState = rememberLazyGridState() val lazyGridState = rememberLazyGridState()
Box { Box(modifier = Modifier.fillMaxSize()) {
LazyVerticalGrid( Column(
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 modifier = Modifier
.fillMaxSize() .fillMaxSize()
.background(colorPalette.background0)
) { ) {
item(key = "header", contentType = 0, span = { GridItemSpan(maxLineSpan) }) { LazyVerticalGrid(
Header(title = "Плейлисты") { state = lazyGridState,
SecondaryTextButton( columns = GridCells.Adaptive(Dimensions.thumbnails.song * 2 + Dimensions.itemsVerticalPadding * 2),
text = "Новый плейлист", contentPadding = LocalPlayerAwareWindowInsets.current
onClick = { isCreatingANewPlaylist = true } .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 modifier = Modifier
.weight(1f) .clickable(onClick = { onBuiltInPlaylist(BuiltInPlaylist.Favorites) })
) .animateItemPlacement()
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") { item(key = "offline") {
PlaylistItem( PlaylistItem(
icon = R.drawable.heart, icon = R.drawable.airplane,
colorTint = colorPalette.red, colorTint = colorPalette.blue,
name = "Любимые", name = "Сохранённые",
songCount = null, songCount = null,
thumbnailSizeDp = thumbnailSizeDp, thumbnailSizeDp = thumbnailSizeDp,
alternative = true, alternative = true,
modifier = Modifier modifier = Modifier
.clickable(onClick = { onBuiltInPlaylist(BuiltInPlaylist.Favorites) }) .clickable(onClick = { onBuiltInPlaylist(BuiltInPlaylist.Offline) })
.animateItemPlacement() .animateItemPlacement()
) )
} }
item(key = "offline") { items(items = items, key = { it.playlist.id }) { playlistPreview ->
PlaylistItem( PlaylistItem(
icon = R.drawable.airplane, playlist = playlistPreview,
colorTint = colorPalette.blue, thumbnailSizeDp = thumbnailSizeDp,
name = "Сохранённые", thumbnailSizePx = thumbnailSizePx,
songCount = null, alternative = true,
thumbnailSizeDp = thumbnailSizeDp, modifier = Modifier
alternative = true, .clickable(onClick = { onPlaylistClick(playlistPreview.playlist) })
modifier = Modifier .animateItemPlacement()
.clickable(onClick = { onBuiltInPlaylist(BuiltInPlaylist.Offline) }) )
.animateItemPlacement() }
) item {
} Box(
modifier = Modifier
items(items = items, key = { it.playlist.id }) { playlistPreview -> .fillMaxSize()
PlaylistItem( .padding(start = 14.dp, end = 10.dp, top = 30.dp)
playlist = playlistPreview, .align(Alignment.CenterHorizontally),
thumbnailSizeDp = thumbnailSizeDp, contentAlignment = Alignment.Center,
thumbnailSizePx = thumbnailSizePx, ) {
alternative = true, YandexAdsBanner(id = "R-M-5961316-1")
modifier = Modifier }
.clickable(onClick = { onPlaylistClick(playlistPreview.playlist) }) }
.animateItemPlacement()
)
} }
} }
FloatingActionsContainerWithScrollToTop( FloatingActionsContainerWithScrollToTop(
lazyGridState = lazyGridState, lazyGridState = lazyGridState,
iconId = R.drawable.search, iconId = R.drawable.search,

View File

@@ -53,6 +53,10 @@ import it.hamy.muza.utils.secondary
import it.hamy.muza.utils.semiBold import it.hamy.muza.utils.semiBold
import it.hamy.muza.utils.trackLoopEnabledKey import it.hamy.muza.utils.trackLoopEnabledKey
import kotlinx.coroutines.flow.distinctUntilChanged 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 @Composable
fun Controls( fun Controls(
@@ -79,8 +83,14 @@ fun Controls(
mutableStateOf<Long?>(null) mutableStateOf<Long?>(null)
} }
LaunchedEffect(mediaId) { var artistsInfo: List<Info>? by remember { mutableStateOf(null) }
Database.likedAt(mediaId).distinctUntilChanged().collect { likedAt = it }
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") val shouldBePlayingTransition = updateTransition(shouldBePlaying, label = "shouldBePlaying")
@@ -113,7 +123,11 @@ fun Controls(
text = artist ?: "", text = artist ?: "",
style = typography.s.semiBold.secondary, style = typography.s.semiBold.secondary,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis,
modifier = Modifier.clickable {
val goTo = artistRoute::global
goTo(artistsInfo?.get(0)?.id)
}
) )
Spacer( Spacer(

View File

@@ -79,6 +79,11 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.withContext 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 @Composable
fun Lyrics( fun Lyrics(
@@ -91,6 +96,10 @@ fun Lyrics(
ensureSongInserted: () -> Unit, ensureSongInserted: () -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
AnimatedVisibility( AnimatedVisibility(
visible = isDisplayed, visible = isDisplayed,
enter = fadeIn(), enter = fadeIn(),
@@ -117,11 +126,15 @@ fun Lyrics(
mutableStateOf(false) mutableStateOf(false)
} }
val clipboardManager = ContextCompat.getSystemService(context, ClipboardManager::class.java)
LaunchedEffect(mediaId, isShowingSynchronizedLyrics) { LaunchedEffect(mediaId, isShowingSynchronizedLyrics) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
Database.lyrics(mediaId).collect { Database.lyrics(mediaId).collect {
if (isShowingSynchronizedLyrics && it?.synced == null) { if (isShowingSynchronizedLyrics && it?.synced == null) {
val mediaMetadata = mediaMetadataProvider() val mediaMetadata = mediaMetadataProvider()
var duration = withContext(Dispatchers.Main) { var duration = withContext(Dispatchers.Main) {
durationProvider() durationProvider()
} }
@@ -352,6 +365,15 @@ fun Lyrics(
} }
) )
MenuEntry(
icon = R.drawable.text,
text = "Скопировать текст",
onClick = {
menuState.hide()
clipboardManager?.setPrimaryClip(ClipData.newPlainText("Lyrics", lyrics?.fixed))
}
)
MenuEntry( MenuEntry(
icon = R.drawable.search, icon = R.drawable.search,
text = "Искать текст в интернете", text = "Искать текст в интернете",

View File

@@ -6,6 +6,8 @@ import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ContentTransform import androidx.compose.animation.ContentTransform
import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.animateContentSize 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.tween
import androidx.compose.animation.core.updateTransition import androidx.compose.animation.core.updateTransition
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
@@ -15,6 +17,9 @@ import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable 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.Box
import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column 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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size 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.material.ripple.rememberRipple
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue 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.alpha
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
import androidx.media3.common.Player 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.smoothScrollToTop
import it.hamy.muza.utils.windows import it.hamy.muza.utils.windows
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
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 kotlin.math.roundToInt import kotlin.math.roundToInt
@ExperimentalFoundationApi @ExperimentalFoundationApi
@@ -188,6 +192,10 @@ fun Queue(
val musicBarsTransition = updateTransition(targetState = mediaItemIndex, label = "") val musicBarsTransition = updateTransition(targetState = mediaItemIndex, label = "")
val deleteHistory = remember {
mutableStateListOf<String>()
}
Column { Column {
Box( Box(
modifier = Modifier modifier = Modifier
@@ -209,8 +217,7 @@ fun Queue(
key = { it.uid.hashCode() } key = { it.uid.hashCode() }
) { window -> ) { window ->
val isPlayingThisMediaItem = mediaItemIndex == window.firstPeriodIndex val isPlayingThisMediaItem = mediaItemIndex == window.firstPeriodIndex
var offsetX by remember { mutableStateOf(0f) } val offsetX = remember { Animatable(Offset(0f, 0f), Offset.VectorConverter) }
SongItem( SongItem(
song = window.mediaItem, song = window.mediaItem,
thumbnailSizePx = thumbnailSizePx, thumbnailSizePx = thumbnailSizePx,
@@ -291,23 +298,43 @@ fun Queue(
reorderingState = reorderingState, reorderingState = reorderingState,
index = window.firstPeriodIndex index = window.firstPeriodIndex
) )
.draggable( .draggable(
orientation = Orientation.Horizontal, orientation = Orientation.Horizontal,
state = rememberDraggableState(onDelta = { delta -> state = rememberDraggableState(onDelta = { delta ->
if (isPlayingThisMediaItem) return@rememberDraggableState if (isPlayingThisMediaItem) return@rememberDraggableState
offsetX += delta runBlocking {
offsetX.snapTo(offsetX.value + Offset(delta, 0f))
}
}), }),
onDragStopped = { velocity -> onDragStopped = { _ ->
if ((offsetX <= -300.0f && velocity <= -3000.0f) || (offsetX >= 300.0f && velocity >= 3000.0f)) { if (offsetX.value.x >= 200.0f || offsetX.value.x <= -200.0f) {
binder.player.removeMediaItem(window.firstPeriodIndex) 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 { } 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) .height(64.dp)
) { ) {
BasicText( BasicText(
text = "${windows.size} песни", text = "${windows.size}",
style = typography.xxs.medium, style = typography.xxs.medium,
modifier = Modifier modifier = Modifier
.background( .background(
@@ -386,7 +413,7 @@ fun Queue(
.animateContentSize() .animateContentSize()
) { ) {
BasicText( BasicText(
text = "Повтор очереди ", text = "Повтор ",
style = typography.xxs.medium, style = typography.xxs.medium,
) )
@@ -402,7 +429,7 @@ fun Queue(
} }
) { ) {
BasicText( BasicText(
text = if (it) "ВКЛ." else "ВЫКЛ.", text = if (it) "вкл." else "откл.",
style = typography.xxs.medium, style = typography.xxs.medium,
) )
} }
@@ -410,4 +437,4 @@ fun Queue(
} }
} }
} }
} }

View File

@@ -48,7 +48,7 @@ fun AppearanceSettings() {
var colorPaletteMode by rememberPreference(colorPaletteModeKey, ColorPaletteMode.System) var colorPaletteMode by rememberPreference(colorPaletteModeKey, ColorPaletteMode.System)
var thumbnailRoundness by rememberPreference( var thumbnailRoundness by rememberPreference(
thumbnailRoundnessKey, thumbnailRoundnessKey,
ThumbnailRoundness.Light ThumbnailRoundness.Слабое
) )
var useSystemFont by rememberPreference(useSystemFontKey, false) var useSystemFont by rememberPreference(useSystemFontKey, false)
var applyFontPadding by rememberPreference(applyFontPaddingKey, false) var applyFontPadding by rememberPreference(applyFontPaddingKey, false)

View File

@@ -190,7 +190,7 @@ fun OtherSettings() {
SwitchSettingEntry( SwitchSettingEntry(
title = "Invincible service", title = "Invincible service",
text = "When turning off battery optimizations is not enough", text = "Обход экономии батареи",
isChecked = isInvincibilityEnabled, isChecked = isInvincibilityEnabled,
onCheckedChange = { isInvincibilityEnabled = it } onCheckedChange = { isInvincibilityEnabled = it }
) )
@@ -199,8 +199,8 @@ fun OtherSettings() {
SettingsEntryGroupText(title = "PROXY") SettingsEntryGroupText(title = "PROXY")
SwitchSettingEntry( SwitchSettingEntry(
title = "HTTP Proxy", title = "Proxy",
text = "Enable HTTP Proxy", text = "Включить proxy",
isChecked = isProxyEnabled, isChecked = isProxyEnabled,
onCheckedChange = { isProxyEnabled = it } onCheckedChange = { isProxyEnabled = it }
) )
@@ -211,7 +211,7 @@ fun OtherSettings() {
selectedValue = proxyMode, onValueSelected = {proxyMode = it}) selectedValue = proxyMode, onValueSelected = {proxyMode = it})
TextDialogSettingEntry( TextDialogSettingEntry(
title = "Хост", title = "Хост",
text = "Введите http хост", text = "Введите хост",
currentText = proxyHost, currentText = proxyHost,
onTextSave = { proxyHost = it }) onTextSave = { proxyHost = it })
TextDialogSettingEntry( TextDialogSettingEntry(

View File

@@ -23,7 +23,7 @@ data class MusicShelfRenderer(
?: emptyList()) to ?: emptyList()) to
(musicResponsiveListItemRenderer (musicResponsiveListItemRenderer
?.flexColumns ?.flexColumns
?.lastOrNull() ?.getOrNull(1)
?.musicResponsiveListItemFlexColumnRenderer ?.musicResponsiveListItemFlexColumnRenderer
?.text ?.text
?.splitBySeparator() ?.splitBySeparator()