mirror of
https://github.com/jellyfin/jellyfin-android.git
synced 2026-10-26 16:45:26 +00:00
Fix issues reported by detekt
This commit is contained in:
1 parent
6c5a90218d
commit
2a08c82bcc
16 files changed
+214
-176
No files matched your search
@@ -15,15 +15,15 @@ import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import org.jellyfin.mobile.player.cast.Chromecast
|
||||
import org.jellyfin.mobile.player.cast.IChromecast
|
||||
import org.jellyfin.mobile.setup.ConnectFragment
|
||||
import org.jellyfin.mobile.webapp.WebViewFragment
|
||||
import org.jellyfin.mobile.player.ui.PlayerFragment
|
||||
import org.jellyfin.mobile.setup.ConnectFragment
|
||||
import org.jellyfin.mobile.utils.Constants
|
||||
import org.jellyfin.mobile.utils.PermissionRequestHelper
|
||||
import org.jellyfin.mobile.utils.SmartOrientationListener
|
||||
import org.jellyfin.mobile.utils.isWebViewSupported
|
||||
import org.jellyfin.mobile.utils.extensions.replaceFragment
|
||||
import org.jellyfin.mobile.utils.isWebViewSupported
|
||||
import org.jellyfin.mobile.webapp.RemotePlayerService
|
||||
import org.jellyfin.mobile.webapp.WebViewFragment
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koin.androidx.fragment.android.setupKoinFragmentFactory
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
@@ -87,9 +87,11 @@ class MainActivity : AppCompatActivity() {
|
||||
is ServerState.Available -> {
|
||||
val currentFragment = findFragmentById(R.id.fragment_container)
|
||||
if (currentFragment !is WebViewFragment || currentFragment.server != state.server) {
|
||||
replaceFragment<WebViewFragment>(Bundle().apply {
|
||||
putParcelable(Constants.FRAGMENT_WEB_VIEW_EXTRA_SERVER, state.server)
|
||||
})
|
||||
replaceFragment<WebViewFragment>(
|
||||
Bundle().apply {
|
||||
putParcelable(Constants.FRAGMENT_WEB_VIEW_EXTRA_SERVER, state.server)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,18 +16,18 @@ import com.google.android.exoplayer2.util.Util
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import okhttp3.OkHttpClient
|
||||
import org.jellyfin.mobile.MainViewModel
|
||||
import org.jellyfin.mobile.player.deviceprofile.DeviceProfileBuilder
|
||||
import org.jellyfin.mobile.bridge.ExternalPlayer
|
||||
import org.jellyfin.mobile.setup.ConnectFragment
|
||||
import org.jellyfin.mobile.webapp.WebViewFragment
|
||||
import org.jellyfin.mobile.player.audio.car.LibraryBrowser
|
||||
import org.jellyfin.mobile.player.deviceprofile.DeviceProfileBuilder
|
||||
import org.jellyfin.mobile.player.interaction.PlayerEvent
|
||||
import org.jellyfin.mobile.player.ui.PlayerFragment
|
||||
import org.jellyfin.mobile.player.source.MediaSourceResolver
|
||||
import org.jellyfin.mobile.player.ui.PlayerFragment
|
||||
import org.jellyfin.mobile.setup.ConnectFragment
|
||||
import org.jellyfin.mobile.utils.Constants
|
||||
import org.jellyfin.mobile.utils.PermissionRequestHelper
|
||||
import org.jellyfin.mobile.utils.isLowRamDevice
|
||||
import org.jellyfin.mobile.webapp.RemoteVolumeProvider
|
||||
import org.jellyfin.mobile.webapp.WebViewFragment
|
||||
import org.jellyfin.mobile.webapp.WebappFunctionChannel
|
||||
import org.koin.android.ext.koin.androidApplication
|
||||
import org.koin.androidx.fragment.dsl.fragment
|
||||
|
||||
@@ -11,7 +11,6 @@ import android.webkit.JavascriptInterface
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.jellyfin.mobile.R
|
||||
import org.jellyfin.mobile.webapp.WebViewFragment
|
||||
import org.jellyfin.mobile.settings.SettingsFragment
|
||||
import org.jellyfin.mobile.utils.Constants
|
||||
import org.jellyfin.mobile.utils.Constants.EXTRA_ALBUM
|
||||
@@ -28,11 +27,12 @@ import org.jellyfin.mobile.utils.Constants.EXTRA_TITLE
|
||||
import org.jellyfin.mobile.utils.extensions.addFragment
|
||||
import org.jellyfin.mobile.utils.extensions.disableFullscreen
|
||||
import org.jellyfin.mobile.utils.extensions.enableFullscreen
|
||||
import org.jellyfin.mobile.utils.requestDownload
|
||||
import org.jellyfin.mobile.utils.extensions.requireMainActivity
|
||||
import org.jellyfin.mobile.utils.requestDownload
|
||||
import org.jellyfin.mobile.utils.runOnUiThread
|
||||
import org.jellyfin.mobile.webapp.RemotePlayerService
|
||||
import org.jellyfin.mobile.webapp.RemoteVolumeProvider
|
||||
import org.jellyfin.mobile.webapp.WebViewFragment
|
||||
import org.jellyfin.mobile.webapp.WebappFunctionChannel
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.util.AuthorizationHeaderBuilder
|
||||
@@ -191,10 +191,14 @@ class NativeInterface(private val fragment: WebViewFragment) : KoinComponent {
|
||||
|
||||
@JavascriptInterface
|
||||
fun execCast(action: String, args: String) {
|
||||
fragment.requireMainActivity().chromecast.execute(action, JSONArray(args), object : JavascriptCallback() {
|
||||
override fun callback(keep: Boolean, err: String?, result: String?) {
|
||||
webappFunctionChannel.call("""window.NativeShell.castCallback("$action", $keep, $err, $result);""")
|
||||
}
|
||||
})
|
||||
fragment.requireMainActivity().chromecast.execute(
|
||||
action,
|
||||
JSONArray(args),
|
||||
object : JavascriptCallback() {
|
||||
override fun callback(keep: Boolean, err: String?, result: String?) {
|
||||
webappFunctionChannel.call("""window.NativeShell.castCallback("$action", $keep, $err, $result);""")
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -29,25 +29,25 @@ import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jellyfin.mobile.BuildConfig
|
||||
import org.jellyfin.mobile.app.PLAYER_EVENT_CHANNEL
|
||||
import org.jellyfin.mobile.player.ui.DisplayPreferences
|
||||
import org.jellyfin.mobile.player.interaction.PlayerEvent
|
||||
import org.jellyfin.mobile.player.interaction.PlayerLifecycleObserver
|
||||
import org.jellyfin.mobile.player.interaction.PlayerMediaSessionCallback
|
||||
import org.jellyfin.mobile.player.interaction.PlayerNotificationHelper
|
||||
import org.jellyfin.mobile.player.source.JellyfinMediaSource
|
||||
import org.jellyfin.mobile.player.queue.QueueManager
|
||||
import org.jellyfin.mobile.player.source.JellyfinMediaSource
|
||||
import org.jellyfin.mobile.player.ui.DisplayPreferences
|
||||
import org.jellyfin.mobile.utils.Constants
|
||||
import org.jellyfin.mobile.utils.Constants.SUPPORTED_VIDEO_PLAYER_PLAYBACK_ACTIONS
|
||||
import org.jellyfin.mobile.utils.applyDefaultAudioAttributes
|
||||
import org.jellyfin.mobile.utils.applyDefaultLocalAudioAttributes
|
||||
import org.jellyfin.mobile.utils.extensions.scaleInRange
|
||||
import org.jellyfin.mobile.utils.extensions.width
|
||||
import org.jellyfin.mobile.utils.getVolumeLevelPercent
|
||||
import org.jellyfin.mobile.utils.getVolumeRange
|
||||
import org.jellyfin.mobile.utils.logTracks
|
||||
import org.jellyfin.mobile.utils.extensions.scaleInRange
|
||||
import org.jellyfin.mobile.utils.seekToOffset
|
||||
import org.jellyfin.mobile.utils.setPlaybackState
|
||||
import org.jellyfin.mobile.utils.toMediaMetadata
|
||||
import org.jellyfin.mobile.utils.extensions.width
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.exception.ApiClientException
|
||||
import org.jellyfin.sdk.api.client.extensions.displayPreferencesApi
|
||||
|
||||
@@ -36,13 +36,13 @@ import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jellyfin.mobile.R
|
||||
import org.jellyfin.mobile.player.cast.CastPlayerProvider
|
||||
import org.jellyfin.mobile.player.cast.ICastPlayerProvider
|
||||
import org.jellyfin.mobile.app.ApiClientController
|
||||
import org.jellyfin.mobile.player.audio.car.LibraryBrowser
|
||||
import org.jellyfin.mobile.player.audio.car.LibraryPage
|
||||
import org.jellyfin.mobile.utils.extensions.mediaUri
|
||||
import org.jellyfin.mobile.player.cast.CastPlayerProvider
|
||||
import org.jellyfin.mobile.player.cast.ICastPlayerProvider
|
||||
import org.jellyfin.mobile.utils.Constants
|
||||
import org.jellyfin.mobile.utils.extensions.mediaUri
|
||||
import org.jellyfin.mobile.utils.toast
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.exception.ApiClientException
|
||||
@@ -142,7 +142,6 @@ class MediaService : MediaBrowserServiceCompat() {
|
||||
newPlayer = if (castPlayerProvider.isCastSessionAvailable) castPlayerProvider.get()!! else exoPlayer
|
||||
)
|
||||
notificationManager.showNotificationForPlayer(currentPlayer)
|
||||
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
@@ -262,10 +261,11 @@ class MediaService : MediaBrowserServiceCompat() {
|
||||
}
|
||||
|
||||
private fun setPlaybackError() {
|
||||
mediaSession.setPlaybackState(PlaybackStateCompat.Builder().apply {
|
||||
setState(PlaybackStateCompat.STATE_ERROR, 0, 1f)
|
||||
setErrorMessage(PlaybackStateCompat.ERROR_CODE_NOT_SUPPORTED, getString(R.string.media_service_item_not_found))
|
||||
}.build())
|
||||
val errorState = PlaybackStateCompat.Builder()
|
||||
.setState(PlaybackStateCompat.STATE_ERROR, 0, 1f)
|
||||
.setErrorMessage(PlaybackStateCompat.ERROR_CODE_NOT_SUPPORTED, getString(R.string.media_service_item_not_found))
|
||||
.build()
|
||||
mediaSession.setPlaybackState(errorState)
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
|
||||
@@ -46,7 +46,7 @@ import org.jellyfin.sdk.model.api.SortOrder
|
||||
import org.jellyfin.sdk.model.serializer.toUUID
|
||||
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
import java.util.UUID
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
class LibraryBrowser(
|
||||
@@ -259,10 +259,12 @@ class LibraryBrowser(
|
||||
setTitle(context.getString(item.second))
|
||||
|
||||
if (item.first == LibraryPage.ALBUMS) {
|
||||
setExtras(Bundle().apply {
|
||||
putInt(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_BROWSABLE, MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_GRID_ITEM)
|
||||
putInt(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_PLAYABLE, MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_GRID_ITEM)
|
||||
})
|
||||
setExtras(
|
||||
Bundle().apply {
|
||||
putInt(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_BROWSABLE, MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_GRID_ITEM)
|
||||
putInt(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_PLAYABLE, MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_GRID_ITEM)
|
||||
}
|
||||
)
|
||||
}
|
||||
}.build()
|
||||
MediaBrowserCompat.MediaItem(description, FLAG_BROWSABLE)
|
||||
|
||||
@@ -168,7 +168,6 @@ object CodecHelpers {
|
||||
else -> null
|
||||
}
|
||||
|
||||
|
||||
private fun getAVCLevel(level: Int): String? = when (level) {
|
||||
CodecProfileLevel.AVCLevel1 -> "1"
|
||||
CodecProfileLevel.AVCLevel11 -> "11"
|
||||
|
||||
+13
-10
@@ -20,10 +20,10 @@ import com.google.android.exoplayer2.Player
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jellyfin.mobile.app.AppPreferences
|
||||
import org.jellyfin.mobile.BuildConfig
|
||||
import org.jellyfin.mobile.MainActivity
|
||||
import org.jellyfin.mobile.R
|
||||
import org.jellyfin.mobile.app.AppPreferences
|
||||
import org.jellyfin.mobile.player.PlayerViewModel
|
||||
import org.jellyfin.mobile.utils.Constants
|
||||
import org.jellyfin.mobile.utils.Constants.VIDEO_PLAYER_NOTIFICATION_ID
|
||||
@@ -114,15 +114,18 @@ class PlayerNotificationHelper(private val viewModel: PlayerViewModel) : KoinCom
|
||||
}
|
||||
|
||||
if (receiverRegistered.compareAndSet(false, true)) {
|
||||
context.registerReceiver(notificationActionReceiver, IntentFilter().apply {
|
||||
addAction(Constants.ACTION_PLAY)
|
||||
addAction(Constants.ACTION_PAUSE)
|
||||
addAction(Constants.ACTION_REWIND)
|
||||
addAction(Constants.ACTION_FAST_FORWARD)
|
||||
addAction(Constants.ACTION_PREVIOUS)
|
||||
addAction(Constants.ACTION_NEXT)
|
||||
addAction(Constants.ACTION_STOP)
|
||||
})
|
||||
context.registerReceiver(
|
||||
notificationActionReceiver,
|
||||
IntentFilter().apply {
|
||||
addAction(Constants.ACTION_PLAY)
|
||||
addAction(Constants.ACTION_PAUSE)
|
||||
addAction(Constants.ACTION_REWIND)
|
||||
addAction(Constants.ACTION_FAST_FORWARD)
|
||||
addAction(Constants.ACTION_PREVIOUS)
|
||||
addAction(Constants.ACTION_NEXT)
|
||||
addAction(Constants.ACTION_STOP)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,22 +28,22 @@ import com.google.android.exoplayer2.Player
|
||||
import com.google.android.exoplayer2.ui.PlayerView
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jellyfin.mobile.R
|
||||
import org.jellyfin.mobile.utils.extensions.aspectRational
|
||||
import org.jellyfin.mobile.utils.extensions.isLandscape
|
||||
import org.jellyfin.mobile.player.interaction.PlayOptions
|
||||
import org.jellyfin.mobile.databinding.ExoPlayerControlViewBinding
|
||||
import org.jellyfin.mobile.databinding.FragmentPlayerBinding
|
||||
import org.jellyfin.mobile.player.PlayerException
|
||||
import org.jellyfin.mobile.player.PlayerViewModel
|
||||
import org.jellyfin.mobile.player.interaction.PlayOptions
|
||||
import org.jellyfin.mobile.utils.Constants
|
||||
import org.jellyfin.mobile.utils.Constants.DEFAULT_CONTROLS_TIMEOUT_MS
|
||||
import org.jellyfin.mobile.utils.Constants.PIP_MAX_RATIONAL
|
||||
import org.jellyfin.mobile.utils.Constants.PIP_MIN_RATIONAL
|
||||
import org.jellyfin.mobile.utils.SmartOrientationListener
|
||||
import org.jellyfin.mobile.utils.brightness
|
||||
import org.jellyfin.mobile.utils.extensions.aspectRational
|
||||
import org.jellyfin.mobile.utils.extensions.disableFullscreen
|
||||
import org.jellyfin.mobile.utils.extensions.enableFullscreen
|
||||
import org.jellyfin.mobile.utils.extensions.isFullscreen
|
||||
import org.jellyfin.mobile.utils.extensions.isLandscape
|
||||
import org.jellyfin.mobile.utils.toast
|
||||
import org.jellyfin.sdk.model.api.MediaStream
|
||||
|
||||
|
||||
@@ -73,136 +73,150 @@ class PlayerGestureHelper(
|
||||
/**
|
||||
* Handles taps when controls are locked
|
||||
*/
|
||||
private val unlockDetector = GestureDetector(playerView.context, object : GestureDetector.SimpleOnGestureListener() {
|
||||
override fun onSingleTapConfirmed(e: MotionEvent?): Boolean {
|
||||
playerLockScreenHelper.peekUnlockButton()
|
||||
return true
|
||||
}
|
||||
})
|
||||
private val unlockDetector = GestureDetector(
|
||||
playerView.context,
|
||||
object : GestureDetector.SimpleOnGestureListener() {
|
||||
override fun onSingleTapConfirmed(e: MotionEvent?): Boolean {
|
||||
playerLockScreenHelper.peekUnlockButton()
|
||||
return true
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
/**
|
||||
* Handles double tap to seek and brightness/volume gestures
|
||||
*/
|
||||
private val gestureDetector = GestureDetector(playerView.context, object : GestureDetector.SimpleOnGestureListener() {
|
||||
override fun onDoubleTap(e: MotionEvent): Boolean {
|
||||
val viewWidth = playerView.measuredWidth
|
||||
val viewHeight = playerView.measuredHeight
|
||||
val viewCenterX = viewWidth / 2
|
||||
val viewCenterY = viewHeight / 2
|
||||
val isFastForward = e.x.toInt() > viewCenterX
|
||||
private val gestureDetector = GestureDetector(
|
||||
playerView.context,
|
||||
object : GestureDetector.SimpleOnGestureListener() {
|
||||
override fun onDoubleTap(e: MotionEvent): Boolean {
|
||||
val viewWidth = playerView.measuredWidth
|
||||
val viewHeight = playerView.measuredHeight
|
||||
val viewCenterX = viewWidth / 2
|
||||
val viewCenterY = viewHeight / 2
|
||||
val isFastForward = e.x.toInt() > viewCenterX
|
||||
|
||||
// Show ripple effect
|
||||
playerView.foreground?.apply {
|
||||
val left = if (isFastForward) viewCenterX else 0
|
||||
val right = if (isFastForward) viewWidth else viewCenterX
|
||||
setBounds(left, viewCenterY - viewCenterX / 2, right, viewCenterY + viewCenterX / 2)
|
||||
setHotspot(e.x, e.y)
|
||||
state = intArrayOf(android.R.attr.state_enabled, android.R.attr.state_pressed)
|
||||
playerView.postDelayed(Constants.DOUBLE_TAP_RIPPLE_DURATION_MS) {
|
||||
state = IntArray(0)
|
||||
}
|
||||
}
|
||||
|
||||
// Fast-forward/rewind
|
||||
with(fragment) { if (isFastForward) onFastForward() else onRewind() }
|
||||
|
||||
// Cancel previous runnable to not hide controller while seeking
|
||||
playerView.removeCallbacks(hidePlayerViewControllerAction)
|
||||
|
||||
// Ensure controller gets hidden after seeking
|
||||
playerView.postDelayed(hidePlayerViewControllerAction, Constants.DEFAULT_CONTROLS_TIMEOUT_MS.toLong())
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onSingleTapConfirmed(e: MotionEvent?): Boolean {
|
||||
playerView.apply {
|
||||
if (!isControllerVisible) showController() else hideController()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onScroll(firstEvent: MotionEvent, currentEvent: MotionEvent, distanceX: Float, distanceY: Float): Boolean {
|
||||
if (!appPreferences.exoPlayerAllowSwipeGestures)
|
||||
return false
|
||||
|
||||
// Check whether swipe was started in excluded region
|
||||
if (firstEvent.y < playerView.resources.dip(Constants.GESTURE_EXCLUSION_AREA_TOP))
|
||||
return false
|
||||
|
||||
// Check whether swipe was oriented vertically
|
||||
if (abs(distanceY / distanceX) < 2)
|
||||
return false
|
||||
|
||||
val viewCenterX = playerView.measuredWidth / 2
|
||||
|
||||
// Distance to swipe to go from min to max
|
||||
val distanceFull = playerView.measuredHeight * Constants.FULL_SWIPE_RANGE_SCREEN_RATIO
|
||||
val ratioChange = distanceY / distanceFull
|
||||
|
||||
if (firstEvent.x.toInt() > viewCenterX) {
|
||||
// Swiping on the right, change volume
|
||||
|
||||
val currentVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
|
||||
if (swipeGestureValueTracker == -1f) swipeGestureValueTracker = currentVolume.toFloat()
|
||||
|
||||
val maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
|
||||
val change = ratioChange * maxVolume
|
||||
swipeGestureValueTracker += change
|
||||
|
||||
val toSet = swipeGestureValueTracker.toInt().coerceIn(0, maxVolume)
|
||||
audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, toSet, 0)
|
||||
|
||||
gestureIndicatorOverlayImage.setImageResource(org.jellyfin.mobile.R.drawable.ic_volume_white_24dp)
|
||||
gestureIndicatorOverlayProgress.max = maxVolume
|
||||
gestureIndicatorOverlayProgress.progress = toSet
|
||||
} else {
|
||||
// Swiping on the left, change brightness
|
||||
|
||||
val window = fragment.requireActivity().window
|
||||
val brightnessRange = BRIGHTNESS_OVERRIDE_OFF..BRIGHTNESS_OVERRIDE_FULL
|
||||
|
||||
// Initialize on first swipe
|
||||
if (swipeGestureValueTracker == -1f) {
|
||||
val brightness = window.brightness
|
||||
swipeGestureValueTracker = when (brightness) {
|
||||
in brightnessRange -> brightness
|
||||
else -> Settings.System.getFloat(fragment.requireActivity().contentResolver, Settings.System.SCREEN_BRIGHTNESS) / Constants.SCREEN_BRIGHTNESS_MAX
|
||||
// Show ripple effect
|
||||
playerView.foreground?.apply {
|
||||
val left = if (isFastForward) viewCenterX else 0
|
||||
val right = if (isFastForward) viewWidth else viewCenterX
|
||||
setBounds(left, viewCenterY - viewCenterX / 2, right, viewCenterY + viewCenterX / 2)
|
||||
setHotspot(e.x, e.y)
|
||||
state = intArrayOf(android.R.attr.state_enabled, android.R.attr.state_pressed)
|
||||
playerView.postDelayed(Constants.DOUBLE_TAP_RIPPLE_DURATION_MS) {
|
||||
state = IntArray(0)
|
||||
}
|
||||
}
|
||||
|
||||
swipeGestureValueTracker = (swipeGestureValueTracker + ratioChange).coerceIn(brightnessRange)
|
||||
window.brightness = swipeGestureValueTracker
|
||||
if (appPreferences.exoPlayerRememberBrightness) {
|
||||
appPreferences.exoPlayerBrightness = swipeGestureValueTracker
|
||||
}
|
||||
// Fast-forward/rewind
|
||||
with(fragment) { if (isFastForward) onFastForward() else onRewind() }
|
||||
|
||||
gestureIndicatorOverlayImage.setImageResource(org.jellyfin.mobile.R.drawable.ic_brightness_white_24dp)
|
||||
gestureIndicatorOverlayProgress.max = Constants.PERCENT_MAX
|
||||
gestureIndicatorOverlayProgress.progress = (swipeGestureValueTracker * Constants.PERCENT_MAX).toInt()
|
||||
// Cancel previous runnable to not hide controller while seeking
|
||||
playerView.removeCallbacks(hidePlayerViewControllerAction)
|
||||
|
||||
// Ensure controller gets hidden after seeking
|
||||
playerView.postDelayed(hidePlayerViewControllerAction, Constants.DEFAULT_CONTROLS_TIMEOUT_MS.toLong())
|
||||
return true
|
||||
}
|
||||
|
||||
gestureIndicatorOverlayLayout.isVisible = true
|
||||
return true
|
||||
}
|
||||
})
|
||||
override fun onSingleTapConfirmed(e: MotionEvent?): Boolean {
|
||||
playerView.apply {
|
||||
if (!isControllerVisible) showController() else hideController()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onScroll(
|
||||
firstEvent: MotionEvent,
|
||||
currentEvent: MotionEvent,
|
||||
distanceX: Float,
|
||||
distanceY: Float,
|
||||
): Boolean {
|
||||
if (!appPreferences.exoPlayerAllowSwipeGestures)
|
||||
return false
|
||||
|
||||
// Check whether swipe was started in excluded region
|
||||
if (firstEvent.y < playerView.resources.dip(Constants.GESTURE_EXCLUSION_AREA_TOP))
|
||||
return false
|
||||
|
||||
// Check whether swipe was oriented vertically
|
||||
if (abs(distanceY / distanceX) < 2)
|
||||
return false
|
||||
|
||||
val viewCenterX = playerView.measuredWidth / 2
|
||||
|
||||
// Distance to swipe to go from min to max
|
||||
val distanceFull = playerView.measuredHeight * Constants.FULL_SWIPE_RANGE_SCREEN_RATIO
|
||||
val ratioChange = distanceY / distanceFull
|
||||
|
||||
if (firstEvent.x.toInt() > viewCenterX) {
|
||||
// Swiping on the right, change volume
|
||||
|
||||
val currentVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
|
||||
if (swipeGestureValueTracker == -1f) swipeGestureValueTracker = currentVolume.toFloat()
|
||||
|
||||
val maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
|
||||
val change = ratioChange * maxVolume
|
||||
swipeGestureValueTracker += change
|
||||
|
||||
val toSet = swipeGestureValueTracker.toInt().coerceIn(0, maxVolume)
|
||||
audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, toSet, 0)
|
||||
|
||||
gestureIndicatorOverlayImage.setImageResource(org.jellyfin.mobile.R.drawable.ic_volume_white_24dp)
|
||||
gestureIndicatorOverlayProgress.max = maxVolume
|
||||
gestureIndicatorOverlayProgress.progress = toSet
|
||||
} else {
|
||||
// Swiping on the left, change brightness
|
||||
|
||||
val window = fragment.requireActivity().window
|
||||
val brightnessRange = BRIGHTNESS_OVERRIDE_OFF..BRIGHTNESS_OVERRIDE_FULL
|
||||
|
||||
// Initialize on first swipe
|
||||
if (swipeGestureValueTracker == -1f) {
|
||||
val brightness = window.brightness
|
||||
swipeGestureValueTracker = when (brightness) {
|
||||
in brightnessRange -> brightness
|
||||
else -> Settings.System.getFloat(fragment.requireActivity().contentResolver, Settings.System.SCREEN_BRIGHTNESS) / Constants.SCREEN_BRIGHTNESS_MAX
|
||||
}
|
||||
}
|
||||
|
||||
swipeGestureValueTracker = (swipeGestureValueTracker + ratioChange).coerceIn(brightnessRange)
|
||||
window.brightness = swipeGestureValueTracker
|
||||
if (appPreferences.exoPlayerRememberBrightness) {
|
||||
appPreferences.exoPlayerBrightness = swipeGestureValueTracker
|
||||
}
|
||||
|
||||
gestureIndicatorOverlayImage.setImageResource(org.jellyfin.mobile.R.drawable.ic_brightness_white_24dp)
|
||||
gestureIndicatorOverlayProgress.max = Constants.PERCENT_MAX
|
||||
gestureIndicatorOverlayProgress.progress = (swipeGestureValueTracker * Constants.PERCENT_MAX).toInt()
|
||||
}
|
||||
|
||||
gestureIndicatorOverlayLayout.isVisible = true
|
||||
return true
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
/**
|
||||
* Handles scale/zoom gesture
|
||||
*/
|
||||
private val zoomGestureDetector = ScaleGestureDetector(playerView.context, object : ScaleGestureDetector.OnScaleGestureListener {
|
||||
override fun onScaleBegin(detector: ScaleGestureDetector): Boolean = fragment.isLandscape()
|
||||
private val zoomGestureDetector = ScaleGestureDetector(
|
||||
playerView.context,
|
||||
object : ScaleGestureDetector.OnScaleGestureListener {
|
||||
override fun onScaleBegin(detector: ScaleGestureDetector): Boolean = fragment.isLandscape()
|
||||
|
||||
override fun onScale(detector: ScaleGestureDetector): Boolean {
|
||||
val scaleFactor = detector.scaleFactor
|
||||
if (abs(scaleFactor - Constants.ZOOM_SCALE_BASE) > Constants.ZOOM_SCALE_THRESHOLD) {
|
||||
isZoomEnabled = scaleFactor > 1
|
||||
updateZoomMode(isZoomEnabled)
|
||||
override fun onScale(detector: ScaleGestureDetector): Boolean {
|
||||
val scaleFactor = detector.scaleFactor
|
||||
if (abs(scaleFactor - Constants.ZOOM_SCALE_BASE) > Constants.ZOOM_SCALE_THRESHOLD) {
|
||||
isZoomEnabled = scaleFactor > 1
|
||||
updateZoomMode(isZoomEnabled)
|
||||
}
|
||||
return true
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onScaleEnd(detector: ScaleGestureDetector) = Unit
|
||||
}).apply { isQuickScaleEnabled = false }
|
||||
override fun onScaleEnd(detector: ScaleGestureDetector) = Unit
|
||||
},
|
||||
).apply { isQuickScaleEnabled = false }
|
||||
|
||||
init {
|
||||
@Suppress("ClickableViewAccessibility")
|
||||
|
||||
@@ -8,8 +8,8 @@ import androidx.core.view.isVisible
|
||||
import com.google.android.exoplayer2.ui.PlayerView
|
||||
import org.jellyfin.mobile.databinding.FragmentPlayerBinding
|
||||
import org.jellyfin.mobile.utils.Constants
|
||||
import org.jellyfin.mobile.utils.isAutoRotateOn
|
||||
import org.jellyfin.mobile.utils.extensions.lockOrientation
|
||||
import org.jellyfin.mobile.utils.isAutoRotateOn
|
||||
|
||||
class PlayerLockScreenHelper(
|
||||
private val playerFragment: PlayerFragment,
|
||||
|
||||
@@ -189,7 +189,13 @@ class PlayerMenus(
|
||||
setOnDismissListener(this@PlayerMenus)
|
||||
}
|
||||
|
||||
private fun buildMenuItems(menu: Menu, groupId: Int, mediaStreams: List<MediaStream>, selectedStream: MediaStream?, showNone: Boolean = false) {
|
||||
private fun buildMenuItems(
|
||||
menu: Menu,
|
||||
groupId: Int,
|
||||
mediaStreams: List<MediaStream>,
|
||||
selectedStream: MediaStream?,
|
||||
showNone: Boolean = false,
|
||||
) {
|
||||
menu.clear()
|
||||
val itemNone = if (showNone) menu.add(groupId, -1, Menu.NONE, fragment.getString(R.string.menu_item_none)) else null
|
||||
val menuItems = mediaStreams.map { mediaStream ->
|
||||
|
||||
@@ -16,14 +16,14 @@ import de.Maxr1998.modernpreferences.helpers.screen
|
||||
import de.Maxr1998.modernpreferences.helpers.singleChoice
|
||||
import de.Maxr1998.modernpreferences.preferences.CheckBoxPreference
|
||||
import de.Maxr1998.modernpreferences.preferences.choice.SelectionItem
|
||||
import org.jellyfin.mobile.app.AppPreferences
|
||||
import org.jellyfin.mobile.R
|
||||
import org.jellyfin.mobile.app.AppPreferences
|
||||
import org.jellyfin.mobile.databinding.FragmentSettingsBinding
|
||||
import org.jellyfin.mobile.utils.Constants
|
||||
import org.jellyfin.mobile.utils.applyWindowInsetsAsMargins
|
||||
import org.jellyfin.mobile.utils.extensions.requireMainActivity
|
||||
import org.jellyfin.mobile.utils.getDownloadsPaths
|
||||
import org.jellyfin.mobile.utils.isPackageInstalled
|
||||
import org.jellyfin.mobile.utils.extensions.requireMainActivity
|
||||
import org.jellyfin.mobile.utils.withThemedContext
|
||||
import org.koin.android.ext.android.inject
|
||||
|
||||
|
||||
@@ -22,12 +22,12 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jellyfin.mobile.MainViewModel
|
||||
import org.jellyfin.mobile.R
|
||||
import org.jellyfin.mobile.app.ApiClientController
|
||||
import org.jellyfin.mobile.databinding.FragmentConnectBinding
|
||||
import org.jellyfin.mobile.utils.Constants
|
||||
import org.jellyfin.mobile.utils.applyWindowInsetsAsMargins
|
||||
import org.jellyfin.mobile.MainViewModel
|
||||
import org.jellyfin.sdk.Jellyfin
|
||||
import org.jellyfin.sdk.discovery.LocalServerDiscovery
|
||||
import org.jellyfin.sdk.discovery.RecommendedServerInfo
|
||||
|
||||
@@ -24,11 +24,11 @@ import androidx.core.content.getSystemService
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import org.jellyfin.mobile.app.AppPreferences
|
||||
import org.jellyfin.mobile.BuildConfig
|
||||
import org.jellyfin.mobile.R
|
||||
import org.jellyfin.mobile.webapp.WebViewFragment
|
||||
import org.jellyfin.mobile.app.AppPreferences
|
||||
import org.jellyfin.mobile.settings.ExternalPlayerPackage
|
||||
import org.jellyfin.mobile.webapp.WebViewFragment
|
||||
import org.koin.android.ext.android.get
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
|
||||
@@ -36,29 +36,29 @@ import androidx.webkit.WebViewCompat
|
||||
import androidx.webkit.WebViewFeature
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jellyfin.mobile.app.AppPreferences
|
||||
import org.jellyfin.mobile.R
|
||||
import org.jellyfin.mobile.app.ApiClientController
|
||||
import org.jellyfin.mobile.app.AppPreferences
|
||||
import org.jellyfin.mobile.bridge.ExternalPlayer
|
||||
import org.jellyfin.mobile.bridge.NativeInterface
|
||||
import org.jellyfin.mobile.bridge.NativePlayer
|
||||
import org.jellyfin.mobile.bridge.NativePlayerHost
|
||||
import org.jellyfin.mobile.player.interaction.PlayOptions
|
||||
import org.jellyfin.mobile.app.ApiClientController
|
||||
import org.jellyfin.mobile.databinding.FragmentWebviewBinding
|
||||
import org.jellyfin.mobile.setup.ConnectFragment
|
||||
import org.jellyfin.mobile.data.entity.ServerEntity
|
||||
import org.jellyfin.mobile.databinding.FragmentWebviewBinding
|
||||
import org.jellyfin.mobile.player.interaction.PlayOptions
|
||||
import org.jellyfin.mobile.player.ui.PlayerFragment
|
||||
import org.jellyfin.mobile.setup.ConnectFragment
|
||||
import org.jellyfin.mobile.utils.Constants
|
||||
import org.jellyfin.mobile.utils.Constants.FRAGMENT_WEB_VIEW_EXTRA_SERVER
|
||||
import org.jellyfin.mobile.utils.extensions.addFragment
|
||||
import org.jellyfin.mobile.utils.applyDefault
|
||||
import org.jellyfin.mobile.utils.applyWindowInsetsAsMargins
|
||||
import org.jellyfin.mobile.utils.dip
|
||||
import org.jellyfin.mobile.utils.extensions.addFragment
|
||||
import org.jellyfin.mobile.utils.extensions.replaceFragment
|
||||
import org.jellyfin.mobile.utils.fadeIn
|
||||
import org.jellyfin.mobile.utils.initLocale
|
||||
import org.jellyfin.mobile.utils.inject
|
||||
import org.jellyfin.mobile.utils.isOutdated
|
||||
import org.jellyfin.mobile.utils.extensions.replaceFragment
|
||||
import org.jellyfin.mobile.utils.requestNoBatteryOptimizations
|
||||
import org.jellyfin.mobile.utils.runOnUiThread
|
||||
import org.json.JSONException
|
||||
@@ -203,14 +203,22 @@ class WebViewFragment : Fragment(), NativePlayerHost {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onReceivedHttpError(view: WebView, request: WebResourceRequest, errorResponse: WebResourceResponse) {
|
||||
override fun onReceivedHttpError(
|
||||
view: WebView,
|
||||
request: WebResourceRequest,
|
||||
errorResponse: WebResourceResponse,
|
||||
) {
|
||||
val errorMessage = errorResponse.data?.run { bufferedReader().use(Reader::readText) }
|
||||
Timber.e("Received WebView HTTP %d error: %s", errorResponse.statusCode, errorMessage)
|
||||
|
||||
if (request.url == Uri.parse(view.url)) onErrorReceived()
|
||||
}
|
||||
|
||||
override fun onReceivedError(view: WebView, request: WebResourceRequest, error: WebResourceErrorCompat) {
|
||||
override fun onReceivedError(
|
||||
view: WebView,
|
||||
request: WebResourceRequest,
|
||||
error: WebResourceErrorCompat,
|
||||
) {
|
||||
val description = if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_RESOURCE_ERROR_GET_DESCRIPTION)) error.description else null
|
||||
val errorCode = if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_RESOURCE_ERROR_GET_CODE)) error.errorCode else ERROR_UNKNOWN
|
||||
Timber.e("Received WebView error %d at %s: %s", errorCode, request.url.toString(), description)
|
||||
|
||||
Reference in new issue
Block a user