diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 660d310..2833c70 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -12,7 +12,7 @@ android { minSdk = 21 targetSdk = 33 versionCode = 20 - versionName = "0.5.4ru" + versionName = "0.5.4.1rus" } splits { @@ -74,6 +74,7 @@ dependencies { implementation(projects.composeRouting) implementation(projects.composeReordering) + implementation(libs.compose.activity) implementation(libs.compose.foundation) implementation(libs.compose.ui) @@ -85,8 +86,10 @@ dependencies { implementation(libs.palette) implementation(libs.exoplayer) + implementation(libs.exoplayer.okhttp) implementation(libs.room) + implementation("androidx.media3:media3-datasource-okhttp:1.0.0-alpha03") kapt(libs.room.compiler) implementation(projects.innertube) diff --git a/app/src/main/kotlin/it/hamy/muza/MainActivity.kt b/app/src/main/kotlin/it/hamy/muza/MainActivity.kt index 9649b02..79e1066 100644 --- a/app/src/main/kotlin/it/hamy/muza/MainActivity.kt +++ b/app/src/main/kotlin/it/hamy/muza/MainActivity.kt @@ -60,6 +60,16 @@ import com.valentinilk.shimmer.LocalShimmerTheme import com.valentinilk.shimmer.defaultShimmerTheme import it.hamy.compose.persist.PersistMap import it.hamy.compose.persist.PersistMapOwner + +import it.hamy.innertube.utils.ProxyPreferenceItem +import it.hamy.innertube.utils.ProxyPreferences +import it.hamy.muza.utils.isProxyEnabledKey +import it.hamy.muza.utils.proxyHostNameKey +import it.hamy.muza.utils.proxyModeKey +import it.hamy.muza.utils.proxyPortKey +import java.net.Proxy + + import it.hamy.innertube.Innertube import it.hamy.innertube.models.bodies.BrowseBody import it.hamy.innertube.requests.playlistPage @@ -127,6 +137,7 @@ class MainActivity : ComponentActivity(), PersistMapOwner { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + @Suppress("DEPRECATION", "UNCHECKED_CAST") persistMap = lastCustomNonConfigurationInstance as? PersistMap ?: PersistMap() @@ -134,6 +145,17 @@ class MainActivity : ComponentActivity(), PersistMapOwner { val launchedFromNotification = intent?.extras?.getBoolean("expandPlayerBottomSheet") == true + with(preferences){ + if(getBoolean(isProxyEnabledKey,false)) { + val hostName = getString(proxyHostNameKey,null) + val proxyPort = getInt(proxyPortKey, 8080) + val proxyMode = getEnum(proxyModeKey, Proxy.Type.HTTP) + hostName?.let { hName-> + ProxyPreferences.preference = ProxyPreferenceItem(hName,proxyPort,proxyMode) + } + } + } + setContent { val coroutineScope = rememberCoroutineScope() val isSystemInDarkTheme = isSystemInDarkTheme() diff --git a/app/src/main/kotlin/it/hamy/muza/service/PlayerService.kt b/app/src/main/kotlin/it/hamy/muza/service/PlayerService.kt index 8aa2df2..1dc6e7c 100644 --- a/app/src/main/kotlin/it/hamy/muza/service/PlayerService.kt +++ b/app/src/main/kotlin/it/hamy/muza/service/PlayerService.kt @@ -43,7 +43,14 @@ import androidx.media3.common.Player import androidx.media3.common.Timeline import androidx.media3.database.StandaloneDatabaseProvider import androidx.media3.datasource.DataSource -import androidx.media3.datasource.DefaultHttpDataSource + +import androidx.media3.datasource.okhttp.OkHttpDataSource +import it.hamy.innertube.utils.ProxyPreferences +import okhttp3.OkHttpClient +import java.net.InetSocketAddress +import java.net.Proxy +import java.time.Duration + import androidx.media3.datasource.ResolvingDataSource import androidx.media3.datasource.cache.Cache import androidx.media3.datasource.cache.CacheDataSource @@ -737,12 +744,24 @@ class PlayerService : InvincibleService(), Player.Listener, PlaybackStatsListene } } + private fun okHttpClient() : OkHttpClient{ + ProxyPreferences.preference?.let{ + return OkHttpClient.Builder() + .proxy(Proxy(it.proxyMode,InetSocketAddress(it.proxyHost,it.proxyPort))) + .connectTimeout(Duration.ofSeconds(16)) + .readTimeout(Duration.ofSeconds(8)) + .build() + } + return OkHttpClient.Builder() + .connectTimeout(Duration.ofSeconds(16)) + .readTimeout(Duration.ofSeconds(8)) + .build() + } + private fun createCacheDataSource(): DataSource.Factory { return CacheDataSource.Factory().setCache(cache).apply { setUpstreamDataSourceFactory( - DefaultHttpDataSource.Factory() - .setConnectTimeoutMs(16000) - .setReadTimeoutMs(8000) + OkHttpDataSource.Factory(okHttpClient()) .setUserAgent("Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0") ) } diff --git a/app/src/main/kotlin/it/hamy/muza/ui/components/themed/MediaItemMenu.kt b/app/src/main/kotlin/it/hamy/muza/ui/components/themed/MediaItemMenu.kt index 05813e8..99db8cf 100644 --- a/app/src/main/kotlin/it/hamy/muza/ui/components/themed/MediaItemMenu.kt +++ b/app/src/main/kotlin/it/hamy/muza/ui/components/themed/MediaItemMenu.kt @@ -89,7 +89,7 @@ fun InHistoryMediaItemMenu( if (isHiding) { ConfirmationDialog( - text = "Do you really want to hide this song? Its playback time and cache will be wiped.\nThis action is irreversible.", + text = "Вы действительно хотите скрыть эту песню? Время воспроизведения и кэш будут удалены.\n" + "Это действие необратимо", onDismiss = { isHiding = false }, onConfirm = { onDismiss() @@ -330,7 +330,7 @@ fun MediaItemMenu( if (isCreatingNewPlaylist && onAddToPlaylist != null) { TextFieldDialog( - hintText = "Enter the playlist name", + hintText = "Введите название плейлиста", onDismiss = { isCreatingNewPlaylist = false }, onDone = { text -> onDismiss() @@ -365,7 +365,7 @@ fun MediaItemMenu( if (onAddToPlaylist != null) { SecondaryTextButton( - text = "New playlist", + text = "Новый плейлист", onClick = { isCreatingNewPlaylist = true }, alternative = true ) @@ -377,7 +377,7 @@ fun MediaItemMenu( MenuEntry( icon = R.drawable.playlist, text = playlistPreview.playlist.name, - secondaryText = "${playlistPreview.songCount} songs", + secondaryText = "${playlistPreview.songCount} песен", onClick = { onDismiss() onAddToPlaylist(playlistPreview.playlist, playlistPreview.songCount) @@ -463,7 +463,7 @@ fun MediaItemMenu( onStartRadio?.let { onStartRadio -> MenuEntry( icon = R.drawable.radio, - text = "Start radio", + text = "Включить радио", onClick = { onDismiss() onStartRadio() @@ -474,7 +474,7 @@ fun MediaItemMenu( onPlayNext?.let { onPlayNext -> MenuEntry( icon = R.drawable.play_skip_forward, - text = "Play next", + text = "Следующая", onClick = { onDismiss() onPlayNext() @@ -485,7 +485,7 @@ fun MediaItemMenu( onEnqueue?.let { onEnqueue -> MenuEntry( icon = R.drawable.enqueue, - text = "Enqueue", + text = "В очередь", onClick = { onDismiss() onEnqueue() @@ -496,7 +496,7 @@ fun MediaItemMenu( onGoToEqualizer?.let { onGoToEqualizer -> MenuEntry( icon = R.drawable.equalizer, - text = "Equalizer", + text = "Эквалайзер", onClick = { onDismiss() onGoToEqualizer() @@ -520,9 +520,9 @@ fun MediaItemMenu( if (isShowingSleepTimerDialog) { if (sleepTimerMillisLeft != null) { ConfirmationDialog( - text = "Do you want to stop the sleep timer?", - cancelText = "No", - confirmText = "Stop", + text = "Вы хотите отключить таймер сна?", + cancelText = "нет", + confirmText = "отключить", onDismiss = { isShowingSleepTimerDialog = false }, onConfirm = { binder?.cancelSleepTimer() @@ -538,7 +538,7 @@ fun MediaItemMenu( } BasicText( - text = "Set sleep timer", + text = "Установить таймер сна", style = typography.s.semiBold, modifier = Modifier .padding(vertical = 8.dp, horizontal = 24.dp) @@ -570,13 +570,13 @@ fun MediaItemMenu( Box(contentAlignment = Alignment.Center) { BasicText( - text = "88h 88m", + text = "88ч 88м", style = typography.s.semiBold, modifier = Modifier .alpha(0f) ) BasicText( - text = "${amount / 6}h ${(amount % 6) * 10}m", + text = "${amount / 6}ч ${(amount % 6) * 10}м", style = typography.s.semiBold ) } @@ -603,12 +603,12 @@ fun MediaItemMenu( .fillMaxWidth() ) { DialogTextButton( - text = "Cancel", + text = "Отмена", onClick = { isShowingSleepTimerDialog = false } ) DialogTextButton( - text = "Set", + text = "Установить", enabled = amount > 0, primary = true, onClick = { @@ -623,12 +623,12 @@ fun MediaItemMenu( MenuEntry( icon = R.drawable.alarm, - text = "Sleep timer", + text = "Таймер сна", onClick = { isShowingSleepTimerDialog = true }, trailingContent = sleepTimerMillisLeft?.let { { BasicText( - text = "${formatAsDuration(it)} left", + text = "Осталось ${formatAsDuration(it)}", style = typography.xxs.medium, modifier = modifier .background( @@ -646,7 +646,7 @@ fun MediaItemMenu( if (onAddToPlaylist != null) { MenuEntry( icon = R.drawable.playlist, - text = "Add to playlist", + text = "Добавить в плейлист", onClick = { isViewingPlaylists = true }, trailingContent = { Image( @@ -666,7 +666,7 @@ fun MediaItemMenu( albumInfo?.let { (albumId) -> MenuEntry( icon = R.drawable.disc, - text = "Go to album", + text = "Перейти в альбом", onClick = { onDismiss() onGoToAlbum(albumId) @@ -679,7 +679,7 @@ fun MediaItemMenu( artistsInfo?.forEach { (authorId, authorName) -> MenuEntry( icon = R.drawable.person, - text = "More of $authorName", + text = "Больше от $authorName", onClick = { onDismiss() onGoToArtist(authorId) @@ -691,7 +691,7 @@ fun MediaItemMenu( onRemoveFromQueue?.let { onRemoveFromQueue -> MenuEntry( icon = R.drawable.trash, - text = "Remove from queue", + text = "Убрать из очереди", onClick = { onDismiss() onRemoveFromQueue() @@ -702,7 +702,7 @@ fun MediaItemMenu( onRemoveFromPlaylist?.let { onRemoveFromPlaylist -> MenuEntry( icon = R.drawable.trash, - text = "Remove from playlist", + text = "Удалить из плейлиста", onClick = { onDismiss() onRemoveFromPlaylist() @@ -713,7 +713,7 @@ fun MediaItemMenu( onHideFromDatabase?.let { onHideFromDatabase -> MenuEntry( icon = R.drawable.trash, - text = "Hide", + text = "Скрыть", onClick = onHideFromDatabase ) } @@ -721,7 +721,7 @@ fun MediaItemMenu( onRemoveFromQuickPicks?.let { MenuEntry( icon = R.drawable.trash, - text = "Hide from \"Quick picks\"", + text = "Скрыть из \"Обзора\"", onClick = { onDismiss() onRemoveFromQuickPicks() diff --git a/app/src/main/kotlin/it/hamy/muza/ui/screens/player/Lyrics.kt b/app/src/main/kotlin/it/hamy/muza/ui/screens/player/Lyrics.kt index c02e2b6..779efb9 100644 --- a/app/src/main/kotlin/it/hamy/muza/ui/screens/player/Lyrics.kt +++ b/app/src/main/kotlin/it/hamy/muza/ui/screens/player/Lyrics.kt @@ -235,7 +235,7 @@ fun Lyrics( .align(Alignment.TopCenter) ) { BasicText( - text = "${if (isShowingSynchronizedLyrics) "Синхронизирован текст" else "Т"}екст песни не доступен", + text = "${if (isShowingSynchronizedLyrics) "Синхронизированный т" else "Т"}екст не доступен", style = typography.xs.center.medium.color(PureBlackColorPalette.text), modifier = Modifier .background(Color.Black.copy(0.4f)) diff --git a/app/src/main/kotlin/it/hamy/muza/ui/screens/player/Queue.kt b/app/src/main/kotlin/it/hamy/muza/ui/screens/player/Queue.kt index 027caf2..a554e75 100644 --- a/app/src/main/kotlin/it/hamy/muza/ui/screens/player/Queue.kt +++ b/app/src/main/kotlin/it/hamy/muza/ui/screens/player/Queue.kt @@ -83,6 +83,13 @@ 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 kotlin.math.roundToInt + @ExperimentalFoundationApi @ExperimentalAnimationApi @Composable @@ -202,6 +209,7 @@ fun Queue( key = { it.uid.hashCode() } ) { window -> val isPlayingThisMediaItem = mediaItemIndex == window.firstPeriodIndex + var offsetX by remember { mutableStateOf(0f) } SongItem( song = window.mediaItem, @@ -283,6 +291,23 @@ fun Queue( reorderingState = reorderingState, index = window.firstPeriodIndex ) + + .draggable( + orientation = Orientation.Horizontal, + state = rememberDraggableState(onDelta = { delta -> + if (isPlayingThisMediaItem) return@rememberDraggableState + offsetX += delta + }), + onDragStopped = { velocity -> + if ((offsetX <= -300.0f && velocity <= -3000.0f) || (offsetX >= 300.0f && velocity >= 3000.0f)) { + binder.player.removeMediaItem(window.firstPeriodIndex) + } else { + offsetX = 0f + } + } + ) + .offset{ IntOffset(offsetX.roundToInt(), 0) } + ) } diff --git a/app/src/main/kotlin/it/hamy/muza/ui/screens/settings/OtherSettings.kt b/app/src/main/kotlin/it/hamy/muza/ui/screens/settings/OtherSettings.kt index 4ffbca7..d238a67 100644 --- a/app/src/main/kotlin/it/hamy/muza/ui/screens/settings/OtherSettings.kt +++ b/app/src/main/kotlin/it/hamy/muza/ui/screens/settings/OtherSettings.kt @@ -10,6 +10,7 @@ import android.provider.Settings import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.WindowInsetsSides @@ -38,10 +39,17 @@ import it.hamy.muza.utils.isAtLeastAndroid12 import it.hamy.muza.utils.isAtLeastAndroid6 import it.hamy.muza.utils.isIgnoringBatteryOptimizations import it.hamy.muza.utils.isInvincibilityEnabledKey +import it.hamy.muza.utils.isProxyEnabledKey import it.hamy.muza.utils.pauseSearchHistoryKey +import it.hamy.muza.utils.proxyHostNameKey +import it.hamy.muza.utils.proxyModeKey +import it.hamy.muza.utils.proxyPortKey import it.hamy.muza.utils.rememberPreference import it.hamy.muza.utils.toast import kotlinx.coroutines.flow.distinctUntilChanged +import java.net.Proxy + + @SuppressLint("BatteryLife") @ExperimentalAnimationApi @@ -72,6 +80,14 @@ fun OtherSettings() { var isInvincibilityEnabled by rememberPreference(isInvincibilityEnabledKey, false) + var isProxyEnabled by rememberPreference(isProxyEnabledKey, false) + + var proxyHost by rememberPreference(proxyHostNameKey, defaultValue = "") + + var proxyPort by rememberPreference(proxyPortKey, defaultValue = 1080) + + var proxyMode by rememberPreference(proxyModeKey, defaultValue = Proxy.Type.HTTP) + var isIgnoringBatteryOptimizations by remember { mutableStateOf(context.isIgnoringBatteryOptimizations) } @@ -178,5 +194,33 @@ fun OtherSettings() { isChecked = isInvincibilityEnabled, onCheckedChange = { isInvincibilityEnabled = it } ) + + + SettingsEntryGroupText(title = "PROXY") + + SwitchSettingEntry( + title = "HTTP Proxy", + text = "Enable HTTP Proxy", + isChecked = isProxyEnabled, + onCheckedChange = { isProxyEnabled = it } + ) + + AnimatedVisibility(visible = isProxyEnabled) { + Column { + EnumValueSelectorSettingsEntry(title = "Proxy", + selectedValue = proxyMode, onValueSelected = {proxyMode = it}) + TextDialogSettingEntry( + title = "Хост", + text = "Введите http хост", + currentText = proxyHost, + onTextSave = { proxyHost = it }) + TextDialogSettingEntry( + title = "Порт", + text = "Введите порт", + currentText = proxyPort.toString(), + onTextSave = { proxyPort = it.toIntOrNull() ?: 1080 }) + } + } + } } diff --git a/app/src/main/kotlin/it/hamy/muza/ui/screens/settings/SettingsScreen.kt b/app/src/main/kotlin/it/hamy/muza/ui/screens/settings/SettingsScreen.kt index 710c6ad..af37200 100644 --- a/app/src/main/kotlin/it/hamy/muza/ui/screens/settings/SettingsScreen.kt +++ b/app/src/main/kotlin/it/hamy/muza/ui/screens/settings/SettingsScreen.kt @@ -21,17 +21,20 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import it.hamy.compose.routing.RouteHandler import it.hamy.muza.R import it.hamy.muza.ui.components.themed.Scaffold import it.hamy.muza.ui.components.themed.Switch +import it.hamy.muza.ui.components.themed.TextFieldDialog import it.hamy.muza.ui.components.themed.ValueSelectorDialog import it.hamy.muza.ui.screens.globalRoutes import it.hamy.muza.ui.styling.LocalAppearance import it.hamy.muza.utils.color import it.hamy.muza.utils.secondary import it.hamy.muza.utils.semiBold +import it.hamy.muza.utils.toast @ExperimentalFoundationApi @ExperimentalAnimationApi @@ -193,6 +196,37 @@ fun SettingsEntry( } } +@Composable +fun TextDialogSettingEntry( + title: String, + text: String, + currentText: String, + onTextSave: (String) -> Unit, + modifier: Modifier = Modifier, + isEnabled: Boolean = true +) { + var showDialog by remember { mutableStateOf(false) } + val context = LocalContext.current + + if (showDialog) { + TextFieldDialog(hintText =title , + onDismiss = { showDialog = false }, + onDone ={value-> + onTextSave(value) + context.toast("Сохранено!") + } , doneText = "Save", initialTextInput = currentText) + } + SettingsEntry( + title = title, + text = text, + isEnabled = isEnabled, + onClick = { showDialog = true }, + trailingContent = { }, + modifier = modifier + ) +} + + @Composable fun SettingsDescription( text: String, diff --git a/app/src/main/kotlin/it/hamy/muza/utils/Preferences.kt b/app/src/main/kotlin/it/hamy/muza/utils/Preferences.kt index 0693dc3..89b5bd0 100644 --- a/app/src/main/kotlin/it/hamy/muza/utils/Preferences.kt +++ b/app/src/main/kotlin/it/hamy/muza/utils/Preferences.kt @@ -38,6 +38,10 @@ const val homeScreenTabIndexKey = "homeScreenTabIndex" const val searchResultScreenTabIndexKey = "searchResultScreenTabIndex" const val artistScreenTabIndexKey = "artistScreenTabIndex" const val pauseSearchHistoryKey = "pauseSearchHistory" +const val isProxyEnabledKey = "isProxyEnabled" +const val proxyHostNameKey = "proxyHostname" +const val proxyPortKey = "proxyPortKey" +const val proxyModeKey = "proxyModeKey" inline fun > SharedPreferences.getEnum( key: String, diff --git a/innertube/src/main/kotlin/it/hamy/innertube/Innertube.kt b/innertube/src/main/kotlin/it/hamy/innertube/Innertube.kt index 4423765..4390723 100644 --- a/innertube/src/main/kotlin/it/hamy/innertube/Innertube.kt +++ b/innertube/src/main/kotlin/it/hamy/innertube/Innertube.kt @@ -15,8 +15,12 @@ import io.ktor.serialization.kotlinx.json.json import it.hamy.innertube.models.NavigationEndpoint import it.hamy.innertube.models.Runs import it.hamy.innertube.models.Thumbnail +import it.hamy.innertube.utils.ProxyPreferences import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.Json +import java.net.InetSocketAddress +import java.net.Proxy + object Innertube { val client = HttpClient(OkHttp) { @@ -44,6 +48,19 @@ object Innertube { parameters.append("prettyPrint", "false") } } + + ProxyPreferences.preference?.let { + engine { + proxy = Proxy( + it.proxyMode, + InetSocketAddress( + it.proxyHost, + it.proxyPort + ) + ) + } + } + } internal const val browse = "/youtubei/v1/browse" diff --git a/innertube/src/main/kotlin/it/hamy/innertube/utils/ProxyPreferences.kt b/innertube/src/main/kotlin/it/hamy/innertube/utils/ProxyPreferences.kt new file mode 100644 index 0000000..28e9f4d --- /dev/null +++ b/innertube/src/main/kotlin/it/hamy/innertube/utils/ProxyPreferences.kt @@ -0,0 +1,13 @@ +package it.hamy.innertube.utils + +import java.net.Proxy + +object ProxyPreferences { + var preference: ProxyPreferenceItem? = null +} + +data class ProxyPreferenceItem( + var proxyHost: String, + var proxyPort: Int, + var proxyMode: Proxy.Type +) \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index ee56d89..9379195 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -7,6 +7,7 @@ dependencyResolutionManagement { google() mavenCentral() maven { setUrl("https://jitpack.io") } + } versionCatalogs { @@ -36,6 +37,8 @@ dependencyResolutionManagement { version("media3", "1.0.0-beta03") library("exoplayer", "androidx.media3", "media3-exoplayer").versionRef("media3") + library("exoplayer-okhttp", "androidx.media3", "media3-datasource-okhttp").versionRef("media3") + version("ktor", "2.1.2") library("ktor-client-core", "io.ktor", "ktor-client-core").versionRef("ktor")