11 Commits

Author SHA1 Message Date
bfa3231823 Добавлен выбор источника Обзора
Some checks failed
CI / build (push) Has been cancelled
2024-02-24 01:02:27 +03:00
abe5d0b378 Исправлена работа лайков и ссылок на профиль исполнителя. 2024-02-24 00:07:30 +03:00
7b1138c428 Исправлена работа лайков и ссылок на профиль исполнителя. 2024-02-24 00:07:15 +03:00
Hamy
b2b12c473c Merge pull request #1 from hammsterr/feature-yandex-ads
Подключение РСЯ
2024-02-12 01:00:19 +03:00
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
23 changed files with 647 additions and 142 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"
alt="Скачать из Гитхаба"
height="80">](https://github.com/hammsterr/muza/releases/latest)

View File

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

View File

@@ -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** { *; }

View File

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

View File

@@ -55,6 +55,7 @@ import it.hamy.muza.models.SongPlaylistMap
import it.hamy.muza.models.SortedSongPlaylistMap
import kotlin.jvm.Throws
import kotlinx.coroutines.flow.Flow
import it.hamy.muza.models.EventWithSong
@Dao
interface Database {
@@ -316,6 +317,11 @@ interface Database {
@RewriteQueriesToDropUnusedColumns
fun trending(now: Long = System.currentTimeMillis()): Flow<Song?>
@Transaction
@Query("SELECT * FROM Event ORDER BY timestamp DESC")
fun events(): Flow<List<EventWithSong>>
@Query("SELECT COUNT (*) FROM Event")
fun eventsCount(): Flow<Int>

View File

@@ -0,0 +1,14 @@
package it.hamy.muza
import it.hamy.muza.preferences.PreferencesHolder
object Dependencies {
lateinit var application: MainApplication
private set
internal fun init(application: MainApplication) {
this.application = application
}
}
open class GlobalPreferencesHolder : PreferencesHolder(Dependencies.application, "preferences")

View File

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

View File

@@ -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,12 @@ class MainApplication : Application(), ImageLoaderFactory {
override fun onCreate() {
super.onCreate()
DatabaseInitializer()
Dependencies.init(this)
MobileAds.initialize(this) {
/**
* Инициализация либы яндекса
*/
}
}
override fun newImageLoader(): ImageLoader {

View File

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

View File

@@ -0,0 +1,16 @@
package it.hamy.muza.models
import androidx.compose.runtime.Immutable
import androidx.room.Embedded
import androidx.room.Relation
@Immutable
data class EventWithSong(
@Embedded val event: Event,
@Relation(
entity = Song::class,
parentColumn = "songId",
entityColumn = "id"
)
val song: Song
)

View File

@@ -0,0 +1,27 @@
package it.hamy.muza.preferences
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import it.hamy.muza.GlobalPreferencesHolder
import it.hamy.muza.R
import kotlin.time.Duration
import kotlin.time.Duration.Companion.days
object DataPreferences : GlobalPreferencesHolder() {
var topListLength by int(10)
var topListPeriod by enum(TopListPeriod.AllTime)
var quickPicksSource by enum(QuickPicksSource.Trending)
enum class TopListPeriod(val displayName: @Composable () -> String, val duration: Duration? = null) {
PastDay(displayName = { "Day" }, duration = 1.days),
PastWeek(displayName = { "Week" }, duration = 7.days),
PastMonth(displayName = { "Month" }, duration = 30.days),
PastYear(displayName = { "Year" }, 365.days),
AllTime(displayName = { "AllTime" })
}
enum class QuickPicksSource(val displayName: @Composable () -> String) {
Trending(displayName = { "Trend" }),
LastInteraction(displayName = { "LastInteraction" })
}
}

View File

@@ -0,0 +1,100 @@
package it.hamy.muza.preferences
import android.app.Application
import android.content.Context
import android.content.SharedPreferences
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.snapshots.Snapshot
import androidx.core.content.edit
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
private val coroutineScope = CoroutineScope(Dispatchers.IO)
fun <T : Any> sharedPreferencesProperty(
getValue: SharedPreferences.(key: String) -> T,
setValue: SharedPreferences.Editor.(key: String, value: T) -> Unit,
defaultValue: T
) = object : ReadWriteProperty<PreferencesHolder, T> {
private var state = mutableStateOf(defaultValue)
private var listener: OnSharedPreferenceChangeListener? = null
override fun getValue(thisRef: PreferencesHolder, property: KProperty<*>): T {
if (listener == null && !Snapshot.current.readOnly) {
state.value = thisRef.getValue(property.name)
listener = OnSharedPreferenceChangeListener { preferences, key ->
if (key == property.name) preferences.getValue(property.name)
.let { if (it != state && !Snapshot.current.readOnly) state.value = it }
}
thisRef.registerOnSharedPreferenceChangeListener(listener)
}
return state.value
}
override fun setValue(thisRef: PreferencesHolder, property: KProperty<*>, value: T) =
coroutineScope.launch {
thisRef.edit(commit = true) {
setValue(property.name, value)
}
}.let { }
}
/**
* A snapshottable, thread-safe, compose-first, extensible SharedPreferences wrapper that supports
* virtually all types, and if it doesn't, one could simply type `fun myNewType(...) = sharedPreferencesProperty(...)`
* and start implementing. Starts off as given defaultValue until we are allowed to subscribe to SharedPreferences
* @sample AppearancePreferences
*/
open class PreferencesHolder(
application: Application,
name: String,
mode: Int = Context.MODE_PRIVATE
) : SharedPreferences by application.getSharedPreferences(name, mode) {
fun boolean(defaultValue: Boolean) = sharedPreferencesProperty(
getValue = { getBoolean(it, defaultValue) },
setValue = { k, v -> putBoolean(k, v) },
defaultValue
)
fun string(defaultValue: String) = sharedPreferencesProperty(
getValue = { getString(it, null) ?: defaultValue },
setValue = { k, v -> putString(k, v) },
defaultValue
)
fun int(defaultValue: Int) = sharedPreferencesProperty(
getValue = { getInt(it, defaultValue) },
setValue = { k, v -> putInt(k, v) },
defaultValue
)
fun float(defaultValue: Float) = sharedPreferencesProperty(
getValue = { getFloat(it, defaultValue) },
setValue = { k, v -> putFloat(k, v) },
defaultValue
)
fun long(defaultValue: Long) = sharedPreferencesProperty(
getValue = { getLong(it, defaultValue) },
setValue = { k, v -> putLong(k, v) },
defaultValue
)
inline fun <reified T : Enum<T>> enum(defaultValue: T) = sharedPreferencesProperty(
getValue = {
getString(it, null)?.let { runCatching { enumValueOf<T>(it) }.getOrNull() } ?: defaultValue
},
setValue = { k, v -> putString(k, v.name) },
defaultValue
)
fun stringSet(defaultValue: Set<String>) = sharedPreferencesProperty(
getValue = { getStringSet(it, null) ?: defaultValue },
setValue = { k, v -> putStringSet(k, v) },
defaultValue
)
}

View File

@@ -0,0 +1,87 @@
package it.hamy.muza.ui.components
import android.os.CountDownTimer
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.AdTheme
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, 110, 110))
/**
* Билдер запроса
*/
val adRequest = AdRequest.Builder()
.setPreferredTheme(AdTheme.DARK)
.build()
val timer = object : CountDownTimer(4000, 1000) {
override fun onTick(millisUntilFinished: Long) {
// Здесь можно выполнить действия, которые нужно сделать каждую секунду
}
override fun onFinish() {
// Здесь вызывается метод loadAd(adRequest) после истечения таймера
loadAd(adRequest)
// Здесь можно повторить таймер, чтобы он всегда повторялся
//start()
}
}
/**
* Слушатель экшнов
*/
setBannerAdEventListener(object : BannerAdEventListener {
override fun onAdLoaded() {
// Запускаем таймер
timer.start()
}
override fun onAdFailedToLoad(p0: AdRequestError) {
/**
* Тут дебажим ошибки
*/
}
override fun onAdClicked() {
}
override fun onLeftApplication() {
}
override fun onReturnedToApplication() {
loadAd(adRequest)
}
override fun onImpression(p0: ImpressionData?) {
}
})
/**
* Запуск баннера
*/
loadAd(adRequest)
}
})
}

