diff --git a/app/src/main/java/eu/kanade/presentation/entries/anime/EpisodeOptionsDialogScreen.kt b/app/src/main/java/eu/kanade/presentation/entries/anime/EpisodeOptionsDialogScreen.kt index 53f9ad3e2..1cf2b900d 100644 --- a/app/src/main/java/eu/kanade/presentation/entries/anime/EpisodeOptionsDialogScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/entries/anime/EpisodeOptionsDialogScreen.kt @@ -45,7 +45,7 @@ import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.animesource.model.Video import eu.kanade.tachiyomi.data.download.anime.AnimeDownloadManager import eu.kanade.tachiyomi.source.anime.AnimeSourceManager -import eu.kanade.tachiyomi.ui.player.EpisodeLoader +import eu.kanade.tachiyomi.ui.player.loader.EpisodeLoader import eu.kanade.tachiyomi.util.system.toast import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.flow.first diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt index fa72f4b0d..3c01503d2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt @@ -98,6 +98,7 @@ import eu.kanade.tachiyomi.util.system.isNavigationBarNeedsScrim import eu.kanade.tachiyomi.util.system.openInBrowser import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.view.setComposeContent +import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.cancel import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.delay @@ -490,7 +491,7 @@ class MainActivity : BaseActivity() { // Get the search query provided in extras, and if not null, perform a global search with it. val query = intent.getStringExtra(SearchManager.QUERY) ?: intent.getStringExtra(Intent.EXTRA_TEXT) - if (query != null && query.isNotEmpty()) { + if (!query.isNullOrEmpty()) { navigator.popUntilRoot() navigator.push(GlobalMangaSearchScreen(query)) } @@ -498,7 +499,7 @@ class MainActivity : BaseActivity() { } INTENT_SEARCH -> { val query = intent.getStringExtra(INTENT_SEARCH_QUERY) - if (query != null && query.isNotEmpty()) { + if (!query.isNullOrEmpty()) { val filter = intent.getStringExtra(INTENT_SEARCH_FILTER) ?: "" navigator.popUntilRoot() navigator.push(GlobalMangaSearchScreen(query, filter)) @@ -507,7 +508,7 @@ class MainActivity : BaseActivity() { } INTENT_ANIMESEARCH -> { val query = intent.getStringExtra(INTENT_SEARCH_QUERY) - if (query != null && query.isNotEmpty()) { + if (!query.isNullOrEmpty()) { val filter = intent.getStringExtra(INTENT_SEARCH_FILTER) ?: "" navigator.popUntilRoot() navigator.push(GlobalAnimeSearchScreen(query, filter)) @@ -548,6 +549,7 @@ class MainActivity : BaseActivity() { registerSecureActivity(this) } + @OptIn(DelicateCoroutinesApi::class) @Deprecated("Deprecated in Java") override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { launchIO { externalIntents.onActivityResult(requestCode, resultCode, data) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/ExternalIntents.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/ExternalIntents.kt index 6f6b5c722..fe7af7339 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/ExternalIntents.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/ExternalIntents.kt @@ -27,12 +27,12 @@ import eu.kanade.tachiyomi.animesource.AnimeSource import eu.kanade.tachiyomi.animesource.model.Video import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource import eu.kanade.tachiyomi.core.Constants.REQUEST_EXTERNAL -import eu.kanade.tachiyomi.data.database.models.anime.toDomainEpisode import eu.kanade.tachiyomi.data.download.anime.AnimeDownloadManager import eu.kanade.tachiyomi.data.track.AnimeTrackService import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.source.anime.AnimeSourceManager import eu.kanade.tachiyomi.source.anime.LocalAnimeSource +import eu.kanade.tachiyomi.ui.player.loader.EpisodeLoader import eu.kanade.tachiyomi.ui.player.settings.PlayerPreferences import eu.kanade.tachiyomi.util.system.isOnline import eu.kanade.tachiyomi.util.system.toast @@ -40,8 +40,8 @@ import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.flow.first import logcat.LogPriority -import tachiyomi.core.util.lang.launchIO -import tachiyomi.core.util.lang.launchUI +import tachiyomi.core.util.lang.withIOContext +import tachiyomi.core.util.lang.withUIContext import tachiyomi.core.util.system.logcat import tachiyomi.domain.entries.anime.model.Anime import tachiyomi.domain.history.anime.interactor.UpsertAnimeHistory @@ -57,32 +57,69 @@ import eu.kanade.tachiyomi.data.database.models.anime.Episode as DbEpisode class ExternalIntents { + /** + * The common variables + * Used to dictate what video is sent an external player. + */ lateinit var anime: Anime - lateinit var episode: Episode lateinit var source: AnimeSource + lateinit var episode: Episode + + /** + * Returns the [Intent] to be sent to an external player. + * + * @param context the application context. + * @param animeId the id of the anime. + * @param episodeId the id of the episode. + */ suspend fun getExternalIntent(context: Context, animeId: Long?, episodeId: Long?): Intent? { anime = getAnime.await(animeId!!) ?: return null source = sourceManager.get(anime.source) ?: return null episode = getEpisodeByAnimeId.await(anime.id).find { it.id == episodeId } ?: return null + val video = EpisodeLoader.getLinks(episode, anime, source).asFlow().first()[0] - val videoUrl = if (video.videoUrl == null) { - makeErrorToast(context, Exception("video URL is null.")) + val videoUrl = getVideoUrl(context, video) ?: return null + + val pkgName = playerPreferences.externalPlayerPreference().get() + + return if (pkgName.isEmpty()) { + Intent(Intent.ACTION_VIEW).apply { + setDataAndTypeAndNormalize(videoUrl, getMime(videoUrl)) + addExtrasAndFlags(false, this) + addVideoHeaders(false, video, this) + } + } else { + standardIntentForPackage(pkgName, context, videoUrl, video) + } + } + + /** + * Returns the [Uri] of the given video. + * + * @param context the application context. + * @param video the video being sent to the external player. + */ + private suspend fun getVideoUrl(context: Context, video: Video): Uri? { + if (video.videoUrl == null) { + makeErrorToast(context, Exception("Video URL is null.")) return null } else { val uri = video.videoUrl!!.toUri() + val isOnDevice = if (anime.source == LocalAnimeSource.ID) { true } else { downloadManager.isEpisodeDownloaded( - episode.name, - episode.scanlator, - anime.title, - anime.source, + episodeName = episode.name, + episodeScanlator = episode.scanlator, + animeTitle = anime.title, + sourceId = anime.source, skipCache = true, ) } - if (isOnDevice && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && uri.scheme != "content") { + + return if (isOnDevice && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && uri.scheme != "content") { FileProvider.getUriForFile( context, context.applicationContext.packageName + ".provider", @@ -92,56 +129,41 @@ class ExternalIntents { uri } } - val pkgName = playerPreferences.externalPlayerPreference().get() - val anime = anime - val lastSecondSeen = if (episode.seen) { - if ((!playerPreferences.preserveWatchingPosition().get()) || - ( - playerPreferences.preserveWatchingPosition().get() && - episode.lastSecondSeen == episode.totalSeconds - ) - ) { - 1L - } else { - episode.lastSecondSeen - } + } + + /** + * Returns the second to start the external player at. + */ + private fun getLastSecondSeen(): Long { + val preserveWatchPos = playerPreferences.preserveWatchingPosition().get() + val isEpisodeWatched = episode.lastSecondSeen == episode.totalSeconds + + return if (episode.seen && (!preserveWatchPos || (preserveWatchPos && isEpisodeWatched))) { + 1L } else { episode.lastSecondSeen } - - return if (pkgName.isEmpty()) { - Intent(Intent.ACTION_VIEW).apply { - setDataAndTypeAndNormalize(videoUrl, getMime(videoUrl)) - putExtra("title", anime.title + " - " + episode.name) - putExtra("position", lastSecondSeen.toInt()) - putExtra("return_result", true) - addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - val headers = video.headers ?: (source as? AnimeHttpSource)?.headers - if (headers != null) { - var headersArray = arrayOf() - for (header in headers) { - headersArray += arrayOf(header.first, header.second) - } - val headersString = headersArray.drop(2).joinToString(": ") - putExtra("headers", headersArray) - putExtra("http-header-fields", headersString) - } - } - } else { - standardIntentForPackage(pkgName, context, videoUrl, episode, video) - } } - private fun makeErrorToast(context: Context, e: Exception?) { - launchUI { context.toast(e?.message ?: "Cannot open episode") } + /** + * Display an error toast in this [context]. + * + * @param context the application context. + * @param e the exception error to be displayed. + */ + private suspend fun makeErrorToast(context: Context, e: Exception?) { + withUIContext { context.toast(e?.message ?: "Cannot open episode") } } - private fun standardIntentForPackage(pkgName: String, context: Context, uri: Uri, episode: Episode, video: Video): Intent { - val lastSecondSeen = if (episode.seen && !playerPreferences.preserveWatchingPosition().get()) { - 0L - } else { - episode.lastSecondSeen - } + /** + * Returns the [Intent] with added data to send to the given external player. + * + * @param pkgName the name of the package to send the [Intent] to. + * @param context the application context. + * @param uri the path data of the video. + * @param video the video being sent to the external player. + */ + private fun standardIntentForPackage(pkgName: String, context: Context, uri: Uri, video: Video): Intent { return Intent(Intent.ACTION_VIEW).apply { if (isPackageInstalled(pkgName, context.packageManager)) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && pkgName.contains("vlc")) { @@ -151,13 +173,13 @@ class ExternalIntents { } } setDataAndType(uri, "video/*") - flags = Intent.FLAG_GRANT_READ_URI_PERMISSION - putExtra("title", episode.name) - putExtra("position", lastSecondSeen.toInt()) - putExtra("return_result", true) - putExtra("secure_uri", true) + addExtrasAndFlags(true, this) + addVideoHeaders(true, video, this) - /*val externalSubs = source.getExternalSubtitleStreams() + // Add support for Subtitles to external players + + /* + val externalSubs = source.getExternalSubtitleStreams() val enabledSubUrl = when { source.selectedSubtitleStream != null -> { externalSubs.find { stream -> stream.index == source.selectedSubtitleStream?.index }?.let { sub -> @@ -174,9 +196,35 @@ class ExternalIntents { putExtra("subs.enable", enabledSubUrl?.let { url -> arrayOf(Uri.parse(url)) } ?: emptyArray()) // VLC - if (enabledSubUrl != null) putExtra("subtitles_location", enabledSubUrl)*/ + if (enabledSubUrl != null) putExtra("subtitles_location", enabledSubUrl) + */ + } + } - // headers + /** + * Adds extras and flags to the given [Intent]. + * + * @param isSupportedPlayer is it a supported external player. + * @param intent the [Intent] that the extras and flags are added to. + */ + private fun addExtrasAndFlags(isSupportedPlayer: Boolean, intent: Intent): Intent { + return intent.apply { + putExtra("title", anime.title + " - " + episode.name) + putExtra("position", getLastSecondSeen().toInt()) + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + if (isSupportedPlayer) putExtra("secure_uri", true) + } + } + + /** + * Adds the headers of the video to the given [Intent]. + * + * @param isSupportedPlayer is it a supported external player. + * @param video the [Video] to get the headers from. + * @param intent the [Intent] that the headers are added to. + */ + private fun addVideoHeaders(isSupportedPlayer: Boolean, video: Video, intent: Intent): Intent { + return intent.apply { val headers = video.headers ?: (source as? AnimeHttpSource)?.headers if (headers != null) { var headersArray = arrayOf() @@ -184,10 +232,17 @@ class ExternalIntents { headersArray += arrayOf(header.first, header.second) } putExtra("headers", headersArray) + val headersString = headersArray.drop(2).joinToString(": ") + if (!isSupportedPlayer) putExtra("http-header-fields", headersString) } } } + /** + * Returns the MIME type based on the video's extension. + * + * @param uri the path data of the video. + */ private fun getMime(uri: Uri): String { return when (uri.path?.substringAfterLast(".")) { "mp4" -> "video/mp4" @@ -198,7 +253,10 @@ class ExternalIntents { } /** - * To ensure that the correct activity is called. + * Returns the specific activity to be called. + * If the package is a part of the supported external players + * + * @param packageName the name of the package. */ private fun getComponent(packageName: String): ComponentName? { return when (packageName) { @@ -210,6 +268,12 @@ class ExternalIntents { } } + /** + * Returns true if the given package is installed on the device. + * + * @param packageName the name of the package to be found. + * @param packageManager the instance of the package manager provided by the device. + */ private fun isPackageInstalled(packageName: String, packageManager: PackageManager): Boolean { return try { packageManager.getPackageInfo(packageName, 0) @@ -219,6 +283,13 @@ class ExternalIntents { } } + /** + * Saves the episode's data based on whats returned by the external player. + * + * @param requestCode the code sent to ensure that the returned [data] is from an external player. + * @param resultCode the code sent to ensure that the returned [data] is valid. + * @param data the [Intent] that contains the episode's position and duration. + */ @Suppress("DEPRECATION") suspend fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { if (requestCode == REQUEST_EXTERNAL && resultCode == Activity.RESULT_OK) { @@ -227,6 +298,8 @@ class ExternalIntents { val currentPosition: Long val duration: Long val cause = data!!.getStringExtra("end_by") ?: "" + + // Check for position and duration as Long values if (cause.isNotEmpty()) { val positionExtra = data.extras?.get("position") currentPosition = if (positionExtra is Int) { @@ -249,7 +322,9 @@ class ExternalIntents { duration = data.getIntExtra("duration", 0).toLong() } } - launchIO { + + // Update the episode's progress and history + withIOContext { if (cause == "playback_completion" || (currentPosition == duration && duration == 0L)) { saveEpisodeProgress(currentExtEpisode, anime, currentExtEpisode.totalSeconds, currentExtEpisode.totalSeconds) } else { @@ -260,6 +335,7 @@ class ExternalIntents { } } + // List of all the required Injectable classes private val upsertHistory: UpsertAnimeHistory = Injekt.get() private val updateEpisode: UpdateEpisode = Injekt.get() private val getAnime: GetAnime = Injekt.get() @@ -274,41 +350,63 @@ class ExternalIntents { private val trackPreferences: TrackPreferences = Injekt.get() private val basePreferences: BasePreferences by injectLazy() - private suspend fun saveEpisodeHistory(episode: Episode) { + /** + * Saves this episode's last seen history if incognito mode isn't on. + * + * @param currentEpisode the episode to update. + */ + private suspend fun saveEpisodeHistory(currentEpisode: Episode) { if (basePreferences.incognitoMode().get()) return upsertHistory.await( - AnimeHistoryUpdate(episode.id, Date()), + AnimeHistoryUpdate(currentEpisode.id, Date()), ) } - private suspend fun saveEpisodeProgress(domainEpisode: Episode?, anime: Anime, seconds: Long, totalSeconds: Long) { + /** + * Saves this episode's progress (last seen second and whether it's seen). + * Only if incognito mode isn't on + * + * @param currentEpisode the episode to update. + * @param anime the anime of the episode. + * @param seconds the position of the episode. + * @param totalSeconds the duration of the episode. + */ + private suspend fun saveEpisodeProgress(currentEpisode: Episode?, anime: Anime, seconds: Long, totalSeconds: Long) { if (basePreferences.incognitoMode().get()) return - val episode = domainEpisode?.toDbEpisode() ?: return + val currentDbEpisode = currentEpisode?.toDbEpisode() ?: return + if (totalSeconds > 0L) { - episode.last_second_seen = seconds - episode.total_seconds = totalSeconds + currentDbEpisode.last_second_seen = seconds + currentDbEpisode.total_seconds = totalSeconds val progress = playerPreferences.progressPreference().get() - if (!episode.seen) episode.seen = episode.last_second_seen >= episode.total_seconds * progress + if (!currentDbEpisode.seen) currentDbEpisode.seen = currentDbEpisode.last_second_seen >= currentDbEpisode.total_seconds * progress updateEpisode.await( EpisodeUpdate( - id = episode.id!!, - seen = episode.seen, - bookmark = episode.bookmark, - lastSecondSeen = episode.last_second_seen, - totalSeconds = episode.total_seconds, + id = currentDbEpisode.id!!, + seen = currentDbEpisode.seen, + bookmark = currentDbEpisode.bookmark, + lastSecondSeen = currentDbEpisode.last_second_seen, + totalSeconds = currentDbEpisode.total_seconds, ), ) - if (trackPreferences.autoUpdateTrack().get() && episode.seen) { - updateTrackEpisodeSeen(episode, anime) + if (trackPreferences.autoUpdateTrack().get() && currentDbEpisode.seen) { + updateTrackEpisodeSeen(currentDbEpisode, anime) } - if (episode.seen) { - deleteEpisodeIfNeeded(episode.toDomainEpisode()!!, anime) + if (currentDbEpisode.seen) { + deleteEpisodeIfNeeded(currentEpisode, anime) } } } - private suspend fun deleteEpisodeIfNeeded(episode: Episode, anime: Anime) { - // Determine which chapter should be deleted and enqueue + /** + * Determines if deleting option is enabled and nth to last episode actually exists. + * If both conditions are satisfied enqueues episode for delete + * + * @param currentEpisode the episode, which is going to be marked as seen. + * @param anime the anime of the episode. + */ + private suspend fun deleteEpisodeIfNeeded(currentEpisode: Episode, anime: Anime) { + // Determine which episode should be deleted and enqueue val sortFunction: (Episode, Episode) -> Int = when (anime.sorting) { Anime.EPISODE_SORTING_SOURCE -> { c1, c2 -> c2.sourceOrder.compareTo(c1.sourceOrder) } Anime.EPISODE_SORTING_NUMBER -> { c1, c2 -> c1.episodeNumber.compareTo(c2.episodeNumber) } @@ -319,25 +417,32 @@ class ExternalIntents { val episodes = getEpisodeByAnimeId.await(anime.id) .sortedWith { e1, e2 -> sortFunction(e1, e2) } - val currentEpisodePosition = episodes.indexOf(episode) + val currentEpisodePosition = episodes.indexOf(currentEpisode) val removeAfterSeenSlots = downloadPreferences.removeAfterReadSlots().get() val episodeToDelete = episodes.getOrNull(currentEpisodePosition - removeAfterSeenSlots) - // Check if deleting option is enabled and chapter exists + // Check if deleting option is enabled and episode exists if (removeAfterSeenSlots != -1 && episodeToDelete != null) { enqueueDeleteSeenEpisodes(episodeToDelete, anime) } } - private fun updateTrackEpisodeSeen(episode: DbEpisode, anime: Anime) { + /** + * Starts the service that updates the last episode seen in sync services. + * This operation will run in a background thread and errors are ignored. + * + * @param currentDbEpisode the episode to be updated. + * @param anime the anime of the episode. + */ + private suspend fun updateTrackEpisodeSeen(currentDbEpisode: DbEpisode, anime: Anime) { if (!trackPreferences.autoUpdateTrack().get()) return - val episodeSeen = episode.episode_number.toDouble() + val episodeSeen = currentDbEpisode.episode_number.toDouble() val trackManager = Injekt.get() val context = Injekt.get() - launchIO { + withIOContext { getTracks.await(anime.id) .mapNotNull { track -> val service = trackManager.getService(track.syncId) @@ -369,22 +474,34 @@ class ExternalIntents { } } - private fun enqueueDeleteSeenEpisodes(episode: Episode, anime: Anime) { - if (!episode.seen) return - - launchIO { - downloadManager.enqueueEpisodesToDelete(listOf(episode), anime) - } + /** + * Enqueues an [Episode] to be deleted later. + * + * @param currentEpisode the episode being deleted. + * @param anime the anime of the episode. + */ + private suspend fun enqueueDeleteSeenEpisodes(currentEpisode: Episode, anime: Anime) { + if (currentEpisode.seen) withIOContext { downloadManager.enqueueEpisodesToDelete(listOf(currentEpisode), anime) } } companion object { + private val externalIntents: ExternalIntents by injectLazy() + + /** + * Used to direct the [Intent] of a chosen episode to an external player. + * + * @param context the application context. + * @param animeId the id of the anime. + * @param episodeId the id of the episode. + */ suspend fun newIntent(context: Context, animeId: Long?, episodeId: Long?): Intent? { return externalIntents.getExternalIntent(context, animeId, episodeId) } } } +// List of supported external players and their packages private const val MPV_PLAYER = "is.xyz.mpv" private const val MX_PLAYER_FREE = "com.mxtech.videoplayer.ad" private const val MX_PLAYER_PRO = "com.mxtech.videoplayer.pro" diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerActivity.kt index 03c7fe1ea..bc2f38def 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerActivity.kt @@ -55,7 +55,10 @@ import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.databinding.PlayerActivityBinding import eu.kanade.tachiyomi.network.NetworkPreferences import eu.kanade.tachiyomi.ui.base.activity.BaseActivity +import eu.kanade.tachiyomi.ui.player.settings.PlayerOptionsSheet import eu.kanade.tachiyomi.ui.player.settings.PlayerPreferences +import eu.kanade.tachiyomi.ui.player.settings.PlayerTracksSheet +import eu.kanade.tachiyomi.ui.player.viewer.Gestures import eu.kanade.tachiyomi.util.AniSkipApi import eu.kanade.tachiyomi.util.SkipType import eu.kanade.tachiyomi.util.Stamp @@ -471,11 +474,7 @@ class PlayerActivity : width = height.also { height = width } } - playerControls.binding.titleMainTxt.updateLayoutParams { - rightToLeft = playerControls.binding.toggleAutoplay.id - rightToRight = ConstraintLayout.LayoutParams.UNSET - } - playerControls.binding.titleSecondaryTxt.updateLayoutParams { + playerControls.binding.episodeListBtn.updateLayoutParams { rightToLeft = playerControls.binding.toggleAutoplay.id rightToRight = ConstraintLayout.LayoutParams.UNSET } @@ -485,24 +484,20 @@ class PlayerActivity : } playerControls.binding.toggleAutoplay.updateLayoutParams { leftToLeft = ConstraintLayout.LayoutParams.UNSET - leftToRight = playerControls.binding.titleMainTxt.id + leftToRight = playerControls.binding.episodeListBtn.id } } else { if (width >= height) { width = height.also { height = width } } - playerControls.binding.titleMainTxt.updateLayoutParams { - rightToLeft = ConstraintLayout.LayoutParams.UNSET - rightToRight = ConstraintLayout.LayoutParams.PARENT_ID - } - playerControls.binding.titleSecondaryTxt.updateLayoutParams { + playerControls.binding.episodeListBtn.updateLayoutParams { rightToLeft = ConstraintLayout.LayoutParams.UNSET rightToRight = ConstraintLayout.LayoutParams.PARENT_ID } playerControls.binding.playerOverflow.updateLayoutParams { topToTop = ConstraintLayout.LayoutParams.UNSET - topToBottom = playerControls.binding.backArrowBtn.id + topToBottom = playerControls.binding.episodeListBtn.id } playerControls.binding.toggleAutoplay.updateLayoutParams { leftToLeft = ConstraintLayout.LayoutParams.PARENT_ID @@ -519,7 +514,6 @@ class PlayerActivity : * Sets up the gestures to be used */ - @Suppress("DEPRECATION") @SuppressLint("ClickableViewAccessibility") private fun setupGestures() { val gestures = Gestures(this, width.toFloat(), height.toFloat()) @@ -740,8 +734,8 @@ class PlayerActivity : if (!playerControls.binding.controlsView.isVisible) { when { - player.paused!! -> { binding.playPauseView.setImageResource(R.drawable.ic_pause_72dp) } - !player.paused!! -> { binding.playPauseView.setImageResource(R.drawable.ic_play_arrow_72dp) } + player.paused!! -> { binding.playPauseView.setImageResource(R.drawable.ic_pause_64dp) } + !player.paused!! -> { binding.playPauseView.setImageResource(R.drawable.ic_play_arrow_64dp) } } AnimationUtils.loadAnimation(this, R.anim.player_fade_in).also { fadeAnimation -> @@ -948,6 +942,7 @@ class PlayerActivity : ::setAudio, audioTracks, selectedAudio, + null, ).show() } @@ -962,6 +957,7 @@ class PlayerActivity : ::setSub, subTracks, selectedSub, + null, ).show() } @@ -980,6 +976,7 @@ class PlayerActivity : ::changeQuality, videoTracks, currentQuality, + null, ).show() } @@ -1206,7 +1203,7 @@ class PlayerActivity : private fun updatePlaybackStatus(paused: Boolean) { if (isPipSupportedAndEnabled && isInPipMode) updatePictureInPictureActions(!paused) - val r = if (paused) R.drawable.ic_play_arrow_72dp else R.drawable.ic_pause_72dp + val r = if (paused) R.drawable.ic_play_arrow_64dp else R.drawable.ic_pause_64dp playerControls.binding.playBtn.setImageResource(r) if (paused) { @@ -1314,7 +1311,6 @@ class PlayerActivity : } } - @Suppress("DEPRECATION") fun startPiP() { if (isInPipMode) return if (isPipSupportedAndEnabled) { @@ -1628,7 +1624,6 @@ class PlayerActivity : private val nextEpisodeRunnable = Runnable { switchEpisode(previous = false, autoPlay = true) } - @Suppress("DEPRECATION") private fun eventPropertyUi(property: String, value: Boolean) { when (property) { "seeking" -> isSeeking(value) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerViewModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerViewModel.kt index 6b088c69b..585987bcf 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerViewModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerViewModel.kt @@ -36,6 +36,7 @@ import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.anilist.Anilist import eu.kanade.tachiyomi.data.track.myanimelist.MyAnimeList import eu.kanade.tachiyomi.source.anime.AnimeSourceManager +import eu.kanade.tachiyomi.ui.player.loader.EpisodeLoader import eu.kanade.tachiyomi.ui.player.settings.PlayerPreferences import eu.kanade.tachiyomi.ui.reader.SaveImageNotifier import eu.kanade.tachiyomi.util.AniSkipApi diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/EpisodeLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/loader/EpisodeLoader.kt similarity index 68% rename from app/src/main/java/eu/kanade/tachiyomi/ui/player/EpisodeLoader.kt rename to app/src/main/java/eu/kanade/tachiyomi/ui/player/loader/EpisodeLoader.kt index c9a1dc299..2f50ecb24 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/EpisodeLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/loader/EpisodeLoader.kt @@ -1,4 +1,4 @@ -package eu.kanade.tachiyomi.ui.player +package eu.kanade.tachiyomi.ui.player.loader import android.net.Uri import eu.kanade.domain.items.episode.model.toSEpisode @@ -16,10 +16,22 @@ import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.lang.Exception +/** + * Loader used to retrieve the video links for a given episode. + */ class EpisodeLoader { - companion object { - var errorMessage = "" + companion object { + + private var errorMessage = "" + + /** + * Returns an observable list of videos of an [episode] based on the type of [source] used. + * + * @param episode the episode being parsed. + * @param anime the anime of the episode. + * @param source the source of the anime. + */ fun getLinks(episode: Episode, anime: Anime, source: AnimeSource): Observable> { val downloadManager: AnimeDownloadManager = Injekt.get() val isDownloaded = downloadManager.isEpisodeDownloaded(episode.name, episode.scanlator, anime.title, anime.source, skipCache = true) @@ -31,11 +43,23 @@ class EpisodeLoader { } } + /** + * Returns true if the given [episode] is downloaded. + * + * @param episode the episode being parsed. + * @param anime the anime of the episode. + */ fun isDownloaded(episode: Episode, anime: Anime): Boolean { val downloadManager: AnimeDownloadManager = Injekt.get() return downloadManager.isEpisodeDownloaded(episode.name, episode.scanlator, anime.title, anime.source, skipCache = true) } + /** + * Returns an observable list of videos when the [episode] is online. + * + * @param episode the episode being parsed. + * @param source the online source of the episode. + */ private fun isHttp(episode: Episode, source: AnimeHttpSource): Observable> { return source.fetchVideoList(episode.toSEpisode()) .flatMapIterable { it } @@ -44,6 +68,14 @@ class EpisodeLoader { }.toList() } + /** + * Returns an observable list of videos when the [episode] is downloaded. + * + * @param episode the episode being parsed. + * @param anime the anime of the episode. + * @param source the source of the anime. + * @param downloadManager the AnimeDownloadManager instance to use. + */ private fun isDownloaded( episode: Episode, anime: Anime, @@ -61,6 +93,11 @@ class EpisodeLoader { } } + /** + * Returns an observable list of videos when the [episode] is from local source. + * + * @param episode the episode being parsed. + */ private fun isLocal( episode: Episode, ): Observable> { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerOptionsSheet.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/PlayerOptionsSheet.kt similarity index 94% rename from app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerOptionsSheet.kt rename to app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/PlayerOptionsSheet.kt index eb7010964..1a0ea7b44 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerOptionsSheet.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/PlayerOptionsSheet.kt @@ -1,4 +1,4 @@ -package eu.kanade.tachiyomi.ui.player +package eu.kanade.tachiyomi.ui.player.settings import android.view.LayoutInflater import android.view.View @@ -7,10 +7,13 @@ import androidx.core.view.isVisible import com.google.android.material.dialog.MaterialAlertDialogBuilder import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.databinding.PlayerOptionsSheetBinding +import eu.kanade.tachiyomi.ui.player.PlayerActivity import eu.kanade.tachiyomi.widget.sheet.PlayerBottomSheetDialog /** * Sheet to show when overflow button in player is clicked. + * + * @param activity the instance of the PlayerActivity in use. */ class PlayerOptionsSheet( private val activity: PlayerActivity, @@ -27,15 +30,19 @@ class PlayerOptionsSheet( binding.setAsCover.setOnClickListener { setAsCover(); this.dismiss() } binding.share.setOnClickListener { share(); this.dismiss() } binding.save.setOnClickListener { save(); this.dismiss() } + binding.toggleSubs.isChecked = activity.screenshotSubs binding.toggleSubs.setOnCheckedChangeListener { _, newValue -> activity.screenshotSubs = newValue } + binding.toggleVolumeBrightnessGestures.isChecked = activity.gestureVolumeBrightness binding.toggleVolumeBrightnessGestures.setOnCheckedChangeListener { _, newValue -> activity.gestureVolumeBrightness = newValue } binding.toggleHorizontalSeekGesture.isChecked = activity.gestureHorizontalSeek binding.toggleHorizontalSeekGesture.setOnCheckedChangeListener { _, newValue -> activity.gestureHorizontalSeek = newValue } + binding.toggleStats.isChecked = activity.stats - binding.statsPage.isVisible = activity.stats binding.toggleStats.setOnCheckedChangeListener(toggleStats) + + binding.statsPage.isVisible = activity.stats binding.statsPage.setSelection(activity.statsPage) binding.statsPage.onItemSelectedListener = setStatsPage diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerTracksSheet.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/PlayerTracksSheet.kt similarity index 75% rename from app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerTracksSheet.kt rename to app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/PlayerTracksSheet.kt index 762735977..98cbdcff5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerTracksSheet.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/PlayerTracksSheet.kt @@ -1,17 +1,26 @@ -package eu.kanade.tachiyomi.ui.player +package eu.kanade.tachiyomi.ui.player.settings import android.view.LayoutInflater import android.view.View import android.widget.TextView import androidx.core.view.children +import androidx.core.view.isVisible import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.animesource.model.Track import eu.kanade.tachiyomi.databinding.PlayerTracksItemBinding import eu.kanade.tachiyomi.databinding.PlayerTracksSheetBinding +import eu.kanade.tachiyomi.ui.player.PlayerActivity import eu.kanade.tachiyomi.widget.sheet.PlayerBottomSheetDialog /** * Sheet to show when track selection buttons in player are clicked. + * + * @param activity the instance of the PlayerActivity in use. + * @param textRes the header text of the sheet + * @param changeTrackMethod the method to run on changing tracks + * @param tracks the given array of tracks + * @param preselectedTrack the index of the current selected track + * @param trackSettings the method to run on clicking the settings button, null if no button */ class PlayerTracksSheet( private val activity: PlayerActivity, @@ -19,6 +28,7 @@ class PlayerTracksSheet( private val changeTrackMethod: (Int) -> Unit, private val tracks: Array, private val preselectedTrack: Int, + private val trackSettings: (() -> Unit)?, ) : PlayerBottomSheetDialog(activity) { private lateinit var binding: PlayerTracksSheetBinding @@ -29,6 +39,11 @@ class PlayerTracksSheet( activity.player.paused = true binding = PlayerTracksSheetBinding.inflate(activity.layoutInflater, null, false) + if (trackSettings != null) { + binding.trackSettingsButton.isVisible = true + binding.trackSettingsButton.setOnClickListener { trackSettings.invoke() } + } + binding.trackSelectionHeader.setText(textRes) tracks.forEachIndexed { i, track -> val trackView = PlayerTracksItemBinding.inflate(activity.layoutInflater).root diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/CustomSeekBar.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/viewer/CustomSeekBar.kt similarity index 98% rename from app/src/main/java/eu/kanade/tachiyomi/ui/player/CustomSeekBar.kt rename to app/src/main/java/eu/kanade/tachiyomi/ui/player/viewer/CustomSeekBar.kt index 67c55b170..1d4ecec8d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/CustomSeekBar.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/viewer/CustomSeekBar.kt @@ -1,4 +1,4 @@ -package eu.kanade.tachiyomi.ui.player +package eu.kanade.tachiyomi.ui.player.viewer import android.content.Context import android.graphics.Canvas diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/DoubleTapSecondsView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/viewer/DoubleTapSecondsView.kt similarity index 97% rename from app/src/main/java/eu/kanade/tachiyomi/ui/player/DoubleTapSecondsView.kt rename to app/src/main/java/eu/kanade/tachiyomi/ui/player/viewer/DoubleTapSecondsView.kt index 435d17b0e..e4a7c388c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/DoubleTapSecondsView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/viewer/DoubleTapSecondsView.kt @@ -1,4 +1,4 @@ -package eu.kanade.tachiyomi.ui.player +package eu.kanade.tachiyomi.ui.player.viewer import android.animation.Animator import android.animation.ValueAnimator @@ -6,11 +6,13 @@ import android.content.Context import android.util.AttributeSet import android.view.LayoutInflater import android.widget.LinearLayout -import android.widget.TextView import androidx.annotation.DrawableRes import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.databinding.PlayerDoubleTapSeekViewBinding +/** + * View that shows the arrows animation when double tapping to seek + */ class DoubleTapSecondsView(context: Context, attrs: AttributeSet?) : LinearLayout(context, attrs) { var binding: PlayerDoubleTapSeekViewBinding @@ -45,9 +47,6 @@ class DoubleTapSecondsView(context: Context, attrs: AttributeSet?) : LinearLayou field = value } - val textView: TextView - get() = binding.doubleTapSeconds - @DrawableRes var icon: Int = R.drawable.ic_play_seek_triangle set(value) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/Gestures.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/viewer/Gestures.kt similarity index 97% rename from app/src/main/java/eu/kanade/tachiyomi/ui/player/Gestures.kt rename to app/src/main/java/eu/kanade/tachiyomi/ui/player/viewer/Gestures.kt index 848b8e93a..87d209bf4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/Gestures.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/viewer/Gestures.kt @@ -1,9 +1,10 @@ -package eu.kanade.tachiyomi.ui.player +package eu.kanade.tachiyomi.ui.player.viewer import android.annotation.SuppressLint import android.view.GestureDetector import android.view.MotionEvent import android.view.View +import eu.kanade.tachiyomi.ui.player.PlayerActivity import eu.kanade.tachiyomi.ui.player.settings.PlayerPreferences import uy.kohesive.injekt.injectLazy import kotlin.math.abs diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerControlsView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/viewer/PlayerControlsView.kt similarity index 97% rename from app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerControlsView.kt rename to app/src/main/java/eu/kanade/tachiyomi/ui/player/viewer/PlayerControlsView.kt index 13c3e2721..be1c79001 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerControlsView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/viewer/PlayerControlsView.kt @@ -1,4 +1,4 @@ -package eu.kanade.tachiyomi.ui.player +package eu.kanade.tachiyomi.ui.player.viewer import android.annotation.SuppressLint import android.content.Context @@ -17,6 +17,7 @@ import androidx.core.view.isVisible import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.databinding.PlayerControlsBinding import eu.kanade.tachiyomi.databinding.PrefSkipIntroLengthBinding +import eu.kanade.tachiyomi.ui.player.PlayerActivity import eu.kanade.tachiyomi.ui.player.settings.PlayerPreferences import `is`.xyz.mpv.MPVLib import `is`.xyz.mpv.PickerDialog @@ -183,6 +184,18 @@ class PlayerControlsView @JvmOverloads constructor(context: Context, attrs: Attr binding.toggleAutoplay.setOnCheckedChangeListener { _, isChecked -> activity.toggleAutoplay(isChecked) } + + binding.titleMainTxt.setOnClickListener { + episodeListDialog() + } + + binding.titleSecondaryTxt.setOnClickListener { + episodeListDialog() + } + + binding.episodeListBtn.setOnClickListener { + episodeListDialog() + } } private val animationHandler = Handler(Looper.getMainLooper()) @@ -454,4 +467,7 @@ class PlayerControlsView @JvmOverloads constructor(context: Context, attrs: Attr show() } } + + private fun episodeListDialog() { + } } diff --git a/app/src/main/res/drawable/ic_navigate_next_24dp.xml b/app/src/main/res/drawable/ic_navigate_next_24dp.xml new file mode 100644 index 000000000..edc833548 --- /dev/null +++ b/app/src/main/res/drawable/ic_navigate_next_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_pause_72dp.xml b/app/src/main/res/drawable/ic_pause_64dp.xml similarity index 86% rename from app/src/main/res/drawable/ic_pause_72dp.xml rename to app/src/main/res/drawable/ic_pause_64dp.xml index 3ef64624d..70f4ce9e9 100644 --- a/app/src/main/res/drawable/ic_pause_72dp.xml +++ b/app/src/main/res/drawable/ic_pause_64dp.xml @@ -1,6 +1,6 @@ diff --git a/app/src/main/res/drawable/ic_play_arrow_72dp.xml b/app/src/main/res/drawable/ic_play_arrow_64dp.xml similarity index 85% rename from app/src/main/res/drawable/ic_play_arrow_72dp.xml rename to app/src/main/res/drawable/ic_play_arrow_64dp.xml index 4973cce32..dfac37274 100644 --- a/app/src/main/res/drawable/ic_play_arrow_72dp.xml +++ b/app/src/main/res/drawable/ic_play_arrow_64dp.xml @@ -1,6 +1,6 @@ diff --git a/app/src/main/res/drawable/ic_skip_next_50dp.xml b/app/src/main/res/drawable/ic_skip_next_40dp.xml similarity index 83% rename from app/src/main/res/drawable/ic_skip_next_50dp.xml rename to app/src/main/res/drawable/ic_skip_next_40dp.xml index 6d32cafd9..34593f65e 100644 --- a/app/src/main/res/drawable/ic_skip_next_50dp.xml +++ b/app/src/main/res/drawable/ic_skip_next_40dp.xml @@ -1,6 +1,6 @@ + tools:ignore="RtlHardcoded,HardcodedText,ContentDescription" > + android:orientation="horizontal" + android:layoutDirection="ltr"> - @@ -121,14 +122,14 @@ - + + - - + android:layout_height="wrap_content"> - + android:layout_height="match_parent" + android:layout_marginVertical="16dp"> + + + + + +