Merge pull request #2 from Quickdesh/move_n_rename_files

Move and rename files
This commit is contained in:
LuftVerbot 2023-05-31 19:06:11 +02:00 committed by GitHub
commit c18e08e2f3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 417 additions and 176 deletions

View file

@ -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

View file

@ -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) }

View file

@ -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
)
) {
}
/**
* 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
}
} 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<String>()
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)
}
/**
* 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 makeErrorToast(context: Context, e: Exception?) {
launchUI { 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<String>()
@ -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<TrackManager>()
val context = Injekt.get<Application>()
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"

View file

@ -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<ConstraintLayout.LayoutParams> {
rightToLeft = playerControls.binding.toggleAutoplay.id
rightToRight = ConstraintLayout.LayoutParams.UNSET
}
playerControls.binding.titleSecondaryTxt.updateLayoutParams<ConstraintLayout.LayoutParams> {
playerControls.binding.episodeListBtn.updateLayoutParams<ConstraintLayout.LayoutParams> {
rightToLeft = playerControls.binding.toggleAutoplay.id
rightToRight = ConstraintLayout.LayoutParams.UNSET
}
@ -485,24 +484,20 @@ class PlayerActivity :
}
playerControls.binding.toggleAutoplay.updateLayoutParams<ConstraintLayout.LayoutParams> {
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<ConstraintLayout.LayoutParams> {
rightToLeft = ConstraintLayout.LayoutParams.UNSET
rightToRight = ConstraintLayout.LayoutParams.PARENT_ID
}
playerControls.binding.titleSecondaryTxt.updateLayoutParams<ConstraintLayout.LayoutParams> {
playerControls.binding.episodeListBtn.updateLayoutParams<ConstraintLayout.LayoutParams> {
rightToLeft = ConstraintLayout.LayoutParams.UNSET
rightToRight = ConstraintLayout.LayoutParams.PARENT_ID
}
playerControls.binding.playerOverflow.updateLayoutParams<ConstraintLayout.LayoutParams> {
topToTop = ConstraintLayout.LayoutParams.UNSET
topToBottom = playerControls.binding.backArrowBtn.id
topToBottom = playerControls.binding.episodeListBtn.id
}
playerControls.binding.toggleAutoplay.updateLayoutParams<ConstraintLayout.LayoutParams> {
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)

View file

@ -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

View file

@ -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<List<Video>> {
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<List<Video>> {
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<List<Video>> {

View file

@ -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

View file

@ -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<Track>,
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

View file

@ -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

View file

@ -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) {

View file

@ -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

View file

@ -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() {
}
}

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/black"
android:pathData="M10,6L8.59,7.41 13.17,12l-4.58,4.59L10,18l6,-6z" />
</vector>

View file

@ -1,6 +1,6 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="72dp"
android:width="72dp"
android:height="64dp"
android:width="64dp"
android:tint="?attr/colorControlNormal"
android:viewportHeight="24"
android:viewportWidth="24" >

View file

@ -1,6 +1,6 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="72dp"
android:width="72dp"
android:height="64dp"
android:width="64dp"
android:tint="?attr/colorControlNormal"
android:viewportHeight="24"
android:viewportWidth="24" >

View file

@ -1,6 +1,6 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="50dp"
android:height="50dp"
android:width="48dp"
android:height="48dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path

View file

@ -1,6 +1,6 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="50dp"
android:height="50dp"
android:width="40dp"
android:height="40dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path

View file

@ -9,7 +9,7 @@
android:clipChildren="false"
android:clipToPadding="false"
tools:context=".ui.player.PlayerActivity"
tools:ignore="RtlHardcoded,HardcodedText" >
tools:ignore="RtlHardcoded,HardcodedText,ContentDescription" >
<is.xyz.mpv.MPVView
android:id="@+id/player"
@ -20,7 +20,8 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
android:orientation="horizontal">
android:orientation="horizontal"
android:layoutDirection="ltr">
<ImageView
android:id="@+id/rew_tap"
@ -85,7 +86,7 @@
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintLeft_toRightOf="@id/mid_bg" />
<eu.kanade.tachiyomi.ui.player.PlayerControlsView
<eu.kanade.tachiyomi.ui.player.viewer.PlayerControlsView
android:id="@+id/player_controls"
android:layout_width="match_parent"
android:layout_height="match_parent" />
@ -121,14 +122,14 @@
<ImageView
android:id="@+id/playPauseView"
android:layout_width="80dp"
android:layout_height="80dp"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_centerInParent="true"
android:contentDescription="Play/Pause"
android:background="@drawable/ic_play_pause_bg"
android:visibility="gone"
app:tint="?attr/colorOnPrimarySurface"
tools:src="@drawable/ic_play_arrow_72dp"
tools:src="@drawable/ic_play_arrow_64dp"
tools:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
@ -137,12 +138,12 @@
<com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/loading_indicator"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_width="72dp"
android:layout_height="72dp"
android:layout_centerInParent="true"
android:indeterminate="true"
app:indicatorColor="?attr/colorPrimary"
app:indicatorSize="65dp"
app:indicatorSize="64dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
@ -158,7 +159,7 @@
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<eu.kanade.tachiyomi.ui.player.DoubleTapSecondsView
<eu.kanade.tachiyomi.ui.player.viewer.DoubleTapSecondsView
android:id="@+id/seconds_view"
android:layout_width="0dp"
android:layout_height="wrap_content"

View file

@ -68,23 +68,41 @@
android:textColor="?attr/colorOnPrimarySurface"
android:textSize="16sp"
android:textStyle="bold"
android:clickable="true"
android:focusable="true"
app:layout_constraintLeft_toRightOf="@id/backArrowBtn"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintRight_toLeftOf="@id/episodeListBtn"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/titleSecondaryTxt"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="10dp"
android:layout_marginLeft="10dp"
android:alpha="0.5"
android:text=""
android:textColor="?attr/colorOnPrimarySurface"
android:textSize="12sp"
android:clickable="true"
android:focusable="true"
app:layout_constraintLeft_toRightOf="@id/backArrowBtn"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintRight_toLeftOf="@id/episodeListBtn"
app:layout_constraintTop_toBottomOf="@id/titleMainTxt" />
<ImageButton
android:id="@+id/episodeListBtn"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginRight="10dp"
android:layout_marginTop="10dp"
android:background="@android:color/transparent"
android:contentDescription="Episode list"
android:src="@drawable/ic_navigate_next_24dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toRightOf="@id/titleMainTxt"
app:layout_constraintRight_toRightOf="parent"
app:tint="?attr/colorOnPrimarySurface" />
<!-- Top Controls (Left)-->
<ImageButton
@ -99,7 +117,7 @@
android:src="@drawable/ic_overflow_24dp"
app:layout_constraintLeft_toRightOf="@id/qualityBtn"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/backArrowBtn"
app:layout_constraintTop_toBottomOf="@id/episodeListBtn"
app:tint="?attr/colorOnPrimarySurface" />
<ImageButton
@ -199,9 +217,9 @@
<ImageButton
android:id="@+id/prevBtn"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_marginRight="255dp"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginRight="256dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/action_previous_episode"
android:padding="@dimen/screen_edge_margin"
@ -209,13 +227,13 @@
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_skip_previous_50dp"
app:srcCompat="@drawable/ic_skip_previous_40dp"
app:tint="?attr/colorOnPrimarySurface" />
<ImageButton
android:id="@+id/play_btn"
android:layout_width="72dp"
android:layout_height="72dp"
android:layout_width="64dp"
android:layout_height="64dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="Play/Pause"
android:onClick="playPause"
@ -226,14 +244,14 @@
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:tint="?attr/colorOnPrimarySurface"
tools:src="@drawable/ic_play_arrow_72dp"
tools:src="@drawable/ic_play_arrow_64dp"
tools:visibility="visible" />
<ImageButton
android:id="@+id/nextBtn"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_marginLeft="255dp"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginLeft="256dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/action_next_episode"
android:padding="@dimen/screen_edge_margin"
@ -241,7 +259,7 @@
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_skip_next_50dp"
app:srcCompat="@drawable/ic_skip_next_40dp"
app:tint="?attr/colorOnPrimarySurface" />
<TextView
@ -375,7 +393,7 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent" />
<eu.kanade.tachiyomi.ui.player.CustomSeekBar
<eu.kanade.tachiyomi.ui.player.viewer.CustomSeekBar
android:id="@+id/playbackSeekbar"
android:layout_width="0dp"
android:layout_height="48dp"

View file

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/optionsScrollView"
android:layout_width="match_parent"
android:layout_height="wrap_content">
@ -13,16 +12,40 @@
android:orientation="vertical"
android:layout_marginBottom="32dp" >
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginVertical="16dp">
<TextView
android:id="@+id/track_selection_header"
android:layout_width="match_parent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:layout_marginVertical="16dp"
android:paddingHorizontal="16dp"
tools:text="@string/quality_dialog_header"
android:textAppearance="@style/TextAppearance.Tachiyomi.SectionHeader" />
android:textAppearance="@style/TextAppearance.Tachiyomi.SectionHeader"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toStartOf="@id/track_settings_button"
app:layout_constraintStart_toStartOf="parent"/>
<ImageButton
android:id="@+id/track_settings_button"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginTop="14dp"
android:layout_marginBottom="16dp"
android:layout_marginEnd="16dp"
android:visibility="gone"
tools:visibility="visible"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_settings_24dp"
android:contentDescription="@string/settings"
app:tint="?attr/colorOnSurface"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>