View File

@@ -0,0 +1,90 @@
package it.hamy.muza.ui.components
import android.os.CountDownTimer
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.AdTheme
import com.yandex.mobile.ads.common.ImpressionData
@Composable
fun YandexAdsBannerQuickPicksCenter(id: String) {
AndroidView(modifier = Modifier.fillMaxSize(), factory = { context ->
BannerAdView(context).apply {
/**
* ID блока рекламы
*/
setAdUnitId(id)
/**
* Размер блока рекламы
*/
setAdSize(BannerAdSize.inlineSize(context, 260, 60))
/**
* Билдер запроса
*/
val adRequest = AdRequest.Builder()
.setPreferredTheme(AdTheme.DARK)
.build()
val timer = object : CountDownTimer(4000, 1000) {
override fun onTick(millisUntilFinished: Long) {
// Здесь можно выполнить действия, которые нужно сделать каждую секунду
}
override fun onFinish() {
// Здесь вызывается метод loadAd(adRequest) после истечения таймера
loadAd(adRequest)
// Здесь можно повторить таймер, чтобы он всегда повторялся
//start()
}
}
/**
* Слушатель экшнов
*/
setBannerAdEventListener(object : BannerAdEventListener {
override fun onAdLoaded() {
// Запускаем таймер
timer.start()
}
override fun onAdFailedToLoad(p0: AdRequestError) {
/**
* Тут дебажим ошибки
*/
loadAd(adRequest)
}
override fun onAdClicked() {
}
override fun onLeftApplication() {
}
override fun onReturnedToApplication() {
loadAd(adRequest)
}
override fun onImpression(p0: ImpressionData?) {
}
})
/**
* Запуск баннера
*/
loadAd(adRequest)
}
})
}

