mirror of
https://github.com/aniyomiorg/aniyomi.git
synced 2024-11-29 17:49: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/
|
// For detecting memory leaks; see https://square.github.io/leakcanary/
|
||||||
// debugImplementation("com.squareup.leakcanary:leakcanary-android:2.7")
|
// 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
|
// FFmpeg
|
||||||
implementation("com.arthenica:ffmpeg-kit-https:4.5.LTS")
|
implementation("com.arthenica:ffmpeg-kit-https:4.5.LTS")
|
||||||
|
|
||||||
|
|
|
@ -93,24 +93,6 @@
|
||||||
<meta-data android:name="com.samsung.android.support.REMOTE_ACTION"
|
<meta-data android:name="com.samsung.android.support.REMOTE_ACTION"
|
||||||
android:resource="@xml/s_pen_actions"/>
|
android:resource="@xml/s_pen_actions"/>
|
||||||
</activity>
|
</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
|
<activity
|
||||||
android:name=".ui.player.NewPlayerActivity"
|
android:name=".ui.player.NewPlayerActivity"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
|
@ -126,17 +108,6 @@
|
||||||
<meta-data android:name="com.samsung.android.support.REMOTE_ACTION"
|
<meta-data android:name="com.samsung.android.support.REMOTE_ACTION"
|
||||||
android:resource="@xml/s_pen_actions"/>
|
android:resource="@xml/s_pen_actions"/>
|
||||||
</activity>
|
</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
|
<activity
|
||||||
android:name=".ui.security.UnlockActivity"
|
android:name=".ui.security.UnlockActivity"
|
||||||
android:theme="@style/Theme.Tachiyomi"
|
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.anime.AnimeController
|
||||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
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.ui.reader.ReaderActivity
|
||||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
|
@ -243,7 +243,7 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||||
val anime = db.getAnime(animeId).executeAsBlocking()
|
val anime = db.getAnime(animeId).executeAsBlocking()
|
||||||
val episode = db.getEpisode(episodeId).executeAsBlocking()
|
val episode = db.getEpisode(episodeId).executeAsBlocking()
|
||||||
if (anime != null && episode != null) {
|
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
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||||
}
|
}
|
||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
|
@ -609,7 +609,7 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||||
* @param episode episode that needs to be opened
|
* @param episode episode that needs to be opened
|
||||||
*/
|
*/
|
||||||
internal fun openEpisodePendingActivity(context: Context, anime: Anime, episode: Episode): PendingIntent {
|
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)
|
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.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.google.android.exoplayer2.util.MimeTypes
|
|
||||||
import eu.kanade.tachiyomi.animesource.AnimeSource
|
import eu.kanade.tachiyomi.animesource.AnimeSource
|
||||||
import eu.kanade.tachiyomi.animesource.model.Video
|
import eu.kanade.tachiyomi.animesource.model.Video
|
||||||
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
|
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 {
|
private fun getMime(uri: Uri): String {
|
||||||
return when (uri.path?.substringAfterLast(".")) {
|
return when (uri.path?.substringAfterLast(".")) {
|
||||||
"mp4" -> MimeTypes.VIDEO_MP4
|
"mp4" -> "video/mp4"
|
||||||
"mkv" -> MimeTypes.VIDEO_MATROSKA
|
"mkv" -> "video/x-matroska"
|
||||||
"m3u8" -> MimeTypes.APPLICATION_M3U8
|
"m3u8" -> "application/x-mpegURL"
|
||||||
else -> "video/any"
|
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.MPVLib
|
||||||
import `is`.xyz.mpv.PickerDialog
|
import `is`.xyz.mpv.PickerDialog
|
||||||
import `is`.xyz.mpv.SpeedPickerDialog
|
import `is`.xyz.mpv.SpeedPickerDialog
|
||||||
|
import `is`.xyz.mpv.StateRestoreCallback
|
||||||
import `is`.xyz.mpv.Utils
|
import `is`.xyz.mpv.Utils
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import nucleus.factory.RequiresPresenter
|
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.base.controller.withFadeTransaction
|
||||||
import eu.kanade.tachiyomi.ui.browse.animesource.browse.ProgressItem
|
import eu.kanade.tachiyomi.ui.browse.animesource.browse.ProgressItem
|
||||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
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.system.toast
|
||||||
import eu.kanade.tachiyomi.util.view.onAnimationsFinished
|
import eu.kanade.tachiyomi.util.view.onAnimationsFinished
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
|
@ -159,7 +159,7 @@ class AnimeHistoryController :
|
||||||
|
|
||||||
val nextEpisode = presenter.getNextEpisode(chapter, anime)
|
val nextEpisode = presenter.getNextEpisode(chapter, anime)
|
||||||
if (nextEpisode != null) {
|
if (nextEpisode != null) {
|
||||||
val newIntent = PlayerActivity.newIntent(activity, anime, nextEpisode)
|
val newIntent = NewPlayerActivity.newIntent(activity, anime, nextEpisode)
|
||||||
startActivity(newIntent)
|
startActivity(newIntent)
|
||||||
} else {
|
} else {
|
||||||
activity.toast(R.string.no_next_episode)
|
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.RootController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
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.logcat
|
||||||
import eu.kanade.tachiyomi.util.system.notificationManager
|
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
|
@ -193,7 +193,7 @@ class AnimeUpdatesController :
|
||||||
*/
|
*/
|
||||||
private fun openEpisode(item: AnimeUpdatesItem) {
|
private fun openEpisode(item: AnimeUpdatesItem) {
|
||||||
val activity = activity ?: return
|
val activity = activity ?: return
|
||||||
val intent = PlayerActivity.newIntent(activity, item.anime, item.episode)
|
val intent = NewPlayerActivity.newIntent(activity, item.anime, item.episode)
|
||||||
startActivity(intent)
|
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