mirror of
https://github.com/aniyomiorg/aniyomi.git
synced 2024-11-29 09:39:03 +03:00
remove old player
This commit is contained in:
parent
523d343a1e
commit
28bfb6a3b5
13 changed files with 11 additions and 3332 deletions
|
@ -297,17 +297,6 @@ dependencies {
|
|||
// For detecting memory leaks; see https://square.github.io/leakcanary/
|
||||
// debugImplementation("com.squareup.leakcanary:leakcanary-android:2.7")
|
||||
|
||||
// Exoplayer
|
||||
val exoplayerVersion = "2.16.0"
|
||||
implementation("com.google.android.exoplayer:exoplayer:$exoplayerVersion")
|
||||
implementation("com.google.android.exoplayer:exoplayer-core:$exoplayerVersion")
|
||||
implementation("com.google.android.exoplayer:exoplayer-dash:$exoplayerVersion")
|
||||
implementation("com.google.android.exoplayer:exoplayer-hls:$exoplayerVersion")
|
||||
implementation("com.google.android.exoplayer:exoplayer-ui:$exoplayerVersion")
|
||||
|
||||
// Doubletap Player
|
||||
implementation("com.github.vkay94:DoubleTapPlayerView:1.0.2")
|
||||
|
||||
// FFmpeg
|
||||
implementation("com.arthenica:ffmpeg-kit-https:4.5.LTS")
|
||||
|
||||
|
|
|
@ -93,24 +93,6 @@
|
|||
<meta-data android:name="com.samsung.android.support.REMOTE_ACTION"
|
||||
android:resource="@xml/s_pen_actions"/>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".ui.player.PlayerActivity"
|
||||
android:theme="@style/Theme.AppCompat.Light.NoActionBar.FullScreen"
|
||||
android:supportsPictureInPicture="true"
|
||||
android:resizeableActivity="true"
|
||||
android:excludeFromRecents="true"
|
||||
android:taskAffinity=".ui.player.PlayerActivity"
|
||||
android:configChanges="keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize"
|
||||
android:screenOrientation="userLandscape"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="com.samsung.android.support.REMOTE_ACTION" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data android:name="com.samsung.android.support.REMOTE_ACTION"
|
||||
android:resource="@xml/s_pen_actions"/>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.player.NewPlayerActivity"
|
||||
android:launchMode="singleTask"
|
||||
|
@ -126,17 +108,6 @@
|
|||
<meta-data android:name="com.samsung.android.support.REMOTE_ACTION"
|
||||
android:resource="@xml/s_pen_actions"/>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.player.MPVActivity"
|
||||
android:launchMode="singleTask"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="com.samsung.android.support.REMOTE_ACTION" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data android:name="com.samsung.android.support.REMOTE_ACTION"
|
||||
android:resource="@xml/s_pen_actions"/>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.security.UnlockActivity"
|
||||
android:theme="@style/Theme.Tachiyomi"
|
||||
|
|
|
@ -27,7 +27,7 @@ import eu.kanade.tachiyomi.source.SourceManager
|
|||
import eu.kanade.tachiyomi.ui.anime.AnimeController
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.ui.player.PlayerActivity
|
||||
import eu.kanade.tachiyomi.ui.player.NewPlayerActivity
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||
|
@ -243,7 +243,7 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||
val anime = db.getAnime(animeId).executeAsBlocking()
|
||||
val episode = db.getEpisode(episodeId).executeAsBlocking()
|
||||
if (anime != null && episode != null) {
|
||||
val intent = PlayerActivity.newIntent(context, anime, episode).apply {
|
||||
val intent = NewPlayerActivity.newIntent(context, anime, episode).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
}
|
||||
context.startActivity(intent)
|
||||
|
@ -609,7 +609,7 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||
* @param episode episode that needs to be opened
|
||||
*/
|
||||
internal fun openEpisodePendingActivity(context: Context, anime: Anime, episode: Episode): PendingIntent {
|
||||
val newIntent = PlayerActivity.newIntent(context, anime, episode)
|
||||
val newIntent = NewPlayerActivity.newIntent(context, anime, episode)
|
||||
return PendingIntent.getActivity(context, AnimeController.REQUEST_INTERNAL, newIntent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,6 @@ import android.content.Context
|
|||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import com.google.android.exoplayer2.util.MimeTypes
|
||||
import eu.kanade.tachiyomi.animesource.AnimeSource
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
|
||||
|
@ -99,9 +98,9 @@ class ExternalIntents(val anime: Anime, val source: AnimeSource) {
|
|||
|
||||
private fun getMime(uri: Uri): String {
|
||||
return when (uri.path?.substringAfterLast(".")) {
|
||||
"mp4" -> MimeTypes.VIDEO_MP4
|
||||
"mkv" -> MimeTypes.VIDEO_MATROSKA
|
||||
"m3u8" -> MimeTypes.APPLICATION_M3U8
|
||||
"mp4" -> "video/mp4"
|
||||
"mkv" -> "video/x-matroska"
|
||||
"m3u8" -> "application/x-mpegURL"
|
||||
else -> "video/any"
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,190 +0,0 @@
|
|||
package eu.kanade.tachiyomi.ui.player
|
||||
|
||||
import android.os.Bundle
|
||||
import eu.kanade.tachiyomi.animesource.AnimeSourceManager
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.data.cache.AnimeCoverCache
|
||||
import eu.kanade.tachiyomi.data.database.AnimeDatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Anime
|
||||
import eu.kanade.tachiyomi.data.database.models.Episode
|
||||
import eu.kanade.tachiyomi.data.download.AnimeDownloadManager
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.track.job.DelayedTrackingStore
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import eu.kanade.tachiyomi.util.episode.getEpisodeSort
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import logcat.LogPriority
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class MPVPresenter(
|
||||
private val db: AnimeDatabaseHelper = Injekt.get(),
|
||||
private val sourceManager: AnimeSourceManager = Injekt.get(),
|
||||
private val downloadManager: AnimeDownloadManager = Injekt.get(),
|
||||
private val coverCache: AnimeCoverCache = Injekt.get(),
|
||||
private val preferences: PreferencesHelper = Injekt.get(),
|
||||
private val delayedTrackingStore: DelayedTrackingStore = Injekt.get(),
|
||||
) : BasePresenter<MPVActivity>() {
|
||||
/**
|
||||
* The anime loaded in the player. It can be null when instantiated for a short time.
|
||||
*/
|
||||
var anime: Anime? = null
|
||||
private set
|
||||
|
||||
/**
|
||||
* The episode id of the currently loaded episode. Used to restore from process kill.
|
||||
*/
|
||||
private var episodeId = -1L
|
||||
|
||||
private var currentEpisode: Episode? = null
|
||||
|
||||
private var currentVideoList: List<Video>? = null
|
||||
|
||||
/**
|
||||
* Episode list for the active anime. It's retrieved lazily and should be accessed for the first
|
||||
* time in a background thread to avoid blocking the UI.
|
||||
*/
|
||||
private val episodeList by lazy {
|
||||
val anime = anime!!
|
||||
val dbEpisodes = db.getEpisodes(anime).executeAsBlocking()
|
||||
|
||||
val selectedEpisode = dbEpisodes.find { it.id == episodeId }
|
||||
?: error("Requested episode of id $episodeId not found in episode list")
|
||||
|
||||
val episodesForPlayer = when {
|
||||
preferences.skipRead() || preferences.skipFiltered() -> {
|
||||
val filteredEpisodes = dbEpisodes.filterNot {
|
||||
when {
|
||||
preferences.skipRead() && it.seen -> true
|
||||
preferences.skipFiltered() -> {
|
||||
anime.seenFilter == Anime.EPISODE_SHOW_SEEN && !it.seen ||
|
||||
anime.seenFilter == Anime.EPISODE_SHOW_UNSEEN && it.seen ||
|
||||
anime.downloadedFilter == Anime.EPISODE_SHOW_DOWNLOADED && !downloadManager.isEpisodeDownloaded(it, anime) ||
|
||||
anime.downloadedFilter == Anime.EPISODE_SHOW_NOT_DOWNLOADED && downloadManager.isEpisodeDownloaded(it, anime) ||
|
||||
anime.bookmarkedFilter == Anime.EPISODE_SHOW_BOOKMARKED && !it.bookmark ||
|
||||
anime.bookmarkedFilter == Anime.EPISODE_SHOW_NOT_BOOKMARKED && it.bookmark
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
if (filteredEpisodes.any { it.id == episodeId }) {
|
||||
filteredEpisodes
|
||||
} else {
|
||||
filteredEpisodes + listOf(selectedEpisode)
|
||||
}
|
||||
}
|
||||
else -> dbEpisodes
|
||||
}
|
||||
|
||||
episodesForPlayer
|
||||
.sortedWith(getEpisodeSort(anime, sortDescending = false))
|
||||
}
|
||||
|
||||
private var hasTrackers: Boolean = false
|
||||
private val checkTrackers: (Anime) -> Unit = { anime ->
|
||||
val tracks = db.getTracks(anime).executeAsBlocking()
|
||||
|
||||
hasTrackers = tracks.size > 0
|
||||
}
|
||||
|
||||
private val incognitoMode = preferences.incognitoMode().get()
|
||||
|
||||
/**
|
||||
* Called when the presenter is created. It retrieves the saved active episode if the process
|
||||
* was restored.
|
||||
*/
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
if (savedState != null) {
|
||||
episodeId = savedState.getLong(::episodeId.name, -1)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the presenter is destroyed. It saves the current progress and cleans up
|
||||
* references on the currently active episodes.
|
||||
*/
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
/*val currentEpisode = currentEpisode
|
||||
if (currentEpisode != null) {
|
||||
saveEpisodeProgress(currentEpisode)
|
||||
saveEpisodeHistory(currentEpisode)
|
||||
}*/
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the presenter instance is being saved. It saves the currently active episode
|
||||
* id and the last page seen.
|
||||
*/
|
||||
override fun onSave(state: Bundle) {
|
||||
super.onSave(state)
|
||||
val currentEpisode = currentEpisode
|
||||
if (currentEpisode != null) {
|
||||
// saveEpisodeProgress(currentEpisode)
|
||||
// saveEpisodeHistory(currentEpisode)
|
||||
state.putLong(::episodeId.name, currentEpisode.id!!)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this presenter is initialized yet.
|
||||
*/
|
||||
fun needsInit(): Boolean {
|
||||
return anime == null
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes this presenter with the given [animeId] and [initialEpisodeId]. This method will
|
||||
* fetch the anime from the database and initialize the initial episode.
|
||||
*/
|
||||
fun init(animeId: Long, initialEpisodeId: Long) {
|
||||
if (!needsInit()) return
|
||||
|
||||
db.getAnime(animeId).asRxObservable()
|
||||
.first()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnNext { init(it, initialEpisodeId) }
|
||||
.subscribeFirst(
|
||||
{ _, _ ->
|
||||
// Ignore onNext event
|
||||
},
|
||||
MPVActivity::setInitialEpisodeError
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes this presenter with the given [anime] and [initialEpisodeId]. This method will
|
||||
* set the episode loader, view subscriptions and trigger an initial load.
|
||||
*/
|
||||
private fun init(anime: Anime, initialEpisodeId: Long) {
|
||||
if (!needsInit()) return
|
||||
|
||||
this.anime = anime
|
||||
if (episodeId == -1L) episodeId = initialEpisodeId
|
||||
|
||||
checkTrackers(anime)
|
||||
|
||||
val source = sourceManager.getOrStub(anime.source)
|
||||
|
||||
currentEpisode = episodeList.first { initialEpisodeId == it.id }
|
||||
launchIO {
|
||||
try {
|
||||
val currentEpisode = currentEpisode ?: throw Exception("bruh")
|
||||
EpisodeLoader.getLinks(currentEpisode, anime, source)
|
||||
.subscribeFirst(
|
||||
{ activity, it ->
|
||||
currentVideoList = it
|
||||
activity.setVideoList(it)
|
||||
},
|
||||
MPVActivity::setInitialEpisodeError
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e) { e.message ?: "error getting links" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -35,6 +35,7 @@ import eu.kanade.tachiyomi.util.system.toast
|
|||
import `is`.xyz.mpv.MPVLib
|
||||
import `is`.xyz.mpv.PickerDialog
|
||||
import `is`.xyz.mpv.SpeedPickerDialog
|
||||
import `is`.xyz.mpv.StateRestoreCallback
|
||||
import `is`.xyz.mpv.Utils
|
||||
import logcat.LogPriority
|
||||
import nucleus.factory.RequiresPresenter
|
||||
|
|
|
@ -1,829 +0,0 @@
|
|||
package eu.kanade.tachiyomi.ui.player
|
||||
|
||||
import android.app.PictureInPictureParams
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Configuration
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import android.webkit.WebSettings
|
||||
import android.widget.ImageButton
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.SeekBar
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import com.github.vkay94.dtpv.DoubleTapPlayerView
|
||||
import com.github.vkay94.dtpv.youtube.YouTubeOverlay
|
||||
import com.google.android.exoplayer2.DefaultLoadControl
|
||||
import com.google.android.exoplayer2.DefaultRenderersFactory
|
||||
import com.google.android.exoplayer2.ExoPlayer
|
||||
import com.google.android.exoplayer2.MediaItem
|
||||
import com.google.android.exoplayer2.PlaybackException
|
||||
import com.google.android.exoplayer2.PlaybackParameters
|
||||
import com.google.android.exoplayer2.Player
|
||||
import com.google.android.exoplayer2.analytics.AnalyticsCollector
|
||||
import com.google.android.exoplayer2.database.StandaloneDatabaseProvider
|
||||
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory
|
||||
import com.google.android.exoplayer2.source.MediaSourceFactory
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray
|
||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelectionArray
|
||||
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout
|
||||
import com.google.android.exoplayer2.upstream.DataSource
|
||||
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSource
|
||||
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource.HttpDataSourceException
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource.InvalidResponseCodeException
|
||||
import com.google.android.exoplayer2.upstream.cache.CacheDataSource
|
||||
import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor
|
||||
import com.google.android.exoplayer2.upstream.cache.SimpleCache
|
||||
import com.google.android.exoplayer2.util.Clock
|
||||
import com.google.android.exoplayer2.util.MimeTypes
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.animesource.AnimeSource
|
||||
import eu.kanade.tachiyomi.animesource.AnimeSourceManager
|
||||
import eu.kanade.tachiyomi.animesource.LocalAnimeSource
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
|
||||
import eu.kanade.tachiyomi.data.database.AnimeDatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Anime
|
||||
import eu.kanade.tachiyomi.data.database.models.AnimeHistory
|
||||
import eu.kanade.tachiyomi.data.database.models.Episode
|
||||
import eu.kanade.tachiyomi.data.download.AnimeDownloadManager
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.data.track.job.DelayedTrackingStore
|
||||
import eu.kanade.tachiyomi.data.track.job.DelayedTrackingUpdateJob
|
||||
import eu.kanade.tachiyomi.databinding.WatcherActivityBinding
|
||||
import eu.kanade.tachiyomi.ui.anime.episode.EpisodeItem
|
||||
import eu.kanade.tachiyomi.ui.base.activity.BaseThemedActivity.Companion.applyAppTheme
|
||||
import eu.kanade.tachiyomi.util.lang.awaitSingle
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||
import eu.kanade.tachiyomi.util.system.isOnline
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import eu.kanade.tachiyomi.widget.listener.SimpleSeekBarListener
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import logcat.LogPriority
|
||||
import rx.schedulers.Schedulers
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
import java.util.Date
|
||||
|
||||
class PlayerActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var binding: WatcherActivityBinding
|
||||
private val preferences: PreferencesHelper = Injekt.get()
|
||||
private val incognitoMode = preferences.incognitoMode().get()
|
||||
private val db: AnimeDatabaseHelper = Injekt.get()
|
||||
private val downloadManager: AnimeDownloadManager = Injekt.get()
|
||||
private val delayedTrackingStore: DelayedTrackingStore = Injekt.get()
|
||||
private lateinit var exoPlayer: ExoPlayer
|
||||
private lateinit var dataSourceFactory: DataSource.Factory
|
||||
private lateinit var dbProvider: StandaloneDatabaseProvider
|
||||
private val cacheSize = 100L * 1024L * 1024L // 100 MB
|
||||
private var simpleCache: SimpleCache? = null
|
||||
private lateinit var cacheFactory: CacheDataSource.Factory
|
||||
private lateinit var mediaSourceFactory: MediaSourceFactory
|
||||
private lateinit var playerView: DoubleTapPlayerView
|
||||
private lateinit var youTubeDoubleTap: YouTubeOverlay
|
||||
private lateinit var skipBtn: TextView
|
||||
private lateinit var nextBtn: ImageButton
|
||||
private lateinit var prevBtn: ImageButton
|
||||
private lateinit var backBtn: TextView
|
||||
private lateinit var settingsBtn: ImageButton
|
||||
private lateinit var fitScreenBtn: ImageButton
|
||||
private lateinit var title: TextView
|
||||
private lateinit var bufferingView: ProgressBar
|
||||
|
||||
private lateinit var episode: Episode
|
||||
private lateinit var anime: Anime
|
||||
private lateinit var source: AnimeSource
|
||||
private lateinit var uri: String
|
||||
private var videos = emptyList<Video>()
|
||||
private var isBuffering = true
|
||||
private var isLocal = false
|
||||
private var currentQuality = 0
|
||||
|
||||
private var duration: Long = 0
|
||||
private var playbackPosition = 0L
|
||||
private var isFullscreen = false
|
||||
private var isPlayerPlaying = true
|
||||
private var mediaItem = MediaItem.Builder()
|
||||
.setUri("bruh")
|
||||
.setMimeType(MimeTypes.VIDEO_MP4)
|
||||
.build()
|
||||
|
||||
private var isInPipMode: Boolean = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
binding = WatcherActivityBinding.inflate(layoutInflater)
|
||||
applyAppTheme(preferences)
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setContentView(binding.root)
|
||||
|
||||
setVisibilities()
|
||||
playerView = binding.playerView
|
||||
playerView.resizeMode = preferences.getPlayerViewMode()
|
||||
youTubeDoubleTap = binding.youtubeOverlay
|
||||
youTubeDoubleTap.seekSeconds(preferences.skipLengthPreference())
|
||||
youTubeDoubleTap
|
||||
.performListener(object : YouTubeOverlay.PerformListener {
|
||||
override fun onAnimationStart() {
|
||||
// Do UI changes when circle scaling animation starts (e.g. hide controller views)
|
||||
youTubeDoubleTap.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
override fun onAnimationEnd() {
|
||||
// Do UI changes when circle scaling animation starts (e.g. show controller views)
|
||||
youTubeDoubleTap.visibility = View.GONE
|
||||
}
|
||||
})
|
||||
backBtn = findViewById(R.id.exo_overlay_back)
|
||||
title = findViewById(R.id.exo_overlay_title)
|
||||
skipBtn = findViewById(R.id.watcher_controls_skip_btn)
|
||||
nextBtn = findViewById(R.id.watcher_controls_next)
|
||||
prevBtn = findViewById(R.id.watcher_controls_prev)
|
||||
settingsBtn = findViewById(R.id.watcher_controls_settings)
|
||||
fitScreenBtn = findViewById(R.id.watcher_controls_fit_screen)
|
||||
bufferingView = findViewById(R.id.exo_buffering)
|
||||
|
||||
anime = intent.getSerializableExtra("anime") as Anime
|
||||
episode = intent.getSerializableExtra("episode") as Episode
|
||||
title.text = baseContext.getString(R.string.playertitle, anime.title, episode.name)
|
||||
source = Injekt.get<AnimeSourceManager>().getOrStub(anime.source)
|
||||
initDummyPlayer()
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
playbackPosition = savedInstanceState.getLong(STATE_RESUME_POSITION)
|
||||
isFullscreen = savedInstanceState.getBoolean(STATE_PLAYER_FULLSCREEN)
|
||||
isPlayerPlaying = savedInstanceState.getBoolean(STATE_PLAYER_PLAYING)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun setVisibilities() {
|
||||
// TODO: replace this atrocity
|
||||
binding.root.systemUiVisibility =
|
||||
View.SYSTEM_UI_FLAG_LOW_PROFILE or
|
||||
View.SYSTEM_UI_FLAG_FULLSCREEN or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
|
||||
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
|
||||
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
window.attributes.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
|
||||
}
|
||||
}
|
||||
|
||||
private fun initDummyPlayer() {
|
||||
mediaSourceFactory = DefaultMediaSourceFactory(DefaultDataSource.Factory(this))
|
||||
exoPlayer = newPlayer()
|
||||
dbProvider = StandaloneDatabaseProvider(baseContext)
|
||||
val cacheFolder = File(baseContext.filesDir, "media")
|
||||
simpleCache = if (SimpleCache.isCacheFolderLocked(cacheFolder)) {
|
||||
null
|
||||
} else {
|
||||
SimpleCache(
|
||||
cacheFolder,
|
||||
LeastRecentlyUsedCacheEvictor(cacheSize),
|
||||
dbProvider
|
||||
)
|
||||
}
|
||||
|
||||
initPlayer()
|
||||
}
|
||||
|
||||
private fun onGetLinksError(e: Throwable? = null) {
|
||||
launchUI {
|
||||
baseContext.toast(e?.message ?: "error getting links", Toast.LENGTH_LONG)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onGetLinks() {
|
||||
val context = this
|
||||
launchUI {
|
||||
isBuffering(false)
|
||||
if (videos.isEmpty()) {
|
||||
onGetLinksError(Exception("Couldn't find any video links."))
|
||||
return@launchUI
|
||||
}
|
||||
dbProvider = StandaloneDatabaseProvider(baseContext)
|
||||
isLocal = EpisodeLoader.isDownloaded(episode, anime) || source is LocalAnimeSource
|
||||
if (isLocal) {
|
||||
uri = videos.firstOrNull()?.videoUrl ?: return@launchUI onGetLinksError(Exception("URI is null."))
|
||||
dataSourceFactory = DefaultDataSource.Factory(context)
|
||||
} else {
|
||||
uri = videos.firstOrNull()?.videoUrl ?: return@launchUI onGetLinksError(Exception("video URL is null."))
|
||||
dataSourceFactory = newDataSourceFactory()
|
||||
}
|
||||
logcat(LogPriority.INFO) { "playing $uri" }
|
||||
if (simpleCache != null) {
|
||||
cacheFactory = CacheDataSource.Factory().apply {
|
||||
setCache(simpleCache!!)
|
||||
setUpstreamDataSourceFactory(dataSourceFactory)
|
||||
}
|
||||
mediaSourceFactory = DefaultMediaSourceFactory(cacheFactory)
|
||||
} else {
|
||||
mediaSourceFactory = DefaultMediaSourceFactory(dataSourceFactory)
|
||||
}
|
||||
mediaItem = MediaItem.Builder()
|
||||
.setUri(uri)
|
||||
.setMimeType(getMime(uri))
|
||||
.build()
|
||||
playbackPosition = episode.last_second_seen
|
||||
changePlayer(playbackPosition, isLocal)
|
||||
}
|
||||
}
|
||||
|
||||
private fun newDataSourceFactory(): DefaultHttpDataSource.Factory {
|
||||
val defaultUserAgentString = WebSettings.getDefaultUserAgent(baseContext)
|
||||
return DefaultHttpDataSource.Factory().apply {
|
||||
val currentHeaders = videos.getOrNull(currentQuality)?.headers
|
||||
val headers = currentHeaders?.toMultimap()
|
||||
?.mapValues { it.value.getOrNull(0) ?: "" }
|
||||
?.toMutableMap()
|
||||
?: (source as AnimeHttpSource).headers.toMultimap()
|
||||
.mapValues { it.value.getOrNull(0) ?: "" }
|
||||
.toMutableMap()
|
||||
setDefaultRequestProperties(headers)
|
||||
setUserAgent(headers["user-agent"] ?: defaultUserAgentString)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getMime(uri: String): String {
|
||||
return when (Uri.parse(uri).path?.substringAfterLast(".")) {
|
||||
"mp4" -> MimeTypes.VIDEO_MP4
|
||||
"mkv" -> MimeTypes.VIDEO_MATROSKA
|
||||
"m3u8" -> MimeTypes.APPLICATION_M3U8
|
||||
else -> MimeTypes.VIDEO_MP4
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
return super.onCreateOptionsMenu(menu)
|
||||
}
|
||||
|
||||
private fun initPlayer() {
|
||||
exoPlayer = newPlayer().apply {
|
||||
playWhenReady = isPlayerPlaying
|
||||
prepare()
|
||||
}
|
||||
exoPlayer.addListener(PlayerEventListener(playerView, baseContext))
|
||||
backBtn.setOnClickListener {
|
||||
onBackPressed()
|
||||
}
|
||||
fitScreenBtn.setOnClickListener {
|
||||
onClickFitScreen()
|
||||
}
|
||||
settingsBtn.setOnClickListener {
|
||||
optionsDialog()
|
||||
}
|
||||
skipBtn.setOnClickListener {
|
||||
exoPlayer.seekTo(exoPlayer.currentPosition + 85000)
|
||||
}
|
||||
nextBtn.setOnClickListener {
|
||||
nextEpisode()
|
||||
}
|
||||
prevBtn.setOnClickListener {
|
||||
previousEpisode()
|
||||
}
|
||||
youTubeDoubleTap.player(exoPlayer)
|
||||
playerView.player = exoPlayer
|
||||
duration = exoPlayer.duration
|
||||
awaitVideoList()
|
||||
}
|
||||
|
||||
private fun isBuffering(param: Boolean) {
|
||||
isBuffering = param
|
||||
if (param) {
|
||||
bufferingView.visibility = View.VISIBLE
|
||||
} else {
|
||||
bufferingView.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun onClickFitScreen() {
|
||||
playerView.resizeMode = when (playerView.resizeMode) {
|
||||
AspectRatioFrameLayout.RESIZE_MODE_FILL -> AspectRatioFrameLayout.RESIZE_MODE_FIT
|
||||
AspectRatioFrameLayout.RESIZE_MODE_FIT -> AspectRatioFrameLayout.RESIZE_MODE_ZOOM
|
||||
AspectRatioFrameLayout.RESIZE_MODE_ZOOM -> AspectRatioFrameLayout.RESIZE_MODE_FILL
|
||||
else -> AspectRatioFrameLayout.RESIZE_MODE_FIT
|
||||
}
|
||||
preferences.setPlayerViewMode(playerView.resizeMode)
|
||||
}
|
||||
|
||||
private inner class HideBarsMaterialAlertDialogBuilder(context: Context) : MaterialAlertDialogBuilder(context) {
|
||||
override fun create(): AlertDialog {
|
||||
return super.create().apply {
|
||||
val window = this.window ?: return@apply
|
||||
val alertWindowInsetsController = WindowInsetsControllerCompat(window, window.decorView)
|
||||
alertWindowInsetsController.hide(WindowInsetsCompat.Type.systemBars())
|
||||
alertWindowInsetsController.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun optionsDialog() {
|
||||
val alert = HideBarsMaterialAlertDialogBuilder(this)
|
||||
|
||||
alert.setTitle(R.string.playback_options_title)
|
||||
alert.setItems(R.array.playback_options) { dialog, which ->
|
||||
when (which) {
|
||||
0 -> {
|
||||
val speedAlert = HideBarsMaterialAlertDialogBuilder(this)
|
||||
|
||||
val linear = LinearLayout(this)
|
||||
|
||||
val items = arrayOf(
|
||||
"10%", "25%", "40%", "50%", "60%", "70%", "75%", "80%",
|
||||
"85%", "90%", "95%", "100% (Normal)", "105%", "110%", "115%", "120%", "125%",
|
||||
"130%", "140%", "150%", "175%", "200%", "250%", "300%"
|
||||
)
|
||||
val floatItems = arrayOf(
|
||||
0.1F, 0.25F, 0.4F, 0.5F, 0.6F, 0.7F, 0.75F, 0.8F, 0.85F, 0.9F,
|
||||
0.95F, 1F, 1.05F, 1.1F, 1.15F, 1.2F, 1.25F, 1.3F, 1.4F, 1.5F, 1.75F, 2F, 2.5F, 3F
|
||||
)
|
||||
var newSpeed = preferences.getPlayerSpeed()
|
||||
|
||||
speedAlert.setTitle(R.string.playback_speed_dialog_title)
|
||||
|
||||
linear.orientation = LinearLayout.VERTICAL
|
||||
val text = TextView(this)
|
||||
text.text = items[floatItems.indexOf(newSpeed)]
|
||||
text.setPadding(30, 10, 10, 10)
|
||||
|
||||
val seek = SeekBar(this).apply {
|
||||
max = items.lastIndex
|
||||
progress = floatItems.indexOf(newSpeed)
|
||||
}
|
||||
seek.setOnSeekBarChangeListener(object : SimpleSeekBarListener() {
|
||||
override fun onProgressChanged(seekBar: SeekBar, value: Int, fromUser: Boolean) {
|
||||
text.text = items[value]
|
||||
newSpeed = floatItems[value]
|
||||
}
|
||||
})
|
||||
|
||||
linear.addView(seek)
|
||||
linear.addView(text)
|
||||
|
||||
speedAlert.setView(linear)
|
||||
|
||||
speedAlert.setPositiveButton(android.R.string.ok) { speedDialog, _ ->
|
||||
exoPlayer.playbackParameters = PlaybackParameters(newSpeed)
|
||||
preferences.setPlayerSpeed(newSpeed)
|
||||
speedDialog.dismiss()
|
||||
}
|
||||
|
||||
speedAlert.setNegativeButton(android.R.string.cancel) { speedDialog, _ ->
|
||||
speedDialog.cancel()
|
||||
}
|
||||
|
||||
speedAlert.setNeutralButton(R.string.playback_speed_dialog_reset) { _, _ ->
|
||||
newSpeed = 1F
|
||||
val newProgress = floatItems.indexOf(newSpeed)
|
||||
text.text = items[newProgress]
|
||||
seek.progress = newProgress
|
||||
exoPlayer.playbackParameters = PlaybackParameters(newSpeed)
|
||||
preferences.setPlayerSpeed(newSpeed)
|
||||
}
|
||||
|
||||
speedAlert.show()
|
||||
}
|
||||
1 -> {
|
||||
if (videos.isNotEmpty()) {
|
||||
val qualityAlert = HideBarsMaterialAlertDialogBuilder(this)
|
||||
|
||||
qualityAlert.setTitle(R.string.playback_quality_dialog_title)
|
||||
|
||||
var requestedQuality = 0
|
||||
val qualities = videos.map { it.quality }.toTypedArray()
|
||||
qualityAlert.setSingleChoiceItems(qualities, currentQuality) { qualityDialog, selectedQuality ->
|
||||
if (selectedQuality > qualities.lastIndex) {
|
||||
qualityDialog.cancel()
|
||||
} else {
|
||||
requestedQuality = selectedQuality
|
||||
}
|
||||
}
|
||||
|
||||
qualityAlert.setPositiveButton(android.R.string.ok) { qualityDialog, _ ->
|
||||
if (requestedQuality != currentQuality) changeQuality(requestedQuality)
|
||||
qualityDialog.dismiss()
|
||||
}
|
||||
|
||||
qualityAlert.setNegativeButton(android.R.string.cancel) { qualityDialog, _ ->
|
||||
qualityDialog.cancel()
|
||||
}
|
||||
|
||||
qualityAlert.show()
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
dialog.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
alert.show()
|
||||
}
|
||||
|
||||
private fun releasePlayer() {
|
||||
youTubeDoubleTap.player(exoPlayer)
|
||||
isPlayerPlaying = exoPlayer.playWhenReady
|
||||
playbackPosition = exoPlayer.currentPosition
|
||||
exoPlayer.release()
|
||||
}
|
||||
|
||||
private fun awaitVideoList() {
|
||||
isBuffering(true)
|
||||
launchIO {
|
||||
try {
|
||||
EpisodeLoader.getLinks(episode, anime, source)
|
||||
.doOnNext {
|
||||
videos = it
|
||||
onGetLinks()
|
||||
}.awaitSingle()
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e) { e.message ?: "error getting links" }
|
||||
onGetLinksError(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun nextEpisode() {
|
||||
if (exoPlayer.isPlaying) exoPlayer.pause()
|
||||
saveEpisodeHistory(EpisodeItem(episode, anime))
|
||||
setEpisodeProgress(episode, anime, exoPlayer.currentPosition, exoPlayer.duration)
|
||||
val oldEpisode = episode
|
||||
episode = getNextEpisode(episode, anime)
|
||||
if (oldEpisode == episode) return
|
||||
title.text = baseContext.getString(R.string.playertitle, anime.title, episode.name)
|
||||
currentQuality = 0
|
||||
awaitVideoList()
|
||||
}
|
||||
|
||||
private fun previousEpisode() {
|
||||
if (exoPlayer.isPlaying) exoPlayer.pause()
|
||||
saveEpisodeHistory(EpisodeItem(episode, anime))
|
||||
setEpisodeProgress(episode, anime, exoPlayer.currentPosition, exoPlayer.duration)
|
||||
val oldEpisode = episode
|
||||
episode = getPreviousEpisode(episode, anime)
|
||||
if (oldEpisode == episode) return
|
||||
title.text = baseContext.getString(R.string.playertitle, anime.title, episode.name)
|
||||
currentQuality = 0
|
||||
awaitVideoList()
|
||||
}
|
||||
|
||||
private fun changeQuality(quality: Int) {
|
||||
baseContext.toast(videos.getOrNull(quality)?.quality, Toast.LENGTH_SHORT)
|
||||
uri = if (isLocal) {
|
||||
videos.getOrNull(quality)?.videoUrl ?: return
|
||||
} else {
|
||||
videos.getOrNull(quality)?.videoUrl ?: return
|
||||
}
|
||||
currentQuality = quality
|
||||
mediaItem = MediaItem.Builder()
|
||||
.setUri(uri)
|
||||
.setMimeType(getMime(uri))
|
||||
.build()
|
||||
changePlayer(exoPlayer.currentPosition, isLocal)
|
||||
}
|
||||
|
||||
private fun changePlayer(resumeAt: Long, isLocal: Boolean) {
|
||||
dataSourceFactory = if (isLocal) {
|
||||
DefaultDataSource.Factory(this)
|
||||
} else {
|
||||
newDataSourceFactory()
|
||||
}
|
||||
if (simpleCache != null) {
|
||||
cacheFactory = CacheDataSource.Factory().apply {
|
||||
setCache(simpleCache!!)
|
||||
setUpstreamDataSourceFactory(dataSourceFactory)
|
||||
}
|
||||
mediaSourceFactory = DefaultMediaSourceFactory(cacheFactory)
|
||||
} else {
|
||||
mediaSourceFactory = DefaultMediaSourceFactory(dataSourceFactory)
|
||||
}
|
||||
exoPlayer.release()
|
||||
exoPlayer = newPlayer()
|
||||
exoPlayer.setMediaSource(mediaSourceFactory.createMediaSource(mediaItem), resumeAt)
|
||||
exoPlayer.prepare()
|
||||
exoPlayer.playWhenReady = true
|
||||
exoPlayer.addListener(PlayerEventListener(playerView, baseContext))
|
||||
youTubeDoubleTap.player(exoPlayer)
|
||||
playerView.player = exoPlayer
|
||||
}
|
||||
|
||||
private fun newPlayer(): ExoPlayer {
|
||||
return ExoPlayer.Builder(
|
||||
this,
|
||||
DefaultRenderersFactory(this),
|
||||
mediaSourceFactory,
|
||||
DefaultTrackSelector(this),
|
||||
DefaultLoadControl(),
|
||||
DefaultBandwidthMeter.Builder(this).build(),
|
||||
AnalyticsCollector(Clock.DEFAULT)
|
||||
).setSeekForwardIncrementMs(85000L)
|
||||
.build().apply {
|
||||
playbackParameters = PlaybackParameters(preferences.getPlayerSpeed())
|
||||
}
|
||||
}
|
||||
|
||||
class PlayerEventListener(private val playerView: DoubleTapPlayerView, val baseContext: Context) : Player.Listener {
|
||||
override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
|
||||
playerView.keepScreenOn = !(
|
||||
playbackState == Player.STATE_IDLE || playbackState == Player.STATE_ENDED ||
|
||||
!playWhenReady
|
||||
)
|
||||
}
|
||||
override fun onTracksChanged(trackGroups: TrackGroupArray, trackSelections: TrackSelectionArray) {}
|
||||
override fun onLoadingChanged(isLoading: Boolean) {}
|
||||
override fun onRepeatModeChanged(repeatMode: Int) {}
|
||||
override fun onPlayerError(error: PlaybackException) {
|
||||
val cause: Throwable? = error.cause
|
||||
if (cause is HttpDataSourceException) {
|
||||
// An HTTP error occurred.
|
||||
// This is the request for which the error occurred.
|
||||
val requestDataSpec = cause.dataSpec
|
||||
for (header in requestDataSpec.httpRequestHeaders) {
|
||||
var message = ""
|
||||
message += header.key + " - " + header.value
|
||||
}
|
||||
// It's possible to find out more about the error both by casting and by
|
||||
// querying the cause.
|
||||
if (cause is InvalidResponseCodeException) {
|
||||
// Cast to InvalidResponseCodeException and retrieve the response code,
|
||||
// message and headers.
|
||||
val errorMessage =
|
||||
"Error " + cause.responseCode.toString() + ": " + cause.message
|
||||
baseContext.toast(errorMessage, Toast.LENGTH_SHORT)
|
||||
} else {
|
||||
cause.cause
|
||||
// Try calling httpError.getCause() to retrieve the underlying cause,
|
||||
// although note that it may be null.
|
||||
}
|
||||
}
|
||||
}
|
||||
override fun onPlaybackParametersChanged(playbackParameters: PlaybackParameters) {}
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
outState.putLong(STATE_RESUME_POSITION, exoPlayer.currentPosition)
|
||||
outState.putBoolean(STATE_PLAYER_FULLSCREEN, isFullscreen)
|
||||
outState.putBoolean(STATE_PLAYER_PLAYING, isPlayerPlaying)
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
playerView.onResume()
|
||||
super.onStart()
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
saveEpisodeHistory(EpisodeItem(episode, anime))
|
||||
setEpisodeProgress(episode, anime, exoPlayer.currentPosition, exoPlayer.duration)
|
||||
if (exoPlayer.isPlaying) exoPlayer.pause()
|
||||
if (isInPipMode) finish()
|
||||
playerView.onPause()
|
||||
super.onStop()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
deletePendingEpisodes()
|
||||
releasePlayer()
|
||||
simpleCache?.release()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)) {
|
||||
finishAndRemoveTask()
|
||||
}
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onUserLeaveHint() {
|
||||
if (exoPlayer.isPlaying) {
|
||||
startPiP()
|
||||
}
|
||||
|
||||
super.onUserLeaveHint()
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent?) {
|
||||
super.onNewIntent(intent)
|
||||
val oldAnime = anime
|
||||
anime = intent?.getSerializableExtra("anime") as? Anime ?: return
|
||||
val oldEpisode = episode
|
||||
episode = intent.getSerializableExtra("episode") as Episode
|
||||
if (oldEpisode == episode) return
|
||||
saveEpisodeHistory(EpisodeItem(oldEpisode, oldAnime))
|
||||
setEpisodeProgress(oldEpisode, oldAnime, exoPlayer.currentPosition, exoPlayer.duration)
|
||||
title.text = baseContext.getString(R.string.playertitle, anime.title, episode.name)
|
||||
source = Injekt.get<AnimeSourceManager>().getOrStub(anime.source)
|
||||
awaitVideoList()
|
||||
}
|
||||
|
||||
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration?) {
|
||||
isInPipMode = isInPictureInPictureMode
|
||||
playerView.useController = !isInPictureInPictureMode
|
||||
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun startPiP() {
|
||||
if (!preferences.pipPlayerPreference()) return
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)) {
|
||||
playerView.useController = false
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
this.enterPictureInPictureMode(PictureInPictureParams.Builder().build())
|
||||
} else {
|
||||
this.enterPictureInPictureMode()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveEpisodeHistory(episode: EpisodeItem) {
|
||||
if (!incognitoMode && !isBuffering) {
|
||||
val history = AnimeHistory.create(episode.episode).apply { last_seen = Date().time }
|
||||
db.updateAnimeHistoryLastSeen(history).asRxCompletable()
|
||||
.onErrorComplete()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setEpisodeProgress(episode: Episode, anime: Anime, seconds: Long, totalSeconds: Long) {
|
||||
if (!incognitoMode && !isBuffering) {
|
||||
if (totalSeconds > 0L) {
|
||||
episode.last_second_seen = seconds
|
||||
episode.total_seconds = totalSeconds
|
||||
val progress = preferences.progressPreference()
|
||||
if (!episode.seen) episode.seen = episode.last_second_seen >= episode.total_seconds * progress
|
||||
val episodes = listOf(EpisodeItem(episode, anime))
|
||||
launchIO {
|
||||
db.updateEpisodesProgress(episodes).executeAsBlocking()
|
||||
if (preferences.autoUpdateTrack() && episode.seen) {
|
||||
updateTrackEpisodeSeen(episode)
|
||||
}
|
||||
if (episode.seen) {
|
||||
deleteEpisodeIfNeeded(episode)
|
||||
deleteEpisodeFromDownloadQueue(episode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun deleteEpisodeFromDownloadQueue(episode: Episode) {
|
||||
downloadManager.getEpisodeDownloadOrNull(episode)?.let { download ->
|
||||
downloadManager.deletePendingDownload(download)
|
||||
}
|
||||
}
|
||||
|
||||
private fun enqueueDeleteSeenEpisodes(episode: Episode) {
|
||||
if (!episode.seen) return
|
||||
val anime = anime
|
||||
|
||||
launchIO {
|
||||
downloadManager.enqueueDeleteEpisodes(listOf(episode), anime)
|
||||
}
|
||||
}
|
||||
|
||||
private fun deleteEpisodeIfNeeded(episode: Episode) {
|
||||
// Determine which chapter should be deleted and enqueue
|
||||
val sortFunction: (Episode, Episode) -> Int = when (anime.sorting) {
|
||||
Anime.EPISODE_SORTING_SOURCE -> { c1, c2 -> c2.source_order.compareTo(c1.source_order) }
|
||||
Anime.EPISODE_SORTING_NUMBER -> { c1, c2 -> c1.episode_number.compareTo(c2.episode_number) }
|
||||
Anime.EPISODE_SORTING_UPLOAD_DATE -> { c1, c2 -> c1.date_upload.compareTo(c2.date_upload) }
|
||||
else -> throw NotImplementedError("Unknown sorting method")
|
||||
}
|
||||
|
||||
val episodes = db.getEpisodes(anime).executeAsBlocking()
|
||||
.sortedWith { e1, e2 -> sortFunction(e1, e2) }
|
||||
|
||||
val currentEpisodePosition = episodes.indexOf(episode)
|
||||
val removeAfterReadSlots = preferences.removeAfterReadSlots()
|
||||
val episodeToDelete = episodes.getOrNull(currentEpisodePosition - removeAfterReadSlots)
|
||||
|
||||
// Check if deleting option is enabled and chapter exists
|
||||
if (removeAfterReadSlots != -1 && episodeToDelete != null) {
|
||||
enqueueDeleteSeenEpisodes(episodeToDelete)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all the pending episodes. This operation will run in a background thread and errors
|
||||
* are ignored.
|
||||
*/
|
||||
private fun deletePendingEpisodes() {
|
||||
launchIO {
|
||||
downloadManager.deletePendingEpisodes()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateTrackEpisodeSeen(episode: Episode) {
|
||||
val episodeSeen = episode.episode_number
|
||||
|
||||
val trackManager = Injekt.get<TrackManager>()
|
||||
|
||||
launchIO {
|
||||
db.getTracks(anime).executeAsBlocking()
|
||||
.mapNotNull { track ->
|
||||
val service = trackManager.getService(track.sync_id)
|
||||
if (service != null && service.isLogged && episodeSeen > track.last_episode_seen) {
|
||||
track.last_episode_seen = episodeSeen
|
||||
|
||||
// We want these to execute even if the presenter is destroyed and leaks
|
||||
// for a while. The view can still be garbage collected.
|
||||
async {
|
||||
runCatching {
|
||||
if (baseContext.isOnline()) {
|
||||
service.update(track, true)
|
||||
db.insertTrack(track).executeAsBlocking()
|
||||
} else {
|
||||
delayedTrackingStore.addItem(track)
|
||||
DelayedTrackingUpdateJob.setupTask(baseContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
.awaitAll()
|
||||
.mapNotNull { it.exceptionOrNull() }
|
||||
.forEach { logcat(LogPriority.WARN, it) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun getNextEpisode(episode: Episode, anime: Anime): Episode {
|
||||
val sortFunction: (Episode, Episode) -> Int = when (anime.sorting) {
|
||||
Anime.EPISODE_SORTING_SOURCE -> { c1, c2 -> c2.source_order.compareTo(c1.source_order) }
|
||||
Anime.EPISODE_SORTING_NUMBER -> { c1, c2 -> c1.episode_number.compareTo(c2.episode_number) }
|
||||
Anime.EPISODE_SORTING_UPLOAD_DATE -> { c1, c2 -> c1.date_upload.compareTo(c2.date_upload) }
|
||||
else -> throw NotImplementedError("Unknown sorting method")
|
||||
}
|
||||
|
||||
val episodes = db.getEpisodes(anime).executeAsBlocking()
|
||||
.sortedWith { e1, e2 -> sortFunction(e1, e2) }
|
||||
|
||||
val currEpisodeIndex = episodes.indexOfFirst { episode.id == it.id }
|
||||
val episodeNumber = episode.episode_number
|
||||
return (currEpisodeIndex + 1 until episodes.size)
|
||||
.map { episodes[it] }
|
||||
.firstOrNull {
|
||||
it.episode_number > episodeNumber &&
|
||||
it.episode_number <= episodeNumber + 1
|
||||
} ?: episode
|
||||
}
|
||||
|
||||
private fun getPreviousEpisode(episode: Episode, anime: Anime): Episode {
|
||||
val sortFunction: (Episode, Episode) -> Int = when (anime.sorting) {
|
||||
Anime.EPISODE_SORTING_SOURCE -> { c1, c2 -> c2.source_order.compareTo(c1.source_order) }
|
||||
Anime.EPISODE_SORTING_NUMBER -> { c1, c2 -> c1.episode_number.compareTo(c2.episode_number) }
|
||||
Anime.EPISODE_SORTING_UPLOAD_DATE -> { c1, c2 -> c1.date_upload.compareTo(c2.date_upload) }
|
||||
else -> throw NotImplementedError("Unknown sorting method")
|
||||
}
|
||||
|
||||
val episodes = db.getEpisodes(anime).executeAsBlocking()
|
||||
.sortedWith { e1, e2 -> sortFunction(e2, e1) }
|
||||
|
||||
val currEpisodeIndex = episodes.indexOfFirst { episode.id == it.id }
|
||||
val episodeNumber = episode.episode_number
|
||||
return (currEpisodeIndex + 1 until episodes.size)
|
||||
.map { episodes[it] }
|
||||
.firstOrNull {
|
||||
it.episode_number < episodeNumber &&
|
||||
it.episode_number >= episodeNumber - 1
|
||||
} ?: episode
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun newIntent(context: Context, anime: Anime, episode: Episode): Intent {
|
||||
return Intent(context, PlayerActivity::class.java).apply {
|
||||
putExtra("anime", anime)
|
||||
putExtra("episode", episode)
|
||||
putExtra("second", episode.last_second_seen)
|
||||
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val STATE_RESUME_POSITION = "resumePosition"
|
||||
private const val STATE_PLAYER_FULLSCREEN = "playerFullscreen"
|
||||
private const val STATE_PLAYER_PLAYING = "playerOnPlay"
|
|
@ -25,7 +25,7 @@ import eu.kanade.tachiyomi.ui.base.controller.RootController
|
|||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||
import eu.kanade.tachiyomi.ui.browse.animesource.browse.ProgressItem
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
import eu.kanade.tachiyomi.ui.player.PlayerActivity
|
||||
import eu.kanade.tachiyomi.ui.player.NewPlayerActivity
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import eu.kanade.tachiyomi.util.view.onAnimationsFinished
|
||||
import kotlinx.coroutines.flow.filter
|
||||
|
@ -159,7 +159,7 @@ class AnimeHistoryController :
|
|||
|
||||
val nextEpisode = presenter.getNextEpisode(chapter, anime)
|
||||
if (nextEpisode != null) {
|
||||
val newIntent = PlayerActivity.newIntent(activity, anime, nextEpisode)
|
||||
val newIntent = NewPlayerActivity.newIntent(activity, anime, nextEpisode)
|
||||
startActivity(newIntent)
|
||||
} else {
|
||||
activity.toast(R.string.no_next_episode)
|
||||
|
|
|
@ -23,7 +23,7 @@ import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
|||
import eu.kanade.tachiyomi.ui.base.controller.RootController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
import eu.kanade.tachiyomi.ui.player.PlayerActivity
|
||||
import eu.kanade.tachiyomi.ui.player.NewPlayerActivity
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
|
@ -193,7 +193,7 @@ class AnimeUpdatesController :
|
|||
*/
|
||||
private fun openEpisode(item: AnimeUpdatesItem) {
|
||||
val activity = activity ?: return
|
||||
val intent = PlayerActivity.newIntent(activity, item.anime, item.episode)
|
||||
val intent = NewPlayerActivity.newIntent(activity, item.anime, item.episode)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,273 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<is.xyz.mpv.MPVView
|
||||
android:id="@+id/player"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/controls"
|
||||
style="?android:attr/buttonBarStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_marginLeft="24dp"
|
||||
android:layout_marginTop="60dp"
|
||||
android:layout_marginRight="24dp"
|
||||
android:layout_marginBottom="60dp"
|
||||
android:background="#bf000000"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/controls_title_group"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone">
|
||||
|
||||
<!-- These two are only used for audio -->
|
||||
<TextView
|
||||
android:id="@+id/titleTextView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:text="-"
|
||||
android:textColor="@color/tint_normal"
|
||||
android:textSize="24sp"
|
||||
android:visibility="gone" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/minorTitleTextView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:text="-"
|
||||
android:textColor="@color/tint_normal"
|
||||
android:textSize="14sp"
|
||||
android:visibility="gone" />
|
||||
|
||||
<!-- This one for video title display -->
|
||||
<TextView
|
||||
android:id="@+id/fullTitleTextView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="middle"
|
||||
android:gravity="center"
|
||||
android:singleLine="true"
|
||||
android:text="-"
|
||||
android:textColor="@color/tint_normal"
|
||||
android:textSize="16sp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/controls_seekbar_group"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="horizontal"
|
||||
android:weightSum="100">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/prevBtn"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="#00000000"
|
||||
android:onClick="playlistPrev"
|
||||
android:src="@drawable/ic_skip_previous_black_24dp"
|
||||
app:tint="@color/tint_normal" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/playbackPositionTxt"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="10"
|
||||
android:gravity="center"
|
||||
android:textColor="@android:color/white" />
|
||||
|
||||
<SeekBar
|
||||
android:id="@+id/playbackSeekbar"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_weight="80"
|
||||
android:progressBackgroundTint="@color/tint_seekbar_bg" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/playbackDurationTxt"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="10"
|
||||
android:gravity="center"
|
||||
android:textColor="@android:color/white" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/nextBtn"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="#00000000"
|
||||
android:onClick="playlistNext"
|
||||
android:src="@drawable/ic_skip_next_black_24dp"
|
||||
app:tint="@color/tint_normal" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/controls_button_group"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/playBtn"
|
||||
style="?android:attr/buttonBarButtonStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:onClick="playPause"
|
||||
android:text="play"
|
||||
android:textColor="@android:color/white"
|
||||
app:tint="@color/tint_normal" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/cycleAudioBtn"
|
||||
style="?android:attr/buttonBarButtonStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:onClick="cycleAudio"
|
||||
android:src="@drawable/ic_audiotrack_black_24dp"
|
||||
android:text="..."
|
||||
android:textColor="@android:color/white"
|
||||
app:tint="@color/tint_normal" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/cycleSubsBtn"
|
||||
style="?android:attr/buttonBarButtonStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:onClick="cycleSub"
|
||||
android:src="@drawable/ic_subtitles_black_24dp"
|
||||
android:text="..."
|
||||
android:textColor="@android:color/white"
|
||||
app:tint="@color/tint_normal" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/cycleDecoderBtn"
|
||||
style="?android:attr/buttonBarButtonStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:onClick="switchDecoder"
|
||||
android:text=".."
|
||||
android:textColor="@android:color/white" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/cycleSpeedBtn"
|
||||
style="?android:attr/buttonBarButtonStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:onClick="cycleSpeed"
|
||||
android:text=".."
|
||||
android:textColor="@android:color/white" />
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="stats"
|
||||
android:id="@+id/statsTextView"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_alignParentStart="true"
|
||||
android:textColor="#ffffff"
|
||||
android:shadowColor="#000000"
|
||||
android:shadowDx="1"
|
||||
android:shadowDy="1"
|
||||
android:shadowRadius="1"
|
||||
android:visibility="gone" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/gestureTextView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:shadowColor="#000000"
|
||||
android:shadowDx="0"
|
||||
android:shadowDy="0"
|
||||
android:shadowRadius="4"
|
||||
android:text="[gesture]"
|
||||
android:textAlignment="center"
|
||||
android:textColor="#ffffff"
|
||||
android:textSize="36sp"
|
||||
android:textStyle="bold"
|
||||
android:visibility="gone" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/top_controls"
|
||||
style="?android:attr/buttonBarStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:alpha="0.5"
|
||||
android:background="#bf000000"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/topLockBtn"
|
||||
style="?android:attr/buttonBarStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:minWidth="40dp"
|
||||
android:minHeight="40dp"
|
||||
android:onClick="lockUI"
|
||||
android:padding="4dp"
|
||||
android:src="@drawable/ic_lock_24dp"
|
||||
app:tint="@color/tint_normal" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/topPiPBtn"
|
||||
style="?android:attr/buttonBarStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:minWidth="40dp"
|
||||
android:minHeight="40dp"
|
||||
android:onClick="goIntoPiP"
|
||||
android:padding="4dp"
|
||||
android:src="@drawable/ic_picture_in_picture_24dp"
|
||||
app:tint="@color/tint_normal" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/topMenuBtn"
|
||||
style="?android:attr/buttonBarStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:minWidth="40dp"
|
||||
android:minHeight="40dp"
|
||||
android:onClick="openTopMenu"
|
||||
android:padding="4dp"
|
||||
android:src="@drawable/ic_settings_black_24dp"
|
||||
app:tint="@color/tint_normal" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- mismatching width/height so that the button appears exactly square-->
|
||||
<ImageButton
|
||||
android:id="@+id/unlockBtn"
|
||||
style="@style/Widget.AppCompat.ImageButton"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="52dp"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_margin="24dp"
|
||||
android:backgroundTint="@color/background_material_dark"
|
||||
android:onClick="unlockUI"
|
||||
android:src="@drawable/ic_lock_open_24dp"
|
||||
app:tint="@android:color/white"
|
||||
android:visibility="gone" />
|
||||
|
||||
</RelativeLayout>
|
|
@ -1,26 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:background="#000000">
|
||||
|
||||
<com.github.vkay94.dtpv.DoubleTapPlayerView
|
||||
android:id="@+id/player_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:controller_layout_id="@layout/watcher_controls_view"
|
||||
app:dtpv_controller="@id/youtube_overlay"
|
||||
app:show_buffering="when_playing"
|
||||
tools:alpha="0.1"/>
|
||||
|
||||
<com.github.vkay94.dtpv.youtube.YouTubeOverlay
|
||||
android:id="@+id/youtube_overlay"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="invisible"
|
||||
app:yt_playerView="@+id/player_view" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
|
@ -1,136 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2016 The Android Open Source Project
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/RelativeLayout01"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layoutDirection="ltr"
|
||||
android:layout_alignParentTop="true"
|
||||
android:background="#70000000"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/exo_overlay_back"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="@dimen/exo_media_button_height"
|
||||
android:layout_gravity="start"
|
||||
android:gravity="center"
|
||||
android:paddingHorizontal="20dp"
|
||||
android:text="@string/player_overlay_back"
|
||||
android:textColor="@color/md_white_1000"
|
||||
android:textStyle="bold"
|
||||
style="@style/ExoMediaButton"/>
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:paddingTop="4dp"
|
||||
android:orientation="horizontal">
|
||||
<TextView
|
||||
android:id="@+id/exo_overlay_title"
|
||||
android:textColor="@color/md_white_1000"
|
||||
android:textStyle="bold"
|
||||
android:gravity="center"
|
||||
android:layout_height="@dimen/exo_media_button_height"
|
||||
android:layout_width="wrap_content"/>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom"
|
||||
android:layoutDirection="ltr"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:background="#70000000"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:paddingTop="4dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageButton android:id="@+id/watcher_controls_fit_screen"
|
||||
style="@style/ExoStyledControls.Button.Bottom.FullScreen"
|
||||
android:contentDescription="@string/action_screen_fit" />
|
||||
|
||||
<ImageButton android:id="@+id/watcher_controls_prev"
|
||||
style="@style/ExoMediaButton.Previous"
|
||||
android:contentDescription="@string/action_previous_episode" />
|
||||
|
||||
<ImageButton android:id="@id/exo_play"
|
||||
style="@style/ExoMediaButton.Play"
|
||||
android:contentDescription="@string/action_play" />
|
||||
|
||||
<ImageButton android:id="@id/exo_pause"
|
||||
style="@style/ExoMediaButton.Pause"
|
||||
android:contentDescription="@string/action_pause" />
|
||||
|
||||
<ImageButton android:id="@+id/watcher_controls_next"
|
||||
style="@style/ExoMediaButton.Next"
|
||||
android:contentDescription="@string/action_next_episode" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/watcher_controls_skip_btn"
|
||||
android:text="@string/watcher_controls_skip_text"
|
||||
android:textColor="@color/md_white_1000"
|
||||
android:textStyle="bold"
|
||||
android:gravity="center"
|
||||
style="@style/ExoMediaButton" />
|
||||
|
||||
<ImageButton android:id="@+id/watcher_controls_settings"
|
||||
style="@style/ExoStyledControls.Button.Bottom.Settings"
|
||||
android:contentDescription="@string/action_next_episode" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView android:id="@id/exo_position"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
android:paddingStart="15dp"
|
||||
android:paddingEnd="4dp"
|
||||
android:includeFontPadding="false"
|
||||
android:textColor="@color/md_white_1000"/>
|
||||
|
||||
<com.google.android.exoplayer2.ui.DefaultTimeBar
|
||||
android:id="@id/exo_progress"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="26dp"/>
|
||||
|
||||
<TextView android:id="@id/exo_duration"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
android:paddingStart="4dp"
|
||||
android:paddingEnd="15dp"
|
||||
android:includeFontPadding="false"
|
||||
android:textColor="@color/md_white_1000"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout></RelativeLayout>
|
Loading…
Reference in a new issue