View File

@@ -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 {

View File

@@ -9,11 +9,14 @@ 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
@@ -40,6 +43,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 +57,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 +73,8 @@ fun HomePlaylists(
mutableStateOf(false)
}
if (isCreatingANewPlaylist) {
TextFieldDialog(
hintText = "Введите название плейлиста",
@@ -100,107 +108,131 @@ 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 = "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()
)
}
item {
Box(
modifier = Modifier
.fillMaxSize()
.padding(start = 14.dp, end = 10.dp, top = 20.dp)
.align(Alignment.CenterHorizontally),
contentAlignment = Alignment.Center,
) {
YandexAdsBanner(id = "R-M-5961316-1")
}
}
}
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()
)
}
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()
)
}
}
FloatingActionsContainerWithScrollToTop(
lazyGridState = lazyGridState,
iconId = R.drawable.search,

View File

@@ -7,6 +7,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@@ -51,6 +52,7 @@ import it.hamy.muza.models.Song
import it.hamy.muza.query
import it.hamy.muza.ui.components.LocalMenuState
import it.hamy.muza.ui.components.ShimmerHost
import it.hamy.muza.ui.components.YandexAdsBannerQuickPicksCenter
import it.hamy.muza.ui.components.themed.FloatingActionsContainerWithScrollToTop
import it.hamy.muza.ui.components.themed.Header
import it.hamy.muza.ui.components.themed.NonQueuedMediaItemMenu
@@ -74,6 +76,7 @@ import it.hamy.muza.utils.isLandscape
import it.hamy.muza.utils.secondary
import it.hamy.muza.utils.semiBold
import kotlinx.coroutines.flow.distinctUntilChanged
import it.hamy.muza.preferences.DataPreferences
@ExperimentalFoundationApi
@ExperimentalAnimationApi
@@ -103,6 +106,32 @@ fun QuickPicks(
}
}
LaunchedEffect(DataPreferences.quickPicksSource) {
suspend fun handleSong(song: Song?) {
if (relatedPageResult == null || trending?.id != song?.id) relatedPageResult =
Innertube.relatedPage(
NextBody(
videoId = (song?.id ?: "J7p4bzqLvCw")
)
)
trending = song
}
when (DataPreferences.quickPicksSource) {
DataPreferences.QuickPicksSource.Trending ->
Database
.trending()
.distinctUntilChanged()
.collect { handleSong(it) }
DataPreferences.QuickPicksSource.LastInteraction ->
Database
.events()
.distinctUntilChanged()
.collect { handleSong(it.firstOrNull()?.song) }
}
}
val songThumbnailSizeDp = Dimensions.thumbnails.song
val songThumbnailSizePx = songThumbnailSizeDp.px
val albumThumbnailSizeDp = 108.dp
@@ -246,6 +275,16 @@ fun QuickPicks(
}
}
Box(
modifier = Modifier
.fillMaxSize()
.padding(start = 0.dp, end = 0.dp, top = 15.dp)
.align(Alignment.CenterHorizontally),
contentAlignment = Alignment.Center,
) {
YandexAdsBannerQuickPicksCenter(id = "R-M-5961316-5")
}
related.albums?.let { albums ->
BasicText(
text = "Похожие альбомы",

View File

@@ -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, mediaId) {
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(

View File

@@ -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 = "Искать текст в интернете",

View File

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

View File

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

View File

@@ -31,6 +31,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import it.hamy.muza.Database
import it.hamy.muza.LocalPlayerAwareWindowInsets
import it.hamy.muza.preferences.DataPreferences
import it.hamy.muza.query
import it.hamy.muza.service.PlayerMediaBrowserService
import it.hamy.muza.ui.components.themed.Header
@@ -47,6 +48,7 @@ import it.hamy.muza.utils.proxyPortKey
import it.hamy.muza.utils.rememberPreference
import it.hamy.muza.utils.toast
import kotlinx.coroutines.flow.distinctUntilChanged
import okhttp3.internal.toImmutableList
import java.net.Proxy
@@ -116,6 +118,17 @@ fun OtherSettings() {
) {
Header(title = "Другое")
SettingsEntryGroupText(title = "ОБЗОР")
ValueSelectorSettingsEntry(
title = "Режим отображения",
selectedValue = DataPreferences.quickPicksSource,
values = enumValues<DataPreferences.QuickPicksSource>().toList().toImmutableList(),
onValueSelected = { DataPreferences.quickPicksSource = it }
)
SettingsGroupSpacer()
SettingsEntryGroupText(title = "АНДРОИД АВТО")
SettingsDescription(text = "Включите опцию \"неизвестные источники\" в настройках разработчика в Андроид Авто.")
@@ -190,28 +203,28 @@ fun OtherSettings() {
SwitchSettingEntry(
title = "Invincible service",
text = "When turning off battery optimizations is not enough",
text = "Обход экономии батареи",
isChecked = isInvincibilityEnabled,
onCheckedChange = { isInvincibilityEnabled = it }
)
SettingsEntryGroupText(title = "PROXY")
SettingsEntryGroupText(title = "ПРОКСИ")
SwitchSettingEntry(
title = "HTTP Proxy",
text = "Enable HTTP Proxy",
title = "Прокси",
text = "Включить прокси",
isChecked = isProxyEnabled,
onCheckedChange = { isProxyEnabled = it }
)
AnimatedVisibility(visible = isProxyEnabled) {
Column {
EnumValueSelectorSettingsEntry(title = "Proxy",
EnumValueSelectorSettingsEntry(title = "Прокси",
selectedValue = proxyMode, onValueSelected = {proxyMode = it})
TextDialogSettingEntry(
title = "Хост",
text = "Введите http хост",
text = "Введите хост",
currentText = proxyHost,
onTextSave = { proxyHost = it })
TextDialogSettingEntry(
@@ -221,6 +234,5 @@ fun OtherSettings() {
onTextSave = { proxyPort = it.toIntOrNull() ?: 1080 })
}
}
}
}

View File

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