Compare commits
13 Commits
a898a70653
...
1.0.0-beta
Author | SHA1 | Date | |
---|---|---|---|
ad1e3068cd
|
|||
de1f19c2b7
|
|||
12bbc2ef5f
|
|||
0186cef79e
|
|||
bc5509cf93
|
|||
ef9a0f00d0
|
|||
b85d7ae025
|
|||
69c9666d2b
|
|||
7d6c300f7e
|
|||
1ebc1194e6
|
|||
c48328723b
|
|||
95c8a72c94
|
|||
fc04e8e222
|
@ -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'
|
||||||
|
@ -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" />
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
|
@ -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())
|
||||||
|
|
||||||
|
@ -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))
|
||||||
|
@ -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()
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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"
|
||||||
|
@ -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"
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
Reference in New Issue
Block a user