13 Commits

Author SHA1 Message Date
ad1e3068cd update changelog for beta2 release 2022-06-06 13:33:21 +02:00
de1f19c2b7 catch exceprion in playheads() and postPlayheads() & update agp
* fix a crash, if there is no internet connection while in playback (closes #60)
* agp 7.2.0 -> 7.2.1
2022-06-06 13:14:41 +02:00
12bbc2ef5f add recommendations to home fragment 2022-05-22 11:21:49 +02:00
0186cef79e fix player progress bar skip intro/next ep button overlapping 2022-05-22 10:39:17 +02:00
bc5509cf93 use newSingleThreadContext instead of mutex for token refresh
fixes #57
2022-05-20 15:07:07 +02:00
ef9a0f00d0 hide the playbutton on media items in library- and searchfragment 2022-05-18 20:59:28 +02:00
b85d7ae025 update kotlin, agp, dependecies
* kotlin 1.6.10 -> 1.6.21
* agp 7.1.3 -> 7.2.0
* splashscreen 1.0.0-beta02 -> 1.0.0-rc1
* coroutines 1.6.0 -> 1.6.1
* serialization-json 1.3.2 -> 1.3.3
2022-05-18 20:58:02 +02:00
69c9666d2b fix crash if media is present in metadb, but season/episode are not present 2022-04-22 23:51:51 +02:00
7d6c300f7e implement runtime cache for Crunchyroll.browse() 2022-04-16 17:52:10 +02:00
1ebc1194e6 add categories support to Crunchyroll.browse() 2022-04-16 17:23:53 +02:00
c48328723b increase touch target height for exo_progress 2022-04-15 17:55:01 +02:00
95c8a72c94 add playhead progress indicator to player episodes list 2022-04-15 17:47:17 +02:00
fc04e8e222 remove kotlin-android-extensions, use viewBinding in Player
also replace exo_progress_placeholder with exoplayer2.ui.DefaultTimeBar since the placehoder wont work with viewbinding
2022-04-15 17:25:31 +02:00
22 changed files with 325 additions and 127 deletions

View File

@ -1,7 +1,6 @@
plugins { plugins {
id 'com.android.application' id 'com.android.application'
id 'kotlin-android' id 'kotlin-android'
id 'kotlin-android-extensions'
id 'org.jetbrains.kotlin.plugin.serialization' version "$kotlin_version" id 'org.jetbrains.kotlin.plugin.serialization' version "$kotlin_version"
} }
@ -39,21 +38,25 @@ android {
} }
kotlinOptions { kotlinOptions {
jvmTarget = '1.8' jvmTarget = '1.8'
kotlin.sourceSets.all {
languageSettings.optIn("kotlin.RequiresOptIn")
} }
} }
namespace 'org.mosad.teapod'
}
dependencies { dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"]) implementation fileTree(dir: "libs", include: ["*.jar"])
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1'
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2' implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.3'
implementation 'androidx.core:core-ktx:1.7.0' implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.core:core-splashscreen:1.0.0-beta02' implementation 'androidx.core:core-splashscreen:1.0.0-rc01'
implementation 'androidx.appcompat:appcompat:1.4.1' implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3' implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
implementation 'androidx.navigation:navigation-fragment-ktx:2.4.1' implementation 'androidx.navigation:navigation-fragment-ktx:2.4.2'
implementation 'androidx.navigation:navigation-ui-ktx:2.4.1' implementation 'androidx.navigation:navigation-ui-ktx:2.4.2'
implementation 'androidx.security:security-crypto:1.1.0-alpha03' implementation 'androidx.security:security-crypto:1.1.0-alpha03'
implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1'

View File

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools">
package="org.mosad.teapod">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />

View File

@ -33,8 +33,6 @@ import io.ktor.client.request.forms.*
import io.ktor.client.statement.* import io.ktor.client.statement.*
import io.ktor.http.* import io.ktor.http.*
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.serialization.SerializationException import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
@ -42,7 +40,6 @@ import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put import kotlinx.serialization.json.put
import org.mosad.teapod.preferences.EncryptedPreferences import org.mosad.teapod.preferences.EncryptedPreferences
import org.mosad.teapod.preferences.Preferences import org.mosad.teapod.preferences.Preferences
import org.mosad.teapod.util.concatenate
private val json = Json { ignoreUnknownKeys = true } private val json = Json { ignoreUnknownKeys = true }
@ -60,7 +57,8 @@ object Crunchyroll {
private lateinit var token: Token private lateinit var token: Token
private var tokenValidUntil: Long = 0 private var tokenValidUntil: Long = 0
private val tokeRefreshMutex = Mutex() @OptIn(DelicateCoroutinesApi::class)
private val tokenRefreshContext = newSingleThreadContext("TokenRefreshContext")
private var accountID = "" private var accountID = ""
@ -68,7 +66,7 @@ object Crunchyroll {
private var signature = "" private var signature = ""
private var keyPairID = "" private var keyPairID = ""
private val browsingCache = arrayListOf<Item>() private val browsingCache = hashMapOf<String, BrowseResult>()
/** /**
* Load the pai token, see: * Load the pai token, see:
@ -102,7 +100,6 @@ object Crunchyroll {
var success = false// is false var success = false// is false
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
// TODO handle exceptions
Log.i(TAG, "getting token ...") Log.i(TAG, "getting token ...")
val status = try { val status = try {
@ -117,7 +114,7 @@ object Crunchyroll {
if (status == HttpStatusCode.Unauthorized) { if (status == HttpStatusCode.Unauthorized) {
Log.e(TAG, "Could not complete login: " + Log.e(TAG, "Could not complete login: " +
"${status.value} ${status.description}. " + "${status.value} ${status.description}. " +
"Propably wrong username or password") "Probably wrong username or password")
} }
status status
@ -143,8 +140,7 @@ object Crunchyroll {
params: List<Pair<String, Any?>> = listOf(), params: List<Pair<String, Any?>> = listOf(),
bodyObject: Any = Any() bodyObject: Any = Any()
): T = coroutineScope { ): T = coroutineScope {
// TODO find a better way to make token refresh thread safe, currently it's blocking withContext(tokenRefreshContext) {
tokeRefreshMutex.withLock {
if (System.currentTimeMillis() > tokenValidUntil) refreshToken() if (System.currentTimeMillis() > tokenValidUntil) refreshToken()
} }
@ -255,7 +251,6 @@ object Crunchyroll {
* General element/media functions: browse, search, objects, season_list * General element/media functions: browse, search, objects, season_list
*/ */
// TODO categories
/** /**
* Browse the media available on crunchyroll. * Browse the media available on crunchyroll.
* *
@ -265,13 +260,14 @@ object Crunchyroll {
* @return A **[BrowseResult]** object is returned. * @return A **[BrowseResult]** object is returned.
*/ */
suspend fun browse( suspend fun browse(
categories: List<Categories> = emptyList(),
sortBy: SortBy = SortBy.ALPHABETICAL, sortBy: SortBy = SortBy.ALPHABETICAL,
seasonTag: String = "", seasonTag: String = "",
start: Int = 0, start: Int = 0,
n: Int = 10 n: Int = 10
): BrowseResult { ): BrowseResult {
val browseEndpoint = "/content/v1/browse" val browseEndpoint = "/content/v1/browse"
val noneOptParams = listOf( val parameters = mutableListOf(
"locale" to Preferences.preferredLocale.toLanguageTag(), "locale" to Preferences.preferredLocale.toLanguageTag(),
"sort_by" to sortBy.str, "sort_by" to sortBy.str,
"start" to start, "start" to start,
@ -279,12 +275,20 @@ object Crunchyroll {
) )
// if a season tag is present add it to the parameters // if a season tag is present add it to the parameters
val parameters = if (seasonTag.isNotEmpty()) { if (seasonTag.isNotEmpty()) {
concatenate(noneOptParams, listOf("season_tag" to seasonTag)) parameters.add("season_tag" to seasonTag)
} else {
noneOptParams
} }
// if a season tag is present add it to the parameters
if (categories.isNotEmpty()) {
parameters.add("categories" to categories.joinToString(",") { it.str })
}
// fetch result if not already cached
if (browsingCache.contains(parameters.toString())) {
Log.d(TAG, "browse result cached: $parameters")
} else {
Log.d(TAG, "browse result not cached, fetching: $parameters")
val browseResult: BrowseResult = try { val browseResult: BrowseResult = try {
requestGet(browseEndpoint, parameters) requestGet(browseEndpoint, parameters)
}catch (ex: SerializationException) { }catch (ex: SerializationException) {
@ -292,11 +296,17 @@ object Crunchyroll {
NoneBrowseResult NoneBrowseResult
} }
// add results to cache TODO improve // if the cache has more than 100 entries clear it, so it doesn't become a memory problem
// Note: this value is totally guessed and should be replaced by a properly researched value
if (browsingCache.size > 100) {
browsingCache.clear() browsingCache.clear()
browsingCache.addAll(browseResult.items) }
return browseResult // add results to cache
browsingCache[parameters.toString()] = browseResult
}
return browsingCache[parameters.toString()] ?: NoneBrowseResult
} }
/** /**
@ -412,6 +422,12 @@ object Crunchyroll {
} }
} }
/**
* Get all available seasons for a series.
*
* @param seriesId The series id for which to get the seasons
* @return A **[Seasons]** object with a list of **[Season]**
*/
suspend fun seasons(seriesId: String): Seasons { suspend fun seasons(seriesId: String): Seasons {
val seasonsEndpoint = "/cms/v2/${token.country}/M3/crunchyroll/seasons" val seasonsEndpoint = "/cms/v2/${token.country}/M3/crunchyroll/seasons"
val parameters = listOf( val parameters = listOf(
@ -430,6 +446,12 @@ object Crunchyroll {
} }
} }
/**
* Get all available episodes for a season.
*
* @param seasonId The season id for which to get the episodes
* @return A **[Episodes]** object with a list of **[Episode]**
*/
suspend fun episodes(seasonId: String): Episodes { suspend fun episodes(seasonId: String): Episodes {
val episodesEndpoint = "/cms/v2/${token.country}/M3/crunchyroll/episodes" val episodesEndpoint = "/cms/v2/${token.country}/M3/crunchyroll/episodes"
val parameters = listOf( val parameters = listOf(
@ -448,6 +470,12 @@ object Crunchyroll {
} }
} }
/**
* Get all available subtitles and streams of a episode.
*
* @param url The playback url of a episode
* @return A **[Playback]** object
*/
suspend fun playback(url: String): Playback { suspend fun playback(url: String): Playback {
return try { return try {
requestGet("", url = url) requestGet("", url = url)
@ -523,7 +551,10 @@ object Crunchyroll {
return try { return try {
requestGet(playheadsEndpoint, parameters) requestGet(playheadsEndpoint, parameters)
} catch (ex: SerializationException) { } catch (ex: SerializationException) {
Log.e(TAG, "SerializationException in upNextSeries().", ex) Log.e(TAG, "SerializationException in playheads().", ex)
emptyMap()
} catch (ex: Throwable) {
Log.e(TAG, "Exception in playheads().", ex.cause)
emptyMap() emptyMap()
} }
} }
@ -543,9 +574,20 @@ object Crunchyroll {
put("playhead", playhead) put("playhead", playhead)
} }
try {
requestPost(playheadsEndpoint, parameters, json) requestPost(playheadsEndpoint, parameters, json)
} catch (ex: Throwable) {
Log.e(TAG, "Exception in postPlayheads()", ex.cause)
}
} }
/**
* Get similar media for a show/movie.
*
* @param seriesId The crunchyroll series id of the media
* @param n The maximum number of results to return, default = 10
* @return A **[SimilarToResult]** object
*/
suspend fun similarTo(seriesId: String, n: Int = 10): SimilarToResult { suspend fun similarTo(seriesId: String, n: Int = 10): SimilarToResult {
val similarToEndpoint = "/content/v1/$accountID/similar_to" val similarToEndpoint = "/content/v1/$accountID/similar_to"
val parameters = listOf( val parameters = listOf(
@ -611,10 +653,32 @@ object Crunchyroll {
} }
} }
suspend fun recommendations(n: Int = 20, start: Int = 0): RecommendationsList {
val recommendationsEndpoint = "/content/v1/$accountID/recommendations"
val parameters = listOf(
"locale" to Preferences.preferredLocale.toLanguageTag(),
"n" to n,
"start" to start,
"variant_id" to 0
)
return try {
requestGet(recommendationsEndpoint, parameters)
}catch (ex: SerializationException) {
Log.e(TAG, "SerializationException in recommendations().", ex)
NoneRecommendationsList
}
}
/** /**
* Account/Profile functions * Account/Profile functions
*/ */
/**
* Get profile information for the currently logged in account.
*
* @return A **[Profile]** object
*/
suspend fun profile(): Profile { suspend fun profile(): Profile {
val profileEndpoint = "/accounts/v1/me/profile" val profileEndpoint = "/accounts/v1/me/profile"
@ -626,6 +690,11 @@ object Crunchyroll {
} }
} }
/**
* Post the preferred content subtitle language.
*
* @param languageTag the preferred language as language tag
*/
suspend fun postPrefSubLanguage(languageTag: String) { suspend fun postPrefSubLanguage(languageTag: String) {
val profileEndpoint = "/accounts/v1/me/profile" val profileEndpoint = "/accounts/v1/me/profile"
val json = buildJsonObject { val json = buildJsonObject {

View File

@ -50,6 +50,25 @@ enum class SortBy(val str: String) {
POPULARITY("popularity") POPULARITY("popularity")
} }
@Suppress("unused")
enum class Categories(val str: String) {
ACTION("action"),
ADVENTURE("adventure"),
COMEDY("comedy"),
DRAMA("drama"),
FANTASY("fantasy"),
MUSIC("music"),
ROMANCE("romance"),
SCI_FI("sci-fi"),
SEINEN("seinen"),
SHOJO("shojo"),
SHONEN("shonen"),
SLICE_OF_LIFE("slice+of+life"),
SPORTS("sports"),
SUPERNATURAL("supernatural"),
THRILLER("thriller")
}
/** /**
* token, index, account. This must pe present for the app to work! * token, index, account. This must pe present for the app to work!
*/ */
@ -105,6 +124,7 @@ typealias SimilarToResult = Collection<Item>
typealias DiscSeasonList = Collection<SeasonListItem> typealias DiscSeasonList = Collection<SeasonListItem>
typealias Watchlist = Collection<Item> typealias Watchlist = Collection<Item>
typealias ContinueWatchingList = Collection<ContinueWatchingItem> typealias ContinueWatchingList = Collection<ContinueWatchingItem>
typealias RecommendationsList = Collection<Item>
@Serializable @Serializable
data class UpNextSeriesItem( data class UpNextSeriesItem(
@ -205,8 +225,14 @@ val NoneBrowseResult = BrowseResult(0, emptyList())
val NoneSimilarToResult = SimilarToResult(0, emptyList()) val NoneSimilarToResult = SimilarToResult(0, emptyList())
val NoneDiscSeasonList = DiscSeasonList(0, emptyList()) val NoneDiscSeasonList = DiscSeasonList(0, emptyList())
val NoneContinueWatchingList = ContinueWatchingList(0, emptyList()) val NoneContinueWatchingList = ContinueWatchingList(0, emptyList())
val NoneRecommendationsList = RecommendationsList(0, emptyList())
val NoneUpNextSeriesItem = UpNextSeriesItem(0, false, false, NoneEpisodePanel) val NoneUpNextSeriesItem = UpNextSeriesItem(
playhead = 0,
fullyWatched = false,
neverWatched = false,
panel = NoneEpisodePanel
)
/** /**
* series data class * series data class

View File

@ -61,6 +61,7 @@ class HomeFragment : Fragment() {
binding.recyclerUpNext.addItemDecoration(MediaItemDecoration(9)) binding.recyclerUpNext.addItemDecoration(MediaItemDecoration(9))
binding.recyclerWatchlist.addItemDecoration(MediaItemDecoration(9)) binding.recyclerWatchlist.addItemDecoration(MediaItemDecoration(9))
binding.recyclerRecommendations.addItemDecoration(MediaItemDecoration(9))
binding.recyclerNewTitles.addItemDecoration(MediaItemDecoration(9)) binding.recyclerNewTitles.addItemDecoration(MediaItemDecoration(9))
binding.recyclerTopTen.addItemDecoration(MediaItemDecoration(9)) binding.recyclerTopTen.addItemDecoration(MediaItemDecoration(9))
@ -79,6 +80,12 @@ class HomeFragment : Fragment() {
} }
) )
binding.recyclerRecommendations.adapter = MediaItemListAdapter(
MediaItemListAdapter.OnClickListener {
activity?.showFragment(MediaFragment(it.id))
}
)
binding.recyclerNewTitles.adapter = MediaItemListAdapter( binding.recyclerNewTitles.adapter = MediaItemListAdapter(
MediaItemListAdapter.OnClickListener { MediaItemListAdapter.OnClickListener {
activity?.showFragment(MediaFragment(it.id)) activity?.showFragment(MediaFragment(it.id))
@ -129,6 +136,9 @@ class HomeFragment : Fragment() {
val adapterWatchlist = binding.recyclerWatchlist.adapter as MediaItemListAdapter val adapterWatchlist = binding.recyclerWatchlist.adapter as MediaItemListAdapter
adapterWatchlist.submitList(uiState.watchlistItems.toItemMediaList()) adapterWatchlist.submitList(uiState.watchlistItems.toItemMediaList())
val adapterRecommendations = binding.recyclerRecommendations.adapter as MediaItemListAdapter
adapterRecommendations.submitList(uiState.recommendationsItems.toItemMediaList())
val adapterNewTitles = binding.recyclerNewTitles.adapter as MediaItemListAdapter val adapterNewTitles = binding.recyclerNewTitles.adapter as MediaItemListAdapter
adapterNewTitles.submitList(uiState.recentlyAddedItems.toItemMediaList()) adapterNewTitles.submitList(uiState.recentlyAddedItems.toItemMediaList())

View File

@ -40,6 +40,7 @@ class HomeViewModel : ViewModel() {
data class Normal( data class Normal(
val upNextItems: List<ContinueWatchingItem>, val upNextItems: List<ContinueWatchingItem>,
val watchlistItems: List<Item>, val watchlistItems: List<Item>,
val recommendationsItems: List<Item>,
val recentlyAddedItems: List<Item>, val recentlyAddedItems: List<Item>,
val topTenItems: List<Item>, val topTenItems: List<Item>,
val highlightItem: Item, val highlightItem: Item,
@ -61,9 +62,11 @@ class HomeViewModel : ViewModel() {
uiState.emit(UiState.Loading) uiState.emit(UiState.Loading)
try { try {
// run the loading in parallel to speed up the process // run the loading in parallel to speed up the process
val upNextJob = viewModelScope.async { Crunchyroll.upNextAccount().items } val upNextJob = viewModelScope.async { Crunchyroll.upNextAccount().items }
val watchlistJob = viewModelScope.async { Crunchyroll.watchlist(50).items } val watchlistJob = viewModelScope.async { Crunchyroll.watchlist(50).items }
val recommendationsJob = viewModelScope.async {
Crunchyroll.recommendations(20).items
}
val recentlyAddedJob = viewModelScope.async { val recentlyAddedJob = viewModelScope.async {
Crunchyroll.browse(sortBy = SortBy.NEWLY_ADDED, n = 50).items Crunchyroll.browse(sortBy = SortBy.NEWLY_ADDED, n = 50).items
} }
@ -77,8 +80,9 @@ class HomeViewModel : ViewModel() {
val highlightItemIsWatchlist = Crunchyroll.isWatchlist(highlightItem.id) val highlightItemIsWatchlist = Crunchyroll.isWatchlist(highlightItem.id)
uiState.emit(UiState.Normal( uiState.emit(UiState.Normal(
upNextJob.await(), watchlistJob.await(), recentlyAddedJob.await(), upNextJob.await(), watchlistJob.await(), recommendationsJob.await(),
topTenJob.await(), highlightItem, highlightItemIsWatchlist recentlyAddedJob.await(), topTenJob.await(), highlightItem,
highlightItemIsWatchlist
)) ))
} catch (e: Exception) { } catch (e: Exception) {
uiState.emit(UiState.Error(e.message)) uiState.emit(UiState.Error(e.message))

View File

@ -47,10 +47,10 @@ import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.Player import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.ui.StyledPlayerControlView import com.google.android.exoplayer2.ui.StyledPlayerControlView
import com.google.android.exoplayer2.util.Util import com.google.android.exoplayer2.util.Util
import kotlinx.android.synthetic.main.activity_player.*
import kotlinx.android.synthetic.main.player_controls.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.mosad.teapod.R import org.mosad.teapod.R
import org.mosad.teapod.databinding.ActivityPlayerBinding
import org.mosad.teapod.databinding.PlayerControlsBinding
import org.mosad.teapod.parser.crunchyroll.NoneEpisode import org.mosad.teapod.parser.crunchyroll.NoneEpisode
import org.mosad.teapod.preferences.Preferences import org.mosad.teapod.preferences.Preferences
import org.mosad.teapod.ui.activity.player.fragment.EpisodeListDialogFragment import org.mosad.teapod.ui.activity.player.fragment.EpisodeListDialogFragment
@ -65,6 +65,8 @@ import kotlin.concurrent.scheduleAtFixedRate
class PlayerActivity : AppCompatActivity() { class PlayerActivity : AppCompatActivity() {
private val model: PlayerViewModel by viewModels() private val model: PlayerViewModel by viewModels()
private lateinit var playerBinding: ActivityPlayerBinding
private lateinit var controlsBinding: PlayerControlsBinding
private lateinit var controller: StyledPlayerControlView private lateinit var controller: StyledPlayerControlView
private lateinit var gestureDetector: GestureDetectorCompat private lateinit var gestureDetector: GestureDetectorCompat
@ -82,6 +84,11 @@ class PlayerActivity : AppCompatActivity() {
setContentView(R.layout.activity_player) setContentView(R.layout.activity_player)
hideBars() // Initial hide the bars hideBars() // Initial hide the bars
playerBinding = ActivityPlayerBinding.bind(findViewById(R.id.player_root))
println(findViewById(R.id.player_controls_root))
controlsBinding = PlayerControlsBinding.bind(findViewById(R.id.player_controls_root))
model.loadMediaAsync( model.loadMediaAsync(
intent.getStringExtra(getString(R.string.intent_season_id)) ?: "", intent.getStringExtra(getString(R.string.intent_season_id)) ?: "",
intent.getStringExtra(getString(R.string.intent_episode_id)) ?: "" intent.getStringExtra(getString(R.string.intent_episode_id)) ?: ""
@ -89,7 +96,7 @@ class PlayerActivity : AppCompatActivity() {
model.currentEpisodeChangedListener.add { onMediaChanged() } model.currentEpisodeChangedListener.add { onMediaChanged() }
gestureDetector = GestureDetectorCompat(this, PlayerGestureListener()) gestureDetector = GestureDetectorCompat(this, PlayerGestureListener())
controller = video_view.findViewById(R.id.exo_controller) controller = playerBinding.videoView.findViewById(R.id.exo_controller)
controller.isAnimationEnabled = false // disable controls (time-bar) animation controller.isAnimationEnabled = false // disable controls (time-bar) animation
initExoPlayer() // call in onCreate, exoplayer lives in view model initExoPlayer() // call in onCreate, exoplayer lives in view model
@ -106,7 +113,7 @@ class PlayerActivity : AppCompatActivity() {
super.onStart() super.onStart()
if (Util.SDK_INT > 23) { if (Util.SDK_INT > 23) {
initPlayer() initPlayer()
video_view?.onResume() playerBinding.videoView.onResume()
} }
} }
@ -116,7 +123,7 @@ class PlayerActivity : AppCompatActivity() {
if (Util.SDK_INT <= 23) { if (Util.SDK_INT <= 23) {
initPlayer() initPlayer()
video_view?.onResume() playerBinding.videoView.onResume()
} }
} }
@ -168,7 +175,7 @@ class PlayerActivity : AppCompatActivity() {
} else { } else {
val width = model.player.videoFormat?.width ?: 0 val width = model.player.videoFormat?.width ?: 0
val height = model.player.videoFormat?.height ?: 0 val height = model.player.videoFormat?.height ?: 0
val contentFrame: View = video_view.findViewById(R.id.exo_content_frame) val contentFrame: View = playerBinding.videoView.findViewById(R.id.exo_content_frame)
val contentRect = with(contentFrame) { val contentRect = with(contentFrame) {
val (x, y) = intArrayOf(0, 0).also(::getLocationInWindow) val (x, y) = intArrayOf(0, 0).also(::getLocationInWindow)
Rect(x, y, x + width, y + height) Rect(x, y, x + width, y + height)
@ -192,7 +199,9 @@ class PlayerActivity : AppCompatActivity() {
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig) super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
// Hide the full-screen UI (controls, etc.) while in picture-in-picture mode. // Hide the full-screen UI (controls, etc.) while in picture-in-picture mode.
video_view.useController = !isInPictureInPictureMode playerBinding.videoView.useController = !isInPictureInPictureMode
// TODO also hide language settings/episodes list
} }
private fun initPlayer() { private fun initPlayer() {
@ -214,17 +223,13 @@ class PlayerActivity : AppCompatActivity() {
override fun onPlaybackStateChanged(state: Int) { override fun onPlaybackStateChanged(state: Int) {
super.onPlaybackStateChanged(state) super.onPlaybackStateChanged(state)
loading.visibility = when (state) { playerBinding.loading.visibility = when (state) {
ExoPlayer.STATE_READY -> View.GONE ExoPlayer.STATE_READY -> View.GONE
ExoPlayer.STATE_BUFFERING -> View.VISIBLE ExoPlayer.STATE_BUFFERING -> View.VISIBLE
else -> View.GONE else -> View.GONE
} }
exo_play_pause.visibility = when (loading.visibility) { controlsBinding.exoPlayPause.isVisible = !playerBinding.loading.isVisible
View.GONE -> View.VISIBLE
View.VISIBLE -> View.INVISIBLE
else -> View.VISIBLE
}
if (state == ExoPlayer.STATE_ENDED && hasNextEpisode() && Preferences.autoplay) { if (state == ExoPlayer.STATE_ENDED && hasNextEpisode() && Preferences.autoplay) {
playNextEpisode() playNextEpisode()
@ -239,10 +244,10 @@ class PlayerActivity : AppCompatActivity() {
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
private fun initVideoView() { private fun initVideoView() {
video_view.player = model.player playerBinding.videoView.player = model.player
// when the player controls get hidden, hide the bars too // when the player controls get hidden, hide the bars too
video_view.setControllerVisibilityListener { playerBinding.videoView.setControllerVisibilityListener {
when (it) { when (it) {
View.GONE -> { View.GONE -> {
hideBars() hideBars()
@ -252,23 +257,23 @@ class PlayerActivity : AppCompatActivity() {
} }
} }
video_view.setOnTouchListener { _, event -> playerBinding.videoView.setOnTouchListener { _, event ->
gestureDetector.onTouchEvent(event) gestureDetector.onTouchEvent(event)
true true
} }
} }
private fun initActions() { private fun initActions() {
exo_close_player.setOnClickListener { controlsBinding.exoClosePlayer.setOnClickListener {
this.finish() this.finish()
} }
rwd_10.setOnButtonClickListener { rewind() } controlsBinding.rwd10.setOnButtonClickListener { rewind() }
ffwd_10.setOnButtonClickListener { fastForward() } controlsBinding.ffwd10.setOnButtonClickListener { fastForward() }
button_next_ep.setOnClickListener { playNextEpisode() } playerBinding.buttonNextEp.setOnClickListener { playNextEpisode() }
button_skip_op.setOnClickListener { skipOpening() } playerBinding.buttonSkipOp.setOnClickListener { skipOpening() }
button_language.setOnClickListener { showLanguageSettings() } controlsBinding.buttonLanguage.setOnClickListener { showLanguageSettings() }
button_episodes.setOnClickListener { showEpisodesList() } controlsBinding.buttonEpisodes.setOnClickListener { showEpisodesList() }
button_next_ep_c.setOnClickListener { playNextEpisode() } controlsBinding.buttonNextEpC.setOnClickListener { playNextEpisode() }
} }
private fun initGUI() { private fun initGUI() {
@ -286,7 +291,7 @@ class PlayerActivity : AppCompatActivity() {
timerUpdates = Timer().scheduleAtFixedRate(0, 500) { timerUpdates = Timer().scheduleAtFixedRate(0, 500) {
lifecycleScope.launch { lifecycleScope.launch {
val currentPosition = model.player.currentPosition val currentPosition = model.player.currentPosition
val btnNextEpIsVisible = button_next_ep.isVisible val btnNextEpIsVisible = playerBinding.buttonNextEp.isVisible
val controlsVisible = controller.isVisible val controlsVisible = controller.isVisible
// make sure remaining time is > 0 // make sure remaining time is > 0
@ -310,10 +315,12 @@ class PlayerActivity : AppCompatActivity() {
model.currentEpisodeMeta?.let { model.currentEpisodeMeta?.let {
if (it.openingDuration > 0 && if (it.openingDuration > 0 &&
currentPosition in it.openingStart..(it.openingStart + 10000) && currentPosition in it.openingStart..(it.openingStart + 10000) &&
!button_skip_op.isVisible !playerBinding.buttonSkipOp.isVisible
) { ) {
showButtonSkipOp() showButtonSkipOp()
} else if (button_skip_op.isVisible && currentPosition !in it.openingStart..(it.openingStart + 10000)) { } else if (playerBinding.buttonSkipOp.isVisible &&
currentPosition !in it.openingStart..(it.openingStart + 10000)
) {
// the button should only be visible, if currentEpisodeMeta != null // the button should only be visible, if currentEpisodeMeta != null
hideButtonSkipOp() hideButtonSkipOp()
} }
@ -328,7 +335,7 @@ class PlayerActivity : AppCompatActivity() {
} }
private fun onPauseOnStop() { private fun onPauseOnStop() {
video_view?.onPause() playerBinding.videoView.onPause()
model.player.pause() model.player.pause()
timerUpdates.cancel() timerUpdates.cancel()
} }
@ -343,7 +350,7 @@ class PlayerActivity : AppCompatActivity() {
val seconds = TimeUnit.MILLISECONDS.toSeconds(remainingTime) % 60 val seconds = TimeUnit.MILLISECONDS.toSeconds(remainingTime) % 60
// if remaining time is below 60 minutes, don't show hours // if remaining time is below 60 minutes, don't show hours
exo_remaining.text = if (TimeUnit.MILLISECONDS.toMinutes(remainingTime) < 60) { controlsBinding.exoRemaining.text = if (TimeUnit.MILLISECONDS.toMinutes(remainingTime) < 60) {
getString(R.string.time_min_sec, minutes, seconds) getString(R.string.time_min_sec, minutes, seconds)
} else { } else {
getString(R.string.time_hour_min_sec, hours, minutes, seconds) getString(R.string.time_hour_min_sec, hours, minutes, seconds)
@ -361,10 +368,10 @@ class PlayerActivity : AppCompatActivity() {
this.finish() this.finish()
} }
exo_text_title.text = model.getMediaTitle() controlsBinding.exoTextTitle.text = model.getMediaTitle()
// hide the next episode button, if there is none // hide the next episode button, if there is none
button_next_ep_c.visibility = if (hasNextEpisode()) View.VISIBLE else View.GONE controlsBinding.buttonNextEpC.isVisible = hasNextEpisode()
} }
/** /**
@ -384,36 +391,36 @@ class PlayerActivity : AppCompatActivity() {
model.seekToOffset(rwdTime) model.seekToOffset(rwdTime)
// hide/show needed components // hide/show needed components
exo_double_tap_indicator.visibility = View.VISIBLE playerBinding.exoDoubleTapIndicator.visibility = View.VISIBLE
ffwd_10_indicator.visibility = View.INVISIBLE playerBinding.ffwd10Indicator.visibility = View.INVISIBLE
rwd_10.visibility = View.INVISIBLE controlsBinding.rwd10.visibility = View.INVISIBLE
rwd_10_indicator.onAnimationEndCallback = { playerBinding.rwd10Indicator.onAnimationEndCallback = {
exo_double_tap_indicator.visibility = View.GONE playerBinding.exoDoubleTapIndicator.visibility = View.GONE
ffwd_10_indicator.visibility = View.VISIBLE playerBinding.ffwd10Indicator.visibility = View.VISIBLE
rwd_10.visibility = View.VISIBLE controlsBinding.rwd10.visibility = View.VISIBLE
} }
// run animation // run animation
rwd_10_indicator.runOnClickAnimation() playerBinding.rwd10Indicator.runOnClickAnimation()
} }
private fun fastForward() { private fun fastForward() {
model.seekToOffset(fwdTime) model.seekToOffset(fwdTime)
// hide/show needed components // hide/show needed components
exo_double_tap_indicator.visibility = View.VISIBLE playerBinding.exoDoubleTapIndicator.visibility = View.VISIBLE
rwd_10_indicator.visibility = View.INVISIBLE playerBinding.rwd10Indicator.visibility = View.INVISIBLE
ffwd_10.visibility = View.INVISIBLE controlsBinding.ffwd10.visibility = View.INVISIBLE
ffwd_10_indicator.onAnimationEndCallback = { playerBinding.ffwd10Indicator.onAnimationEndCallback = {
exo_double_tap_indicator.visibility = View.GONE playerBinding.exoDoubleTapIndicator.visibility = View.GONE
rwd_10_indicator.visibility = View.VISIBLE playerBinding.rwd10Indicator.visibility = View.VISIBLE
ffwd_10.visibility = View.VISIBLE controlsBinding.ffwd10.visibility = View.VISIBLE
} }
// run animation // run animation
ffwd_10_indicator.runOnClickAnimation() playerBinding.ffwd10Indicator.runOnClickAnimation()
} }
private fun playNextEpisode() { private fun playNextEpisode() {
@ -434,10 +441,10 @@ class PlayerActivity : AppCompatActivity() {
* TODO improve the show animation * TODO improve the show animation
*/ */
private fun showButtonNextEp() { private fun showButtonNextEp() {
button_next_ep.isVisible = true playerBinding.buttonNextEp.isVisible = true
button_next_ep.alpha = 0.0f playerBinding.buttonNextEp.alpha = 0.0f
button_next_ep.animate() playerBinding.buttonNextEp.animate()
.alpha(1.0f) .alpha(1.0f)
.setListener(null) .setListener(null)
} }
@ -447,33 +454,32 @@ class PlayerActivity : AppCompatActivity() {
* TODO improve the hide animation * TODO improve the hide animation
*/ */
private fun hideButtonNextEp() { private fun hideButtonNextEp() {
button_next_ep.animate() playerBinding.buttonNextEp.animate()
.alpha(0.0f) .alpha(0.0f)
.setListener(object : AnimatorListenerAdapter() { .setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator?) { override fun onAnimationEnd(animation: Animator?) {
super.onAnimationEnd(animation) super.onAnimationEnd(animation)
button_next_ep.isVisible = false playerBinding.buttonNextEp.isVisible = false
} }
}) })
} }
private fun showButtonSkipOp() { private fun showButtonSkipOp() {
button_skip_op.isVisible = true playerBinding.buttonSkipOp.isVisible = true
button_skip_op.alpha = 0.0f playerBinding.buttonSkipOp.alpha = 0.0f
button_skip_op.animate() playerBinding.buttonSkipOp.animate()
.alpha(1.0f) .alpha(1.0f)
.setListener(null) .setListener(null)
} }
private fun hideButtonSkipOp() { private fun hideButtonSkipOp() {
button_skip_op.animate() playerBinding.buttonSkipOp.animate()
.alpha(0.0f) .alpha(0.0f)
.setListener(object : AnimatorListenerAdapter() { .setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator?) { override fun onAnimationEnd(animation: Animator?) {
super.onAnimationEnd(animation) super.onAnimationEnd(animation)
button_skip_op.isVisible = false playerBinding.buttonSkipOp.isVisible = false
} }
}) })
@ -516,7 +522,7 @@ class PlayerActivity : AppCompatActivity() {
*/ */
override fun onDoubleTap(e: MotionEvent?): Boolean { override fun onDoubleTap(e: MotionEvent?): Boolean {
val eventPosX = e?.x?.toInt() ?: 0 val eventPosX = e?.x?.toInt() ?: 0
val viewCenterX = video_view.measuredWidth / 2 val viewCenterX = playerBinding.videoView.measuredWidth / 2
// if the event position is on the left side rewind, if it's on the right forward // if the event position is on the left side rewind, if it's on the right forward
if (eventPosX < viewCenterX) rewind() else fastForward() if (eventPosX < viewCenterX) rewind() else fastForward()

View File

@ -37,10 +37,7 @@ import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.mosad.teapod.R import org.mosad.teapod.R
import org.mosad.teapod.parser.crunchyroll.Crunchyroll import org.mosad.teapod.parser.crunchyroll.*
import org.mosad.teapod.parser.crunchyroll.NoneEpisode
import org.mosad.teapod.parser.crunchyroll.NoneEpisodes
import org.mosad.teapod.parser.crunchyroll.NonePlayback
import org.mosad.teapod.preferences.Preferences import org.mosad.teapod.preferences.Preferences
import org.mosad.teapod.util.metadb.EpisodeMeta import org.mosad.teapod.util.metadb.EpisodeMeta
import org.mosad.teapod.util.metadb.Meta import org.mosad.teapod.util.metadb.Meta
@ -67,6 +64,8 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
internal set internal set
var currentEpisodeMeta: EpisodeMeta? = null var currentEpisodeMeta: EpisodeMeta? = null
internal set internal set
var currentPlayheads: PlayheadsMap = mutableMapOf()
internal set
// var tmdbTVSeason: TMDBTVSeason? =null // var tmdbTVSeason: TMDBTVSeason? =null
// internal set // internal set
@ -121,7 +120,15 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
fun loadMediaAsync(seasonId: String, episodeId: String) = viewModelScope.launch { fun loadMediaAsync(seasonId: String, episodeId: String) = viewModelScope.launch {
episodes = Crunchyroll.episodes(seasonId) episodes = Crunchyroll.episodes(seasonId)
mediaMeta = loadMediaMeta(episodes.items.first().seriesId)
listOf(
viewModelScope.launch { mediaMeta = loadMediaMeta(episodes.items.first().seriesId) },
viewModelScope.launch {
val episodeIDs = episodes.items.map { it.id }
currentPlayheads = Crunchyroll.playheads(episodeIDs)
}
).joinAll()
Log.d(classTag, "meta: $mediaMeta") Log.d(classTag, "meta: $mediaMeta")
@ -161,11 +168,12 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
episode.id == episodeId episode.id == episodeId
} ?: NoneEpisode } ?: NoneEpisode
// TODO improve handling of none present seasons/episodes
// update current episode meta // update current episode meta
currentEpisodeMeta = if (mediaMeta is TVShowMeta && currentEpisode.episodeNumber != null) { currentEpisodeMeta = if (mediaMeta is TVShowMeta && currentEpisode.episodeNumber != null) {
(mediaMeta as TVShowMeta) (mediaMeta as TVShowMeta)
.seasons[currentEpisode.seasonNumber - 1] .seasons.getOrNull(currentEpisode.seasonNumber - 1)
.episodes[currentEpisode.episodeNumber!! - 1] ?.episodes?.getOrNull(currentEpisode.episodeNumber!! - 1)
} else { } else {
null null
} }
@ -271,6 +279,11 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
viewModelScope.launch { Crunchyroll.postPlayheads(currentEpisode.id, playhead.toInt()) } viewModelScope.launch { Crunchyroll.postPlayheads(currentEpisode.id, playhead.toInt()) }
Log.i(javaClass.name, "Set playhead for episode ${currentEpisode.id} to $playhead sec.") Log.i(javaClass.name, "Set playhead for episode ${currentEpisode.id} to $playhead sec.")
} }
viewModelScope.launch {
val episodeIDs = episodes.items.map { it.id }
currentPlayheads = Crunchyroll.playheads(episodeIDs)
}
} }
} }

View File

@ -43,7 +43,7 @@ class EpisodeListDialogFragment : DialogFragment() {
val adapterRecEpisodes = EpisodeItemAdapter( val adapterRecEpisodes = EpisodeItemAdapter(
model.episodes.items, model.episodes.items,
null, null,
mapOf(), model.currentPlayheads.toMap(),
EpisodeItemAdapter.OnClickListener { episode -> EpisodeItemAdapter.OnClickListener { episode ->
dismiss() dismiss()
model.setCurrentEpisode(episode.id, startPlayback = true) model.setCurrentEpisode(episode.id, startPlayback = true)

View File

@ -51,7 +51,7 @@ class EpisodeItemAdapter(
(holder as EpisodeViewHolder).bind(episode, playhead, tmdbEpisode) (holder as EpisodeViewHolder).bind(episode, playhead, tmdbEpisode)
} }
ViewType.PLAYER.ordinal -> { ViewType.PLAYER.ordinal -> {
(holder as PlayerEpisodeViewHolder).bind(episode, currentSelected) (holder as PlayerEpisodeViewHolder).bind(episode, playhead, currentSelected)
} }
} }
} }
@ -122,7 +122,7 @@ class EpisodeItemAdapter(
RecyclerView.ViewHolder(binding.root) { RecyclerView.ViewHolder(binding.root) {
// -1, since position should never be < 0 // -1, since position should never be < 0
fun bind(episode: Episode, currentSelected: Int) { fun bind(episode: Episode, playhead: PlayheadObject?, currentSelected: Int) {
val context = binding.root.context val context = binding.root.context
val titleText = if (episode.episodeNumber != null) { val titleText = if (episode.episodeNumber != null) {
@ -145,6 +145,14 @@ class EpisodeItemAdapter(
.into(binding.imageEpisode) .into(binding.imageEpisode)
} }
// add watched progress
val playheadProgress = playhead?.playhead?.let {
((it.toFloat() / (episode.durationMs / 1000)) * 100).toInt()
} ?: 0
binding.progressPlayhead.setProgressCompat(playheadProgress, false)
binding.progressPlayhead.visibility = if (playheadProgress <= 0)
View.GONE else View.VISIBLE
// hide the play icon, if it's the current episode // hide the play icon, if it's the current episode
binding.imageEpisodePlay.visibility = if (currentSelected == bindingAdapterPosition) { binding.imageEpisodePlay.visibility = if (currentSelected == bindingAdapterPosition) {
View.GONE View.GONE

View File

@ -2,6 +2,7 @@ package org.mosad.teapod.util.adapter
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import org.mosad.teapod.databinding.ItemMediaBinding import org.mosad.teapod.databinding.ItemMediaBinding
@ -30,6 +31,7 @@ class MediaItemAdapter(private val items: List<ItemMedia>) : RecyclerView.Adapte
inner class MediaViewHolder(val binding: ItemMediaBinding) : inner class MediaViewHolder(val binding: ItemMediaBinding) :
RecyclerView.ViewHolder(binding.root) { RecyclerView.ViewHolder(binding.root) {
init { init {
binding.imageEpisodePlay.isVisible = false // hide the play button for media items
binding.root.setOnClickListener { binding.root.setOnClickListener {
onItemClick?.invoke( onItemClick?.invoke(
items[bindingAdapterPosition].id, items[bindingAdapterPosition].id,

View File

@ -2,7 +2,7 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/player_layout" android:id="@+id/player_root"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="#000000" android:background="#000000"
@ -77,7 +77,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="bottom|end" android:layout_gravity="bottom|end"
android:layout_marginEnd="12dp" android:layout_marginEnd="12dp"
android:layout_marginBottom="70dp" android:layout_marginBottom="72dp"
android:gravity="center" android:gravity="center"
android:text="@string/next_episode" android:text="@string/next_episode"
android:textAllCaps="false" android:textAllCaps="false"
@ -93,7 +93,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="bottom|end" android:layout_gravity="bottom|end"
android:layout_marginEnd="12dp" android:layout_marginEnd="12dp"
android:layout_marginBottom="70dp" android:layout_marginBottom="72dp"
android:gravity="center" android:gravity="center"
android:text="@string/skip_opening" android:text="@string/skip_opening"
android:textAllCaps="false" android:textAllCaps="false"

View File

@ -163,6 +163,34 @@
tools:listitem="@layout/item_media" /> tools:listitem="@layout/item_media" />
</LinearLayout> </LinearLayout>
<LinearLayout
android:id="@+id/linear_recommendations"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingBottom="7dp">
<TextView
android:id="@+id/text_recommendations"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="10dp"
android:paddingTop="15dp"
android:paddingEnd="5dp"
android:paddingBottom="5dp"
android:text="@string/recommendations"
android:textSize="16sp"
android:textStyle="bold" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_recommendations"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_media" />
</LinearLayout>
<LinearLayout <LinearLayout
android:id="@+id/linear_new_titles" android:id="@+id/linear_new_titles"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@ -26,7 +26,16 @@
android:background="@drawable/bg_circle__black_transparent_24dp" android:background="@drawable/bg_circle__black_transparent_24dp"
android:contentDescription="@string/button_play" android:contentDescription="@string/button_play"
app:srcCompat="@drawable/ic_baseline_play_arrow_24" app:srcCompat="@drawable/ic_baseline_play_arrow_24"
app:tint="#FFFFFF" /> app:tint="@color/player_white" />
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progress_playhead"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:max="100"
app:trackColor="#00FFFFFF"
app:trackThickness="2dp" />
</FrameLayout> </FrameLayout>
<TextView <TextView

View File

@ -2,6 +2,7 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/player_controls_root"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="#73000000"> android:background="#73000000">
@ -94,11 +95,13 @@
android:layout_marginEnd="12dp" android:layout_marginEnd="12dp"
android:layout_marginBottom="@dimen/player_styled_progress_margin_bottom"> android:layout_marginBottom="@dimen/player_styled_progress_margin_bottom">
<View <com.google.android.exoplayer2.ui.DefaultTimeBar
android:id="@+id/exo_progress_placeholder" android:id="@id/exo_progress"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="@dimen/player_styled_progress_layout_height" android:layout_height="@dimen/player_styled_progress_layout_height"
android:layout_marginBottom="2dp" android:contentDescription="@string/desc_time_bar"
app:bar_height="3dp"
app:touch_target_height="@dimen/player_styled_progress_layout_height"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/exo_remaining" app:layout_constraintEnd_toStartOf="@+id/exo_remaining"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
@ -107,9 +110,10 @@
<TextView <TextView
android:id="@+id/exo_remaining" android:id="@+id/exo_remaining"
style="@style/ExoStyledControls.TimeText.Position" style="@style/ExoStyledControls.TimeText.Position"
android:layout_height="0dp" android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" /> app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout

View File

@ -9,6 +9,7 @@
<string name="highlight_media">Highlight</string> <string name="highlight_media">Highlight</string>
<string name="up_next">Weiterschauen</string> <string name="up_next">Weiterschauen</string>
<string name="my_list">Meine Liste</string> <string name="my_list">Meine Liste</string>
<string name="recommendations">Empfehlungen</string>
<string name="new_episodes">Neue Episoden</string> <string name="new_episodes">Neue Episoden</string>
<string name="new_simulcasts">Neue Simulcasts</string> <string name="new_simulcasts">Neue Simulcasts</string>
<string name="new_titles">Neue Titel</string> <string name="new_titles">Neue Titel</string>
@ -84,6 +85,7 @@
<string name="episodes">Folgen</string> <string name="episodes">Folgen</string>
<string name="episode">Folge</string> <string name="episode">Folge</string>
<string name="no_subtitles">Aus</string> <string name="no_subtitles">Aus</string>
<string name="desc_time_bar">Zeitleiste</string>
<!-- Onboarding --> <!-- Onboarding -->
<string name="skip">Überspringen</string> <string name="skip">Überspringen</string>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<dimen name="player_styled_progress_layout_height">48dp</dimen> <dimen name="player_styled_progress_layout_height">28dp</dimen>
<dimen name="player_styled_progress_margin_bottom">52dp</dimen> <dimen name="player_styled_progress_margin_bottom">52dp</dimen>
</resources> </resources>

View File

@ -9,6 +9,7 @@
<string name="highlight_media">Highlight</string> <string name="highlight_media">Highlight</string>
<string name="up_next">Up next</string> <string name="up_next">Up next</string>
<string name="my_list">My list</string> <string name="my_list">My list</string>
<string name="recommendations">Recommendations</string>
<string name="new_episodes">New episodes</string> <string name="new_episodes">New episodes</string>
<string name="new_simulcasts">New simulcasts</string> <string name="new_simulcasts">New simulcasts</string>
<string name="new_titles">New titles</string> <string name="new_titles">New titles</string>
@ -106,6 +107,7 @@
<string name="episodes">Episodes</string> <string name="episodes">Episodes</string>
<string name="episode">Episode</string> <string name="episode">Episode</string>
<string name="no_subtitles">None</string> <string name="no_subtitles">None</string>
<string name="desc_time_bar">time bar</string>
<!-- Onboarding --> <!-- Onboarding -->
<string name="skip">Skip</string> <string name="skip">Skip</string>

View File

@ -51,7 +51,7 @@
</style> </style>
<!-- player theme --> <!-- player theme -->
<style name="PlayerTheme" parent="Theme.MaterialComponents.Light.NoActionBar"> <style name="PlayerTheme" parent="AppTheme">
<item name="android:windowNoTitle">true</item> <item name="android:windowNoTitle">true</item>
<item name="android:windowActionBar">false</item> <item name="android:windowActionBar">false</item>
<item name="android:windowFullscreen">true</item> <item name="android:windowFullscreen">true</item>
@ -86,7 +86,8 @@
<item name="android:popupBackground">?themeSecondary</item> <item name="android:popupBackground">?themeSecondary</item>
</style> </style>
<style name="FullScreenDialogStyle" parent="Theme.MaterialComponents.Light.NoActionBar"> <!-- fullscreen dialog fragments -->
<style name="FullScreenDialogStyle" parent="AppTheme">
<item name="android:windowFullscreen">true</item> <item name="android:windowFullscreen">true</item>
<item name="android:windowIsFloating">false</item> <item name="android:windowIsFloating">false</item>
<item name="android:windowBackground">@android:color/transparent</item> <item name="android:windowBackground">@android:color/transparent</item>

View File

@ -1,6 +1,6 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript { buildscript {
ext.kotlin_version = "1.6.10" ext.kotlin_version = "1.6.21"
ext.ktor_version = "1.6.8" ext.ktor_version = "1.6.8"
ext.exo_version = "2.17.1" ext.exo_version = "2.17.1"
repositories { repositories {
@ -8,7 +8,7 @@ buildscript {
mavenCentral() mavenCentral()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:7.1.3' classpath 'com.android.tools.build:gradle:7.2.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong // NOTE: Do not place your application dependencies here; they belong

View File

@ -1,4 +1,10 @@
Dies ist der zweite beta Release von Teapod 1.0.0 mit Unterstützung für Cunchyroll. Dies ist der zweite beta Release von Teapod 1.0.0 mit Unterstützung für Cunchyroll.
* Unterstützung für Crunchyroll hinzugefügt (Ein premium Account wird benötigt) * Unterstützung für Crunchyroll hinzugefügt (Ein premium Account wird benötigt)
* Crunchyroll metadb Unterstützung hinzugefügt * Crunchyroll metadb Unterstützung hinzugefügt (#54)
* Playhead Updates lassen sich nun ausschalten
* Ähnliche Titel zum Mediafragment hinzugefügt
* Empfehlungen für dich zum Homefragment hinzugefügt
* Einen Crash beim login wurde behoben
Alle Änderungen https://git.mosad.xyz/Seil0/teapod/compare/1.0.0-beta1...1.0.0-beta2

View File

@ -1,4 +1,10 @@
This is the second beta release of Teapod 1.0.0 with support for crunchyroll. This is the second beta release of Teapod 1.0.0 with support for crunchyroll.
* Added support for crunchyroll (a premium account is needed) * Support for crunchyroll (a premium account is needed)
* Added crunchyroll metadb support * Crunchyroll metadb support (#54)
* Added a option to disable playhead updates/reporting
* Show similar titles in the media fragment
* Added recommendations to the home fragment
* Fixed a crash on login, which made the app unusable
Full changelog https://git.mosad.xyz/Seil0/teapod/compare/1.0.0-beta1...1.0.0-beta2