Compare commits
45 Commits
95c8a72c94
...
1.0.0
Author | SHA1 | Date | |
---|---|---|---|
d33de371d1 | |||
1ecd25bb06
|
|||
fa28eb35ab
|
|||
d3fe81224b
|
|||
34c7f9d081
|
|||
e835715b9c
|
|||
001141337d
|
|||
5cd3d25ebe
|
|||
215e01c53a
|
|||
1751963574
|
|||
9c3548a866
|
|||
ebd96f9849
|
|||
85b17d7a76
|
|||
f128efea0d
|
|||
da94003368
|
|||
3fdc2aff1b | |||
326da147f1
|
|||
f398c82f62
|
|||
821f8b5590
|
|||
0028cb6dd7
|
|||
127bd030b9
|
|||
3cadaa5c7a
|
|||
97966f5ad3
|
|||
4c55bb771f
|
|||
8eb737a831
|
|||
522b893dc8
|
|||
69e0b6bcca
|
|||
c34b95795f
|
|||
9059306e90
|
|||
ed0c0a4c61
|
|||
03a79346b7
|
|||
ad1e3068cd
|
|||
de1f19c2b7
|
|||
12bbc2ef5f
|
|||
0186cef79e
|
|||
bc5509cf93
|
|||
ef9a0f00d0
|
|||
b85d7ae025
|
|||
69c9666d2b
|
|||
7d6c300f7e
|
|||
1ebc1194e6
|
|||
c48328723b
|
|||
19552d3950 | |||
49e0b1ec29 | |||
af66d968cc |
@ -5,15 +5,15 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 31
|
compileSdkVersion 33
|
||||||
buildToolsVersion "30.0.3"
|
buildToolsVersion "30.0.3"
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "org.mosad.teapod"
|
applicationId "org.mosad.teapod"
|
||||||
minSdkVersion 23
|
minSdkVersion 23
|
||||||
targetSdkVersion 31
|
targetSdkVersion 32
|
||||||
versionCode 9010 //00.09.010
|
versionCode 100000 //01.00.000
|
||||||
versionName "1.0.0-beta2"
|
versionName "1.0.0"
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
resValue "string", "build_time", buildTime()
|
resValue "string", "build_time", buildTime()
|
||||||
@ -38,39 +38,46 @@ 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.4'
|
||||||
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.9.0'
|
||||||
implementation 'androidx.core:core-splashscreen:1.0.0-beta02'
|
implementation 'androidx.core:core-splashscreen:1.0.0'
|
||||||
implementation 'androidx.appcompat:appcompat:1.4.1'
|
implementation 'androidx.appcompat:appcompat:1.5.1'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.4.1'
|
implementation 'androidx.navigation:navigation-fragment-ktx:2.5.2'
|
||||||
implementation 'androidx.navigation:navigation-ui-ktx:2.4.1'
|
implementation 'androidx.navigation:navigation-ui-ktx:2.5.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.5.1'
|
||||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1'
|
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
|
||||||
|
|
||||||
implementation 'com.google.android.material:material:1.5.0'
|
implementation 'com.google.android.material:material:1.6.1'
|
||||||
implementation "com.google.android.exoplayer:exoplayer-core:$exo_version"
|
implementation "com.google.android.exoplayer:exoplayer-core:$exo_version"
|
||||||
implementation "com.google.android.exoplayer:exoplayer-hls:$exo_version"
|
implementation "com.google.android.exoplayer:exoplayer-hls:$exo_version"
|
||||||
implementation "com.google.android.exoplayer:exoplayer-dash:$exo_version"
|
implementation "com.google.android.exoplayer:exoplayer-dash:$exo_version"
|
||||||
implementation "com.google.android.exoplayer:exoplayer-ui:$exo_version"
|
implementation "com.google.android.exoplayer:exoplayer-ui:$exo_version"
|
||||||
implementation "com.google.android.exoplayer:extension-mediasession:$exo_version"
|
implementation "com.google.android.exoplayer:extension-mediasession:$exo_version"
|
||||||
|
|
||||||
implementation 'com.github.bumptech.glide:glide:4.13.1'
|
implementation 'com.facebook.shimmer:shimmer:0.5.0'
|
||||||
|
|
||||||
|
implementation 'com.github.bumptech.glide:glide:4.13.2'
|
||||||
implementation 'jp.wasabeef:glide-transformations:4.3.0'
|
implementation 'jp.wasabeef:glide-transformations:4.3.0'
|
||||||
|
|
||||||
implementation "io.ktor:ktor-client-core:$ktor_version"
|
implementation "io.ktor:ktor-client-core:$ktor_version"
|
||||||
implementation "io.ktor:ktor-client-android:$ktor_version"
|
implementation "io.ktor:ktor-client-android:$ktor_version"
|
||||||
implementation "io.ktor:ktor-client-serialization:$ktor_version"
|
implementation "io.ktor:ktor-client-content-negotiation:$ktor_version"
|
||||||
|
implementation "io.ktor:ktor-serialization-kotlinx-json:$ktor_version"
|
||||||
|
|
||||||
testImplementation 'junit:junit:4.13.2'
|
testImplementation 'junit:junit:4.13.2'
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
||||||
|
@ -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" />
|
||||||
|
|
||||||
|
@ -25,16 +25,14 @@ package org.mosad.teapod.parser.crunchyroll
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
import io.ktor.client.call.*
|
import io.ktor.client.call.*
|
||||||
import io.ktor.client.features.*
|
import io.ktor.client.plugins.*
|
||||||
import io.ktor.client.features.json.*
|
import io.ktor.client.plugins.contentnegotiation.*
|
||||||
import io.ktor.client.features.json.serializer.*
|
|
||||||
import io.ktor.client.request.*
|
import io.ktor.client.request.*
|
||||||
import io.ktor.client.request.forms.*
|
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 io.ktor.serialization.kotlinx.json.*
|
||||||
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,16 +40,15 @@ 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 }
|
|
||||||
|
|
||||||
object Crunchyroll {
|
object Crunchyroll {
|
||||||
private val TAG = javaClass.name
|
private val TAG = javaClass.name
|
||||||
|
|
||||||
private val client = HttpClient {
|
private val client = HttpClient {
|
||||||
install(JsonFeature) {
|
install(ContentNegotiation) {
|
||||||
serializer = KotlinxSerializer(json)
|
json(Json {
|
||||||
|
ignoreUnknownKeys = true
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private const val baseUrl = "https://beta-api.crunchyroll.com"
|
private const val baseUrl = "https://beta-api.crunchyroll.com"
|
||||||
@ -60,15 +57,17 @@ 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 = ""
|
||||||
|
private var externalID = ""
|
||||||
|
|
||||||
private var policy = ""
|
private var policy = ""
|
||||||
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:
|
||||||
@ -78,7 +77,7 @@ object Crunchyroll {
|
|||||||
*/
|
*/
|
||||||
fun initBasicApiToken() = runBlocking {
|
fun initBasicApiToken() = runBlocking {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
basicApiToken = (client.get(basicApiTokenUrl) as HttpResponse).readText()
|
basicApiToken = client.get { url(basicApiTokenUrl) }.bodyAsText()
|
||||||
Log.i(TAG, "basic auth token: $basicApiToken")
|
Log.i(TAG, "basic auth token: $basicApiToken")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -102,14 +101,13 @@ 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 {
|
||||||
val response: HttpResponse = client.submitForm("$baseUrl$tokenEndpoint", formParameters = formData) {
|
val response: HttpResponse = client.submitForm("$baseUrl$tokenEndpoint", formParameters = formData) {
|
||||||
header("Authorization", "Basic $basicApiToken")
|
header("Authorization", "Basic $basicApiToken")
|
||||||
}
|
}
|
||||||
token = response.receive()
|
token = response.body()
|
||||||
tokenValidUntil = System.currentTimeMillis() + (token.expiresIn * 1000)
|
tokenValidUntil = System.currentTimeMillis() + (token.expiresIn * 1000)
|
||||||
response.status
|
response.status
|
||||||
} catch (ex: ClientRequestException) {
|
} catch (ex: ClientRequestException) {
|
||||||
@ -117,7 +115,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 +141,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()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -158,10 +155,10 @@ object Crunchyroll {
|
|||||||
|
|
||||||
// for json set body and content type
|
// for json set body and content type
|
||||||
if (bodyObject is JsonObject) {
|
if (bodyObject is JsonObject) {
|
||||||
body = bodyObject
|
setBody(bodyObject)
|
||||||
contentType(ContentType.Application.Json)
|
contentType(ContentType.Application.Json)
|
||||||
}
|
}
|
||||||
}
|
}.body()
|
||||||
|
|
||||||
response
|
response
|
||||||
}
|
}
|
||||||
@ -249,13 +246,13 @@ object Crunchyroll {
|
|||||||
}
|
}
|
||||||
|
|
||||||
accountID = account.accountId
|
accountID = account.accountId
|
||||||
|
externalID = account.externalId
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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 +262,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,24 +277,38 @@ 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
} else {
|
||||||
noneOptParams
|
Log.d(TAG, "browse result not cached, fetching: $parameters")
|
||||||
|
val browseResult: BrowseResult = try {
|
||||||
|
requestGet(browseEndpoint, parameters)
|
||||||
|
}catch (ex: SerializationException) {
|
||||||
|
Log.e(TAG, "SerializationException in browse().", ex)
|
||||||
|
NoneBrowseResult
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
// add results to cache
|
||||||
|
browsingCache[parameters.toString()] = browseResult
|
||||||
}
|
}
|
||||||
|
|
||||||
val browseResult: BrowseResult = try {
|
return browsingCache[parameters.toString()] ?: NoneBrowseResult
|
||||||
requestGet(browseEndpoint, parameters)
|
|
||||||
}catch (ex: SerializationException) {
|
|
||||||
Log.e(TAG, "SerializationException in browse().", ex)
|
|
||||||
NoneBrowseResult
|
|
||||||
}
|
|
||||||
|
|
||||||
// add results to cache TODO improve
|
|
||||||
browsingCache.clear()
|
|
||||||
browsingCache.addAll(browseResult.items)
|
|
||||||
|
|
||||||
return browseResult
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -321,7 +333,7 @@ object Crunchyroll {
|
|||||||
|
|
||||||
return try {
|
return try {
|
||||||
requestGet(searchEndpoint, parameters)
|
requestGet(searchEndpoint, parameters)
|
||||||
}catch (ex: SerializationException) {
|
} catch (ex: SerializationException) {
|
||||||
Log.e(TAG, "SerializationException in search(), with query = \"$query\".", ex)
|
Log.e(TAG, "SerializationException in search(), with query = \"$query\".", ex)
|
||||||
NoneSearchResult
|
NoneSearchResult
|
||||||
}
|
}
|
||||||
@ -345,7 +357,7 @@ object Crunchyroll {
|
|||||||
|
|
||||||
return try {
|
return try {
|
||||||
requestGet(episodesEndpoint, parameters)
|
requestGet(episodesEndpoint, parameters)
|
||||||
}catch (ex: SerializationException) {
|
} catch (ex: SerializationException) {
|
||||||
Log.e(TAG, "SerializationException in objects().", ex)
|
Log.e(TAG, "SerializationException in objects().", ex)
|
||||||
NoneCollection
|
NoneCollection
|
||||||
}
|
}
|
||||||
@ -361,7 +373,7 @@ object Crunchyroll {
|
|||||||
|
|
||||||
return try {
|
return try {
|
||||||
requestGet(seasonListEndpoint, parameters)
|
requestGet(seasonListEndpoint, parameters)
|
||||||
}catch (ex: SerializationException) {
|
} catch (ex: SerializationException) {
|
||||||
Log.e(TAG, "SerializationException in seasonList().", ex)
|
Log.e(TAG, "SerializationException in seasonList().", ex)
|
||||||
NoneDiscSeasonList
|
NoneDiscSeasonList
|
||||||
}
|
}
|
||||||
@ -385,7 +397,7 @@ object Crunchyroll {
|
|||||||
|
|
||||||
return try {
|
return try {
|
||||||
requestGet(seriesEndpoint, parameters)
|
requestGet(seriesEndpoint, parameters)
|
||||||
}catch (ex: SerializationException) {
|
} catch (ex: SerializationException) {
|
||||||
Log.e(TAG, "SerializationException in series().", ex)
|
Log.e(TAG, "SerializationException in series().", ex)
|
||||||
NoneSeries
|
NoneSeries
|
||||||
}
|
}
|
||||||
@ -406,12 +418,18 @@ object Crunchyroll {
|
|||||||
|
|
||||||
return try {
|
return try {
|
||||||
requestGet(upNextSeriesEndpoint, parameters)
|
requestGet(upNextSeriesEndpoint, parameters)
|
||||||
}catch (ex: SerializationException) {
|
} catch (ex: SerializationException) {
|
||||||
Log.e(TAG, "SerializationException in upNextSeries().", ex)
|
Log.e(TAG, "SerializationException in upNextSeries().", ex)
|
||||||
NoneUpNextSeriesItem
|
NoneUpNextSeriesItem
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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(
|
||||||
@ -424,12 +442,18 @@ object Crunchyroll {
|
|||||||
|
|
||||||
return try {
|
return try {
|
||||||
requestGet(seasonsEndpoint, parameters)
|
requestGet(seasonsEndpoint, parameters)
|
||||||
}catch (ex: SerializationException) {
|
} catch (ex: SerializationException) {
|
||||||
Log.e(TAG, "SerializationException in seasons().", ex)
|
Log.e(TAG, "SerializationException in seasons().", ex)
|
||||||
NoneSeasons
|
NoneSeasons
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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(
|
||||||
@ -442,16 +466,22 @@ object Crunchyroll {
|
|||||||
|
|
||||||
return try {
|
return try {
|
||||||
requestGet(episodesEndpoint, parameters)
|
requestGet(episodesEndpoint, parameters)
|
||||||
}catch (ex: SerializationException) {
|
} catch (ex: SerializationException) {
|
||||||
Log.e(TAG, "SerializationException in episodes().", ex)
|
Log.e(TAG, "SerializationException in episodes().", ex)
|
||||||
NoneEpisodes
|
NoneEpisodes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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)
|
||||||
}catch (ex: SerializationException) {
|
} catch (ex: SerializationException) {
|
||||||
Log.e(TAG, "SerializationException in playback(), with url = $url.", ex)
|
Log.e(TAG, "SerializationException in playback(), with url = $url.", ex)
|
||||||
NonePlayback
|
NonePlayback
|
||||||
}
|
}
|
||||||
@ -474,7 +504,7 @@ object Crunchyroll {
|
|||||||
return try {
|
return try {
|
||||||
(requestGet(watchlistSeriesEndpoint, parameters) as JsonObject)
|
(requestGet(watchlistSeriesEndpoint, parameters) as JsonObject)
|
||||||
.containsKey(seriesId)
|
.containsKey(seriesId)
|
||||||
}catch (ex: SerializationException) {
|
} catch (ex: SerializationException) {
|
||||||
Log.e(TAG, "SerializationException in isWatchlist() with seriesId = $seriesId", ex)
|
Log.e(TAG, "SerializationException in isWatchlist() with seriesId = $seriesId", ex)
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
@ -522,8 +552,11 @@ 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 +576,20 @@ object Crunchyroll {
|
|||||||
put("playhead", playhead)
|
put("playhead", playhead)
|
||||||
}
|
}
|
||||||
|
|
||||||
requestPost(playheadsEndpoint, parameters, json)
|
try {
|
||||||
|
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(
|
||||||
@ -556,7 +600,7 @@ object Crunchyroll {
|
|||||||
|
|
||||||
return try {
|
return try {
|
||||||
requestGet(similarToEndpoint, parameters)
|
requestGet(similarToEndpoint, parameters)
|
||||||
}catch (ex: SerializationException) {
|
} catch (ex: SerializationException) {
|
||||||
Log.e(TAG, "SerializationException in similarTo().", ex)
|
Log.e(TAG, "SerializationException in similarTo().", ex)
|
||||||
NoneSimilarToResult
|
NoneSimilarToResult
|
||||||
}
|
}
|
||||||
@ -581,7 +625,7 @@ object Crunchyroll {
|
|||||||
|
|
||||||
val list: ContinueWatchingList = try {
|
val list: ContinueWatchingList = try {
|
||||||
requestGet(watchlistEndpoint, parameters)
|
requestGet(watchlistEndpoint, parameters)
|
||||||
}catch (ex: SerializationException) {
|
} catch (ex: SerializationException) {
|
||||||
Log.e(TAG, "SerializationException in watchlist().", ex)
|
Log.e(TAG, "SerializationException in watchlist().", ex)
|
||||||
NoneContinueWatchingList
|
NoneContinueWatchingList
|
||||||
}
|
}
|
||||||
@ -605,27 +649,54 @@ object Crunchyroll {
|
|||||||
|
|
||||||
return try {
|
return try {
|
||||||
requestGet(watchlistEndpoint, parameters)
|
requestGet(watchlistEndpoint, parameters)
|
||||||
}catch (ex: SerializationException) {
|
} catch (ex: SerializationException) {
|
||||||
Log.e(TAG, "SerializationException in upNextAccount().", ex)
|
Log.e(TAG, "SerializationException in upNextAccount().", ex)
|
||||||
NoneContinueWatchingList
|
NoneContinueWatchingList
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
requestGet(profileEndpoint)
|
requestGet(profileEndpoint)
|
||||||
}catch (ex: SerializationException) {
|
} catch (ex: SerializationException) {
|
||||||
Log.e(TAG, "SerializationException in profile().", ex)
|
Log.e(TAG, "SerializationException in profile().", ex)
|
||||||
NoneProfile
|
NoneProfile
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
||||||
@ -635,4 +706,20 @@ object Crunchyroll {
|
|||||||
requestPatch(profileEndpoint, bodyObject = json)
|
requestPatch(profileEndpoint, bodyObject = json)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get additional profile (benefits) information for the currently logged in account.
|
||||||
|
*
|
||||||
|
* * @return A **[Profile]** object
|
||||||
|
*/
|
||||||
|
suspend fun benefits(): Benefits {
|
||||||
|
val profileEndpoint = "/subs/v1/subscriptions/$externalID/benefits"
|
||||||
|
|
||||||
|
return try {
|
||||||
|
requestGet(profileEndpoint)
|
||||||
|
} catch (ex: SerializationException) {
|
||||||
|
Log.e(TAG, "SerializationException in benefits().", ex)
|
||||||
|
NoneBenefits
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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,8 @@ 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>
|
||||||
|
typealias Benefits = Collection<Benefit>
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class UpNextSeriesItem(
|
data class UpNextSeriesItem(
|
||||||
@ -205,8 +226,15 @@ 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 NoneBenefits = Benefits(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
|
||||||
@ -354,9 +382,9 @@ data class Streams(
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Stream(
|
data class Stream(
|
||||||
@SerialName("hardsub_locale") val hardsubLocale: String,
|
@SerialName("hardsub_locale") val hardsubLocale: String = "", // default/nullable value since might be optional
|
||||||
@SerialName("url") val url: String,
|
@SerialName("url") val url: String = "", // default/nullable value since optional
|
||||||
@SerialName("vcodec") val vcodec: String,
|
@SerialName("vcodec") val vcodec: String = "", // default/nullable value since optional
|
||||||
)
|
)
|
||||||
|
|
||||||
val NonePlayback = Playback(
|
val NonePlayback = Playback(
|
||||||
@ -386,3 +414,16 @@ val NoneProfile = Profile(
|
|||||||
preferredContentSubtitleLanguage = "",
|
preferredContentSubtitleLanguage = "",
|
||||||
username = ""
|
username = ""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* benefit data class
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
data class Benefit(
|
||||||
|
@SerialName("benefit") val benefit: String,
|
||||||
|
@SerialName("source") val source: String,
|
||||||
|
)
|
||||||
|
val NoneBenefit = Benefit(
|
||||||
|
benefit = "",
|
||||||
|
source = ""
|
||||||
|
)
|
||||||
|
@ -26,6 +26,7 @@ import android.content.Intent
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
|
import androidx.activity.addCallback
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
@ -78,16 +79,14 @@ class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListen
|
|||||||
supportFragmentManager.commit {
|
supportFragmentManager.commit {
|
||||||
replace(R.id.nav_host_fragment, activeBaseFragment, activeBaseFragment.javaClass.simpleName)
|
replace(R.id.nav_host_fragment, activeBaseFragment, activeBaseFragment.javaClass.simpleName)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBackPressed() {
|
onBackPressedDispatcher.addCallback {
|
||||||
if (supportFragmentManager.backStackEntryCount > 0) {
|
if (supportFragmentManager.backStackEntryCount > 0) {
|
||||||
supportFragmentManager.popBackStack()
|
supportFragmentManager.popBackStack()
|
||||||
} else {
|
|
||||||
if (activeBaseFragment !is HomeFragment) {
|
|
||||||
binding.navView.selectedItemId = R.id.navigation_home
|
|
||||||
} else {
|
} else {
|
||||||
super.onBackPressed()
|
if (activeBaseFragment !is HomeFragment) {
|
||||||
|
binding.navView.selectedItemId = R.id.navigation_home
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,7 @@ import kotlinx.coroutines.runBlocking
|
|||||||
import org.mosad.teapod.BuildConfig
|
import org.mosad.teapod.BuildConfig
|
||||||
import org.mosad.teapod.R
|
import org.mosad.teapod.R
|
||||||
import org.mosad.teapod.databinding.FragmentAccountBinding
|
import org.mosad.teapod.databinding.FragmentAccountBinding
|
||||||
|
import org.mosad.teapod.parser.crunchyroll.Benefits
|
||||||
import org.mosad.teapod.parser.crunchyroll.Crunchyroll
|
import org.mosad.teapod.parser.crunchyroll.Crunchyroll
|
||||||
import org.mosad.teapod.parser.crunchyroll.Profile
|
import org.mosad.teapod.parser.crunchyroll.Profile
|
||||||
import org.mosad.teapod.parser.crunchyroll.supportedLocals
|
import org.mosad.teapod.parser.crunchyroll.supportedLocals
|
||||||
@ -33,6 +34,9 @@ class AccountFragment : Fragment() {
|
|||||||
private var profile: Deferred<Profile> = lifecycleScope.async {
|
private var profile: Deferred<Profile> = lifecycleScope.async {
|
||||||
Crunchyroll.profile()
|
Crunchyroll.profile()
|
||||||
}
|
}
|
||||||
|
private var benefits: Deferred<Benefits> = lifecycleScope.async {
|
||||||
|
Crunchyroll.benefits()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
binding = FragmentAccountBinding.inflate(inflater, container, false)
|
binding = FragmentAccountBinding.inflate(inflater, container, false)
|
||||||
@ -44,14 +48,18 @@ class AccountFragment : Fragment() {
|
|||||||
|
|
||||||
binding.textAccountLogin.text = EncryptedPreferences.login
|
binding.textAccountLogin.text = EncryptedPreferences.login
|
||||||
|
|
||||||
// TODO reimplement for cr, if possible (maybe account status would be better? (premium))
|
// load account status and tier (async) info before anything else
|
||||||
// load subscription (async) info before anything else
|
|
||||||
binding.textAccountSubscription.text = getString(R.string.account_subscription, getString(R.string.loading))
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
binding.textAccountSubscription.text = getString(
|
benefits.await().apply {
|
||||||
R.string.account_subscription,
|
this.items.firstOrNull { it.benefit == "cr_premium" }?.let {
|
||||||
"TODO"
|
binding.textAccountSubscription.text = getString(R.string.account_premium)
|
||||||
)
|
}
|
||||||
|
|
||||||
|
this.items.firstOrNull { it.benefit == "cr_fan_pack" }?.let {
|
||||||
|
binding.textAccountSubscriptionDesc.text =
|
||||||
|
getString(R.string.account_tier, getString(R.string.account_tier_mega_fan))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// add preferred subtitles
|
// add preferred subtitles
|
||||||
@ -80,12 +88,6 @@ class AccountFragment : Fragment() {
|
|||||||
showLoginDialog()
|
showLoginDialog()
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.linearAccountSubscription.setOnClickListener {
|
|
||||||
// TODO
|
|
||||||
//startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(AoDParser.getSubscriptionUrl())))
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
binding.linearSettingsContentLanguage.setOnClickListener {
|
binding.linearSettingsContentLanguage.setOnClickListener {
|
||||||
showContentLanguageSelection()
|
showContentLanguageSelection()
|
||||||
}
|
}
|
||||||
|
@ -27,22 +27,25 @@ import android.util.Log
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.view.children
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
|
import com.facebook.shimmer.ShimmerFrameLayout
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.mosad.teapod.R
|
import org.mosad.teapod.R
|
||||||
import org.mosad.teapod.databinding.FragmentHomeBinding
|
import org.mosad.teapod.databinding.FragmentHomeBinding
|
||||||
import org.mosad.teapod.ui.activity.main.MainActivity
|
|
||||||
import org.mosad.teapod.ui.activity.main.viewmodel.HomeViewModel
|
import org.mosad.teapod.ui.activity.main.viewmodel.HomeViewModel
|
||||||
import org.mosad.teapod.util.adapter.MediaEpisodeListAdapter
|
import org.mosad.teapod.util.adapter.MediaEpisodeListAdapter
|
||||||
import org.mosad.teapod.util.adapter.MediaItemListAdapter
|
import org.mosad.teapod.util.adapter.MediaItemListAdapter
|
||||||
import org.mosad.teapod.util.decoration.MediaItemDecoration
|
import org.mosad.teapod.util.decoration.MediaItemDecoration
|
||||||
import org.mosad.teapod.util.setDrawableTop
|
import org.mosad.teapod.util.setDrawableTop
|
||||||
import org.mosad.teapod.util.showFragment
|
import org.mosad.teapod.util.showFragment
|
||||||
|
import org.mosad.teapod.util.startPlayer
|
||||||
import org.mosad.teapod.util.toItemMediaList
|
import org.mosad.teapod.util.toItemMediaList
|
||||||
|
|
||||||
class HomeFragment : Fragment() {
|
class HomeFragment : Fragment() {
|
||||||
@ -61,15 +64,13 @@ 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))
|
||||||
|
|
||||||
binding.recyclerUpNext.adapter = MediaEpisodeListAdapter(
|
binding.recyclerUpNext.adapter = MediaEpisodeListAdapter(
|
||||||
MediaEpisodeListAdapter.OnClickListener {
|
MediaEpisodeListAdapter.OnClickListener {
|
||||||
val activity = activity
|
activity?.startPlayer(it.panel.episodeMetadata.seasonId, it.panel.id)
|
||||||
if (activity is MainActivity) {
|
|
||||||
activity.startPlayer(it.panel.episodeMetadata.seasonId, it.panel.id)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -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))
|
||||||
@ -99,16 +106,6 @@ class HomeFragment : Fragment() {
|
|||||||
// TODO since this might take a few seconds show a loading animation for the watchlist button
|
// TODO since this might take a few seconds show a loading animation for the watchlist button
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.buttonPlayHighlight.setOnClickListener {
|
|
||||||
// TODO implement
|
|
||||||
lifecycleScope.launch {
|
|
||||||
//val media = AoDParser.getMediaById(0)
|
|
||||||
|
|
||||||
// Log.d(javaClass.name, "Starting Player with mediaId: ${media.aodId}")
|
|
||||||
//(activity as MainActivity).startPlayer(media.aodId, media.playlist.first().mediaId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
model.onUiState(viewLifecycleOwner.lifecycleScope) { uiState ->
|
model.onUiState(viewLifecycleOwner.lifecycleScope) { uiState ->
|
||||||
@ -129,6 +126,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())
|
||||||
|
|
||||||
@ -151,10 +151,27 @@ class HomeFragment : Fragment() {
|
|||||||
binding.textHighlightInfo.setOnClickListener {
|
binding.textHighlightInfo.setOnClickListener {
|
||||||
activity?.showFragment(MediaFragment(uiState.highlightItem.id))
|
activity?.showFragment(MediaFragment(uiState.highlightItem.id))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
binding.buttonPlayHighlight.setOnClickListener {
|
||||||
|
val panel = uiState.highlightItemUpNext.panel
|
||||||
|
activity?.startPlayer(panel.episodeMetadata.seasonId, panel.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// disable the shimmer effect
|
||||||
|
disableShimmer()
|
||||||
|
|
||||||
|
// make highlights layout visible again
|
||||||
|
binding.linearHighlight.isVisible = true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun bindUiStateLoading() {
|
private fun bindUiStateLoading() {
|
||||||
// currently not used
|
// hide highlights layout
|
||||||
|
binding.linearHighlight.isVisible = false
|
||||||
|
println(binding.root.childCount)
|
||||||
|
binding.root.children.filter { it is ShimmerFrameLayout }.forEach {
|
||||||
|
it as ShimmerFrameLayout
|
||||||
|
it.startShimmer()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun bindUiStateError(uiState: HomeViewModel.UiState.Error) {
|
private fun bindUiStateError(uiState: HomeViewModel.UiState.Error) {
|
||||||
@ -162,4 +179,34 @@ class HomeFragment : Fragment() {
|
|||||||
Log.e(classTag, "A error occurred while loading a UiState: ${uiState.message}")
|
Log.e(classTag, "A error occurred while loading a UiState: ${uiState.message}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable the shimmer effect for all shimmer layouts and hide them.
|
||||||
|
*/
|
||||||
|
private fun disableShimmer() {
|
||||||
|
binding.shimmerLayoutHighlight.apply {
|
||||||
|
stopShimmer()
|
||||||
|
isVisible = false
|
||||||
|
}
|
||||||
|
binding.shimmerLayoutUpNext.apply {
|
||||||
|
stopShimmer()
|
||||||
|
isVisible = false
|
||||||
|
}
|
||||||
|
binding.shimmerLayoutWatchlist.apply {
|
||||||
|
stopShimmer()
|
||||||
|
isVisible = false
|
||||||
|
}
|
||||||
|
binding.shimmerLayoutRecommendations.apply {
|
||||||
|
stopShimmer()
|
||||||
|
isVisible = false
|
||||||
|
}
|
||||||
|
binding.shimmerLayoutNewTitles.apply {
|
||||||
|
stopShimmer()
|
||||||
|
isVisible = false
|
||||||
|
}
|
||||||
|
binding.shimmerLayoutTopTen.apply {
|
||||||
|
stopShimmer()
|
||||||
|
isVisible = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -40,9 +40,11 @@ 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,
|
||||||
|
val highlightItemUpNext: UpNextSeriesItem,
|
||||||
val highlightIsWatchlist:Boolean
|
val highlightIsWatchlist:Boolean
|
||||||
) : UiState()
|
) : UiState()
|
||||||
data class Error(val message: String?) : UiState()
|
data class Error(val message: String?) : UiState()
|
||||||
@ -61,9 +63,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
|
||||||
}
|
}
|
||||||
@ -74,11 +78,17 @@ class HomeViewModel : ViewModel() {
|
|||||||
val recentlyAddedItems = recentlyAddedJob.await()
|
val recentlyAddedItems = recentlyAddedJob.await()
|
||||||
// FIXME crashes on newTitles.items.size == 0
|
// FIXME crashes on newTitles.items.size == 0
|
||||||
val highlightItem = recentlyAddedItems[Random.nextInt(recentlyAddedItems.size)]
|
val highlightItem = recentlyAddedItems[Random.nextInt(recentlyAddedItems.size)]
|
||||||
val highlightItemIsWatchlist = Crunchyroll.isWatchlist(highlightItem.id)
|
val highlightItemUpNextJob = viewModelScope.async {
|
||||||
|
Crunchyroll.upNextSeries(highlightItem.id)
|
||||||
|
}
|
||||||
|
val highlightItemIsWatchlistJob = viewModelScope.async {
|
||||||
|
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,
|
||||||
|
highlightItemUpNextJob.await(), highlightItemIsWatchlistJob.await()
|
||||||
))
|
))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
uiState.emit(UiState.Error(e.message))
|
uiState.emit(UiState.Error(e.message))
|
||||||
@ -111,9 +121,6 @@ class HomeViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -3,13 +3,14 @@ package org.mosad.teapod.ui.activity.onboarding
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import androidx.activity.addCallback
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||||
import com.google.android.material.tabs.TabLayoutMediator
|
import com.google.android.material.tabs.TabLayoutMediator
|
||||||
import org.mosad.teapod.ui.activity.main.MainActivity
|
|
||||||
import org.mosad.teapod.databinding.ActivityOnboardingBinding
|
import org.mosad.teapod.databinding.ActivityOnboardingBinding
|
||||||
|
import org.mosad.teapod.ui.activity.main.MainActivity
|
||||||
|
|
||||||
class OnboardingActivity : AppCompatActivity() {
|
class OnboardingActivity : AppCompatActivity() {
|
||||||
|
|
||||||
@ -35,13 +36,11 @@ class OnboardingActivity : AppCompatActivity() {
|
|||||||
if (fragments.size <= 1) {
|
if (fragments.size <= 1) {
|
||||||
binding.tabLayout.visibility = View.GONE
|
binding.tabLayout.visibility = View.GONE
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBackPressed() {
|
onBackPressedDispatcher.addCallback {
|
||||||
if (binding.viewPager.currentItem == 0) {
|
if (binding.viewPager.currentItem != 0) {
|
||||||
super.onBackPressed()
|
binding.viewPager.currentItem = binding.viewPager.currentItem - 1
|
||||||
} else {
|
}
|
||||||
binding.viewPager.currentItem = binding.viewPager.currentItem - 1
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,7 +70,7 @@ class PlayerActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private lateinit var controller: StyledPlayerControlView
|
private lateinit var controller: StyledPlayerControlView
|
||||||
private lateinit var gestureDetector: GestureDetectorCompat
|
private lateinit var gestureDetector: GestureDetectorCompat
|
||||||
private lateinit var timerUpdates: TimerTask
|
private lateinit var controlsUpdates: TimerTask
|
||||||
|
|
||||||
private var wasInPiP = false
|
private var wasInPiP = false
|
||||||
private var remainingTime: Long = 0
|
private var remainingTime: Long = 0
|
||||||
@ -85,8 +85,6 @@ class PlayerActivity : AppCompatActivity() {
|
|||||||
hideBars() // Initial hide the bars
|
hideBars() // Initial hide the bars
|
||||||
|
|
||||||
playerBinding = ActivityPlayerBinding.bind(findViewById(R.id.player_root))
|
playerBinding = ActivityPlayerBinding.bind(findViewById(R.id.player_root))
|
||||||
|
|
||||||
println(findViewById(R.id.player_controls_root))
|
|
||||||
controlsBinding = PlayerControlsBinding.bind(findViewById(R.id.player_controls_root))
|
controlsBinding = PlayerControlsBinding.bind(findViewById(R.id.player_controls_root))
|
||||||
|
|
||||||
model.loadMediaAsync(
|
model.loadMediaAsync(
|
||||||
@ -194,9 +192,11 @@ class PlayerActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
override fun onPictureInPictureModeChanged(
|
override fun onPictureInPictureModeChanged(
|
||||||
isInPictureInPictureMode: Boolean,
|
isInPictureInPictureMode: Boolean,
|
||||||
newConfig: Configuration?
|
newConfig: Configuration
|
||||||
) {
|
) {
|
||||||
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
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.
|
||||||
playerBinding.videoView.useController = !isInPictureInPictureMode
|
playerBinding.videoView.useController = !isInPictureInPictureMode
|
||||||
@ -229,7 +229,11 @@ class PlayerActivity : AppCompatActivity() {
|
|||||||
else -> View.GONE
|
else -> View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
controlsBinding.exoPlayPause.isVisible = !playerBinding.loading.isVisible
|
// don't use isVisible to hide exoPlayPause, as it will set the visibility to GONE
|
||||||
|
controlsBinding.exoPlayPause.visibility = when(playerBinding.loading.isVisible) {
|
||||||
|
true -> View.INVISIBLE
|
||||||
|
false -> View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
if (state == ExoPlayer.STATE_ENDED && hasNextEpisode() && Preferences.autoplay) {
|
if (state == ExoPlayer.STATE_ENDED && hasNextEpisode() && Preferences.autoplay) {
|
||||||
playNextEpisode()
|
playNextEpisode()
|
||||||
@ -284,11 +288,11 @@ class PlayerActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun initTimeUpdates() {
|
private fun initTimeUpdates() {
|
||||||
if (this::timerUpdates.isInitialized) {
|
if (this::controlsUpdates.isInitialized) {
|
||||||
timerUpdates.cancel()
|
controlsUpdates.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
timerUpdates = Timer().scheduleAtFixedRate(0, 500) {
|
controlsUpdates = Timer().scheduleAtFixedRate(0, 500) {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
val currentPosition = model.player.currentPosition
|
val currentPosition = model.player.currentPosition
|
||||||
val btnNextEpIsVisible = playerBinding.buttonNextEp.isVisible
|
val btnNextEpIsVisible = playerBinding.buttonNextEp.isVisible
|
||||||
@ -298,12 +302,14 @@ class PlayerActivity : AppCompatActivity() {
|
|||||||
if (model.player.duration > 0) {
|
if (model.player.duration > 0) {
|
||||||
remainingTime = model.player.duration - currentPosition
|
remainingTime = model.player.duration - currentPosition
|
||||||
remainingTime = if (remainingTime < 0) 0 else remainingTime
|
remainingTime = if (remainingTime < 0) 0 else remainingTime
|
||||||
|
} else {
|
||||||
|
remainingTime = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO add metaDB ending_start support
|
// TODO add metaDB ending_start support
|
||||||
// if remaining time < 20 sec, a next ep is set, autoplay is enabled and not in pip:
|
// if remaining time > 1 and < 20 sec, a next ep is set, autoplay is enabled
|
||||||
// show next ep button
|
// and not in pip: show next ep button
|
||||||
if (remainingTime in 1..20000) {
|
if (remainingTime in 1000..20000) {
|
||||||
if (!btnNextEpIsVisible && hasNextEpisode() && Preferences.autoplay && !isInPiPMode()) {
|
if (!btnNextEpIsVisible && hasNextEpisode() && Preferences.autoplay && !isInPiPMode()) {
|
||||||
showButtonNextEp()
|
showButtonNextEp()
|
||||||
}
|
}
|
||||||
@ -337,7 +343,7 @@ class PlayerActivity : AppCompatActivity() {
|
|||||||
private fun onPauseOnStop() {
|
private fun onPauseOnStop() {
|
||||||
playerBinding.videoView.onPause()
|
playerBinding.videoView.onPause()
|
||||||
model.player.pause()
|
model.player.pause()
|
||||||
timerUpdates.cancel()
|
controlsUpdates.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -424,8 +430,16 @@ class PlayerActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun playNextEpisode() {
|
private fun playNextEpisode() {
|
||||||
model.playNextEpisode()
|
// disable the next episode buttons, so a user can't double click it
|
||||||
|
playerBinding.buttonNextEp.isClickable = false
|
||||||
|
controlsBinding.buttonNextEpC.isClickable = false
|
||||||
|
|
||||||
hideButtonNextEp()
|
hideButtonNextEp()
|
||||||
|
model.playNextEpisode()
|
||||||
|
|
||||||
|
// enable the next episode buttons when playNextEpisode() has returned
|
||||||
|
playerBinding.buttonNextEp.isClickable = true
|
||||||
|
controlsBinding.buttonNextEpC.isClickable = true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun skipOpening() {
|
private fun skipOpening() {
|
||||||
@ -457,12 +471,11 @@ class PlayerActivity : AppCompatActivity() {
|
|||||||
playerBinding.buttonNextEp.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)
|
||||||
playerBinding.buttonNextEp.isVisible = false
|
playerBinding.buttonNextEp.isVisible = false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showButtonSkipOp() {
|
private fun showButtonSkipOp() {
|
||||||
@ -478,7 +491,7 @@ class PlayerActivity : AppCompatActivity() {
|
|||||||
playerBinding.buttonSkipOp.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)
|
||||||
playerBinding.buttonSkipOp.isVisible = false
|
playerBinding.buttonSkipOp.isVisible = false
|
||||||
}
|
}
|
||||||
@ -510,7 +523,7 @@ class PlayerActivity : AppCompatActivity() {
|
|||||||
/**
|
/**
|
||||||
* on single tap hide or show the controls
|
* on single tap hide or show the controls
|
||||||
*/
|
*/
|
||||||
override fun onSingleTapConfirmed(e: MotionEvent?): Boolean {
|
override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
|
||||||
if (!isInPiPMode()) {
|
if (!isInPiPMode()) {
|
||||||
if (controller.isVisible) controller.hide() else controller.show()
|
if (controller.isVisible) controller.hide() else controller.show()
|
||||||
}
|
}
|
||||||
@ -521,8 +534,8 @@ class PlayerActivity : AppCompatActivity() {
|
|||||||
/**
|
/**
|
||||||
* on double tap rewind or forward
|
* on double tap rewind or forward
|
||||||
*/
|
*/
|
||||||
override fun onDoubleTap(e: MotionEvent?): Boolean {
|
override fun onDoubleTap(e: MotionEvent): Boolean {
|
||||||
val eventPosX = e?.x?.toInt() ?: 0
|
val eventPosX = e.x.toInt()
|
||||||
val viewCenterX = playerBinding.videoView.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
|
||||||
@ -534,14 +547,14 @@ class PlayerActivity : AppCompatActivity() {
|
|||||||
/**
|
/**
|
||||||
* not used
|
* not used
|
||||||
*/
|
*/
|
||||||
override fun onDoubleTapEvent(e: MotionEvent?): Boolean {
|
override fun onDoubleTapEvent(e: MotionEvent): Boolean {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* on long press toggle pause/play
|
* on long press toggle pause/play
|
||||||
*/
|
*/
|
||||||
override fun onLongPress(e: MotionEvent?) {
|
override fun onLongPress(e: MotionEvent) {
|
||||||
model.togglePausePlay()
|
model.togglePausePlay()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,10 +32,7 @@ import com.google.android.exoplayer2.ExoPlayer
|
|||||||
import com.google.android.exoplayer2.MediaItem
|
import com.google.android.exoplayer2.MediaItem
|
||||||
import com.google.android.exoplayer2.Player
|
import com.google.android.exoplayer2.Player
|
||||||
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
|
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.joinAll
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import org.mosad.teapod.R
|
import org.mosad.teapod.R
|
||||||
import org.mosad.teapod.parser.crunchyroll.*
|
import org.mosad.teapod.parser.crunchyroll.*
|
||||||
import org.mosad.teapod.preferences.Preferences
|
import org.mosad.teapod.preferences.Preferences
|
||||||
@ -44,6 +41,7 @@ import org.mosad.teapod.util.metadb.Meta
|
|||||||
import org.mosad.teapod.util.metadb.MetaDBController
|
import org.mosad.teapod.util.metadb.MetaDBController
|
||||||
import org.mosad.teapod.util.metadb.TVShowMeta
|
import org.mosad.teapod.util.metadb.TVShowMeta
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import kotlin.concurrent.scheduleAtFixedRate
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PlayerViewModel handles all stuff related to media/episodes.
|
* PlayerViewModel handles all stuff related to media/episodes.
|
||||||
@ -55,6 +53,7 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
|
|||||||
|
|
||||||
val player = ExoPlayer.Builder(application).build()
|
val player = ExoPlayer.Builder(application).build()
|
||||||
private val mediaSession = MediaSessionCompat(application, "TEAPOD_PLAYER_SESSION")
|
private val mediaSession = MediaSessionCompat(application, "TEAPOD_PLAYER_SESSION")
|
||||||
|
private val playheadAutoUpdate: TimerTask
|
||||||
|
|
||||||
val currentEpisodeChangedListener = ArrayList<() -> Unit>()
|
val currentEpisodeChangedListener = ArrayList<() -> Unit>()
|
||||||
private var currentPlayhead: Long = 0
|
private var currentPlayhead: Long = 0
|
||||||
@ -96,6 +95,14 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
|
|||||||
if (!isPlaying) updatePlayhead()
|
if (!isPlaying) updatePlayhead()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
playheadAutoUpdate = Timer().scheduleAtFixedRate(0, 30000) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
if (player.isPlaying){
|
||||||
|
updatePlayhead()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
@ -128,8 +135,6 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
|
|||||||
currentPlayheads = Crunchyroll.playheads(episodeIDs)
|
currentPlayheads = Crunchyroll.playheads(episodeIDs)
|
||||||
}
|
}
|
||||||
).joinAll()
|
).joinAll()
|
||||||
|
|
||||||
|
|
||||||
Log.d(classTag, "meta: $mediaMeta")
|
Log.d(classTag, "meta: $mediaMeta")
|
||||||
|
|
||||||
setCurrentEpisode(episodeId)
|
setCurrentEpisode(episodeId)
|
||||||
@ -168,11 +173,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
|
||||||
}
|
}
|
||||||
@ -275,7 +281,8 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
|
|||||||
val playhead = (player.currentPosition / 1000)
|
val playhead = (player.currentPosition / 1000)
|
||||||
|
|
||||||
if (playhead > 0 && Preferences.updatePlayhead) {
|
if (playhead > 0 && Preferences.updatePlayhead) {
|
||||||
viewModelScope.launch { Crunchyroll.postPlayheads(currentEpisode.id, playhead.toInt()) }
|
// don't use viewModelScope here. This task may needs to finish, when ViewModel will be cleared
|
||||||
|
CoroutineScope(Dispatchers.IO).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.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,8 +51,8 @@ class EpisodeListDialogFragment : DialogFragment() {
|
|||||||
EpisodeItemAdapter.ViewType.PLAYER
|
EpisodeItemAdapter.ViewType.PLAYER
|
||||||
)
|
)
|
||||||
|
|
||||||
// episodeNumber starts at 1, we need the episode index -> - 1
|
// get the position/index of the currently playing episode
|
||||||
adapterRecEpisodes.currentSelected = model.currentEpisode.episodeNumber?.minus(1) ?: 0
|
adapterRecEpisodes.currentSelected = model.episodes.items.indexOfFirst { it.id == model.currentEpisode.id }
|
||||||
|
|
||||||
binding.recyclerEpisodesPlayer.adapter = adapterRecEpisodes
|
binding.recyclerEpisodesPlayer.adapter = adapterRecEpisodes
|
||||||
binding.recyclerEpisodesPlayer.scrollToPosition(adapterRecEpisodes.currentSelected)
|
binding.recyclerEpisodesPlayer.scrollToPosition(adapterRecEpisodes.currentSelected)
|
||||||
|
@ -28,7 +28,7 @@ class FastForwardButton(context: Context, attrs: AttributeSet?): FrameLayout(con
|
|||||||
repeatCount = 1
|
repeatCount = 1
|
||||||
repeatMode = ObjectAnimator.REVERSE
|
repeatMode = ObjectAnimator.REVERSE
|
||||||
addListener(object : AnimatorListenerAdapter() {
|
addListener(object : AnimatorListenerAdapter() {
|
||||||
override fun onAnimationStart(animation: Animator?) {
|
override fun onAnimationStart(animation: Animator) {
|
||||||
binding.imageButton.isEnabled = false // disable button
|
binding.imageButton.isEnabled = false // disable button
|
||||||
binding.imageButton.setBackgroundResource(R.drawable.ic_baseline_forward_24)
|
binding.imageButton.setBackgroundResource(R.drawable.ic_baseline_forward_24)
|
||||||
}
|
}
|
||||||
@ -39,7 +39,7 @@ class FastForwardButton(context: Context, attrs: AttributeSet?): FrameLayout(con
|
|||||||
duration = animationDuration
|
duration = animationDuration
|
||||||
addListener(object : AnimatorListenerAdapter() {
|
addListener(object : AnimatorListenerAdapter() {
|
||||||
// the label animation takes longer then the button animation, reset stuff in here
|
// the label animation takes longer then the button animation, reset stuff in here
|
||||||
override fun onAnimationEnd(animation: Animator?) {
|
override fun onAnimationEnd(animation: Animator) {
|
||||||
binding.imageButton.isEnabled = true // enable button
|
binding.imageButton.isEnabled = true // enable button
|
||||||
binding.imageButton.setBackgroundResource(R.drawable.ic_baseline_forward_10_24)
|
binding.imageButton.setBackgroundResource(R.drawable.ic_baseline_forward_10_24)
|
||||||
|
|
||||||
|
@ -28,7 +28,7 @@ class RewindButton(context: Context, attrs: AttributeSet): FrameLayout(context,
|
|||||||
repeatCount = 1
|
repeatCount = 1
|
||||||
repeatMode = ObjectAnimator.REVERSE
|
repeatMode = ObjectAnimator.REVERSE
|
||||||
addListener(object : AnimatorListenerAdapter() {
|
addListener(object : AnimatorListenerAdapter() {
|
||||||
override fun onAnimationStart(animation: Animator?) {
|
override fun onAnimationStart(animation: Animator) {
|
||||||
binding.imageButton.isEnabled = false // disable button
|
binding.imageButton.isEnabled = false // disable button
|
||||||
binding.imageButton.setBackgroundResource(R.drawable.ic_baseline_rewind_24)
|
binding.imageButton.setBackgroundResource(R.drawable.ic_baseline_rewind_24)
|
||||||
}
|
}
|
||||||
@ -38,7 +38,7 @@ class RewindButton(context: Context, attrs: AttributeSet): FrameLayout(context,
|
|||||||
labelAnimation = ObjectAnimator.ofFloat(binding.textView, View.TRANSLATION_X, -35f).apply {
|
labelAnimation = ObjectAnimator.ofFloat(binding.textView, View.TRANSLATION_X, -35f).apply {
|
||||||
duration = animationDuration
|
duration = animationDuration
|
||||||
addListener(object : AnimatorListenerAdapter() {
|
addListener(object : AnimatorListenerAdapter() {
|
||||||
override fun onAnimationEnd(animation: Animator?) {
|
override fun onAnimationEnd(animation: Animator) {
|
||||||
binding.imageButton.isEnabled = true // enable button
|
binding.imageButton.isEnabled = true // enable button
|
||||||
binding.imageButton.setBackgroundResource(R.drawable.ic_baseline_rewind_10_24)
|
binding.imageButton.setBackgroundResource(R.drawable.ic_baseline_rewind_10_24)
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ import androidx.fragment.app.Fragment
|
|||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import androidx.fragment.app.commit
|
import androidx.fragment.app.commit
|
||||||
import org.mosad.teapod.R
|
import org.mosad.teapod.R
|
||||||
|
import org.mosad.teapod.ui.activity.player.PlayerActivity
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -24,6 +25,20 @@ fun FragmentActivity.showFragment(fragment: Fragment) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the player as new activity.
|
||||||
|
*
|
||||||
|
* @param seasonId The ID of the season the episode to be played is in
|
||||||
|
* @param episodeId The ID of the episode to play
|
||||||
|
*/
|
||||||
|
fun Activity.startPlayer(seasonId: String, episodeId: String) {
|
||||||
|
val intent = Intent(this, PlayerActivity::class.java).apply {
|
||||||
|
putExtra(getString(R.string.intent_season_id), seasonId)
|
||||||
|
putExtra(getString(R.string.intent_episode_id), episodeId)
|
||||||
|
}
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* hide the status and navigation bar
|
* hide the status and navigation bar
|
||||||
*/
|
*/
|
||||||
|
@ -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,
|
||||||
|
@ -24,11 +24,12 @@ package org.mosad.teapod.util.metadb
|
|||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
import io.ktor.client.features.*
|
import io.ktor.client.call.*
|
||||||
import io.ktor.client.features.json.*
|
import io.ktor.client.plugins.*
|
||||||
import io.ktor.client.features.json.serializer.*
|
import io.ktor.client.plugins.contentnegotiation.*
|
||||||
import io.ktor.client.request.*
|
import io.ktor.client.request.*
|
||||||
import io.ktor.http.*
|
import io.ktor.http.*
|
||||||
|
import io.ktor.serialization.kotlinx.json.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
@ -40,8 +41,8 @@ object MetaDBController {
|
|||||||
private const val repoUrl = "https://gitlab.com/Seil0/teapodmetadb/-/raw/main/crunchy/"
|
private const val repoUrl = "https://gitlab.com/Seil0/teapodmetadb/-/raw/main/crunchy/"
|
||||||
|
|
||||||
private val client = HttpClient {
|
private val client = HttpClient {
|
||||||
install(JsonFeature) {
|
install(ContentNegotiation) {
|
||||||
serializer = KotlinxSerializer(Json)
|
json()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,7 +50,7 @@ object MetaDBController {
|
|||||||
private var metaCacheList = arrayListOf<Meta>()
|
private var metaCacheList = arrayListOf<Meta>()
|
||||||
|
|
||||||
suspend fun list() = withContext(Dispatchers.IO) {
|
suspend fun list() = withContext(Dispatchers.IO) {
|
||||||
val raw: String = client.get("$repoUrl/list.json")
|
val raw: String = client.get("$repoUrl/list.json").body()
|
||||||
mediaList = Json.decodeFromString(raw)
|
mediaList = Json.decodeFromString(raw)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,7 +71,7 @@ object MetaDBController {
|
|||||||
|
|
||||||
private suspend fun getTVShowMetadataFromDB(crSeriesId: String): TVShowMeta? = withContext(Dispatchers.IO) {
|
private suspend fun getTVShowMetadataFromDB(crSeriesId: String): TVShowMeta? = withContext(Dispatchers.IO) {
|
||||||
return@withContext try {
|
return@withContext try {
|
||||||
val raw: String = client.get("$repoUrl/tv/$crSeriesId/media.json")
|
val raw: String = client.get("$repoUrl/tv/$crSeriesId/media.json").body()
|
||||||
val meta: TVShowMeta = Json.decodeFromString(raw)
|
val meta: TVShowMeta = Json.decodeFromString(raw)
|
||||||
metaCacheList.add(meta)
|
metaCacheList.add(meta)
|
||||||
|
|
||||||
|
@ -25,10 +25,10 @@ package org.mosad.teapod.util.tmdb
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
import io.ktor.client.call.*
|
import io.ktor.client.call.*
|
||||||
import io.ktor.client.features.json.*
|
import io.ktor.client.plugins.contentnegotiation.*
|
||||||
import io.ktor.client.features.json.serializer.*
|
|
||||||
import io.ktor.client.request.*
|
import io.ktor.client.request.*
|
||||||
import io.ktor.client.statement.*
|
import io.ktor.client.statement.*
|
||||||
|
import io.ktor.serialization.kotlinx.json.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.invoke
|
import kotlinx.coroutines.invoke
|
||||||
@ -46,10 +46,11 @@ import org.mosad.teapod.util.concatenate
|
|||||||
class TMDBApiController {
|
class TMDBApiController {
|
||||||
private val classTag = javaClass.name
|
private val classTag = javaClass.name
|
||||||
|
|
||||||
private val json = Json { ignoreUnknownKeys = true }
|
|
||||||
private val client = HttpClient {
|
private val client = HttpClient {
|
||||||
install(JsonFeature) {
|
install(ContentNegotiation) {
|
||||||
serializer = KotlinxSerializer(json)
|
json(Json {
|
||||||
|
ignoreUnknownKeys = true
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,7 +79,7 @@ class TMDBApiController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
response.receive<T>()
|
response.body<T>()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,7 +90,7 @@ class TMDBApiController {
|
|||||||
* NoneTMDBSearchMovie if nothing was found
|
* NoneTMDBSearchMovie if nothing was found
|
||||||
*/
|
*/
|
||||||
suspend fun searchMovie(query: String): TMDBSearch<TMDBSearchResultMovie> {
|
suspend fun searchMovie(query: String): TMDBSearch<TMDBSearchResultMovie> {
|
||||||
val searchEndpoint = "/search/multi"
|
val searchEndpoint = "/search/movie"
|
||||||
val parameters = listOf("query" to query, "include_adult" to false)
|
val parameters = listOf("query" to query, "include_adult" to false)
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
|
@ -32,7 +32,7 @@ import kotlinx.serialization.Serializable
|
|||||||
|
|
||||||
interface TMDBResult {
|
interface TMDBResult {
|
||||||
val id: Int
|
val id: Int
|
||||||
val name: String
|
val name: String? // for movies tmdb return string or null
|
||||||
val overview: String? // for movies tmdb return string or null
|
val overview: String? // for movies tmdb return string or null
|
||||||
val posterPath: String?
|
val posterPath: String?
|
||||||
val backdropPath: String?
|
val backdropPath: String?
|
||||||
@ -40,7 +40,7 @@ interface TMDBResult {
|
|||||||
|
|
||||||
data class TMDBBase(
|
data class TMDBBase(
|
||||||
override val id: Int,
|
override val id: Int,
|
||||||
override val name: String,
|
override val name: String?,
|
||||||
override val overview: String?,
|
override val overview: String?,
|
||||||
override val posterPath: String?,
|
override val posterPath: String?,
|
||||||
override val backdropPath: String?
|
override val backdropPath: String?
|
||||||
@ -59,7 +59,7 @@ data class TMDBSearch<T>(
|
|||||||
@Serializable
|
@Serializable
|
||||||
data class TMDBSearchResultMovie(
|
data class TMDBSearchResultMovie(
|
||||||
@SerialName("id") override val id: Int,
|
@SerialName("id") override val id: Int,
|
||||||
@SerialName("title") override val name: String,
|
@SerialName("title") override val name: String?,
|
||||||
@SerialName("overview") override val overview: String?,
|
@SerialName("overview") override val overview: String?,
|
||||||
@SerialName("poster_path") override val posterPath: String?,
|
@SerialName("poster_path") override val posterPath: String?,
|
||||||
@SerialName("backdrop_path") override val backdropPath: String?,
|
@SerialName("backdrop_path") override val backdropPath: String?,
|
||||||
@ -68,7 +68,7 @@ data class TMDBSearchResultMovie(
|
|||||||
@Serializable
|
@Serializable
|
||||||
data class TMDBSearchResultTVShow(
|
data class TMDBSearchResultTVShow(
|
||||||
@SerialName("id") override val id: Int,
|
@SerialName("id") override val id: Int,
|
||||||
@SerialName("name") override val name: String,
|
@SerialName("name") override val name: String?,
|
||||||
@SerialName("overview") override val overview: String?,
|
@SerialName("overview") override val overview: String?,
|
||||||
@SerialName("poster_path") override val posterPath: String?,
|
@SerialName("poster_path") override val posterPath: String?,
|
||||||
@SerialName("backdrop_path") override val backdropPath: String?,
|
@SerialName("backdrop_path") override val backdropPath: String?,
|
||||||
@ -92,7 +92,7 @@ data class TMDBMovie(
|
|||||||
@SerialName("release_date") val releaseDate: String,
|
@SerialName("release_date") val releaseDate: String,
|
||||||
@SerialName("runtime") val runtime: Int?,
|
@SerialName("runtime") val runtime: Int?,
|
||||||
@SerialName("status") val status: String,
|
@SerialName("status") val status: String,
|
||||||
// TODO generes
|
// TODO genres
|
||||||
) : TMDBResult
|
) : TMDBResult
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@ -105,7 +105,7 @@ data class TMDBTVShow(
|
|||||||
@SerialName("first_air_date") val firstAirDate: String,
|
@SerialName("first_air_date") val firstAirDate: String,
|
||||||
@SerialName("last_air_date") val lastAirDate: String,
|
@SerialName("last_air_date") val lastAirDate: String,
|
||||||
@SerialName("status") val status: String,
|
@SerialName("status") val status: String,
|
||||||
// TODO generes
|
// TODO genres
|
||||||
) : TMDBResult
|
) : TMDBResult
|
||||||
|
|
||||||
// use null for nullable types, the gui needs to handle/implement a fallback for null values
|
// use null for nullable types, the gui needs to handle/implement a fallback for null values
|
||||||
|
7
app/src/main/res/drawable/placeholder_image.xml
Normal file
7
app/src/main/res/drawable/placeholder_image.xml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<solid android:color="?shapeTextBackground"/>
|
||||||
|
<size
|
||||||
|
android:width="1920px"
|
||||||
|
android:height="1080px"/>
|
||||||
|
</shape>
|
@ -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"
|
||||||
|
@ -112,7 +112,7 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:text="@string/account_subscription"
|
android:text="@string/loading"
|
||||||
android:textSize="16sp" />
|
android:textSize="16sp" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
@ -120,7 +120,7 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:text="@string/account_subscription_desc"
|
android:text="@string/account_tier"
|
||||||
android:textColor="?textSecondary" />
|
android:textColor="?textSecondary" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
@ -17,6 +17,16 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<com.facebook.shimmer.ShimmerFrameLayout
|
||||||
|
android:id="@+id/shimmer_layout_highlight"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:visibility="gone">
|
||||||
|
|
||||||
|
<include layout="@layout/item_highlight_shimmer" />
|
||||||
|
|
||||||
|
</com.facebook.shimmer.ShimmerFrameLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/linear_highlight"
|
android:id="@+id/linear_highlight"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@ -126,6 +136,23 @@
|
|||||||
android:textSize="16sp"
|
android:textSize="16sp"
|
||||||
android:textStyle="bold" />
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<com.facebook.shimmer.ShimmerFrameLayout
|
||||||
|
android:id="@+id/shimmer_layout_up_next"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:visibility="gone">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
<include layout="@layout/item_media_shimmer" />
|
||||||
|
<include layout="@layout/item_media_shimmer" />
|
||||||
|
<include layout="@layout/item_media_shimmer" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</com.facebook.shimmer.ShimmerFrameLayout>
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/recycler_up_next"
|
android:id="@+id/recycler_up_next"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@ -154,6 +181,23 @@
|
|||||||
android:textSize="16sp"
|
android:textSize="16sp"
|
||||||
android:textStyle="bold" />
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<com.facebook.shimmer.ShimmerFrameLayout
|
||||||
|
android:id="@+id/shimmer_layout_watchlist"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:visibility="gone">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
<include layout="@layout/item_media_shimmer" />
|
||||||
|
<include layout="@layout/item_media_shimmer" />
|
||||||
|
<include layout="@layout/item_media_shimmer" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</com.facebook.shimmer.ShimmerFrameLayout>
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/recycler_watchlist"
|
android:id="@+id/recycler_watchlist"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@ -163,6 +207,51 @@
|
|||||||
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" />
|
||||||
|
|
||||||
|
<com.facebook.shimmer.ShimmerFrameLayout
|
||||||
|
android:id="@+id/shimmer_layout_recommendations"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:visibility="gone">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
<include layout="@layout/item_media_shimmer" />
|
||||||
|
<include layout="@layout/item_media_shimmer" />
|
||||||
|
<include layout="@layout/item_media_shimmer" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</com.facebook.shimmer.ShimmerFrameLayout>
|
||||||
|
|
||||||
|
<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"
|
||||||
@ -182,6 +271,23 @@
|
|||||||
android:textSize="16sp"
|
android:textSize="16sp"
|
||||||
android:textStyle="bold" />
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<com.facebook.shimmer.ShimmerFrameLayout
|
||||||
|
android:id="@+id/shimmer_layout_new_titles"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:visibility="gone">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
<include layout="@layout/item_media_shimmer" />
|
||||||
|
<include layout="@layout/item_media_shimmer" />
|
||||||
|
<include layout="@layout/item_media_shimmer" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</com.facebook.shimmer.ShimmerFrameLayout>
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/recycler_new_titles"
|
android:id="@+id/recycler_new_titles"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@ -210,6 +316,23 @@
|
|||||||
android:textSize="16sp"
|
android:textSize="16sp"
|
||||||
android:textStyle="bold" />
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<com.facebook.shimmer.ShimmerFrameLayout
|
||||||
|
android:id="@+id/shimmer_layout_top_ten"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:visibility="gone">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
<include layout="@layout/item_media_shimmer" />
|
||||||
|
<include layout="@layout/item_media_shimmer" />
|
||||||
|
<include layout="@layout/item_media_shimmer" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</com.facebook.shimmer.ShimmerFrameLayout>
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/recycler_top_ten"
|
android:id="@+id/recycler_top_ten"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
96
app/src/main/res/layout/item_highlight_shimmer.xml
Normal file
96
app/src/main/res/layout/item_highlight_shimmer.xml
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?themePrimary">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/shimmer_image_highlight"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:src="@drawable/placeholder_image"
|
||||||
|
app:layout_constraintDimensionRatio="H,16:9"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:ignore="ContentDescription" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/shimmer_linear_highlight"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?themePrimary"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingBottom="7dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/shimmer_image_highlight">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/image_dummy_text"
|
||||||
|
android:layout_width="128dp"
|
||||||
|
android:layout_height="21dp"
|
||||||
|
android:layout_marginTop="7dp"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
app:srcCompat="@drawable/shape_rounded_corner"
|
||||||
|
tools:ignore="ContentDescription" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginTop="7dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:layout_weight="1" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/shimmer_text_highlight_my_list"
|
||||||
|
android:layout_width="64dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center"
|
||||||
|
android:textSize="12sp"
|
||||||
|
app:drawableTint="?shapeTextBackground"
|
||||||
|
app:drawableTopCompat="@drawable/ic_baseline_add_24" />
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:layout_weight="1" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/shimmer_button_play_highlight"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center"
|
||||||
|
android:textSize="16sp"
|
||||||
|
app:backgroundTint="?shapeTextBackground" />
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:layout_weight="1" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/shimmer_text_highlight_info"
|
||||||
|
android:layout_width="64dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center"
|
||||||
|
app:drawableTint="?shapeTextBackground"
|
||||||
|
app:drawableTopCompat="@drawable/ic_outline_info_24" />
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:layout_weight="1" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -2,16 +2,16 @@
|
|||||||
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
|
<com.google.android.material.card.MaterialCardView 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:layout_width="195dp"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:backgroundTint="?themeSecondary"
|
android:backgroundTint="?themeSecondary"
|
||||||
android:visibility="visible"
|
|
||||||
app:cardCornerRadius="7dp"
|
app:cardCornerRadius="7dp"
|
||||||
app:cardElevation="4dp">
|
app:cardElevation="4dp">
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintWidth_max="195dp">
|
||||||
|
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
android:id="@+id/frame_image_progress"
|
android:id="@+id/frame_image_progress"
|
||||||
@ -21,7 +21,8 @@
|
|||||||
app:layout_constraintDimensionRatio="H,16:9"
|
app:layout_constraintDimensionRatio="H,16:9"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent">
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintWidth="195dp">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/image_poster"
|
android:id="@+id/image_poster"
|
||||||
@ -53,7 +54,7 @@
|
|||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/text_title"
|
android:id="@+id/text_title"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:lines="2"
|
android:lines="2"
|
||||||
@ -62,6 +63,8 @@
|
|||||||
android:text="@string/text_title_ex"
|
android:text="@string/text_title_ex"
|
||||||
android:textAlignment="center"
|
android:textAlignment="center"
|
||||||
android:textSize="15sp"
|
android:textSize="15sp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/frame_image_progress" />
|
app:layout_constraintTop_toBottomOf="@+id/frame_image_progress" />
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
50
app/src/main/res/layout/item_media_shimmer.xml
Normal file
50
app/src/main/res/layout/item_media_shimmer.xml
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="4dp"
|
||||||
|
android:layout_marginEnd="3dp"
|
||||||
|
android:backgroundTint="?themeSecondary"
|
||||||
|
app:cardCornerRadius="7dp"
|
||||||
|
app:cardElevation="4dp">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:layout_constraintWidth_max="195dp">
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/frame_image_progress"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
app:layout_constraintDimensionRatio="H,16:9"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintWidth="195dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/image_poster"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="?shapeTextBackground"
|
||||||
|
tools:ignore="ContentDescription" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/image_dummy_text"
|
||||||
|
android:layout_width="128dp"
|
||||||
|
android:layout_height="19dp"
|
||||||
|
android:layout_margin="11dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/frame_image_progress"
|
||||||
|
app:srcCompat="@drawable/shape_rounded_corner"
|
||||||
|
tools:ignore="ContentDescription" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
@ -101,6 +101,7 @@
|
|||||||
android:layout_height="@dimen/player_styled_progress_layout_height"
|
android:layout_height="@dimen/player_styled_progress_layout_height"
|
||||||
android:contentDescription="@string/desc_time_bar"
|
android:contentDescription="@string/desc_time_bar"
|
||||||
app:bar_height="3dp"
|
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"
|
||||||
|
@ -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>
|
||||||
@ -36,6 +37,8 @@
|
|||||||
<string name="account_login_desc">Zum bearbeiten tippen</string>
|
<string name="account_login_desc">Zum bearbeiten tippen</string>
|
||||||
<string name="account_subscription">Abo %1$s</string>
|
<string name="account_subscription">Abo %1$s</string>
|
||||||
<string name="account_subscription_desc">Zum verlängern tippen</string>
|
<string name="account_subscription_desc">Zum verlängern tippen</string>
|
||||||
|
<string name="account_premium">Premium Mitglied</string>
|
||||||
|
<string name="account_tier">Typ: %1$s</string>
|
||||||
<string name="info">Info</string>
|
<string name="info">Info</string>
|
||||||
<string name="info_about_desc">Version %1$s (%2$s)</string>
|
<string name="info_about_desc">Version %1$s (%2$s)</string>
|
||||||
<string name="settings">Einstellungen</string>
|
<string name="settings">Einstellungen</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>
|
||||||
@ -48,6 +49,11 @@
|
|||||||
<string name="account_login_desc">Tap to edit</string>
|
<string name="account_login_desc">Tap to edit</string>
|
||||||
<string name="account_subscription">Subscription %1$s</string>
|
<string name="account_subscription">Subscription %1$s</string>
|
||||||
<string name="account_subscription_desc">Tap to extend</string>
|
<string name="account_subscription_desc">Tap to extend</string>
|
||||||
|
<string name="account_premium">Premium member</string>
|
||||||
|
<string name="account_tier">Tier: %1$s</string>
|
||||||
|
<string name="account_tier_fan" translatable="false">Fan</string>
|
||||||
|
<string name="account_tier_mega_fan" translatable="false">Mega Fan</string>
|
||||||
|
<string name="account_tier_ultimate_fan" translatable="false">Ultimate Fan</string>
|
||||||
<string name="settings">Settings</string>
|
<string name="settings">Settings</string>
|
||||||
<string name="settings_content_language">Preferred content language</string>
|
<string name="settings_content_language">Preferred content language</string>
|
||||||
<string name="settings_content_language_desc">English</string>
|
<string name="settings_content_language_desc">English</string>
|
||||||
|
@ -1,17 +0,0 @@
|
|||||||
package org.mosad.teapod
|
|
||||||
|
|
||||||
import org.junit.Test
|
|
||||||
|
|
||||||
import org.junit.Assert.*
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Example local unit test, which will execute on the development machine (host).
|
|
||||||
*
|
|
||||||
* See [testing documentation](http://d.android.com/tools/testing).
|
|
||||||
*/
|
|
||||||
class ExampleUnitTest {
|
|
||||||
@Test
|
|
||||||
fun addition_isCorrect() {
|
|
||||||
assertEquals(4, 2 + 2)
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,24 @@
|
|||||||
|
package org.mosad.teapod.parser.crunchyroll
|
||||||
|
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import org.junit.Assert
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class DataTypesTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testTokenType() {
|
||||||
|
val testToken = javaClass.getResource("/token.json")!!.readText()
|
||||||
|
val token: Token = Json.decodeFromString(testToken)
|
||||||
|
|
||||||
|
Assert.assertEquals("TestAccessToken-1_TestAccessToken", token.accessToken)
|
||||||
|
Assert.assertEquals("00000000-0000-0000-0000-000000000000", token.refreshToken)
|
||||||
|
Assert.assertEquals(300, token.expiresIn)
|
||||||
|
Assert.assertEquals("Bearer", token.tokenType)
|
||||||
|
Assert.assertEquals("account content offline_access reviews talkbox", token.scope)
|
||||||
|
Assert.assertEquals("DE", token.country)
|
||||||
|
Assert.assertEquals("00000000-0000-0000-0000-000000000000", token.accountId)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
9
app/src/test/resources/token.json
Normal file
9
app/src/test/resources/token.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"access_token":"TestAccessToken-1_TestAccessToken",
|
||||||
|
"refresh_token":"00000000-0000-0000-0000-000000000000",
|
||||||
|
"expires_in":300,
|
||||||
|
"token_type":"Bearer",
|
||||||
|
"scope":"account content offline_access reviews talkbox",
|
||||||
|
"country":"DE",
|
||||||
|
"account_id":"00000000-0000-0000-0000-000000000000"
|
||||||
|
}
|
@ -1,14 +1,14 @@
|
|||||||
// 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.7.10"
|
||||||
ext.ktor_version = "1.6.8"
|
ext.ktor_version = "2.1.1"
|
||||||
ext.exo_version = "2.17.1"
|
ext.exo_version = "2.17.1"
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:7.1.3'
|
classpath 'com.android.tools.build:gradle:7.3.0'
|
||||||
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
|
||||||
|
6
fastlane/metadata/android/de/changelogs/100000.txt
Normal file
6
fastlane/metadata/android/de/changelogs/100000.txt
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
Dies ist der erste stabile Release von Teapod mit Unterstützung für Cunchyroll.
|
||||||
|
|
||||||
|
* Unterstützung für Crunchyroll hinzugefügt (Ein premium Account wird benötigt)
|
||||||
|
* Diverse UI/UX Verbesserungen
|
||||||
|
|
||||||
|
Alle Änderungen: https://git.mosad.xyz/Seil0/teapod/compare/0.4.2...1.0.0
|
@ -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
|
||||||
|
9
fastlane/metadata/android/de/changelogs/9020.txt
Normal file
9
fastlane/metadata/android/de/changelogs/9020.txt
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
Dies ist der dritte beta Release von Teapod 1.0.0 mit Unterstützung für Cunchyroll.
|
||||||
|
|
||||||
|
* Diverse UI/UX Verbesserungen
|
||||||
|
* Playhead Updates werden nun alle 30 Sekunden durchgeführt
|
||||||
|
* Fehlende Playhead Updates beim schließen des Players behoben (#62)
|
||||||
|
* Abo Status und Stufe zum Accountscreen hinzugefügt
|
||||||
|
* Das Verhalten des "Nächste Episode" Buttons wurde verbessert (#53)
|
||||||
|
|
||||||
|
Alle Änderungen https://git.mosad.xyz/Seil0/teapod/compare/1.0.0-beta2...1.0.0-beta3
|
6
fastlane/metadata/android/en-US/changelogs/100000.txt
Normal file
6
fastlane/metadata/android/en-US/changelogs/100000.txt
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
This is the first stable release of Teapod with support for crunchyroll.
|
||||||
|
|
||||||
|
* Support for crunchyroll (a premium account is needed)
|
||||||
|
* UI/UX improvements
|
||||||
|
|
||||||
|
Full changelog: https://git.mosad.xyz/Seil0/teapod/compare/0.4.2...1.0.0
|
@ -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
|
||||||
|
9
fastlane/metadata/android/en-US/changelogs/9020.txt
Normal file
9
fastlane/metadata/android/en-US/changelogs/9020.txt
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
This is the third beta release of Teapod 1.0.0 with support for crunchyroll.
|
||||||
|
|
||||||
|
* UI/UX improvements
|
||||||
|
* Playhead is now updated every 30 seconds
|
||||||
|
* Fixed missing playhead updates when closing the player (#62)
|
||||||
|
* Add subscription status and tier info to the account screen
|
||||||
|
* Improved the behaviour of the "next episde" button (#53)
|
||||||
|
|
||||||
|
Full changelog https://git.mosad.xyz/Seil0/teapod/compare/1.0.0-beta2...1.0.0-beta3
|
BIN
fastlane/metadata/android/en-US/images/icon.png
Normal file
BIN
fastlane/metadata/android/en-US/images/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,5 +1,5 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
Reference in New Issue
Block a user