4 Commits

12 changed files with 345 additions and 15 deletions

View File

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

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

@@ -14,6 +14,7 @@ class MainApplication : Application(), ImageLoaderFactory {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
DatabaseInitializer() DatabaseInitializer()
Dependencies.init(this)
MobileAds.initialize(this) { MobileAds.initialize(this) {
/** /**
* Инициализация либы яндекса * Инициализация либы яндекса

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

@@ -1,7 +1,6 @@
package it.hamy.muza.ui.components package it.hamy.muza.ui.components
import android.util.Log import android.os.CountDownTimer
import android.widget.Toast
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -11,6 +10,7 @@ import com.yandex.mobile.ads.banner.BannerAdSize
import com.yandex.mobile.ads.banner.BannerAdView import com.yandex.mobile.ads.banner.BannerAdView
import com.yandex.mobile.ads.common.AdRequest import com.yandex.mobile.ads.common.AdRequest
import com.yandex.mobile.ads.common.AdRequestError import com.yandex.mobile.ads.common.AdRequestError
import com.yandex.mobile.ads.common.AdTheme
import com.yandex.mobile.ads.common.ImpressionData import com.yandex.mobile.ads.common.ImpressionData
@Composable @Composable
@@ -24,17 +24,35 @@ fun YandexAdsBanner(id: String) {
/** /**
* Размер блока рекламы * Размер блока рекламы
*/ */
setAdSize(BannerAdSize.inlineSize(context, 140, 60)) setAdSize(BannerAdSize.inlineSize(context, 110, 110))
/** /**
* Билдер запроса * Билдер запроса
*/ */
val adRequest = AdRequest.Builder().build() 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 { setBannerAdEventListener(object : BannerAdEventListener {
override fun onAdLoaded() { override fun onAdLoaded() {
// Запускаем таймер
timer.start()
} }
override fun onAdFailedToLoad(p0: AdRequestError) { override fun onAdFailedToLoad(p0: AdRequestError) {
@@ -52,7 +70,7 @@ fun YandexAdsBanner(id: String) {
} }
override fun onReturnedToApplication() { override fun onReturnedToApplication() {
loadAd(adRequest)
} }
override fun onImpression(p0: ImpressionData?) { override fun onImpression(p0: ImpressionData?) {

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

@@ -23,8 +23,6 @@ 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
@@ -174,6 +172,10 @@ fun HomePlaylists(
} }
} }
item(key = "favorites") { item(key = "favorites") {
PlaylistItem( PlaylistItem(
icon = R.drawable.heart, icon = R.drawable.heart,
@@ -213,18 +215,23 @@ fun HomePlaylists(
.animateItemPlacement() .animateItemPlacement()
) )
} }
item { item {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(start = 14.dp, end = 10.dp, top = 30.dp) .padding(start = 14.dp, end = 10.dp, top = 20.dp)
.align(Alignment.CenterHorizontally), .align(Alignment.CenterHorizontally),
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
) { ) {
YandexAdsBanner(id = "R-M-5961316-1") YandexAdsBanner(id = "R-M-5961316-1")
} }
} }
} }
} }
FloatingActionsContainerWithScrollToTop( FloatingActionsContainerWithScrollToTop(
lazyGridState = lazyGridState, lazyGridState = lazyGridState,

View File

@@ -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 = "Похожие альбомы",

View File

@@ -85,7 +85,7 @@ fun Controls(
var artistsInfo: List<Info>? by remember { mutableStateOf(null) } var artistsInfo: List<Info>? by remember { mutableStateOf(null) }
LaunchedEffect(Unit) { LaunchedEffect(Unit, mediaId) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
if (artistsInfo == null) artistsInfo = Database.songArtistInfo(mediaId) if (artistsInfo == null) artistsInfo = Database.songArtistInfo(mediaId)

View File

@@ -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 = "Включите опцию \"неизвестные источники\" в настройках разработчика в Андроид Авто.")
@@ -196,18 +209,18 @@ fun OtherSettings() {
) )
SettingsEntryGroupText(title = "PROXY") SettingsEntryGroupText(title = "ПРОКСИ")
SwitchSettingEntry( SwitchSettingEntry(
title = "Proxy", title = "Прокси",
text = "Включить 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 = "Хост",
@@ -221,6 +234,5 @@ fun OtherSettings() {
onTextSave = { proxyPort = it.toIntOrNull() ?: 1080 }) onTextSave = { proxyPort = it.toIntOrNull() ?: 1080 })
} }
} }
} }
} }