Compare commits
26 Commits
1.0.0-beta
...
1.0.0-beta
Author | SHA1 | Date |
---|---|---|
Jannik | e835715b9c | |
Jannik | 001141337d | |
Jannik | 5cd3d25ebe | |
Jannik | 215e01c53a | |
Jannik | 1751963574 | |
Jannik | 9c3548a866 | |
Jannik | ebd96f9849 | |
Jannik | 85b17d7a76 | |
Jannik | f128efea0d | |
Jannik | da94003368 | |
Jannik | 3fdc2aff1b | |
Jannik | 326da147f1 | |
Jannik | f398c82f62 | |
Jannik | 821f8b5590 | |
Jannik | 0028cb6dd7 | |
Jannik | 127bd030b9 | |
Jannik | 3cadaa5c7a | |
Jannik | 97966f5ad3 | |
Jannik | 4c55bb771f | |
Jannik | 8eb737a831 | |
Jannik | 522b893dc8 | |
Jannik | 69e0b6bcca | |
Jannik | c34b95795f | |
Jannik | 9059306e90 | |
Jannik | ed0c0a4c61 | |
Jannik | 03a79346b7 |
|
@ -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 9020 //00.09.020
|
||||||
versionName "1.0.0-beta2"
|
versionName "1.0.0-beta3"
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
resValue "string", "build_time", buildTime()
|
resValue "string", "build_time", buildTime()
|
||||||
|
@ -48,33 +48,36 @@ android {
|
||||||
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.1'
|
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
|
||||||
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.3'
|
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-rc01'
|
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.2'
|
implementation 'androidx.navigation:navigation-fragment-ktx:2.5.2'
|
||||||
implementation 'androidx.navigation:navigation-ui-ktx:2.4.2'
|
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'
|
||||||
|
|
|
@ -25,13 +25,13 @@ 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.serialization.SerializationException
|
import kotlinx.serialization.SerializationException
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
@ -41,14 +41,14 @@ 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
|
||||||
|
|
||||||
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"
|
||||||
|
@ -61,6 +61,7 @@ object Crunchyroll {
|
||||||
private val tokenRefreshContext = newSingleThreadContext("TokenRefreshContext")
|
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 = ""
|
||||||
|
@ -76,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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -106,7 +107,7 @@ object Crunchyroll {
|
||||||
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) {
|
||||||
|
@ -154,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
|
||||||
}
|
}
|
||||||
|
@ -245,6 +246,7 @@ object Crunchyroll {
|
||||||
}
|
}
|
||||||
|
|
||||||
accountID = account.accountId
|
accountID = account.accountId
|
||||||
|
externalID = account.externalId
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -331,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
|
||||||
}
|
}
|
||||||
|
@ -355,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
|
||||||
}
|
}
|
||||||
|
@ -371,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
|
||||||
}
|
}
|
||||||
|
@ -395,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
|
||||||
}
|
}
|
||||||
|
@ -416,7 +418,7 @@ 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
|
||||||
}
|
}
|
||||||
|
@ -440,7 +442,7 @@ 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
|
||||||
}
|
}
|
||||||
|
@ -464,7 +466,7 @@ 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
|
||||||
}
|
}
|
||||||
|
@ -479,7 +481,7 @@ object Crunchyroll {
|
||||||
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
|
||||||
}
|
}
|
||||||
|
@ -502,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
|
||||||
}
|
}
|
||||||
|
@ -598,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
|
||||||
}
|
}
|
||||||
|
@ -623,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
|
||||||
}
|
}
|
||||||
|
@ -647,7 +649,7 @@ 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
|
||||||
}
|
}
|
||||||
|
@ -664,7 +666,7 @@ object Crunchyroll {
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
requestGet(recommendationsEndpoint, parameters)
|
requestGet(recommendationsEndpoint, parameters)
|
||||||
}catch (ex: SerializationException) {
|
} catch (ex: SerializationException) {
|
||||||
Log.e(TAG, "SerializationException in recommendations().", ex)
|
Log.e(TAG, "SerializationException in recommendations().", ex)
|
||||||
NoneRecommendationsList
|
NoneRecommendationsList
|
||||||
}
|
}
|
||||||
|
@ -684,7 +686,7 @@ object Crunchyroll {
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
@ -704,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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -125,6 +125,7 @@ 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 RecommendationsList = Collection<Item>
|
||||||
|
typealias Benefits = Collection<Benefit>
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class UpNextSeriesItem(
|
data class UpNextSeriesItem(
|
||||||
|
@ -226,6 +227,7 @@ 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 NoneRecommendationsList = RecommendationsList(0, emptyList())
|
||||||
|
val NoneBenefits = Benefits(0, emptyList())
|
||||||
|
|
||||||
val NoneUpNextSeriesItem = UpNextSeriesItem(
|
val NoneUpNextSeriesItem = UpNextSeriesItem(
|
||||||
playhead = 0,
|
playhead = 0,
|
||||||
|
@ -380,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(
|
||||||
|
@ -412,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,12 +27,15 @@ 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
|
||||||
|
@ -161,10 +164,44 @@ class HomeFragment : Fragment() {
|
||||||
binding.textHighlightInfo.setOnClickListener {
|
binding.textHighlightInfo.setOnClickListener {
|
||||||
activity?.showFragment(MediaFragment(uiState.highlightItem.id))
|
activity?.showFragment(MediaFragment(uiState.highlightItem.id))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// disable the shimmer effect and hide the shimmer layouts
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
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) {
|
||||||
|
|
|
@ -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,7 +471,7 @@ 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
|
||||||
}
|
}
|
||||||
|
@ -477,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
|
||||||
}
|
}
|
||||||
|
@ -509,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()
|
||||||
}
|
}
|
||||||
|
@ -520,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
|
||||||
|
@ -533,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)
|
||||||
|
@ -276,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)
|
||||||
|
|
||||||
|
|
|
@ -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>()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
|
@ -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"
|
||||||
|
@ -182,6 +226,23 @@
|
||||||
android:textSize="16sp"
|
android:textSize="16sp"
|
||||||
android:textStyle="bold" />
|
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
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/recycler_recommendations"
|
android:id="@+id/recycler_recommendations"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
@ -210,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"
|
||||||
|
@ -238,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"
|
||||||
|
|
|
@ -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">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/shimmer_text_highlight_title"
|
||||||
|
android:layout_width="120dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:layout_marginTop="7dp"
|
||||||
|
android:background="?shapeTextBackground"
|
||||||
|
android:textSize="16sp" />
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
<?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_constraintBottom_toTopOf="@+id/text_title"
|
||||||
|
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>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_title"
|
||||||
|
android:layout_width="128dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="11dp"
|
||||||
|
android:background="?shapeTextBackground"
|
||||||
|
android:textSize="15sp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/frame_image_progress" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
|
@ -37,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>
|
||||||
|
|
|
@ -49,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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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.21"
|
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.2.1'
|
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
|
||||||
|
|
|
@ -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
|
|
@ -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
|
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue