Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bfa3231823 | |||
| abe5d0b378 | |||
| 7b1138c428 | |||
|
|
b2b12c473c | ||
|
|
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"
|
[<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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
2
app/proguard-rules.pro
vendored
2
app/proguard-rules.pro
vendored
@@ -32,3 +32,5 @@
|
|||||||
-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** { *; }
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ import it.hamy.muza.models.SongPlaylistMap
|
|||||||
import it.hamy.muza.models.SortedSongPlaylistMap
|
import it.hamy.muza.models.SortedSongPlaylistMap
|
||||||
import kotlin.jvm.Throws
|
import kotlin.jvm.Throws
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import it.hamy.muza.models.EventWithSong
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface Database {
|
interface Database {
|
||||||
@@ -316,6 +317,11 @@ interface Database {
|
|||||||
@RewriteQueriesToDropUnusedColumns
|
@RewriteQueriesToDropUnusedColumns
|
||||||
fun trending(now: Long = System.currentTimeMillis()): Flow<Song?>
|
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")
|
@Query("SELECT COUNT (*) FROM Event")
|
||||||
fun eventsCount(): Flow<Int>
|
fun eventsCount(): Flow<Int>
|
||||||
|
|
||||||
|
|||||||
14
app/src/main/kotlin/it/hamy/muza/Dependencies.kt
Normal file
14
app/src/main/kotlin/it/hamy/muza/Dependencies.kt
Normal 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")
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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,12 @@ class MainApplication : Application(), ImageLoaderFactory {
|
|||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
DatabaseInitializer()
|
DatabaseInitializer()
|
||||||
|
Dependencies.init(this)
|
||||||
|
MobileAds.initialize(this) {
|
||||||
|
/**
|
||||||
|
* Инициализация либы яндекса
|
||||||
|
*/
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun newImageLoader(): ImageLoader {
|
override fun newImageLoader(): ImageLoader {
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
16
app/src/main/kotlin/it/hamy/muza/models/EventWithSong.kt
Normal file
16
app/src/main/kotlin/it/hamy/muza/models/EventWithSong.kt
Normal 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
|
||||||
|
)
|
||||||
@@ -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" })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -9,11 +9,14 @@ 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
|
||||||
@@ -40,6 +43,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 +57,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 +73,8 @@ fun HomePlaylists(
|
|||||||
mutableStateOf(false)
|
mutableStateOf(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (isCreatingANewPlaylist) {
|
if (isCreatingANewPlaylist) {
|
||||||
TextFieldDialog(
|
TextFieldDialog(
|
||||||
hintText = "Введите название плейлиста",
|
hintText = "Введите название плейлиста",
|
||||||
@@ -100,7 +108,11 @@ fun HomePlaylists(
|
|||||||
|
|
||||||
val lazyGridState = rememberLazyGridState()
|
val lazyGridState = rememberLazyGridState()
|
||||||
|
|
||||||
Box {
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
LazyVerticalGrid(
|
LazyVerticalGrid(
|
||||||
state = lazyGridState,
|
state = lazyGridState,
|
||||||
columns = GridCells.Adaptive(Dimensions.thumbnails.song * 2 + Dimensions.itemsVerticalPadding * 2),
|
columns = GridCells.Adaptive(Dimensions.thumbnails.song * 2 + Dimensions.itemsVerticalPadding * 2),
|
||||||
@@ -112,7 +124,7 @@ fun HomePlaylists(
|
|||||||
alignment = Alignment.CenterHorizontally
|
alignment = Alignment.CenterHorizontally
|
||||||
),
|
),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxWidth()
|
||||||
.background(colorPalette.background0)
|
.background(colorPalette.background0)
|
||||||
) {
|
) {
|
||||||
item(key = "header", contentType = 0, span = { GridItemSpan(maxLineSpan) }) {
|
item(key = "header", contentType = 0, span = { GridItemSpan(maxLineSpan) }) {
|
||||||
@@ -160,6 +172,10 @@ fun HomePlaylists(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
item(key = "favorites") {
|
item(key = "favorites") {
|
||||||
PlaylistItem(
|
PlaylistItem(
|
||||||
icon = R.drawable.heart,
|
icon = R.drawable.heart,
|
||||||
@@ -199,8 +215,24 @@ fun HomePlaylists(
|
|||||||
.animateItemPlacement()
|
.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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
FloatingActionsContainerWithScrollToTop(
|
FloatingActionsContainerWithScrollToTop(
|
||||||
lazyGridState = lazyGridState,
|
lazyGridState = lazyGridState,
|
||||||
iconId = R.drawable.search,
|
iconId = R.drawable.search,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ 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.snapping.rememberSnapFlingBehavior
|
import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
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.query
|
||||||
import it.hamy.muza.ui.components.LocalMenuState
|
import it.hamy.muza.ui.components.LocalMenuState
|
||||||
import it.hamy.muza.ui.components.ShimmerHost
|
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.FloatingActionsContainerWithScrollToTop
|
||||||
import it.hamy.muza.ui.components.themed.Header
|
import it.hamy.muza.ui.components.themed.Header
|
||||||
import it.hamy.muza.ui.components.themed.NonQueuedMediaItemMenu
|
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.secondary
|
||||||
import it.hamy.muza.utils.semiBold
|
import it.hamy.muza.utils.semiBold
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import it.hamy.muza.preferences.DataPreferences
|
||||||
|
|
||||||
@ExperimentalFoundationApi
|
@ExperimentalFoundationApi
|
||||||
@ExperimentalAnimationApi
|
@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 songThumbnailSizeDp = Dimensions.thumbnails.song
|
||||||
val songThumbnailSizePx = songThumbnailSizeDp.px
|
val songThumbnailSizePx = songThumbnailSizeDp.px
|
||||||
val albumThumbnailSizeDp = 108.dp
|
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 ->
|
related.albums?.let { albums ->
|
||||||
BasicText(
|
BasicText(
|
||||||
text = "Похожие альбомы",
|
text = "Похожие альбомы",
|
||||||
|
|||||||
@@ -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,9 +83,15 @@ fun Controls(
|
|||||||
mutableStateOf<Long?>(null)
|
mutableStateOf<Long?>(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(mediaId) {
|
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 }
|
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(
|
||||||
|
|||||||
@@ -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 = "Искать текст в интернете",
|
||||||
|
|||||||
@@ -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 {
|
} else {
|
||||||
offsetX = 0f
|
offsetX.animateTo(Offset(1500.0f, offsetX.value.y))
|
||||||
|
}
|
||||||
|
|
||||||
|
binder.player.removeMediaItem(indexToDelete)
|
||||||
|
deleteHistory.removeFirst()
|
||||||
|
} else {
|
||||||
|
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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import it.hamy.muza.Database
|
import it.hamy.muza.Database
|
||||||
import it.hamy.muza.LocalPlayerAwareWindowInsets
|
import it.hamy.muza.LocalPlayerAwareWindowInsets
|
||||||
|
import it.hamy.muza.preferences.DataPreferences
|
||||||
import it.hamy.muza.query
|
import it.hamy.muza.query
|
||||||
import it.hamy.muza.service.PlayerMediaBrowserService
|
import it.hamy.muza.service.PlayerMediaBrowserService
|
||||||
import it.hamy.muza.ui.components.themed.Header
|
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.rememberPreference
|
||||||
import it.hamy.muza.utils.toast
|
import it.hamy.muza.utils.toast
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import okhttp3.internal.toImmutableList
|
||||||
import java.net.Proxy
|
import java.net.Proxy
|
||||||
|
|
||||||
|
|
||||||
@@ -116,6 +118,17 @@ fun OtherSettings() {
|
|||||||
) {
|
) {
|
||||||
Header(title = "Другое")
|
Header(title = "Другое")
|
||||||
|
|
||||||
|
SettingsEntryGroupText(title = "ОБЗОР")
|
||||||
|
|
||||||
|
ValueSelectorSettingsEntry(
|
||||||
|
title = "Режим отображения",
|
||||||
|
selectedValue = DataPreferences.quickPicksSource,
|
||||||
|
values = enumValues<DataPreferences.QuickPicksSource>().toList().toImmutableList(),
|
||||||
|
onValueSelected = { DataPreferences.quickPicksSource = it }
|
||||||
|
)
|
||||||
|
|
||||||
|
SettingsGroupSpacer()
|
||||||
|
|
||||||
SettingsEntryGroupText(title = "АНДРОИД АВТО")
|
SettingsEntryGroupText(title = "АНДРОИД АВТО")
|
||||||
|
|
||||||
SettingsDescription(text = "Включите опцию \"неизвестные источники\" в настройках разработчика в Андроид Авто.")
|
SettingsDescription(text = "Включите опцию \"неизвестные источники\" в настройках разработчика в Андроид Авто.")
|
||||||
@@ -190,28 +203,28 @@ 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 }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
SettingsEntryGroupText(title = "PROXY")
|
SettingsEntryGroupText(title = "ПРОКСИ")
|
||||||
|
|
||||||
SwitchSettingEntry(
|
SwitchSettingEntry(
|
||||||
title = "HTTP Proxy",
|
title = "Прокси",
|
||||||
text = "Enable HTTP Proxy",
|
text = "Включить прокси",
|
||||||
isChecked = isProxyEnabled,
|
isChecked = isProxyEnabled,
|
||||||
onCheckedChange = { isProxyEnabled = it }
|
onCheckedChange = { isProxyEnabled = it }
|
||||||
)
|
)
|
||||||
|
|
||||||
AnimatedVisibility(visible = isProxyEnabled) {
|
AnimatedVisibility(visible = isProxyEnabled) {
|
||||||
Column {
|
Column {
|
||||||
EnumValueSelectorSettingsEntry(title = "Proxy",
|
EnumValueSelectorSettingsEntry(title = "Прокси",
|
||||||
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(
|
||||||
@@ -221,6 +234,5 @@ fun OtherSettings() {
|
|||||||
onTextSave = { proxyPort = it.toIntOrNull() ?: 1080 })
|
onTextSave = { proxyPort = it.toIntOrNull() ?: 1080 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ data class MusicShelfRenderer(
|
|||||||
?: emptyList()) to
|
?: emptyList()) to
|
||||||
(musicResponsiveListItemRenderer
|
(musicResponsiveListItemRenderer
|
||||||
?.flexColumns
|
?.flexColumns
|
||||||
?.lastOrNull()
|
?.getOrNull(1)
|
||||||
?.musicResponsiveListItemFlexColumnRenderer
|
?.musicResponsiveListItemFlexColumnRenderer
|
||||||
?.text
|
?.text
|
||||||
?.splitBySeparator()
|
?.splitBySeparator()
|
||||||
|
|||||||
Reference in New Issue
Block a user