Compare commits
No commits in common. "develop" and "1.0.0-beta1" have entirely different histories.
develop
...
1.0.0-beta
|
@ -26,4 +26,4 @@ Currently you need to have an Crunchyroll account to contribute to Teapod. Contr
|
||||||
#### Why is it called Teapod?
|
#### Why is it called Teapod?
|
||||||
Teapod is a Acronym for "The ultimate anime app on demand", hence this project is called Teapod and not Teapot.
|
Teapod is a Acronym for "The ultimate anime app on demand", hence this project is called Teapod and not Teapot.
|
||||||
|
|
||||||
Teapod © 2020-2023 [@Seil0](https://git.mosad.xyz/Seil0)
|
Teapod © 2020-2022 [@Seil0](https://git.mosad.xyz/Seil0)
|
||||||
|
|
|
@ -1,26 +1,20 @@
|
||||||
plugins {
|
plugins {
|
||||||
id 'com.android.application'
|
id 'com.android.application'
|
||||||
id 'kotlin-android'
|
id 'kotlin-android'
|
||||||
|
id 'kotlin-android-extensions'
|
||||||
id 'org.jetbrains.kotlin.plugin.serialization' version "$kotlin_version"
|
id 'org.jetbrains.kotlin.plugin.serialization' version "$kotlin_version"
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlin {
|
|
||||||
jvmToolchain 11
|
|
||||||
sourceSets.configureEach {
|
|
||||||
languageSettings.optIn("kotlin.RequiresOptIn")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdk 34
|
compileSdkVersion 30
|
||||||
buildToolsVersion = '34.0.0'
|
buildToolsVersion "30.0.3"
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "org.mosad.teapod"
|
applicationId "org.mosad.teapod"
|
||||||
minSdk 23
|
minSdkVersion 23
|
||||||
targetSdk 33
|
targetSdkVersion 30
|
||||||
versionCode 100992 //01.00.000
|
versionCode 9000 //00.09.000
|
||||||
versionName "1.1.0-beta3"
|
versionName "1.0.0-beta1"
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
resValue "string", "build_time", buildTime()
|
resValue "string", "build_time", buildTime()
|
||||||
|
@ -29,7 +23,6 @@ android {
|
||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
viewBinding true
|
viewBinding true
|
||||||
buildConfig true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
@ -40,47 +33,51 @@ android {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace 'org.mosad.teapod'
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = '1.8'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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.7.3'
|
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2'
|
||||||
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0'
|
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.1'
|
||||||
|
|
||||||
implementation 'androidx.core:core-ktx:1.12.0'
|
implementation 'androidx.core:core-ktx:1.6.0'
|
||||||
implementation 'androidx.core:core-splashscreen:1.0.1'
|
implementation 'androidx.appcompat:appcompat:1.3.1'
|
||||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.0'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5'
|
||||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.7.7'
|
implementation 'androidx.navigation:navigation-ui-ktx:2.3.5'
|
||||||
implementation 'androidx.navigation:navigation-ui-ktx:2.7.7'
|
implementation 'androidx.security:security-crypto:1.1.0-alpha03'
|
||||||
implementation 'androidx.security:security-crypto:1.1.0-alpha06'
|
|
||||||
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.7.0'
|
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
|
||||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0'
|
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1'
|
||||||
implementation "androidx.paging:paging-runtime-ktx:3.2.1"
|
|
||||||
|
|
||||||
implementation 'com.google.android.material:material:1.11.0'
|
implementation 'com.google.android.material:material:1.4.0'
|
||||||
implementation "com.google.android.exoplayer:exoplayer-core:$exo_version"
|
implementation 'com.google.code.gson:gson:2.8.8' // TODO remove, still used by metadb
|
||||||
implementation "com.google.android.exoplayer:exoplayer-hls:$exo_version"
|
implementation 'com.google.android.exoplayer:exoplayer-core:2.15.0'
|
||||||
implementation "com.google.android.exoplayer:exoplayer-dash:$exo_version"
|
implementation 'com.google.android.exoplayer:exoplayer-hls:2.15.0'
|
||||||
implementation "com.google.android.exoplayer:exoplayer-ui:$exo_version"
|
implementation 'com.google.android.exoplayer:exoplayer-dash:2.15.0'
|
||||||
implementation "com.google.android.exoplayer:extension-mediasession:$exo_version"
|
implementation 'com.google.android.exoplayer:exoplayer-ui:2.15.0'
|
||||||
|
implementation 'com.google.android.exoplayer:extension-mediasession:2.15.0'
|
||||||
|
|
||||||
implementation 'com.facebook.shimmer:shimmer:0.5.0'
|
implementation 'com.github.bumptech.glide:glide:4.12.0'
|
||||||
|
|
||||||
implementation 'com.github.bumptech.glide:glide:4.15.1'
|
|
||||||
implementation 'jp.wasabeef:glide-transformations:4.3.0'
|
implementation 'jp.wasabeef:glide-transformations:4.3.0'
|
||||||
|
implementation 'com.afollestad.material-dialogs:core:3.3.0' // TODO remove once unused
|
||||||
|
implementation 'com.afollestad.material-dialogs:bottomsheets:3.3.0' // TODO remove once unused
|
||||||
|
|
||||||
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-content-negotiation:$ktor_version"
|
implementation "io.ktor:ktor-client-serialization:$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.5'
|
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,10 @@
|
||||||
|
|
||||||
-keep class org.json.** { *; }
|
-keep class org.json.** { *; }
|
||||||
|
|
||||||
|
#Gson
|
||||||
|
-keepattributes Signature
|
||||||
|
-dontwarn sun.misc.**
|
||||||
|
|
||||||
# kotlinx.serialization
|
# kotlinx.serialization
|
||||||
# Keep `Companion` object fields of serializable classes.
|
# Keep `Companion` object fields of serializable classes.
|
||||||
# This avoids serializer lookup through `getDeclaredClasses` as done for named companion objects.
|
# This avoids serializer lookup through `getDeclaredClasses` as done for named companion objects.
|
||||||
|
@ -52,9 +56,6 @@
|
||||||
# @Serializable and @Polymorphic are used at runtime for polymorphic serialization.
|
# @Serializable and @Polymorphic are used at runtime for polymorphic serialization.
|
||||||
-keepattributes RuntimeVisibleAnnotations,AnnotationDefault
|
-keepattributes RuntimeVisibleAnnotations,AnnotationDefault
|
||||||
|
|
||||||
# This is generated automatically by the Android Gradle plugin.
|
|
||||||
-dontwarn org.slf4j.impl.StaticLoggerBinder
|
|
||||||
|
|
||||||
#misc
|
#misc
|
||||||
-dontwarn java.lang.instrument.ClassFileTransformer
|
-dontwarn java.lang.instrument.ClassFileTransformer
|
||||||
-dontwarn java.lang.ClassValue
|
-dontwarn java.lang.ClassValue
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<?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" />
|
||||||
|
|
||||||
|
@ -10,29 +11,34 @@
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/AppTheme">
|
android:theme="@style/AppTheme.Dark">
|
||||||
<activity
|
<activity
|
||||||
android:exported="true"
|
android:name="org.mosad.teapod.ui.activity.SplashActivity"
|
||||||
android:name="org.mosad.teapod.ui.activity.main.MainActivity"
|
android:label="@string/app_name"
|
||||||
android:screenOrientation="portrait"
|
android:theme="@style/SplashTheme"
|
||||||
android:theme="@style/Theme.App.Starting">
|
android:screenOrientation="portrait">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:exported="false"
|
|
||||||
android:name="org.mosad.teapod.ui.activity.onboarding.OnboardingActivity"
|
android:name="org.mosad.teapod.ui.activity.onboarding.OnboardingActivity"
|
||||||
|
android:label="@string/app_name"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="portrait"
|
||||||
android:launchMode="singleTop"
|
android:launchMode="singleTop"
|
||||||
android:windowSoftInputMode="adjustPan">
|
android:windowSoftInputMode="adjustPan">
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:exported="false"
|
android:name="org.mosad.teapod.ui.activity.main.MainActivity"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:screenOrientation="portrait">
|
||||||
|
</activity>
|
||||||
|
<activity
|
||||||
android:name="org.mosad.teapod.ui.activity.player.PlayerActivity"
|
android:name="org.mosad.teapod.ui.activity.player.PlayerActivity"
|
||||||
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|layoutDirection"
|
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|layoutDirection"
|
||||||
android:autoRemoveFromRecents="true"
|
android:autoRemoveFromRecents="true"
|
||||||
|
android:label="@string/app_name"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:parentActivityName="org.mosad.teapod.ui.activity.main.MainActivity"
|
android:parentActivityName="org.mosad.teapod.ui.activity.main.MainActivity"
|
||||||
android:supportsPictureInPicture="true"
|
android:supportsPictureInPicture="true"
|
||||||
|
|
|
@ -25,46 +25,46 @@ 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.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.*
|
|
||||||
import io.ktor.serialization.kotlinx.json.*
|
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.serialization.SerializationException
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.JsonObject
|
import kotlinx.serialization.json.JsonObject
|
||||||
import kotlinx.serialization.json.buildJsonObject
|
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(ContentNegotiation) {
|
install(JsonFeature) {
|
||||||
json(Json {
|
serializer = KotlinxSerializer(json)
|
||||||
ignoreUnknownKeys = true
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private const val baseUrl = "https://beta-api.crunchyroll.com"
|
private const val baseUrl = "https://beta-api.crunchyroll.com"
|
||||||
private const val staticUrl = "https://static.crunchyroll.com"
|
|
||||||
private const val basicApiTokenUrl = "https://gitlab.com/-/snippets/2274956/raw/main/snippetfile1.txt"
|
private const val basicApiTokenUrl = "https://gitlab.com/-/snippets/2274956/raw/main/snippetfile1.txt"
|
||||||
private var basicApiToken: String = ""
|
private var basicApiToken: String = ""
|
||||||
|
|
||||||
private lateinit var token: Token
|
private lateinit var token: Token
|
||||||
private var tokenValidUntil: Long = 0
|
private var tokenValidUntil: Long = 0
|
||||||
@OptIn(DelicateCoroutinesApi::class, ExperimentalCoroutinesApi::class)
|
|
||||||
private val tokenRefreshContext = newSingleThreadContext("TokenRefreshContext")
|
|
||||||
|
|
||||||
private var accountID = ""
|
private var accountID = ""
|
||||||
private var externalID = ""
|
|
||||||
|
|
||||||
private val browsingCache = hashMapOf<String, BrowseResult>()
|
private var policy = ""
|
||||||
|
private var signature = ""
|
||||||
|
private var keyPairID = ""
|
||||||
|
|
||||||
|
private val browsingCache = arrayListOf<Item>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load the pai token, see:
|
* Load the pai token, see:
|
||||||
|
@ -74,7 +74,7 @@ object Crunchyroll {
|
||||||
*/
|
*/
|
||||||
fun initBasicApiToken() = runBlocking {
|
fun initBasicApiToken() = runBlocking {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
basicApiToken = client.get { url(basicApiTokenUrl) }.bodyAsText()
|
basicApiToken = (client.get(basicApiTokenUrl) as HttpResponse).readText()
|
||||||
Log.i(TAG, "basic auth token: $basicApiToken")
|
Log.i(TAG, "basic auth token: $basicApiToken")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -98,27 +98,15 @@ object Crunchyroll {
|
||||||
|
|
||||||
var success = false// is false
|
var success = false// is false
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
Log.i(TAG, "getting token ...")
|
// TODO handle exceptions
|
||||||
|
val response: HttpResponse = client.submitForm("$baseUrl$tokenEndpoint", formParameters = formData) {
|
||||||
val status = try {
|
header("Authorization", "Basic $basicApiToken")
|
||||||
val response: HttpResponse = client.submitForm("$baseUrl$tokenEndpoint", formParameters = formData) {
|
|
||||||
header("Authorization", "Basic $basicApiToken")
|
|
||||||
}
|
|
||||||
token = response.body()
|
|
||||||
tokenValidUntil = System.currentTimeMillis() + (token.expiresIn * 1000)
|
|
||||||
response.status
|
|
||||||
} catch (ex: ClientRequestException) {
|
|
||||||
val status = ex.response.status
|
|
||||||
if (status == HttpStatusCode.Unauthorized) {
|
|
||||||
Log.e(TAG, "Could not complete login: " +
|
|
||||||
"${status.value} ${status.description}. " +
|
|
||||||
"Probably wrong username or password")
|
|
||||||
}
|
|
||||||
|
|
||||||
status
|
|
||||||
}
|
}
|
||||||
Log.i(TAG, "Login complete with code $status")
|
token = response.receive()
|
||||||
success = (status == HttpStatusCode.OK)
|
tokenValidUntil = System.currentTimeMillis() + (token.expiresIn * 1000)
|
||||||
|
|
||||||
|
Log.i(TAG, "login complete with code ${response.status}")
|
||||||
|
success = (response.status == HttpStatusCode.OK)
|
||||||
}
|
}
|
||||||
|
|
||||||
return@runBlocking success
|
return@runBlocking success
|
||||||
|
@ -138,12 +126,10 @@ object Crunchyroll {
|
||||||
params: List<Pair<String, Any?>> = listOf(),
|
params: List<Pair<String, Any?>> = listOf(),
|
||||||
bodyObject: Any = Any()
|
bodyObject: Any = Any()
|
||||||
): T = coroutineScope {
|
): T = coroutineScope {
|
||||||
withContext(tokenRefreshContext) {
|
if (System.currentTimeMillis() > tokenValidUntil) refreshToken()
|
||||||
if (System.currentTimeMillis() > tokenValidUntil) refreshToken()
|
|
||||||
}
|
|
||||||
|
|
||||||
return@coroutineScope (Dispatchers.IO) {
|
return@coroutineScope (Dispatchers.IO) {
|
||||||
val response = client.request(url) {
|
val response: T = client.request(url) {
|
||||||
method = httpMethod
|
method = httpMethod
|
||||||
header("Authorization", "${token.tokenType} ${token.accessToken}")
|
header("Authorization", "${token.tokenType} ${token.accessToken}")
|
||||||
params.forEach {
|
params.forEach {
|
||||||
|
@ -152,24 +138,21 @@ object Crunchyroll {
|
||||||
|
|
||||||
// for json set body and content type
|
// for json set body and content type
|
||||||
if (bodyObject is JsonObject) {
|
if (bodyObject is JsonObject) {
|
||||||
setBody(bodyObject)
|
body = bodyObject
|
||||||
contentType(ContentType.Application.Json)
|
contentType(ContentType.Application.Json)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
response.body<T>()
|
response
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Send a HTTP GET request with [params] to the [endpoint] at [url], if url is empty use baseUrl
|
|
||||||
*/
|
|
||||||
private suspend inline fun <reified T> requestGet(
|
private suspend inline fun <reified T> requestGet(
|
||||||
endpoint: String,
|
endpoint: String,
|
||||||
params: List<Pair<String, Any?>> = listOf(),
|
params: List<Pair<String, Any?>> = listOf(),
|
||||||
url: String = ""
|
url: String = ""
|
||||||
): T {
|
): T {
|
||||||
val path = url.ifEmpty { baseUrl }.plus(endpoint)
|
val path = url.ifEmpty { "$baseUrl$endpoint" }
|
||||||
|
|
||||||
return request(path, HttpMethod.Get, params)
|
return request(path, HttpMethod.Get, params)
|
||||||
}
|
}
|
||||||
|
@ -208,10 +191,27 @@ object Crunchyroll {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Basic functions: account
|
* Basic functions: index, account
|
||||||
* Needed for other functions to work properly!
|
* Needed for other functions to work properly!
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the identifiers necessary for streaming. If the identifiers are
|
||||||
|
* retrieved, set the corresponding global var. The identifiers are valid for 24h.
|
||||||
|
*/
|
||||||
|
suspend fun index() {
|
||||||
|
val indexEndpoint = "/index/v2"
|
||||||
|
|
||||||
|
val index: Index = requestGet(indexEndpoint)
|
||||||
|
policy = index.cms.policy
|
||||||
|
signature = index.cms.signature
|
||||||
|
keyPairID = index.cms.keyPairId
|
||||||
|
|
||||||
|
Log.i(TAG, "Policy : $policy")
|
||||||
|
Log.i(TAG, "Signature : $signature")
|
||||||
|
Log.i(TAG, "Key Pair ID : $keyPairID")
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve the account id and set the corresponding global var.
|
* Retrieve the account id and set the corresponding global var.
|
||||||
* The account id is needed for other calls.
|
* The account id is needed for other calls.
|
||||||
|
@ -223,103 +223,72 @@ object Crunchyroll {
|
||||||
|
|
||||||
val account: Account = try {
|
val account: Account = try {
|
||||||
requestGet(indexEndpoint)
|
requestGet(indexEndpoint)
|
||||||
} catch (ex: Exception) {
|
} catch (ex: SerializationException) {
|
||||||
Log.e(TAG, "SerializationException in account(). This is bad!", ex)
|
Log.e(TAG, "SerializationException in account(). This is bad!", ex)
|
||||||
NoneAccount
|
NoneAccount
|
||||||
}
|
}
|
||||||
|
|
||||||
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.
|
||||||
*
|
*
|
||||||
* @param start start of the item list, used for pagination, default = 0
|
* @param sortBy
|
||||||
* @param n number of items to return, default = 10
|
* @param n Number of items to return, defaults to 10
|
||||||
* @param sortBy the sort order, see **[SortBy]**
|
*
|
||||||
* @param ratings add user rating to the objects, default = false
|
|
||||||
* @param seasonTag filter by season tag, if present
|
|
||||||
* @param categories filter by category, if present
|
|
||||||
* @return A **[BrowseResult]** object is returned.
|
* @return A **[BrowseResult]** object is returned.
|
||||||
*/
|
*/
|
||||||
suspend fun browse(
|
suspend fun browse(
|
||||||
start: Int = 0,
|
|
||||||
n: Int = 10,
|
|
||||||
sortBy: SortBy = SortBy.ALPHABETICAL,
|
sortBy: SortBy = SortBy.ALPHABETICAL,
|
||||||
ratings: Boolean = false,
|
|
||||||
seasonTag: String = "",
|
seasonTag: String = "",
|
||||||
categories: List<Categories> = emptyList()
|
start: Int = 0,
|
||||||
|
n: Int = 10
|
||||||
): BrowseResult {
|
): BrowseResult {
|
||||||
val browseEndpoint = "/content/v2/discover/browse"
|
val browseEndpoint = "/content/v1/browse"
|
||||||
val parameters = mutableListOf(
|
val noneOptParams = listOf(
|
||||||
"start" to start,
|
"locale" to Preferences.preferredLocale.toLanguageTag(),
|
||||||
"n" to n,
|
|
||||||
"sort_by" to sortBy.str,
|
"sort_by" to sortBy.str,
|
||||||
"ratings" to ratings,
|
"start" to start,
|
||||||
"preferred_audio_language" to Preferences.preferredSubtitleLocale.toLanguageTag(),
|
"n" to n
|
||||||
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag(),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// if a season tag is present add it to the parameters
|
// if a season tag is present add it to the parameters
|
||||||
if (seasonTag.isNotEmpty()) {
|
val parameters = if (seasonTag.isNotEmpty()) {
|
||||||
parameters.add("season_tag" to seasonTag)
|
concatenate(noneOptParams, listOf("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 {
|
||||||
Log.d(TAG, "browse result not cached, fetching: $parameters")
|
noneOptParams
|
||||||
val browseResult: BrowseResult = try {
|
|
||||||
requestGet(browseEndpoint, parameters)
|
|
||||||
}catch (ex: Exception) {
|
|
||||||
Log.e(TAG, "SerializationException in browse().", ex)
|
|
||||||
NoneBrowseResult
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// if the cache has more than 10 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 > 10) {
|
|
||||||
browsingCache.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
// add results to cache
|
|
||||||
browsingCache[parameters.toString()] = browseResult
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return browsingCache[parameters.toString()] ?: NoneBrowseResult
|
val browseResult: BrowseResult = try {
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search fo a query term.
|
* TODO
|
||||||
* Note: currently this function only supports series/tv shows.
|
|
||||||
*
|
|
||||||
* @param query The query term as String
|
|
||||||
* @param n The maximum number of results to return, default = 10
|
|
||||||
* @param ratings add user rating to the objects, default = false
|
|
||||||
* @return A **[SearchResult]** object
|
|
||||||
*/
|
*/
|
||||||
suspend fun search(query: String, n: Int = 10, ratings: Boolean = false): SearchResult {
|
suspend fun search(query: String, n: Int = 10): SearchResult {
|
||||||
val searchEndpoint = "/content/v2/discover/search"
|
val searchEndpoint = "/content/v1/search"
|
||||||
val parameters = listOf(
|
val parameters = listOf(
|
||||||
|
"locale" to Preferences.preferredLocale.toLanguageTag(),
|
||||||
"q" to query,
|
"q" to query,
|
||||||
"n" to n,
|
"n" to n,
|
||||||
"type" to "series",
|
"type" to "series"
|
||||||
"ratings" to ratings,
|
|
||||||
"preferred_audio_language" to Preferences.preferredSubtitleLocale.toLanguageTag(),
|
|
||||||
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag()
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO episodes have thumbnails as image, and not poster_tall/poster_tall,
|
// TODO episodes have thumbnails as image, and not poster_tall/poster_tall,
|
||||||
|
@ -327,8 +296,8 @@ object Crunchyroll {
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
requestGet(searchEndpoint, parameters)
|
requestGet(searchEndpoint, parameters)
|
||||||
} catch (ex: Exception) {
|
}catch (ex: SerializationException) {
|
||||||
Log.e(TAG, "Exception in search(), with query = \"$query\".", ex)
|
Log.e(TAG, "SerializationException in search(), with query = \"$query\".", ex)
|
||||||
NoneSearchResult
|
NoneSearchResult
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -338,22 +307,38 @@ object Crunchyroll {
|
||||||
* Note: episode objects are currently not supported
|
* Note: episode objects are currently not supported
|
||||||
*
|
*
|
||||||
* @param objects The object IDs as list of Strings
|
* @param objects The object IDs as list of Strings
|
||||||
* @param ratings add user rating to the objects
|
|
||||||
* @return A **[Collection]** of Panels
|
* @return A **[Collection]** of Panels
|
||||||
*/
|
*/
|
||||||
suspend fun objects(objects: List<String>, ratings: Boolean = false): CollectionV2<Item> {
|
suspend fun objects(objects: List<String>): Collection<Item> {
|
||||||
val episodesEndpoint = "/content/v2/cms/objects/${objects.joinToString(",")}"
|
val episodesEndpoint = "/cms/v2/DE/M3/crunchyroll/objects/${objects.joinToString(",")}"
|
||||||
val parameters = listOf(
|
val parameters = listOf(
|
||||||
"ratings" to ratings,
|
"locale" to Preferences.preferredLocale.toLanguageTag(),
|
||||||
"preferred_audio_language" to Preferences.preferredAudioLocale.toLanguageTag(),
|
"Signature" to signature,
|
||||||
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag()
|
"Policy" to policy,
|
||||||
|
"Key-Pair-Id" to keyPairID
|
||||||
)
|
)
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
requestGet(episodesEndpoint, parameters)
|
requestGet(episodesEndpoint, parameters)
|
||||||
} catch (ex: Exception) {
|
}catch (ex: SerializationException) {
|
||||||
Log.e(TAG, "Exception in objects().", ex)
|
Log.e(TAG, "SerializationException in objects().", ex)
|
||||||
NoneCollectionV2
|
NoneCollection
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List all available seasons as **[SeasonListItem]**.
|
||||||
|
*/
|
||||||
|
@Suppress("unused")
|
||||||
|
suspend fun seasonList(): DiscSeasonList {
|
||||||
|
val seasonListEndpoint = "/content/v1/season_list"
|
||||||
|
val parameters = listOf("locale" to Preferences.preferredLocale.toLanguageTag())
|
||||||
|
|
||||||
|
return try {
|
||||||
|
requestGet(seasonListEndpoint, parameters)
|
||||||
|
}catch (ex: SerializationException) {
|
||||||
|
Log.e(TAG, "SerializationException in seasonList().", ex)
|
||||||
|
NoneDiscSeasonList
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -365,118 +350,87 @@ object Crunchyroll {
|
||||||
* series id == crunchyroll id?
|
* series id == crunchyroll id?
|
||||||
*/
|
*/
|
||||||
suspend fun series(seriesId: String): Series {
|
suspend fun series(seriesId: String): Series {
|
||||||
val seriesEndpoint = "/content/v2/cms/series/$seriesId"
|
val seriesEndpoint = "/cms/v2/${token.country}/M3/crunchyroll/series/$seriesId"
|
||||||
val parameters = listOf(
|
val parameters = listOf(
|
||||||
"preferred_audio_language" to Preferences.preferredAudioLocale.toLanguageTag(),
|
"locale" to Preferences.preferredLocale.toLanguageTag(),
|
||||||
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag()
|
"Signature" to signature,
|
||||||
|
"Policy" to policy,
|
||||||
|
"Key-Pair-Id" to keyPairID
|
||||||
)
|
)
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
requestGet(seriesEndpoint, parameters)
|
requestGet(seriesEndpoint, parameters)
|
||||||
} catch (ex: Exception) {
|
}catch (ex: SerializationException) {
|
||||||
Log.e(TAG, "Exception in series() for id $seriesId.", ex)
|
Log.e(TAG, "SerializationException in series().", ex)
|
||||||
NoneSeries
|
NoneSeries
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the next episode for a series.
|
* TODO
|
||||||
*
|
|
||||||
* FIXME up_next returns no content if the is no next episode
|
|
||||||
*
|
|
||||||
* @param seriesId The series id for which to call up next
|
|
||||||
* @return A **[UpNextSeriesItem]** with a Panel representing the up next episode
|
|
||||||
*/
|
*/
|
||||||
suspend fun upNextSeries(seriesId: String): UpNextSeriesList {
|
suspend fun upNextSeries(seriesId: String): UpNextSeriesItem {
|
||||||
val upNextSeriesEndpoint = "/content/v2/discover/up_next/$seriesId"
|
val upNextSeriesEndpoint = "/content/v1/up_next_series"
|
||||||
val parameters = listOf(
|
val parameters = listOf(
|
||||||
"preferred_audio_language" to Preferences.preferredAudioLocale.toLanguageTag(),
|
"series_id" to seriesId,
|
||||||
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag()
|
"locale" to Preferences.preferredLocale.toLanguageTag()
|
||||||
)
|
)
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
requestGet(upNextSeriesEndpoint, parameters)
|
requestGet(upNextSeriesEndpoint, parameters)
|
||||||
} catch (ex: NoTransformationFoundException) {
|
}catch (ex: SerializationException) {
|
||||||
// should be 204 No Content
|
Log.e(TAG, "SerializationException in upNextSeries().", ex)
|
||||||
NoneUpNextSeriesList
|
NoneUpNextSeriesItem
|
||||||
} catch (ex: JsonConvertException) {
|
|
||||||
Log.e(TAG, "JsonConvertException in upNextSeries() with seriesId=$seriesId", ex)
|
|
||||||
NoneUpNextSeriesList
|
|
||||||
} catch (ex: Exception) {
|
|
||||||
Log.e(TAG, "Exception in upNextSeries() for seriesId $seriesId.", ex)
|
|
||||||
NoneUpNextSeriesList
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 = "/content/v2/cms/series/$seriesId/seasons"
|
val seasonsEndpoint = "/cms/v2/${token.country}/M3/crunchyroll/seasons"
|
||||||
val parameters = listOf(
|
val parameters = listOf(
|
||||||
"preferred_audio_language" to Preferences.preferredSubtitleLocale.toLanguageTag(),
|
"series_id" to seriesId,
|
||||||
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag()
|
"locale" to Preferences.preferredLocale.toLanguageTag(),
|
||||||
|
"Signature" to signature,
|
||||||
|
"Policy" to policy,
|
||||||
|
"Key-Pair-Id" to keyPairID
|
||||||
)
|
)
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
requestGet(seasonsEndpoint, parameters)
|
requestGet(seasonsEndpoint, parameters)
|
||||||
} catch (ex: Exception) {
|
}catch (ex: SerializationException) {
|
||||||
Log.e(TAG, "Exception in seasons() for seriesId $seriesId.", 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 = "/content/v2/cms/seasons/$seasonId/episodes"
|
val episodesEndpoint = "/cms/v2/${token.country}/M3/crunchyroll/episodes"
|
||||||
val parameters = listOf(
|
val parameters = listOf(
|
||||||
"preferred_audio_language" to Preferences.preferredSubtitleLocale.toLanguageTag(),
|
"season_id" to seasonId,
|
||||||
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag()
|
"locale" to Preferences.preferredLocale.toLanguageTag(),
|
||||||
|
"Signature" to signature,
|
||||||
|
"Policy" to policy,
|
||||||
|
"Key-Pair-Id" to keyPairID
|
||||||
)
|
)
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
requestGet(episodesEndpoint, parameters)
|
requestGet(episodesEndpoint, parameters)
|
||||||
} catch (ex: Exception) {
|
}catch (ex: SerializationException) {
|
||||||
Log.e(TAG, "Exception in episodes() for seasonId $seasonId.", ex)
|
Log.e(TAG, "SerializationException in episodes().", ex)
|
||||||
NoneEpisodes
|
NoneEpisodes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
suspend fun playback(url: String): Playback {
|
||||||
* Get all available subtitles and streams of a episode.
|
|
||||||
*
|
|
||||||
* @param url The streams url of a episode
|
|
||||||
* @return A **[Streams]** object
|
|
||||||
*/
|
|
||||||
suspend fun streams(url: String): Streams {
|
|
||||||
val parameters = listOf(
|
|
||||||
"preferred_audio_language" to Preferences.preferredSubtitleLocale.toLanguageTag(),
|
|
||||||
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag()
|
|
||||||
)
|
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
requestGet(url, parameters)
|
requestGet("", url = url)
|
||||||
} catch (ex: Exception) {
|
}catch (ex: SerializationException) {
|
||||||
Log.e(TAG, "Exception in streams() with url $url.", ex)
|
Log.e(TAG, "SerializationException in playback(), with url = $url.", ex)
|
||||||
NoneStreams
|
NonePlayback
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun streamsFromMediaGUID(mediaGUID: String): Streams {
|
|
||||||
val streamsEndpoint = "/content/v2/cms/videos/$mediaGUID/streams"
|
|
||||||
return streams(streamsEndpoint)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Additional media functions: watchlist (series), playhead, similar to
|
* Additional media functions: watchlist (series), playhead
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -486,18 +440,14 @@ object Crunchyroll {
|
||||||
* @return **[Boolean]**: ture if it was found, else false
|
* @return **[Boolean]**: ture if it was found, else false
|
||||||
*/
|
*/
|
||||||
suspend fun isWatchlist(seriesId: String): Boolean {
|
suspend fun isWatchlist(seriesId: String): Boolean {
|
||||||
val watchlistSeriesEndpoint = "/content/v2/$accountID/watchlist"
|
val watchlistSeriesEndpoint = "/content/v1/watchlist/$accountID/$seriesId"
|
||||||
val parameters = listOf(
|
val parameters = listOf("locale" to Preferences.preferredLocale.toLanguageTag())
|
||||||
"content_ids" to seriesId,
|
|
||||||
"preferred_audio_language" to Preferences.preferredAudioLocale.toLanguageTag(),
|
|
||||||
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag()
|
|
||||||
)
|
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
(requestGet(watchlistSeriesEndpoint, parameters) as CollectionV2<IsWatchlistItem>)
|
(requestGet(watchlistSeriesEndpoint, parameters) as JsonObject)
|
||||||
.total == 1
|
.containsKey(seriesId)
|
||||||
} catch (ex: Exception) {
|
}catch (ex: SerializationException) {
|
||||||
Log.e(TAG, "Exception in isWatchlist() with seriesId $seriesId", ex)
|
Log.e(TAG, "SerializationException in isWatchlist() with seriesId = $seriesId", ex)
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -508,21 +458,14 @@ object Crunchyroll {
|
||||||
* @param seriesId The crunchyroll series id of the media to check
|
* @param seriesId The crunchyroll series id of the media to check
|
||||||
*/
|
*/
|
||||||
suspend fun postWatchlist(seriesId: String) {
|
suspend fun postWatchlist(seriesId: String) {
|
||||||
val watchlistPostEndpoint = "/content/v2/$accountID/watchlist"
|
val watchlistPostEndpoint = "/content/v1/watchlist/$accountID"
|
||||||
val parameters = listOf(
|
val parameters = listOf("locale" to Preferences.preferredLocale.toLanguageTag())
|
||||||
"preferred_audio_language" to Preferences.preferredAudioLocale.toLanguageTag(),
|
|
||||||
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag()
|
|
||||||
)
|
|
||||||
|
|
||||||
val json = buildJsonObject {
|
val json = buildJsonObject {
|
||||||
put("content_id", seriesId)
|
put("content_id", seriesId)
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
requestPost(watchlistPostEndpoint, parameters, json)
|
||||||
requestPost(watchlistPostEndpoint, parameters, json)
|
|
||||||
} catch (ex: Exception) {
|
|
||||||
Log.e(TAG, "Exception in postWatchlist() with seriesId $seriesId", ex)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -531,17 +474,10 @@ object Crunchyroll {
|
||||||
* @param seriesId The crunchyroll series id of the media to check
|
* @param seriesId The crunchyroll series id of the media to check
|
||||||
*/
|
*/
|
||||||
suspend fun deleteWatchlist(seriesId: String) {
|
suspend fun deleteWatchlist(seriesId: String) {
|
||||||
val watchlistDeleteEndpoint = "/content/v2/$accountID/watchlist/$seriesId"
|
val watchlistDeleteEndpoint = "/content/v1/watchlist/$accountID/$seriesId"
|
||||||
val parameters = listOf(
|
val parameters = listOf("locale" to Preferences.preferredLocale.toLanguageTag())
|
||||||
"preferred_audio_language" to Preferences.preferredAudioLocale.toLanguageTag(),
|
|
||||||
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag()
|
|
||||||
)
|
|
||||||
|
|
||||||
try {
|
requestDelete(watchlistDeleteEndpoint, parameters)
|
||||||
requestDelete(watchlistDeleteEndpoint, parameters)
|
|
||||||
} catch (ex: Exception) {
|
|
||||||
Log.e(TAG, "Exception in deleteWatchlist() with seriesId $seriesId", ex)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -552,88 +488,28 @@ object Crunchyroll {
|
||||||
* @param episodeIDs A **[List]** of episodes IDs as strings.
|
* @param episodeIDs A **[List]** of episodes IDs as strings.
|
||||||
* @return A **[Map]**<String, **[PlayheadObject]**> containing playback info.
|
* @return A **[Map]**<String, **[PlayheadObject]**> containing playback info.
|
||||||
*/
|
*/
|
||||||
suspend fun playheads(episodeIDs: List<String>): Playheads {
|
suspend fun playheads(episodeIDs: List<String>): PlayheadsMap {
|
||||||
val playheadsEndpoint = "/content/v2/$accountID/playheads"
|
val playheadsEndpoint = "/content/v1/playheads/$accountID/${episodeIDs.joinToString(",")}"
|
||||||
val parameters = listOf(
|
val parameters = listOf("locale" to Preferences.preferredLocale.toLanguageTag())
|
||||||
"content_ids" to episodeIDs.joinToString(","),
|
|
||||||
"preferred_audio_language" to Preferences.preferredAudioLocale.toLanguageTag(),
|
|
||||||
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag()
|
|
||||||
)
|
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
requestGet(playheadsEndpoint, parameters)
|
requestGet(playheadsEndpoint, parameters)
|
||||||
} catch (ex: Exception) {
|
}catch (ex: SerializationException) {
|
||||||
Log.e(TAG, "Exception in playheads().", ex.cause)
|
Log.e(TAG, "SerializationException in upNextSeries().", ex)
|
||||||
NonePlayheads
|
emptyMap()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Post the playhead to crunchy (playhead position,watched state)
|
|
||||||
*
|
|
||||||
* @param episodeId A episode ID as strings.
|
|
||||||
* @param playhead The episodes playhead in seconds.
|
|
||||||
*/
|
|
||||||
suspend fun postPlayheads(episodeId: String, playhead: Int) {
|
suspend fun postPlayheads(episodeId: String, playhead: Int) {
|
||||||
val playheadsEndpoint = "/content/v1/playheads/$accountID"
|
val playheadsEndpoint = "/content/v1/playheads/$accountID"
|
||||||
val parameters = listOf("locale" to Preferences.preferredSubtitleLocale.toLanguageTag())
|
val parameters = listOf("locale" to Preferences.preferredLocale.toLanguageTag())
|
||||||
|
|
||||||
val json = buildJsonObject {
|
val json = buildJsonObject {
|
||||||
put("content_id", episodeId)
|
put("content_id", episodeId)
|
||||||
put("playhead", playhead)
|
put("playhead", playhead)
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
requestPost(playheadsEndpoint, parameters, json)
|
||||||
requestPost(playheadsEndpoint, parameters, json)
|
|
||||||
} catch (ex: Exception) {
|
|
||||||
Log.e(TAG, "Exception in postPlayheads()", ex.cause)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the intro meta data including start, end and duration of the intro.
|
|
||||||
*
|
|
||||||
* @param episodeId A episode ID as strings.
|
|
||||||
*/
|
|
||||||
suspend fun datalabIntro(episodeId: String): DatalabIntro {
|
|
||||||
val datalabIntroEndpoint = "/datalab-intro-v2/$episodeId.json"
|
|
||||||
|
|
||||||
/*
|
|
||||||
* wtf crunchyroll, why do you return an xml error message when some data is missing,
|
|
||||||
* this is a json endpoint. For fucks sake, return at least a valid json message.
|
|
||||||
*/
|
|
||||||
return try {
|
|
||||||
val response: HttpResponse = requestGet(datalabIntroEndpoint, url = staticUrl)
|
|
||||||
Json.decodeFromString(response.bodyAsText())
|
|
||||||
} catch (ex: Exception) {
|
|
||||||
Log.e(TAG, "Exception in datalabIntro(). EpisodeId=$episodeId", ex)
|
|
||||||
NoneDatalabIntro
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
* @param ratings add user rating to the objects
|
|
||||||
* @return A **[SimilarToResult]** object
|
|
||||||
*/
|
|
||||||
suspend fun similarTo(seriesId: String, n: Int = 10, ratings: Boolean = false): SimilarToResult {
|
|
||||||
val similarToEndpoint = "/content/v2/discover/$accountID/similar_to/$seriesId"
|
|
||||||
val parameters = listOf(
|
|
||||||
"n" to n,
|
|
||||||
"ratings" to ratings,
|
|
||||||
"preferred_audio_language" to Preferences.preferredSubtitleLocale.toLanguageTag(),
|
|
||||||
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag(),
|
|
||||||
)
|
|
||||||
|
|
||||||
return try {
|
|
||||||
requestGet(similarToEndpoint, parameters)
|
|
||||||
} catch (ex: Exception) {
|
|
||||||
Log.e(TAG, "Exception in similarTo().", ex)
|
|
||||||
NoneSimilarToResult
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -644,70 +520,44 @@ object Crunchyroll {
|
||||||
* List items present in the watchlist.
|
* List items present in the watchlist.
|
||||||
*
|
*
|
||||||
* @param n Number of items to return, defaults to 20.
|
* @param n Number of items to return, defaults to 20.
|
||||||
* @return A **[Collection]** containing up to n **[Item]**.
|
* @return A **[Watchlist]** containing up to n **[Item]**.
|
||||||
*/
|
*/
|
||||||
suspend fun watchlist(n: Int = 20): CollectionV2<Item> {
|
suspend fun watchlist(n: Int = 20): Watchlist {
|
||||||
val watchlistEndpoint = "/content/v2/discover/$accountID/watchlist"
|
val watchlistEndpoint = "/content/v1/$accountID/watchlist"
|
||||||
val parameters = listOf(
|
val parameters = listOf(
|
||||||
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag(),
|
"locale" to Preferences.preferredLocale.toLanguageTag(),
|
||||||
"n" to n,
|
"n" to n
|
||||||
"preferred_audio_language" to Preferences.preferredAudioLocale.toLanguageTag()
|
|
||||||
)
|
)
|
||||||
|
|
||||||
val list: Watchlist = try {
|
val list: ContinueWatchingList = try {
|
||||||
requestGet(watchlistEndpoint, parameters)
|
requestGet(watchlistEndpoint, parameters)
|
||||||
} catch (ex: Exception) {
|
}catch (ex: SerializationException) {
|
||||||
Log.e(TAG, "Exception in watchlist().", ex)
|
Log.e(TAG, "SerializationException in watchlist().", ex)
|
||||||
NoneWatchlist
|
NoneContinueWatchingList
|
||||||
}
|
}
|
||||||
|
|
||||||
val objects = list.data.map{ it.panel.episodeMetadata.seriesId }
|
val objects = list.items.map{ it.panel.episodeMetadata.seriesId }
|
||||||
return objects(objects)
|
return objects(objects)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List the next up episodes for the logged in account.
|
* List the next up episodes for the logged in account.
|
||||||
*
|
*
|
||||||
* @param n Number of items to return, default = 20
|
* @param n Number of items to return, defaults to 20.
|
||||||
* @return A **[HistoryList]** containing up to n **[UpNextAccountItem]**.
|
* @return A **[ContinueWatchingList]** containing up to n **[ContinueWatchingItem]**.
|
||||||
*/
|
*/
|
||||||
suspend fun upNextAccount(n: Int = 10): HistoryList {
|
suspend fun upNextAccount(n: Int = 20): ContinueWatchingList {
|
||||||
val watchlistEndpoint = "/content/v2/discover/$accountID/history"
|
val watchlistEndpoint = "/content/v1/$accountID/up_next_account"
|
||||||
val parameters = listOf(
|
val parameters = listOf(
|
||||||
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag(),
|
"locale" to Preferences.preferredLocale.toLanguageTag(),
|
||||||
"n" to n
|
"n" to n
|
||||||
)
|
)
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
requestGet(watchlistEndpoint, parameters)
|
requestGet(watchlistEndpoint, parameters)
|
||||||
} catch (ex: Exception) {
|
}catch (ex: SerializationException) {
|
||||||
Log.e(TAG, "Exception in upNextAccount().", ex)
|
Log.e(TAG, "SerializationException in upNextAccount().", ex)
|
||||||
NoneHistoryList
|
NoneContinueWatchingList
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a collection of recommendations for the currently logged in account.
|
|
||||||
*
|
|
||||||
* @param start start of the item list, used for pagination, default = 0
|
|
||||||
* @param n number of items to return, default = 10
|
|
||||||
* @param ratings add user rating to the objects, default = false
|
|
||||||
* @return A **[RecommendationsList]** containing up to n **[Item]**.
|
|
||||||
*/
|
|
||||||
suspend fun recommendations(start: Int = 0, n: Int = 10, ratings: Boolean = false): RecommendationsList {
|
|
||||||
val recommendationsEndpoint = "/content/v2/discover/$accountID/recommendations"
|
|
||||||
val parameters = listOf(
|
|
||||||
"start" to start,
|
|
||||||
"n" to n,
|
|
||||||
"ratings" to ratings,
|
|
||||||
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag(),
|
|
||||||
)
|
|
||||||
|
|
||||||
return try {
|
|
||||||
requestGet(recommendationsEndpoint, parameters)
|
|
||||||
} catch (ex: Exception) {
|
|
||||||
Log.e(TAG, "Exception in recommendations().", ex)
|
|
||||||
NoneRecommendationsList
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -715,28 +565,18 @@ object Crunchyroll {
|
||||||
* 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: Exception) {
|
}catch (ex: SerializationException) {
|
||||||
Log.e(TAG, "Exception in profile().", ex)
|
Log.e(TAG, "SerializationException in profile().", ex)
|
||||||
NoneProfile
|
NoneProfile
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
suspend fun postPrefSubLanguage(languageTag: String) {
|
||||||
* Post the preferred content subtitle language.
|
|
||||||
*
|
|
||||||
* @param languageTag the preferred language as language tag
|
|
||||||
*/
|
|
||||||
suspend fun setPreferredSubtitleLanguage(languageTag: String) {
|
|
||||||
val profileEndpoint = "/accounts/v1/me/profile"
|
val profileEndpoint = "/accounts/v1/me/profile"
|
||||||
val json = buildJsonObject {
|
val json = buildJsonObject {
|
||||||
put("preferred_content_subtitle_language", languageTag)
|
put("preferred_content_subtitle_language", languageTag)
|
||||||
|
@ -745,34 +585,4 @@ object Crunchyroll {
|
||||||
requestPatch(profileEndpoint, bodyObject = json)
|
requestPatch(profileEndpoint, bodyObject = json)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Patch the preferred content audio language.
|
|
||||||
*
|
|
||||||
* @param languageTag the preferred language as language tag
|
|
||||||
*/
|
|
||||||
suspend fun setPreferredAudioLanguage(languageTag: String) {
|
|
||||||
val profileEndpoint = "/accounts/v1/me/profile"
|
|
||||||
val json = buildJsonObject {
|
|
||||||
put("preferred_content_audio_language", languageTag)
|
|
||||||
}
|
|
||||||
|
|
||||||
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: Exception) {
|
|
||||||
Log.e(TAG, "Exception in benefits().", ex)
|
|
||||||
NoneBenefits
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,47 +24,19 @@ package org.mosad.teapod.parser.crunchyroll
|
||||||
|
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import java.util.Locale
|
import java.util.*
|
||||||
|
|
||||||
val supportedAudioLocals = listOf(
|
val supportedLocals = listOf(
|
||||||
Locale.forLanguageTag("ar-SA"),
|
Locale.forLanguageTag("ar-SA"),
|
||||||
Locale.forLanguageTag("ca-ES"),
|
|
||||||
Locale.forLanguageTag("de-DE"),
|
|
||||||
Locale.forLanguageTag("en-US"),
|
|
||||||
Locale.forLanguageTag("en-IN"),
|
|
||||||
Locale.forLanguageTag("es-419"),
|
|
||||||
Locale.forLanguageTag("es-ES"),
|
|
||||||
Locale.forLanguageTag("fr-FR"),
|
|
||||||
Locale.forLanguageTag("hi-IN"),
|
|
||||||
Locale.forLanguageTag("it-IT"),
|
|
||||||
Locale.forLanguageTag("ko-KR"),
|
|
||||||
Locale.forLanguageTag("pl-PL"),
|
|
||||||
Locale.forLanguageTag("pt-BR"),
|
|
||||||
Locale.forLanguageTag("pt-PT"),
|
|
||||||
Locale.forLanguageTag("ru-RU"),
|
|
||||||
Locale.forLanguageTag("ta-IN"),
|
|
||||||
Locale.forLanguageTag("th-TH"),
|
|
||||||
Locale.forLanguageTag("zh-CN"),
|
|
||||||
Locale.forLanguageTag("zh-TW"),
|
|
||||||
Locale.ROOT
|
|
||||||
)
|
|
||||||
|
|
||||||
val supportedSubtitleLocals = listOf(
|
|
||||||
Locale.forLanguageTag("ar-SA"),
|
|
||||||
Locale.forLanguageTag("ca-ES"),
|
|
||||||
Locale.forLanguageTag("de-DE"),
|
Locale.forLanguageTag("de-DE"),
|
||||||
Locale.forLanguageTag("en-US"),
|
Locale.forLanguageTag("en-US"),
|
||||||
Locale.forLanguageTag("es-419"),
|
Locale.forLanguageTag("es-419"),
|
||||||
Locale.forLanguageTag("es-ES"),
|
Locale.forLanguageTag("es-ES"),
|
||||||
Locale.forLanguageTag("fr-FR"),
|
Locale.forLanguageTag("fr-FR"),
|
||||||
Locale.forLanguageTag("hi-IN"),
|
|
||||||
Locale.forLanguageTag("it-IT"),
|
Locale.forLanguageTag("it-IT"),
|
||||||
Locale.forLanguageTag("ms-MY"),
|
|
||||||
Locale.forLanguageTag("pl-PL"),
|
|
||||||
Locale.forLanguageTag("pt-BR"),
|
Locale.forLanguageTag("pt-BR"),
|
||||||
Locale.forLanguageTag("pt-PT"),
|
Locale.forLanguageTag("pt-PT"),
|
||||||
Locale.forLanguageTag("ru-RU"),
|
Locale.forLanguageTag("ru-RU"),
|
||||||
Locale.forLanguageTag("tr-TR"),
|
|
||||||
Locale.ROOT
|
Locale.ROOT
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -72,35 +44,12 @@ val supportedSubtitleLocals = listOf(
|
||||||
* data classes for browse
|
* data classes for browse
|
||||||
* TODO make class names more clear/possibly overlapping for now
|
* TODO make class names more clear/possibly overlapping for now
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
|
||||||
* Enum of all supported sorting orders.
|
|
||||||
*/
|
|
||||||
enum class SortBy(val str: String) {
|
enum class SortBy(val str: String) {
|
||||||
ALPHABETICAL("alphabetical"),
|
ALPHABETICAL("alphabetical"),
|
||||||
NEWLY_ADDED("newly_added"),
|
NEWLY_ADDED("newly_added"),
|
||||||
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!
|
||||||
*/
|
*/
|
||||||
|
@ -144,28 +93,31 @@ val NoneAccount = Account("", "", false, "")
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class CollectionV1<T>(
|
data class Collection<T>(
|
||||||
@SerialName("total") val total: Int,
|
@SerialName("total") val total: Int,
|
||||||
@SerialName("items") val items: List<T>
|
@SerialName("items") val items: List<T>
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
typealias SearchResult = Collection<SearchCollection>
|
||||||
data class CollectionV2<T>(
|
typealias SearchCollection = Collection<Item>
|
||||||
@SerialName("total") val total: Int,
|
typealias BrowseResult = Collection<Item>
|
||||||
@SerialName("data") val data: List<T>
|
typealias DiscSeasonList = Collection<SeasonListItem>
|
||||||
)
|
typealias Watchlist = Collection<Item>
|
||||||
|
typealias ContinueWatchingList = Collection<ContinueWatchingItem>
|
||||||
|
|
||||||
typealias SearchResult = CollectionV2<SearchTypedList<Item>>
|
@Serializable
|
||||||
typealias BrowseResult = CollectionV2<Item>
|
data class UpNextSeriesItem(
|
||||||
typealias SimilarToResult = CollectionV2<Item>
|
@SerialName("playhead") val playhead: Int,
|
||||||
typealias RecommendationsList = CollectionV2<Item>
|
@SerialName("fully_watched") val fullyWatched: Boolean,
|
||||||
typealias Benefits = CollectionV1<Benefit>
|
@SerialName("never_watched") val neverWatched: Boolean,
|
||||||
|
@SerialName("panel") val panel: EpisodePanel,
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* panel data classes
|
* panel data classes
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// the data class Item is used in browse, search, watchlist and similar to
|
// the data class Item is used in browse and search
|
||||||
// TODO rename to MediaPanel
|
// TODO rename to MediaPanel
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Item(
|
data class Item(
|
||||||
|
@ -176,7 +128,6 @@ data class Item(
|
||||||
val description: String,
|
val description: String,
|
||||||
val images: Images
|
val images: Images
|
||||||
// TODO series_metadata etc.
|
// TODO series_metadata etc.
|
||||||
// TODO add slug_title if present in search, browse, similar to
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
@ -187,48 +138,38 @@ data class Images(val poster_tall: List<List<Poster>>, val poster_wide: List<Lis
|
||||||
data class Poster(val height: Int, val width: Int, val source: String, val type: String)
|
data class Poster(val height: Int, val width: Int, val source: String, val type: String)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* up next & watchlist data classes
|
* season list data classes
|
||||||
*/
|
*/
|
||||||
|
|
||||||
typealias Watchlist = CollectionV2<WatchlistItem>
|
|
||||||
typealias HistoryList = CollectionV2<UpNextAccountItem>
|
|
||||||
typealias UpNextSeriesList = CollectionV2<UpNextSeriesItem>
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class WatchlistItem(
|
data class SeasonListItem(
|
||||||
@SerialName("panel") val panel: EpisodePanel,
|
|
||||||
@SerialName("new") val new: Boolean,
|
|
||||||
@SerialName("playhead") val playhead: Int,
|
|
||||||
@SerialName("fully_watched") val fullyWatched: Boolean = false,
|
|
||||||
@SerialName("never_watched") val neverWatched: Boolean = false,
|
|
||||||
@SerialName("is_favorite") val isFavorite: Boolean,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class IsWatchlistItem(
|
|
||||||
@SerialName("id") val id: String,
|
@SerialName("id") val id: String,
|
||||||
@SerialName("is_favorite") val isFavorite: Boolean,
|
@SerialName("localization") val localization: SeasonListLocalization
|
||||||
@SerialName("date_added") val dateAdded: String
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class UpNextAccountItem(
|
data class SeasonListLocalization(
|
||||||
|
@SerialName("title") val title: String,
|
||||||
|
@SerialName("description") val description: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* continue_watching_item data classes
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
data class ContinueWatchingItem(
|
||||||
@SerialName("panel") val panel: EpisodePanel,
|
@SerialName("panel") val panel: EpisodePanel,
|
||||||
@SerialName("new") val new: Boolean,
|
@SerialName("new") val new: Boolean,
|
||||||
|
@SerialName("new_content") val newContent: Boolean,
|
||||||
|
// not present in up_next_account -> continue_watching_item
|
||||||
|
// @SerialName("is_favorite") val isFavorite: Boolean,
|
||||||
|
// @SerialName("never_watched") val neverWatched: Boolean,
|
||||||
|
// @SerialName("completion_status") val completionStatus: Boolean,
|
||||||
@SerialName("playhead") val playhead: Int,
|
@SerialName("playhead") val playhead: Int,
|
||||||
|
// not present in watchlist -> continue_watching_item
|
||||||
@SerialName("fully_watched") val fullyWatched: Boolean = false,
|
@SerialName("fully_watched") val fullyWatched: Boolean = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
// EpisodePanel is used in ContinueWatchingItem
|
||||||
data class UpNextSeriesItem(
|
|
||||||
@SerialName("panel") val panel: EpisodePanel,
|
|
||||||
@SerialName("playhead") val playhead: Int,
|
|
||||||
@SerialName("fully_watched") val fullyWatched: Boolean,
|
|
||||||
@SerialName("never_watched") val neverWatched: Boolean,
|
|
||||||
|
|
||||||
)
|
|
||||||
|
|
||||||
// EpisodePanel is used in ContinueWatchingItem and UpNextSeriesItem
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class EpisodePanel(
|
data class EpisodePanel(
|
||||||
@SerialName("id") val id: String,
|
@SerialName("id") val id: String,
|
||||||
|
@ -238,59 +179,60 @@ data class EpisodePanel(
|
||||||
@SerialName("description") val description: String,
|
@SerialName("description") val description: String,
|
||||||
@SerialName("episode_metadata") val episodeMetadata: EpisodeMetadata,
|
@SerialName("episode_metadata") val episodeMetadata: EpisodeMetadata,
|
||||||
@SerialName("images") val images: Thumbnail,
|
@SerialName("images") val images: Thumbnail,
|
||||||
// @SerialName("streams_link") val streamsLink: String,
|
@SerialName("playback") val playback: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class EpisodeMetadata(
|
data class EpisodeMetadata(
|
||||||
@SerialName("duration_ms") val durationMs: Int,
|
@SerialName("duration_ms") val durationMs: Int,
|
||||||
@SerialName("episode_number") val episodeNumber: Int? = null, // default/nullable value since optional
|
|
||||||
@SerialName("season_id") val seasonId: String,
|
@SerialName("season_id") val seasonId: String,
|
||||||
@SerialName("season_number") val seasonNumber: Int,
|
|
||||||
@SerialName("season_title") val seasonTitle: String,
|
|
||||||
@SerialName("series_id") val seriesId: String,
|
@SerialName("series_id") val seriesId: String,
|
||||||
@SerialName("series_title") val seriesTitle: String,
|
@SerialName("series_title") val seriesTitle: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
val NoneCollectionV2 = CollectionV2<Item>(0, emptyList())
|
val NoneItem = Item("", "", "", "", "", Images(emptyList(), emptyList()))
|
||||||
|
val NoneEpisodeMetadata = EpisodeMetadata(0, "", "", "")
|
||||||
|
val NoneEpisodePanel = EpisodePanel("", "", "", "", "", NoneEpisodeMetadata, Thumbnail(listOf()), "")
|
||||||
|
|
||||||
|
val NoneCollection = Collection<Item>(0, emptyList())
|
||||||
val NoneSearchResult = SearchResult(0, emptyList())
|
val NoneSearchResult = SearchResult(0, emptyList())
|
||||||
val NoneBrowseResult = BrowseResult(0, emptyList())
|
val NoneBrowseResult = BrowseResult(0, emptyList())
|
||||||
val NoneSimilarToResult = SimilarToResult(0, emptyList())
|
val NoneDiscSeasonList = DiscSeasonList(0, emptyList())
|
||||||
val NoneWatchlist = Watchlist(0, emptyList())
|
val NoneContinueWatchingList = ContinueWatchingList(0, emptyList())
|
||||||
val NoneHistoryList = HistoryList(0, emptyList())
|
|
||||||
val NoneUpNextSeriesList = UpNextSeriesList(0, emptyList())
|
val NoneUpNextSeriesItem = UpNextSeriesItem(0, false, false, NoneEpisodePanel)
|
||||||
val NoneRecommendationsList = RecommendationsList(0, emptyList())
|
|
||||||
val NoneBenefits = Benefits(0, emptyList())
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* series data class
|
* Series data type
|
||||||
*/
|
*/
|
||||||
|
|
||||||
typealias Series = CollectionV2<SeriesItem>
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class SeriesItem(
|
data class Series(
|
||||||
@SerialName("id") val id: String,
|
@SerialName("id") val id: String,
|
||||||
@SerialName("title") val title: String,
|
@SerialName("title") val title: String,
|
||||||
@SerialName("description") val description: String,
|
@SerialName("description") val description: String,
|
||||||
@SerialName("images") val images: Images,
|
@SerialName("images") val images: Images,
|
||||||
@SerialName("is_simulcast") val isSimulcast: Boolean,
|
@SerialName("maturity_ratings") val maturityRatings: List<String>
|
||||||
@SerialName("maturity_ratings") val maturityRatings: List<String>,
|
|
||||||
@SerialName("audio_locales") val audioLocales: List<String>,
|
|
||||||
@SerialName("episode_count") val episodeCount: Int
|
|
||||||
)
|
)
|
||||||
|
val NoneSeries = Series("", "", "", Images(emptyList(), emptyList()), emptyList())
|
||||||
val NoneSeriesItem = SeriesItem("", "", "", Images(emptyList(), emptyList()), false, emptyList(), emptyList(), 0)
|
|
||||||
val NoneSeries = Series(1, listOf(NoneSeriesItem))
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Seasons data classes
|
* Seasons data type
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Seasons(
|
data class Seasons(
|
||||||
@SerialName("total") val total: Int,
|
@SerialName("total") val total: Int,
|
||||||
@SerialName("data") val data: List<Season>
|
@SerialName("items") val items: List<Season>
|
||||||
)
|
) {
|
||||||
|
fun getPreferredSeason(local: Locale): Season {
|
||||||
|
return items.firstOrNull { season ->
|
||||||
|
// try to get the the first seasons which matches the preferred local
|
||||||
|
season.slugTitle.endsWith("${local.getDisplayLanguage(Locale.ENGLISH)}-dub", true)
|
||||||
|
} ?: items.firstOrNull { season ->
|
||||||
|
// if there is no season with the preferred local, try to find a subbed season
|
||||||
|
season.isSubbed
|
||||||
|
} ?: items.first() // if no preferred language and no sub, use the first season
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Season(
|
data class Season(
|
||||||
|
@ -308,12 +250,12 @@ val NoneSeason = Season("", "", "", "", 0, isSubbed = false, isDubbed = false)
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Episodes data classes
|
* Episodes data type
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Episodes(
|
data class Episodes(
|
||||||
@SerialName("total") val total: Int,
|
@SerialName("total") val total: Int,
|
||||||
@SerialName("data") val data: List<Episode>
|
@SerialName("items") val items: List<Episode>
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
@ -333,8 +275,7 @@ data class Episode(
|
||||||
@SerialName("is_dubbed") val isDubbed: Boolean,
|
@SerialName("is_dubbed") val isDubbed: Boolean,
|
||||||
@SerialName("images") val images: Thumbnail,
|
@SerialName("images") val images: Thumbnail,
|
||||||
@SerialName("duration_ms") val durationMs: Int,
|
@SerialName("duration_ms") val durationMs: Int,
|
||||||
@SerialName("versions") val versions: List<Version>? = null,
|
@SerialName("playback") val playback: String,
|
||||||
@SerialName("streams_link") val streamsLink: String,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
@ -342,17 +283,6 @@ data class Thumbnail(
|
||||||
@SerialName("thumbnail") val thumbnail: List<List<Poster>>
|
@SerialName("thumbnail") val thumbnail: List<List<Poster>>
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class Version(
|
|
||||||
@SerialName("audio_locale") val audioLocale: String,
|
|
||||||
@SerialName("guid") val guid: String,
|
|
||||||
@SerialName("is_premium_only") val isPremiumOnly: Boolean,
|
|
||||||
@SerialName("media_guid") val mediaGUID: String,
|
|
||||||
@SerialName("original") val original: Boolean,
|
|
||||||
@SerialName("season_guid") val seasonGUID: String,
|
|
||||||
@SerialName("variant") val variant: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
val NoneEpisodes = Episodes(0, listOf())
|
val NoneEpisodes = Episodes(0, listOf())
|
||||||
val NoneEpisode = Episode(
|
val NoneEpisode = Episode(
|
||||||
id = "",
|
id = "",
|
||||||
|
@ -370,21 +300,10 @@ val NoneEpisode = Episode(
|
||||||
isDubbed = false,
|
isDubbed = false,
|
||||||
images = Thumbnail(listOf()),
|
images = Thumbnail(listOf()),
|
||||||
durationMs = 0,
|
durationMs = 0,
|
||||||
versions = emptyList(),
|
playback = ""
|
||||||
streamsLink = ""
|
|
||||||
)
|
)
|
||||||
|
|
||||||
val NoneVersion = Version(
|
typealias PlayheadsMap = Map<String, PlayheadObject>
|
||||||
audioLocale = "",
|
|
||||||
guid = "",
|
|
||||||
isPremiumOnly = false,
|
|
||||||
mediaGUID = "",
|
|
||||||
original = true,
|
|
||||||
seasonGUID = "",
|
|
||||||
variant = ""
|
|
||||||
)
|
|
||||||
|
|
||||||
typealias Playheads = CollectionV2<PlayheadObject>
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class PlayheadObject(
|
data class PlayheadObject(
|
||||||
|
@ -394,72 +313,60 @@ data class PlayheadObject(
|
||||||
@SerialName("last_modified") val lastModified: String,
|
@SerialName("last_modified") val lastModified: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
val NonePlayheads = Playheads(0, emptyList())
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Meta data for a episode intro. All time values are in seconds.
|
* Playback/stream data type
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
data class DatalabIntro(
|
data class Playback(
|
||||||
@SerialName("media_id") val mediaId: String,
|
@SerialName("audio_locale") val audioLocale: String,
|
||||||
@SerialName("startTime") val startTime: Float,
|
@SerialName("subtitles") val subtitles: Map<String, Subtitle>,
|
||||||
@SerialName("endTime") val endTime: Float,
|
@SerialName("streams") val streams: Streams,
|
||||||
@SerialName("duration") val duration: Float,
|
|
||||||
@SerialName("comparedWith") val comparedWith: String,
|
|
||||||
@SerialName("ordering") val ordering: String,
|
|
||||||
@SerialName("last_updated") val lastUpdated: String,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
val NoneDatalabIntro = DatalabIntro("", 0f, 0f, 0f, "", "", "")
|
@Serializable
|
||||||
|
data class Subtitle(
|
||||||
|
@SerialName("locale") val locale: String,
|
||||||
|
@SerialName("url") val url: String,
|
||||||
|
@SerialName("format") val format: String,
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
|
||||||
* playback/stream data classes
|
|
||||||
*/
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Streams(
|
data class Streams(
|
||||||
@SerialName("total") val total: Int,
|
|
||||||
@SerialName("data") val data: List<StreamList>,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class StreamList(
|
|
||||||
@SerialName("adaptive_dash") val adaptive_dash: Map<String, Stream>,
|
@SerialName("adaptive_dash") val adaptive_dash: Map<String, Stream>,
|
||||||
@SerialName("adaptive_hls") val adaptive_hls: Map<String, Stream>,
|
@SerialName("adaptive_hls") val adaptive_hls: Map<String, Stream>,
|
||||||
@SerialName("download_dash") val downloadDash: Map<String, Stream>,
|
|
||||||
@SerialName("download_hls") val download_hls: Map<String, Stream>,
|
@SerialName("download_hls") val download_hls: Map<String, Stream>,
|
||||||
// @SerialName("drm_adaptive_dash") val drmAdaptiveDash: Map<String, Stream>,
|
@SerialName("drm_adaptive_dash") val drm_adaptive_dash: Map<String, Stream>,
|
||||||
// @SerialName("drm_adaptive_hls") val drmAdaptiveHls: Map<String, Stream>,
|
@SerialName("drm_adaptive_hls") val drm_adaptive_hls: Map<String, Stream>,
|
||||||
// @SerialName("drm_download_dash") val drmDownloadDash: Map<String, Stream>,
|
@SerialName("drm_download_hls") val drm_download_hls: Map<String, Stream>,
|
||||||
// @SerialName("drm_download_hls") val drmDownloadHls: Map<String, Stream>,
|
@SerialName("trailer_dash") val trailer_dash: Map<String, Stream>,
|
||||||
// @SerialName("vo_adaptive_dash") val vo_adaptive_dash: Map<String, Stream>,
|
@SerialName("trailer_hls") val trailer_hls: Map<String, Stream>,
|
||||||
// @SerialName("vo_adaptive_hls") val vo_adaptive_hls: Map<String, Stream>,
|
@SerialName("vo_adaptive_dash") val vo_adaptive_dash: Map<String, Stream>,
|
||||||
// @SerialName("vo_drm_adaptive_dash") val vo_drm_adaptive_dash: Map<String, Stream>,
|
@SerialName("vo_adaptive_hls") val vo_adaptive_hls: Map<String, Stream>,
|
||||||
// @SerialName("vo_drm_adaptive_hls") val vo_drm_adaptive_hls: Map<String, Stream>,
|
@SerialName("vo_drm_adaptive_dash") val vo_drm_adaptive_dash: Map<String, Stream>,
|
||||||
|
@SerialName("vo_drm_adaptive_hls") val vo_drm_adaptive_hls: Map<String, Stream>,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Stream(
|
data class Stream(
|
||||||
@SerialName("hardsub_locale") val hardsubLocale: String = "", // default/nullable value since might be optional
|
@SerialName("hardsub_locale") val hardsubLocale: String,
|
||||||
@SerialName("url") val url: String = "", // default/nullable value since optional
|
@SerialName("url") val url: String,
|
||||||
@SerialName("vcodec") val vcodec: String = "", // default/nullable value since optional
|
@SerialName("vcodec") val vcodec: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
val NoneStreams = Streams(
|
val NonePlayback = Playback(
|
||||||
0,
|
"",
|
||||||
arrayListOf(StreamList(
|
mapOf(),
|
||||||
mapOf(), mapOf(), mapOf(), mapOf()
|
Streams(
|
||||||
))
|
mapOf(), mapOf(), mapOf(), mapOf(), mapOf(), mapOf(),
|
||||||
|
mapOf(), mapOf(), mapOf(), mapOf(), mapOf(), mapOf(),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
|
||||||
* profile data class
|
|
||||||
*/
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Profile(
|
data class Profile(
|
||||||
@SerialName("avatar") val avatar: String,
|
@SerialName("avatar") val avatar: String,
|
||||||
@SerialName("email") val email: String,
|
@SerialName("email") val email: String,
|
||||||
@SerialName("maturity_rating") val maturityRating: String,
|
@SerialName("maturity_rating") val maturityRating: String,
|
||||||
@SerialName("preferred_content_audio_language") val preferredContentAudioLanguage: String,
|
|
||||||
@SerialName("preferred_content_subtitle_language") val preferredContentSubtitleLanguage: String,
|
@SerialName("preferred_content_subtitle_language") val preferredContentSubtitleLanguage: String,
|
||||||
@SerialName("username") val username: String,
|
@SerialName("username") val username: String,
|
||||||
)
|
)
|
||||||
|
@ -467,31 +374,6 @@ val NoneProfile = Profile(
|
||||||
avatar = "",
|
avatar = "",
|
||||||
email = "",
|
email = "",
|
||||||
maturityRating = "",
|
maturityRating = "",
|
||||||
preferredContentAudioLanguage = "",
|
|
||||||
preferredContentSubtitleLanguage = "",
|
preferredContentSubtitleLanguage = "",
|
||||||
username = ""
|
username = ""
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
|
||||||
* benefit data class
|
|
||||||
*/
|
|
||||||
@Serializable
|
|
||||||
data class Benefit(
|
|
||||||
@SerialName("benefit") val benefit: String,
|
|
||||||
@SerialName("source") val source: String,
|
|
||||||
)
|
|
||||||
@Suppress("unused")
|
|
||||||
val NoneBenefit = Benefit(
|
|
||||||
benefit = "",
|
|
||||||
source = ""
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* search result typed list data class
|
|
||||||
*/
|
|
||||||
@Serializable
|
|
||||||
data class SearchTypedList<T>(
|
|
||||||
@SerialName("type") val type: String,
|
|
||||||
@SerialName("count") val count: Int,
|
|
||||||
@SerialName("items") val items: List<T>
|
|
||||||
)
|
|
||||||
|
|
|
@ -8,19 +8,15 @@ import java.util.*
|
||||||
|
|
||||||
object Preferences {
|
object Preferences {
|
||||||
|
|
||||||
var preferredAudioLocale: Locale = Locale.forLanguageTag("en-US")
|
var preferredLocale: Locale = Locale.forLanguageTag("en-US") // TODO this should be saved (potential offline usage) but fetched on start
|
||||||
internal set
|
internal set
|
||||||
var preferredSubtitleLocale: Locale = Locale.forLanguageTag("en-US")
|
var preferSubbed = false
|
||||||
internal set
|
internal set
|
||||||
var autoplay = true
|
var autoplay = true
|
||||||
internal set
|
internal set
|
||||||
var devSettings = false
|
var devSettings = false
|
||||||
internal set
|
internal set
|
||||||
var theme = DataTypes.Theme.SYSTEM
|
var theme = DataTypes.Theme.DARK
|
||||||
internal set
|
|
||||||
|
|
||||||
// dev settings
|
|
||||||
var updatePlayhead = true
|
|
||||||
internal set
|
internal set
|
||||||
|
|
||||||
private fun getSharedPref(context: Context): SharedPreferences {
|
private fun getSharedPref(context: Context): SharedPreferences {
|
||||||
|
@ -30,22 +26,22 @@ object Preferences {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun savePreferredAudioLocal(context: Context, preferredLocale: Locale) {
|
fun savePreferredLocal(context: Context, preferredLocale: Locale) {
|
||||||
with(getSharedPref(context).edit()) {
|
with(getSharedPref(context).edit()) {
|
||||||
putString(context.getString(R.string.save_key_preferred_local), preferredLocale.toLanguageTag())
|
putString(context.getString(R.string.save_key_preferred_local), preferredLocale.toLanguageTag())
|
||||||
apply()
|
apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
this.preferredAudioLocale = preferredLocale
|
this.preferredLocale = preferredLocale
|
||||||
}
|
}
|
||||||
|
|
||||||
fun savePreferredSubtitleLocal(context: Context, preferredLocale: Locale) {
|
fun savePreferSecondary(context: Context, preferSubbed: Boolean) {
|
||||||
with(getSharedPref(context).edit()) {
|
with(getSharedPref(context).edit()) {
|
||||||
putString(context.getString(R.string.save_key_preferred_local), preferredLocale.toLanguageTag())
|
putBoolean(context.getString(R.string.save_key_prefer_secondary), preferSubbed)
|
||||||
apply()
|
apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
this.preferredSubtitleLocale = preferredLocale
|
this.preferSubbed = preferSubbed
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveAutoplay(context: Context, autoplay: Boolean) {
|
fun saveAutoplay(context: Context, autoplay: Boolean) {
|
||||||
|
@ -75,31 +71,20 @@ object Preferences {
|
||||||
this.theme = theme
|
this.theme = theme
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveUpdatePlayhead(context: Context, updatePlayhead: Boolean) {
|
|
||||||
with(getSharedPref(context).edit()) {
|
|
||||||
putBoolean(context.getString(R.string.save_key_update_playhead), updatePlayhead)
|
|
||||||
apply()
|
|
||||||
}
|
|
||||||
|
|
||||||
this.updatePlayhead = updatePlayhead
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* initially load the stored values
|
* initially load the stored values
|
||||||
*/
|
*/
|
||||||
fun load(context: Context) {
|
fun load(context: Context) {
|
||||||
val sharedPref = getSharedPref(context)
|
val sharedPref = getSharedPref(context)
|
||||||
|
|
||||||
preferredAudioLocale = Locale.forLanguageTag(
|
preferredLocale = Locale.forLanguageTag(
|
||||||
sharedPref.getString(
|
|
||||||
context.getString(R.string.save_key_preferred_audio_local), "en-US"
|
|
||||||
) ?: "en-US"
|
|
||||||
)
|
|
||||||
preferredSubtitleLocale = Locale.forLanguageTag(
|
|
||||||
sharedPref.getString(
|
sharedPref.getString(
|
||||||
context.getString(R.string.save_key_preferred_local), "en-US"
|
context.getString(R.string.save_key_preferred_local), "en-US"
|
||||||
) ?: "en-US"
|
) ?: "en-US"
|
||||||
)
|
)
|
||||||
|
preferSubbed = sharedPref.getBoolean(
|
||||||
|
context.getString(R.string.save_key_prefer_secondary), false
|
||||||
|
)
|
||||||
autoplay = sharedPref.getBoolean(
|
autoplay = sharedPref.getBoolean(
|
||||||
context.getString(R.string.save_key_autoplay), true
|
context.getString(R.string.save_key_autoplay), true
|
||||||
)
|
)
|
||||||
|
@ -108,13 +93,8 @@ object Preferences {
|
||||||
)
|
)
|
||||||
theme = DataTypes.Theme.valueOf(
|
theme = DataTypes.Theme.valueOf(
|
||||||
sharedPref.getString(
|
sharedPref.getString(
|
||||||
context.getString(R.string.save_key_theme), DataTypes.Theme.SYSTEM.toString()
|
context.getString(R.string.save_key_theme), DataTypes.Theme.DARK.toString()
|
||||||
) ?: DataTypes.Theme.SYSTEM.toString()
|
) ?: DataTypes.Theme.DARK.toString()
|
||||||
)
|
|
||||||
|
|
||||||
// dev settings
|
|
||||||
updatePlayhead = sharedPref.getBoolean(
|
|
||||||
context.getString(R.string.save_key_update_playhead), true
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
package org.mosad.teapod.ui.activity
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import org.mosad.teapod.ui.activity.main.MainActivity
|
||||||
|
|
||||||
|
|
||||||
|
class SplashActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
val intent = Intent(this, MainActivity::class.java)
|
||||||
|
startActivity(intent)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
|
@ -26,10 +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.appcompat.app.AppCompatDelegate
|
|
||||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.commit
|
import androidx.fragment.app.commit
|
||||||
import com.google.android.material.navigation.NavigationBarView
|
import com.google.android.material.navigation.NavigationBarView
|
||||||
|
@ -41,11 +38,12 @@ import org.mosad.teapod.preferences.EncryptedPreferences
|
||||||
import org.mosad.teapod.preferences.Preferences
|
import org.mosad.teapod.preferences.Preferences
|
||||||
import org.mosad.teapod.ui.activity.main.fragments.AccountFragment
|
import org.mosad.teapod.ui.activity.main.fragments.AccountFragment
|
||||||
import org.mosad.teapod.ui.activity.main.fragments.HomeFragment
|
import org.mosad.teapod.ui.activity.main.fragments.HomeFragment
|
||||||
import org.mosad.teapod.ui.activity.main.fragments.MyListsFragment
|
|
||||||
import org.mosad.teapod.ui.activity.main.fragments.LibraryFragment
|
import org.mosad.teapod.ui.activity.main.fragments.LibraryFragment
|
||||||
|
import org.mosad.teapod.ui.activity.main.fragments.SearchFragment
|
||||||
import org.mosad.teapod.ui.activity.onboarding.OnboardingActivity
|
import org.mosad.teapod.ui.activity.onboarding.OnboardingActivity
|
||||||
|
import org.mosad.teapod.ui.activity.player.PlayerActivity
|
||||||
|
import org.mosad.teapod.ui.components.LoginDialog
|
||||||
import org.mosad.teapod.util.DataTypes
|
import org.mosad.teapod.util.DataTypes
|
||||||
import org.mosad.teapod.util.metadb.MetaDBController
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.system.measureTimeMillis
|
import kotlin.system.measureTimeMillis
|
||||||
|
|
||||||
|
@ -64,20 +62,10 @@ class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListen
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
// Handle the splash screen transition.
|
|
||||||
installSplashScreen()
|
|
||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
load() // start the initial loading
|
load() // start the initial loading
|
||||||
|
theme.applyStyle(getThemeResource(), true)
|
||||||
// theming
|
|
||||||
val mode = when (Preferences.theme) {
|
|
||||||
DataTypes.Theme.LIGHT -> AppCompatDelegate.MODE_NIGHT_NO
|
|
||||||
DataTypes.Theme.DARK -> AppCompatDelegate.MODE_NIGHT_YES
|
|
||||||
else -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
|
||||||
}
|
|
||||||
AppCompatDelegate.setDefaultNightMode(mode)
|
|
||||||
|
|
||||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||||
binding.navView.setOnItemSelectedListener(this)
|
binding.navView.setOnItemSelectedListener(this)
|
||||||
|
@ -86,14 +74,16 @@ 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)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onBackPressedDispatcher.addCallback {
|
override fun onBackPressed() {
|
||||||
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 {
|
||||||
if (activeBaseFragment !is HomeFragment) {
|
super.onBackPressed()
|
||||||
binding.navView.selectedItemId = R.id.navigation_home
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -108,14 +98,14 @@ class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListen
|
||||||
activeBaseFragment = HomeFragment()
|
activeBaseFragment = HomeFragment()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
R.id.navigation_my_lists -> {
|
|
||||||
activeBaseFragment = MyListsFragment()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.navigation_library -> {
|
R.id.navigation_library -> {
|
||||||
activeBaseFragment = LibraryFragment()
|
activeBaseFragment = LibraryFragment()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
R.id.navigation_search -> {
|
||||||
|
activeBaseFragment = SearchFragment()
|
||||||
|
true
|
||||||
|
}
|
||||||
R.id.navigation_account -> {
|
R.id.navigation_account -> {
|
||||||
activeBaseFragment = AccountFragment()
|
activeBaseFragment = AccountFragment()
|
||||||
true
|
true
|
||||||
|
@ -130,12 +120,12 @@ class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListen
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
// private fun getThemeResource(): Int {
|
private fun getThemeResource(): Int {
|
||||||
// return when (Preferences.theme) {
|
return when (Preferences.theme) {
|
||||||
// DataTypes.Theme.LIGHT -> R.style.AppTheme_Light
|
DataTypes.Theme.LIGHT -> R.style.AppTheme_Light
|
||||||
// else -> R.style.AppTheme_Dark
|
else -> R.style.AppTheme_Dark
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* initial loading and login are run in parallel, as initial loading doesn't require
|
* initial loading and login are run in parallel, as initial loading doesn't require
|
||||||
|
@ -147,9 +137,6 @@ class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListen
|
||||||
Preferences.load(this)
|
Preferences.load(this)
|
||||||
EncryptedPreferences.readCredentials(this)
|
EncryptedPreferences.readCredentials(this)
|
||||||
|
|
||||||
// load meta db at the start, it doesn't depend on any third party
|
|
||||||
val metaJob = initMetaDB()
|
|
||||||
|
|
||||||
// always initialize the api token
|
// always initialize the api token
|
||||||
Crunchyroll.initBasicApiToken()
|
Crunchyroll.initBasicApiToken()
|
||||||
|
|
||||||
|
@ -161,34 +148,39 @@ class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListen
|
||||||
) {
|
) {
|
||||||
showOnboarding()
|
showOnboarding()
|
||||||
} else {
|
} else {
|
||||||
runBlocking {
|
runBlocking { initCrunchyroll().joinAll() }
|
||||||
initCrunchyroll().joinAll()
|
|
||||||
metaJob.join() // meta loading should be done here
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Log.i(classTag, "loading in $time ms")
|
Log.i(classTag, "loading in $time ms")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initCrunchyroll(): List<Job> {
|
private fun initCrunchyroll(): List<Job> {
|
||||||
val scope = CoroutineScope(Dispatchers.IO + CoroutineName("InitialCrunchyLoading"))
|
val scope = CoroutineScope(Dispatchers.IO + CoroutineName("InitialLoadingScope"))
|
||||||
return listOf(
|
return listOf(
|
||||||
|
scope.launch { Crunchyroll.index() },
|
||||||
scope.launch { Crunchyroll.account() },
|
scope.launch { Crunchyroll.account() },
|
||||||
scope.launch {
|
scope.launch {
|
||||||
// update the local preferred content language, since it may have changed
|
// update the local preferred content language, since it may have changed
|
||||||
val profile = Crunchyroll.profile()
|
val locale = Locale.forLanguageTag(Crunchyroll.profile().preferredContentSubtitleLanguage)
|
||||||
|
Preferences.savePreferredLocal(this@MainActivity, locale)
|
||||||
|
|
||||||
val audioLocale = Locale.forLanguageTag(profile.preferredContentAudioLanguage)
|
|
||||||
val subtitleLocale = Locale.forLanguageTag(profile.preferredContentSubtitleLanguage)
|
|
||||||
Preferences.savePreferredAudioLocal(this@MainActivity, audioLocale)
|
|
||||||
Preferences.savePreferredSubtitleLocal(this@MainActivity, subtitleLocale)
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initMetaDB(): Job {
|
private fun showLoginDialog() {
|
||||||
val scope = CoroutineScope(Dispatchers.IO + CoroutineName("InitialMetaDBLoading"))
|
LoginDialog(this, false).positiveButton {
|
||||||
return scope.launch { MetaDBController.list() }
|
EncryptedPreferences.saveCredentials(login, password, context)
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
// if (!AoDParser.login()) {
|
||||||
|
// showLoginDialog()
|
||||||
|
// Log.w(javaClass.name, "Login failed, please try again.")
|
||||||
|
// }
|
||||||
|
}.negativeButton {
|
||||||
|
Log.i(classTag, "Login canceled, exiting.")
|
||||||
|
finish()
|
||||||
|
}.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -199,6 +191,17 @@ class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListen
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* start the player as new activity
|
||||||
|
*/
|
||||||
|
fun 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)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* use custom restart instead of recreate(), since it has animations
|
* use custom restart instead of recreate(), since it has animations
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -107,14 +107,16 @@ class AboutFragment : Fragment() {
|
||||||
"https://github.com/material-components/material-components-android", License.APACHE2),
|
"https://github.com/material-components/material-components-android", License.APACHE2),
|
||||||
ThirdPartyComponent("ExoPlayer", "2014 - 2020", "The Android Open Source Project",
|
ThirdPartyComponent("ExoPlayer", "2014 - 2020", "The Android Open Source Project",
|
||||||
"https://github.com/google/ExoPlayer", License.APACHE2),
|
"https://github.com/google/ExoPlayer", License.APACHE2),
|
||||||
|
ThirdPartyComponent("Gson", "2008", "Google Inc.",
|
||||||
|
"https://github.com/google/gson", License.APACHE2),
|
||||||
ThirdPartyComponent("Material design icons", "2020", "Google Inc.",
|
ThirdPartyComponent("Material design icons", "2020", "Google Inc.",
|
||||||
"https://github.com/google/material-design-icons", License.APACHE2),
|
"https://github.com/google/material-design-icons", License.APACHE2),
|
||||||
|
ThirdPartyComponent("Material Dialogs", "", "Aidan Follestad",
|
||||||
|
"https://github.com/afollestad/material-dialogs", License.APACHE2),
|
||||||
ThirdPartyComponent("Ktor", "2014-2021", "JetBrains s.r.o and contributors",
|
ThirdPartyComponent("Ktor", "2014-2021", "JetBrains s.r.o and contributors",
|
||||||
"https://ktor.io/", License.APACHE2),
|
"https://ktor.io/", License.APACHE2),
|
||||||
ThirdPartyComponent("kotlinx.coroutines", "2016-2021", "JetBrains s.r.o",
|
ThirdPartyComponent("kotlinx.coroutines", "2016-2021", "JetBrains s.r.o",
|
||||||
"https://github.com/Kotlin/kotlinx.coroutines", License.APACHE2),
|
"https://github.com/Kotlin/kotlinx.coroutines", License.APACHE2),
|
||||||
ThirdPartyComponent(" kotlinx.serialization", "2017-2021", "JetBrains s.r.o",
|
|
||||||
"https://github.com/Kotlin/kotlinx.serialization", License.APACHE2),
|
|
||||||
ThirdPartyComponent("Glide", "2014", "Google Inc.",
|
ThirdPartyComponent("Glide", "2014", "Google Inc.",
|
||||||
"https://github.com/bumptech/glide", License.BSD2),
|
"https://github.com/bumptech/glide", License.BSD2),
|
||||||
ThirdPartyComponent("Glide Transformations", "2020", "Wasabeef",
|
ThirdPartyComponent("Glide Transformations", "2020", "Wasabeef",
|
||||||
|
|
|
@ -1,26 +1,30 @@
|
||||||
package org.mosad.teapod.ui.activity.main.fragments
|
package org.mosad.teapod.ui.activity.main.fragments
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
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.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.Deferred
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
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.supportedAudioLocals
|
import org.mosad.teapod.parser.crunchyroll.supportedLocals
|
||||||
import org.mosad.teapod.parser.crunchyroll.supportedSubtitleLocals
|
|
||||||
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.ui.activity.main.MainActivity
|
import org.mosad.teapod.ui.activity.main.MainActivity
|
||||||
import org.mosad.teapod.ui.components.LoginModalBottomSheet
|
import org.mosad.teapod.ui.components.LoginDialog
|
||||||
import org.mosad.teapod.util.DataTypes.Theme
|
import org.mosad.teapod.util.DataTypes.Theme
|
||||||
import org.mosad.teapod.util.showFragment
|
import org.mosad.teapod.util.showFragment
|
||||||
import org.mosad.teapod.util.toDisplayString
|
import org.mosad.teapod.util.toDisplayString
|
||||||
|
@ -32,8 +36,27 @@ 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()
|
private val getUriExport = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
|
if (result.resultCode == Activity.RESULT_OK) {
|
||||||
|
result.data?.data?.also { uri ->
|
||||||
|
//StorageController.exportMyList(requireContext(), uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val getUriImport = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
|
if (result.resultCode == Activity.RESULT_OK) {
|
||||||
|
result.data?.data?.also { uri ->
|
||||||
|
// val success = StorageController.importMyList(requireContext(), uri)
|
||||||
|
// if (success == 0) {
|
||||||
|
// Toast.makeText(
|
||||||
|
// context, getString(R.string.import_data_success),
|
||||||
|
// Toast.LENGTH_SHORT
|
||||||
|
// ).show()
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
|
@ -46,38 +69,30 @@ class AccountFragment : Fragment() {
|
||||||
|
|
||||||
binding.textAccountLogin.text = EncryptedPreferences.login
|
binding.textAccountLogin.text = EncryptedPreferences.login
|
||||||
|
|
||||||
// load account status and tier (async) info before anything else
|
// TODO reimplement for cr, if possible (maybe account status would be better? (premium))
|
||||||
|
// load subscription (async) info before anything else
|
||||||
|
binding.textAccountSubscription.text = getString(R.string.account_subscription, getString(R.string.loading))
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
benefits.await().apply {
|
binding.textAccountSubscription.text = getString(
|
||||||
this.items.firstOrNull { it.benefit == "cr_premium" }?.let {
|
R.string.account_subscription,
|
||||||
binding.textAccountSubscription.text = getString(R.string.account_premium)
|
"TODO"
|
||||||
}
|
)
|
||||||
|
|
||||||
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
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
binding.textSettingsAudioLanguageDesc.text = Locale.forLanguageTag(
|
binding.textSettingsContentLanguageDesc.text = Locale.forLanguageTag(
|
||||||
profile.await().preferredContentAudioLanguage
|
|
||||||
).displayLanguage
|
|
||||||
binding.textSettingsSubtitleLanguageDesc.text = Locale.forLanguageTag(
|
|
||||||
profile.await().preferredContentSubtitleLanguage
|
profile.await().preferredContentSubtitleLanguage
|
||||||
).displayLanguage
|
).displayLanguage
|
||||||
}
|
}
|
||||||
|
binding.switchSecondary.isChecked = Preferences.preferSubbed
|
||||||
binding.switchAutoplay.isChecked = Preferences.autoplay
|
binding.switchAutoplay.isChecked = Preferences.autoplay
|
||||||
binding.textThemeSelected.text = when (Preferences.theme) {
|
binding.textThemeSelected.text = when (Preferences.theme) {
|
||||||
Theme.SYSTEM -> getString(R.string.theme_system)
|
|
||||||
Theme.LIGHT -> getString(R.string.theme_light)
|
|
||||||
Theme.DARK -> getString(R.string.theme_dark)
|
Theme.DARK -> getString(R.string.theme_dark)
|
||||||
|
else -> getString(R.string.theme_light)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.linearDevSettings.isVisible = Preferences.devSettings
|
binding.linearDevSettings.isVisible = Preferences.devSettings
|
||||||
binding.switchUpdatePlayhead.isChecked = Preferences.updatePlayhead
|
|
||||||
|
|
||||||
binding.textInfoAboutDesc.text = getString(R.string.info_about_desc, BuildConfig.VERSION_NAME, getString(R.string.build_time))
|
binding.textInfoAboutDesc.text = getString(R.string.info_about_desc, BuildConfig.VERSION_NAME, getString(R.string.build_time))
|
||||||
|
|
||||||
|
@ -86,15 +101,21 @@ class AccountFragment : Fragment() {
|
||||||
|
|
||||||
private fun initActions() {
|
private fun initActions() {
|
||||||
binding.linearAccountLogin.setOnClickListener {
|
binding.linearAccountLogin.setOnClickListener {
|
||||||
showLoginDialog()
|
showLoginDialog(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.linearSettingsAudioLanguage.setOnClickListener {
|
binding.linearAccountSubscription.setOnClickListener {
|
||||||
showAudioLanguageSelection()
|
// TODO
|
||||||
|
//startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(AoDParser.getSubscriptionUrl())))
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.linearSettingsSubtitleLanguage.setOnClickListener {
|
|
||||||
showSubtitleLanguageSelection()
|
binding.linearSettingsContentLanguage.setOnClickListener {
|
||||||
|
showContentLanguageSelection()
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.switchSecondary.setOnClickListener {
|
||||||
|
Preferences.savePreferSecondary(requireContext(), binding.switchSecondary.isChecked)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.switchAutoplay.setOnClickListener {
|
binding.switchAutoplay.setOnClickListener {
|
||||||
|
@ -109,116 +130,76 @@ class AccountFragment : Fragment() {
|
||||||
activity?.showFragment(AboutFragment())
|
activity?.showFragment(AboutFragment())
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.switchUpdatePlayhead.setOnClickListener {
|
|
||||||
Preferences.saveUpdatePlayhead(requireContext(), binding.switchUpdatePlayhead.isChecked)
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.linearExportData.setOnClickListener {
|
binding.linearExportData.setOnClickListener {
|
||||||
// unused
|
val i = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||||
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
|
type = "text/json"
|
||||||
|
putExtra(Intent.EXTRA_TITLE, "my-list.json")
|
||||||
|
}
|
||||||
|
getUriExport.launch(i)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.linearImportData.setOnClickListener {
|
binding.linearImportData.setOnClickListener {
|
||||||
// unused
|
val i = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
||||||
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
|
type = "*/*"
|
||||||
|
}
|
||||||
|
getUriImport.launch(i)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showLoginDialog() {
|
private fun showLoginDialog(firstTry: Boolean) {
|
||||||
val loginModal = LoginModalBottomSheet().apply {
|
LoginDialog(requireContext(), firstTry).positiveButton {
|
||||||
|
EncryptedPreferences.saveCredentials(login, password, context)
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
// if (!AoDParser.login()) {
|
||||||
|
// showLoginDialog(false)
|
||||||
|
// Log.w(javaClass.name, "Login failed, please try again.")
|
||||||
|
// }
|
||||||
|
}.show {
|
||||||
login = EncryptedPreferences.login
|
login = EncryptedPreferences.login
|
||||||
password = ""
|
password = ""
|
||||||
positiveAction = {
|
|
||||||
EncryptedPreferences.saveCredentials(login, password, requireContext())
|
|
||||||
|
|
||||||
// TODO only dismiss if login was successful
|
|
||||||
this.dismiss()
|
|
||||||
}
|
|
||||||
negativeAction = {
|
|
||||||
this.dismiss()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
activity?.let { loginModal.show(it.supportFragmentManager, LoginModalBottomSheet.TAG) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showAudioLanguageSelection() {
|
private fun showContentLanguageSelection() {
|
||||||
// we should be able to use the index of supportedLocals for language selection, items is GUI only
|
// we should be able to use the index of supportedLocals for language selection, items is GUI only
|
||||||
val items = supportedAudioLocals.map {
|
val items = supportedLocals.map {
|
||||||
it.toDisplayString(getString(R.string.settings_content_language_none))
|
it.toDisplayString(getString(R.string.settings_content_language_none))
|
||||||
}.toTypedArray()
|
}.toTypedArray()
|
||||||
|
|
||||||
var initialSelection: Int
|
var initialSelection: Int
|
||||||
// profile should be completed here, therefore blocking
|
// profile should be completed here, therefore blocking
|
||||||
runBlocking {
|
runBlocking {
|
||||||
initialSelection = supportedAudioLocals.indexOf(Locale.forLanguageTag(
|
initialSelection = supportedLocals.indexOf(Locale.forLanguageTag(
|
||||||
profile.await().preferredContentAudioLanguage))
|
|
||||||
if (initialSelection < 0) initialSelection = supportedAudioLocals.lastIndex
|
|
||||||
}
|
|
||||||
|
|
||||||
MaterialAlertDialogBuilder(requireContext())
|
|
||||||
.setTitle(R.string.settings_audio_language)
|
|
||||||
.setSingleChoiceItems(items, initialSelection){ dialog, which ->
|
|
||||||
updateAudioLanguage(supportedAudioLocals[which])
|
|
||||||
dialog.dismiss()
|
|
||||||
}
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showSubtitleLanguageSelection() {
|
|
||||||
// we should be able to use the index of supportedLocals for language selection, items is GUI only
|
|
||||||
val items = supportedSubtitleLocals.map {
|
|
||||||
it.toDisplayString(getString(R.string.settings_content_language_none))
|
|
||||||
}.toTypedArray()
|
|
||||||
|
|
||||||
var initialSelection: Int
|
|
||||||
// profile should be completed here, therefore blocking
|
|
||||||
runBlocking {
|
|
||||||
initialSelection = supportedSubtitleLocals.indexOf(Locale.forLanguageTag(
|
|
||||||
profile.await().preferredContentSubtitleLanguage))
|
profile.await().preferredContentSubtitleLanguage))
|
||||||
if (initialSelection < 0) initialSelection = supportedSubtitleLocals.lastIndex
|
if (initialSelection < 0) initialSelection = supportedLocals.lastIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
MaterialAlertDialogBuilder(requireContext())
|
MaterialAlertDialogBuilder(requireContext())
|
||||||
.setTitle(R.string.settings_audio_language)
|
.setTitle(R.string.settings_content_language)
|
||||||
.setSingleChoiceItems(items, initialSelection){ dialog, which ->
|
.setSingleChoiceItems(items, initialSelection){ dialog, which ->
|
||||||
updateSubtitleLanguage(supportedSubtitleLocals[which])
|
updatePrefContentLanguage(supportedLocals[which])
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
}
|
}
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
private fun updateAudioLanguage(preferredLocale: Locale) {
|
private fun updatePrefContentLanguage(preferredLocale: Locale) {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
Crunchyroll.setPreferredAudioLanguage(preferredLocale.toLanguageTag())
|
Crunchyroll.postPrefSubLanguage(preferredLocale.toLanguageTag())
|
||||||
|
|
||||||
}.invokeOnCompletion {
|
}.invokeOnCompletion {
|
||||||
// update the local preferred audio language
|
// update the local preferred content language
|
||||||
Preferences.savePreferredAudioLocal(requireContext(), preferredLocale)
|
Preferences.savePreferredLocal(requireContext(), preferredLocale)
|
||||||
|
|
||||||
// update profile since the language selection might have changed
|
// update profile since the language selection might have changed
|
||||||
profile = lifecycleScope.async { Crunchyroll.profile() }
|
profile = lifecycleScope.async { Crunchyroll.profile() }
|
||||||
profile.invokeOnCompletion {
|
profile.invokeOnCompletion {
|
||||||
// update language once loading profile is completed
|
// update language once loading profile is completed
|
||||||
binding.textSettingsAudioLanguageDesc.text = Locale.forLanguageTag(
|
binding.textSettingsContentLanguageDesc.text = Locale.forLanguageTag(
|
||||||
profile.getCompleted().preferredContentAudioLanguage
|
|
||||||
).displayLanguage
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
private fun updateSubtitleLanguage(preferredLocal: Locale) {
|
|
||||||
lifecycleScope.launch {
|
|
||||||
Crunchyroll.setPreferredSubtitleLanguage(preferredLocal.toLanguageTag())
|
|
||||||
|
|
||||||
}.invokeOnCompletion {
|
|
||||||
// update the local preferred subtitle language
|
|
||||||
Preferences.savePreferredSubtitleLocal(requireContext(), preferredLocal)
|
|
||||||
|
|
||||||
// update profile since the language selection might have changed
|
|
||||||
profile = lifecycleScope.async { Crunchyroll.profile() }
|
|
||||||
profile.invokeOnCompletion {
|
|
||||||
// update language once loading profile is completed
|
|
||||||
binding.textSettingsAudioLanguageDesc.text = Locale.forLanguageTag(
|
|
||||||
profile.getCompleted().preferredContentSubtitleLanguage
|
profile.getCompleted().preferredContentSubtitleLanguage
|
||||||
).displayLanguage
|
).displayLanguage
|
||||||
}
|
}
|
||||||
|
@ -227,19 +208,17 @@ class AccountFragment : Fragment() {
|
||||||
|
|
||||||
private fun showThemeDialog() {
|
private fun showThemeDialog() {
|
||||||
val items = arrayOf(
|
val items = arrayOf(
|
||||||
resources.getString(R.string.theme_system),
|
|
||||||
resources.getString(R.string.theme_light),
|
resources.getString(R.string.theme_light),
|
||||||
resources.getString(R.string.theme_dark)
|
resources.getString(R.string.theme_dark)
|
||||||
)
|
)
|
||||||
|
|
||||||
MaterialAlertDialogBuilder(requireContext())
|
MaterialAlertDialogBuilder(requireContext())
|
||||||
.setTitle(R.string.theme)
|
.setTitle(R.string.settings_content_language)
|
||||||
.setSingleChoiceItems(items, Preferences.theme.ordinal){ _, which ->
|
.setSingleChoiceItems(items, Preferences.theme.ordinal){ _, which ->
|
||||||
when(which) {
|
when(which) {
|
||||||
0 -> Preferences.saveTheme(requireContext(), Theme.SYSTEM)
|
0 -> Preferences.saveTheme(requireContext(), Theme.LIGHT)
|
||||||
1 -> Preferences.saveTheme(requireContext(), Theme.LIGHT)
|
1 -> Preferences.saveTheme(requireContext(), Theme.DARK)
|
||||||
2 -> Preferences.saveTheme(requireContext(), Theme.DARK)
|
else -> Preferences.saveTheme(requireContext(), Theme.DARK)
|
||||||
else -> Preferences.saveTheme(requireContext(), Theme.SYSTEM)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
(activity as MainActivity).restart()
|
(activity as MainActivity).restart()
|
||||||
|
|
|
@ -1,65 +1,34 @@
|
||||||
/**
|
|
||||||
* Teapod
|
|
||||||
*
|
|
||||||
* Copyright 2020-2022 <seil0@mosad.xyz>
|
|
||||||
*
|
|
||||||
* This program is free software; you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation; either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program; if not, write to the Free Software
|
|
||||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
|
||||||
* MA 02110-1301, USA.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.mosad.teapod.ui.activity.main.fragments
|
package org.mosad.teapod.ui.activity.main.fragments
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
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 android.widget.LinearLayout
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
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.lifecycle.Lifecycle
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.facebook.shimmer.ShimmerFrameLayout
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.joinAll
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.mosad.teapod.R
|
|
||||||
import org.mosad.teapod.databinding.FragmentHomeBinding
|
import org.mosad.teapod.databinding.FragmentHomeBinding
|
||||||
import org.mosad.teapod.ui.activity.main.viewmodel.HomeViewModel
|
import org.mosad.teapod.parser.crunchyroll.Crunchyroll
|
||||||
import org.mosad.teapod.util.adapter.MediaEpisodeListAdapter
|
import org.mosad.teapod.parser.crunchyroll.Item
|
||||||
import org.mosad.teapod.util.adapter.MediaItemListAdapter
|
import org.mosad.teapod.parser.crunchyroll.SortBy
|
||||||
import org.mosad.teapod.util.playerIntent
|
import org.mosad.teapod.util.adapter.MediaItemAdapter
|
||||||
import org.mosad.teapod.util.setDrawableTop
|
import org.mosad.teapod.util.decoration.MediaItemDecoration
|
||||||
import org.mosad.teapod.util.showFragment
|
import org.mosad.teapod.util.showFragment
|
||||||
import org.mosad.teapod.util.toItemMediaList
|
import org.mosad.teapod.util.toItemMediaList
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
class HomeFragment : Fragment() {
|
class HomeFragment : Fragment() {
|
||||||
|
|
||||||
private val classTag = javaClass.name
|
|
||||||
private val model: HomeViewModel by viewModels()
|
|
||||||
private lateinit var binding: FragmentHomeBinding
|
private lateinit var binding: FragmentHomeBinding
|
||||||
|
private lateinit var adapterUpNext: MediaItemAdapter
|
||||||
|
private lateinit var adapterWatchlist: MediaItemAdapter
|
||||||
|
private lateinit var adapterNewTitles: MediaItemAdapter
|
||||||
|
private lateinit var adapterTopTen: MediaItemAdapter
|
||||||
|
|
||||||
private val itemOffset = 21
|
private lateinit var highlightMedia: Item
|
||||||
|
|
||||||
private val playerResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
|
||||||
model.updateUpNextItems()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
binding = FragmentHomeBinding.inflate(inflater, container, false)
|
binding = FragmentHomeBinding.inflate(inflater, container, false)
|
||||||
|
@ -69,165 +38,124 @@ class HomeFragment : Fragment() {
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
binding.recyclerUpNext.adapter = MediaEpisodeListAdapter(
|
lifecycleScope.launch {
|
||||||
MediaEpisodeListAdapter.OnClickListener {
|
context?.let {
|
||||||
playerResult.launch(playerIntent(it.panel.episodeMetadata.seasonId, it.panel.id))
|
initHighlight()
|
||||||
},
|
initRecyclerViews()
|
||||||
itemOffset
|
initActions()
|
||||||
)
|
|
||||||
|
|
||||||
binding.recyclerWatchlist.adapter = MediaItemListAdapter(
|
|
||||||
MediaItemListAdapter.OnClickListener {
|
|
||||||
activity?.showFragment(MediaFragment(it.id))
|
|
||||||
},
|
|
||||||
itemOffset
|
|
||||||
)
|
|
||||||
|
|
||||||
binding.recyclerRecommendations.adapter = MediaItemListAdapter(
|
|
||||||
MediaItemListAdapter.OnClickListener {
|
|
||||||
activity?.showFragment(MediaFragment(it.id))
|
|
||||||
},
|
|
||||||
itemOffset
|
|
||||||
)
|
|
||||||
|
|
||||||
binding.recyclerNewTitles.adapter = MediaItemListAdapter(
|
|
||||||
MediaItemListAdapter.OnClickListener {
|
|
||||||
activity?.showFragment(MediaFragment(it.id))
|
|
||||||
},
|
|
||||||
itemOffset
|
|
||||||
)
|
|
||||||
|
|
||||||
binding.recyclerTopTen.adapter = MediaItemListAdapter(
|
|
||||||
MediaItemListAdapter.OnClickListener {
|
|
||||||
activity?.showFragment(MediaFragment(it.id))
|
|
||||||
},
|
|
||||||
itemOffset
|
|
||||||
)
|
|
||||||
|
|
||||||
binding.textHighlightMyList.setOnClickListener {
|
|
||||||
model.toggleHighlightWatchlist()
|
|
||||||
|
|
||||||
// disable the watchlist button until the result has been loaded
|
|
||||||
binding.textHighlightMyList.isClickable = false
|
|
||||||
// TODO since this might take a few seconds show a loading animation for the watchlist button
|
|
||||||
}
|
|
||||||
|
|
||||||
// set the shimmer items size as it's depending on the screen size
|
|
||||||
setShimmerLayoutItemSize(binding.shimmerLayoutUpNext)
|
|
||||||
setShimmerLayoutItemSize(binding.shimmerLayoutWatchlist)
|
|
||||||
setShimmerLayoutItemSize(binding.shimmerLayoutRecommendations)
|
|
||||||
setShimmerLayoutItemSize(binding.shimmerLayoutNewTitles)
|
|
||||||
setShimmerLayoutItemSize(binding.shimmerLayoutTopTen)
|
|
||||||
|
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
|
||||||
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
|
||||||
model.onUiState(viewLifecycleOwner.lifecycleScope) { uiState ->
|
|
||||||
when (uiState) {
|
|
||||||
is HomeViewModel.UiState.Normal -> bindUiStateNormal(uiState)
|
|
||||||
is HomeViewModel.UiState.Loading -> bindUiStateLoading()
|
|
||||||
is HomeViewModel.UiState.Error -> bindUiStateError(uiState)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun bindUiStateNormal(uiState: HomeViewModel.UiState.Normal) {
|
private fun initHighlight() {
|
||||||
val adapterUpNext = binding.recyclerUpNext.adapter as MediaEpisodeListAdapter
|
lifecycleScope.launch {
|
||||||
adapterUpNext.submitList(uiState.upNextItems.filter { !it.fullyWatched })
|
val newTitles = Crunchyroll.browse(sortBy = SortBy.NEWLY_ADDED, n = 10)
|
||||||
|
// FIXME crashes on newTitles.items.size == 0
|
||||||
|
highlightMedia = newTitles.items[Random.nextInt(newTitles.items.size)]
|
||||||
|
|
||||||
val adapterWatchlist = binding.recyclerWatchlist.adapter as MediaItemListAdapter
|
// add media item to gui
|
||||||
adapterWatchlist.submitList(uiState.watchlistItems.toItemMediaList())
|
binding.textHighlightTitle.text = highlightMedia.title
|
||||||
|
Glide.with(requireContext()).load(highlightMedia.images.poster_wide[0][3].source)
|
||||||
|
.into(binding.imageHighlight)
|
||||||
|
|
||||||
val adapterRecommendations = binding.recyclerRecommendations.adapter as MediaItemListAdapter
|
// TODO watchlist indicator
|
||||||
adapterRecommendations.submitList(uiState.recommendationsItems.toItemMediaList())
|
// if (StorageController.myList.contains(0)) {
|
||||||
|
// binding.textHighlightMyList.setDrawableTop(R.drawable.ic_baseline_check_24)
|
||||||
val adapterNewTitles = binding.recyclerNewTitles.adapter as MediaItemListAdapter
|
// } else {
|
||||||
adapterNewTitles.submitList(uiState.recentlyAddedItems.toItemMediaList())
|
// binding.textHighlightMyList.setDrawableTop(R.drawable.ic_baseline_add_24)
|
||||||
|
// }
|
||||||
val adapterTopTen = binding.recyclerTopTen.adapter as MediaItemListAdapter
|
|
||||||
adapterTopTen.submitList(uiState.topTenItems.toItemMediaList())
|
|
||||||
|
|
||||||
// highlight item
|
|
||||||
binding.textHighlightTitle.text = uiState.highlightItem.title
|
|
||||||
Glide.with(requireContext()).load(uiState.highlightItem.images.poster_wide[0][3].source)
|
|
||||||
.into(binding.imageHighlight)
|
|
||||||
|
|
||||||
val iconHighlightWatchlist = if (uiState.highlightIsWatchlist) {
|
|
||||||
R.drawable.ic_baseline_check_24
|
|
||||||
} else {
|
|
||||||
R.drawable.ic_baseline_add_24
|
|
||||||
}
|
}
|
||||||
binding.textHighlightMyList.setDrawableTop(iconHighlightWatchlist)
|
|
||||||
binding.textHighlightMyList.isClickable = true
|
|
||||||
|
|
||||||
binding.textHighlightInfo.setOnClickListener {
|
|
||||||
activity?.showFragment(MediaFragment(uiState.highlightItem.id))
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.buttonPlayHighlight.setOnClickListener {
|
|
||||||
val panel = uiState.highlightItemUpNext.panel
|
|
||||||
playerResult.launch(playerIntent(panel.episodeMetadata.seasonId, panel.id))
|
|
||||||
}
|
|
||||||
|
|
||||||
// disable the shimmer effect
|
|
||||||
disableShimmer()
|
|
||||||
|
|
||||||
// make highlights layout visible again
|
|
||||||
binding.linearHighlight.isVisible = true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun bindUiStateLoading() {
|
|
||||||
// hide highlights layout
|
|
||||||
binding.linearHighlight.isVisible = false
|
|
||||||
|
|
||||||
binding.shimmerLayoutUpNext.startShimmer()
|
|
||||||
binding.shimmerLayoutWatchlist.startShimmer()
|
|
||||||
binding.shimmerLayoutRecommendations.startShimmer()
|
|
||||||
binding.shimmerLayoutNewTitles.startShimmer()
|
|
||||||
binding.shimmerLayoutTopTen.startShimmer()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setShimmerLayoutItemSize(shimmerLayout: ShimmerFrameLayout) {
|
|
||||||
(shimmerLayout.children.first() as? LinearLayout)?.children?.forEach { child ->
|
|
||||||
child.layoutParams.apply {
|
|
||||||
width = (resources.displayMetrics.widthPixels / requireContext().resources.getInteger(R.integer.item_media_columns)) - itemOffset
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun bindUiStateError(uiState: HomeViewModel.UiState.Error) {
|
|
||||||
// currently not used
|
|
||||||
Log.e(classTag, "A error occurred while loading a UiState: ${uiState.message}")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Disable the shimmer effect for all shimmer layouts and hide them.
|
* Suspend, since adapters need to be initialized before we can initialize the actions.
|
||||||
*/
|
*/
|
||||||
private fun disableShimmer() {
|
private suspend fun initRecyclerViews() {
|
||||||
binding.shimmerLayoutHighlight.apply {
|
binding.recyclerWatchlist.addItemDecoration(MediaItemDecoration(9))
|
||||||
stopShimmer()
|
binding.recyclerNewEpisodes.addItemDecoration(MediaItemDecoration(9))
|
||||||
isVisible = false
|
binding.recyclerNewTitles.addItemDecoration(MediaItemDecoration(9))
|
||||||
|
binding.recyclerTopTen.addItemDecoration(MediaItemDecoration(9))
|
||||||
|
|
||||||
|
val asyncJobList = arrayListOf<Job>()
|
||||||
|
|
||||||
|
// continue watching
|
||||||
|
val upNextJob = lifecycleScope.launch {
|
||||||
|
// TODO create EpisodeItemAdapter, which will start the playback of the selected episode immediately
|
||||||
|
adapterUpNext = MediaItemAdapter(Crunchyroll.upNextAccount().items
|
||||||
|
.filter { !it.fullyWatched }.toItemMediaList())
|
||||||
|
binding.recyclerNewEpisodes.adapter = adapterUpNext
|
||||||
}
|
}
|
||||||
binding.shimmerLayoutUpNext.apply {
|
asyncJobList.add(upNextJob)
|
||||||
stopShimmer()
|
|
||||||
isVisible = false
|
// watchlist
|
||||||
|
val watchlistJob = lifecycleScope.launch {
|
||||||
|
adapterWatchlist = MediaItemAdapter(Crunchyroll.watchlist(50).toItemMediaList())
|
||||||
|
binding.recyclerWatchlist.adapter = adapterWatchlist
|
||||||
}
|
}
|
||||||
binding.shimmerLayoutWatchlist.apply {
|
asyncJobList.add(watchlistJob)
|
||||||
stopShimmer()
|
|
||||||
isVisible = false
|
// new simulcasts
|
||||||
|
val simulcastsJob = lifecycleScope.launch {
|
||||||
|
// val latestSeasonTag = Crunchyroll.seasonList().items.first().id
|
||||||
|
// val newSimulcasts = Crunchyroll.browse(seasonTag = latestSeasonTag, n = 50)
|
||||||
|
val newSimulcasts = Crunchyroll.browse(sortBy = SortBy.NEWLY_ADDED, n = 50)
|
||||||
|
|
||||||
|
adapterNewTitles = MediaItemAdapter(newSimulcasts.toItemMediaList())
|
||||||
|
binding.recyclerNewTitles.adapter = adapterNewTitles
|
||||||
}
|
}
|
||||||
binding.shimmerLayoutRecommendations.apply {
|
asyncJobList.add(simulcastsJob)
|
||||||
stopShimmer()
|
|
||||||
isVisible = false
|
// newly added / top ten
|
||||||
|
val newlyAddedJob = lifecycleScope.launch {
|
||||||
|
adapterTopTen = MediaItemAdapter(Crunchyroll.browse(sortBy = SortBy.POPULARITY, n = 10).toItemMediaList())
|
||||||
|
binding.recyclerTopTen.adapter = adapterTopTen
|
||||||
}
|
}
|
||||||
binding.shimmerLayoutNewTitles.apply {
|
asyncJobList.add(newlyAddedJob)
|
||||||
stopShimmer()
|
|
||||||
isVisible = false
|
asyncJobList.joinAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initActions() {
|
||||||
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
binding.shimmerLayoutTopTen.apply {
|
|
||||||
stopShimmer()
|
binding.textHighlightMyList.setOnClickListener {
|
||||||
isVisible = false
|
// TODO implement
|
||||||
|
// if (StorageController.myList.contains(0)) {
|
||||||
|
// StorageController.myList.remove(0)
|
||||||
|
// binding.textHighlightMyList.setDrawableTop(R.drawable.ic_baseline_add_24)
|
||||||
|
// } else {
|
||||||
|
// StorageController.myList.add(0)
|
||||||
|
// binding.textHighlightMyList.setDrawableTop(R.drawable.ic_baseline_check_24)
|
||||||
|
// }
|
||||||
|
// StorageController.saveMyList(requireContext())
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.textHighlightInfo.setOnClickListener {
|
||||||
|
activity?.showFragment(MediaFragment(highlightMedia.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
adapterUpNext.onItemClick = { id, _ ->
|
||||||
|
activity?.showFragment(MediaFragment(id))
|
||||||
|
}
|
||||||
|
|
||||||
|
adapterWatchlist.onItemClick = { id, _ ->
|
||||||
|
activity?.showFragment(MediaFragment(id))
|
||||||
|
}
|
||||||
|
|
||||||
|
adapterNewTitles.onItemClick = { id, _ ->
|
||||||
|
activity?.showFragment(MediaFragment(id))
|
||||||
|
}
|
||||||
|
|
||||||
|
adapterTopTen.onItemClick = { id, _ ->
|
||||||
|
activity?.showFragment(MediaFragment(id)) //(mediaId))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,30 +1,29 @@
|
||||||
package org.mosad.teapod.ui.activity.main.fragments
|
package org.mosad.teapod.ui.activity.main.fragments
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
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.appcompat.widget.SearchView
|
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.viewModels
|
|
||||||
import androidx.lifecycle.Lifecycle
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.mosad.teapod.databinding.FragmentLibraryBinding
|
import org.mosad.teapod.databinding.FragmentLibraryBinding
|
||||||
import org.mosad.teapod.ui.activity.main.viewmodel.LibraryFragmentViewModel
|
import org.mosad.teapod.parser.crunchyroll.Crunchyroll
|
||||||
import org.mosad.teapod.util.adapter.MediaItemListAdapter
|
import org.mosad.teapod.util.ItemMedia
|
||||||
|
import org.mosad.teapod.util.adapter.MediaItemAdapter
|
||||||
|
import org.mosad.teapod.util.decoration.MediaItemDecoration
|
||||||
import org.mosad.teapod.util.showFragment
|
import org.mosad.teapod.util.showFragment
|
||||||
|
|
||||||
class LibraryFragment : Fragment() {
|
class LibraryFragment : Fragment() {
|
||||||
|
|
||||||
private lateinit var binding: FragmentLibraryBinding
|
private lateinit var binding: FragmentLibraryBinding
|
||||||
private lateinit var adapter: MediaItemListAdapter
|
private lateinit var adapter: MediaItemAdapter
|
||||||
private val model: LibraryFragmentViewModel by viewModels()
|
|
||||||
|
private val itemList = arrayListOf<ItemMedia>()
|
||||||
|
private val pageSize = 30
|
||||||
|
private var nextItemIndex = 0
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
binding = FragmentLibraryBinding.inflate(inflater, container, false)
|
binding = FragmentLibraryBinding.inflate(inflater, container, false)
|
||||||
|
@ -34,79 +33,57 @@ class LibraryFragment : Fragment() {
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
// TODO replace with pagination3
|
// init async
|
||||||
// https://medium.com/swlh/paging3-recyclerview-pagination-made-easy-333c7dfa8797
|
lifecycleScope.launch {
|
||||||
binding.recyclerMediaSearch.addOnScrollListener(PaginationScrollListener())
|
// create and set the adapter, needs context
|
||||||
|
context?.let {
|
||||||
|
val initialResults = Crunchyroll.browse(n = pageSize)
|
||||||
|
itemList.addAll(initialResults.items.map { item ->
|
||||||
|
ItemMedia(item.id, item.title, item.images.poster_wide[0][0].source)
|
||||||
|
})
|
||||||
|
nextItemIndex += pageSize
|
||||||
|
|
||||||
adapter = MediaItemListAdapter(MediaItemListAdapter.OnClickListener {
|
adapter = MediaItemAdapter(itemList)
|
||||||
binding.searchText.clearFocus()
|
adapter.onItemClick = { mediaIdStr, _ ->
|
||||||
activity?.showFragment(MediaFragment(it.id))
|
activity?.showFragment(MediaFragment(mediaIdStr))
|
||||||
})
|
|
||||||
binding.recyclerMediaSearch.adapter = adapter
|
|
||||||
|
|
||||||
binding.searchText.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
|
||||||
override fun onQueryTextSubmit(query: String?): Boolean {
|
|
||||||
query?.let { model.search(it) }
|
|
||||||
return false // return false to dismiss the keyboard
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onQueryTextChange(newText: String?): Boolean {
|
|
||||||
newText?.let { model.search(it) }
|
|
||||||
return false // return false to dismiss the keyboard
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
|
||||||
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
|
||||||
model.onUiState(viewLifecycleOwner.lifecycleScope) { uiState ->
|
|
||||||
when (uiState) {
|
|
||||||
is LibraryFragmentViewModel.UiState.Browse -> bindUiStateBrowse(uiState)
|
|
||||||
is LibraryFragmentViewModel.UiState.Search -> bindUiStateSearch(uiState)
|
|
||||||
is LibraryFragmentViewModel.UiState.Loading -> bindUiStateLoading()
|
|
||||||
is LibraryFragmentViewModel.UiState.Error -> bindUiStateError(uiState)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
binding.recyclerMediaLibrary.adapter = adapter
|
||||||
|
binding.recyclerMediaLibrary.addItemDecoration(MediaItemDecoration(9))
|
||||||
|
// TODO replace with pagination3
|
||||||
|
// https://medium.com/swlh/paging3-recyclerview-pagination-made-easy-333c7dfa8797
|
||||||
|
binding.recyclerMediaLibrary.addOnScrollListener(PaginationScrollListener())
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun bindUiStateBrowse(uiState: LibraryFragmentViewModel.UiState.Browse) {
|
|
||||||
adapter.submitList(uiState.itemList)
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
|
||||||
private fun bindUiStateSearch(uiState: LibraryFragmentViewModel.UiState.Search) {
|
|
||||||
adapter.submitList(uiState.itemList)
|
|
||||||
adapter.notifyDataSetChanged() // this is needed, else the adapter will not update
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun bindUiStateLoading() {
|
|
||||||
// currently not used
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun bindUiStateError(uiState: LibraryFragmentViewModel.UiState.Error) {
|
|
||||||
// currently not used
|
|
||||||
Log.e(javaClass.name, "A error occurred while loading a UiState: ${uiState.message}")
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class PaginationScrollListener: RecyclerView.OnScrollListener() {
|
inner class PaginationScrollListener: RecyclerView.OnScrollListener() {
|
||||||
|
private var isLoading = false
|
||||||
|
|
||||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||||
super.onScrolled(recyclerView, dx, dy)
|
super.onScrolled(recyclerView, dx, dy)
|
||||||
|
val layoutManager = recyclerView.layoutManager as GridLayoutManager?
|
||||||
|
|
||||||
if (!model.isLazyLoading) {
|
if (!isLoading) layoutManager?.let {
|
||||||
val layoutManager = recyclerView.layoutManager as? GridLayoutManager
|
// itemList.size - 5 to start loading a bit earlier than the actual end
|
||||||
layoutManager?.let {
|
if (layoutManager.findLastCompletelyVisibleItemPosition() >= (itemList.size - 5)) {
|
||||||
// adapter.itemCount - 10 to start loading a bit earlier than the actual end
|
// load new browse results async
|
||||||
if (layoutManager.findLastCompletelyVisibleItemPosition() >= (adapter.itemCount - 10)) {
|
isLoading = true
|
||||||
model.onLazyLoad().invokeOnCompletion {
|
lifecycleScope.launch {
|
||||||
adapter.notifyItemRangeInserted(adapter.itemCount, model.PAGESIZE)
|
val firstNewItemIndex = itemList.lastIndex + 1
|
||||||
}
|
val results = Crunchyroll.browse(start = nextItemIndex, n = pageSize)
|
||||||
|
itemList.addAll(results.items.map { item ->
|
||||||
|
ItemMedia(item.id, item.title, item.images.poster_wide[0][0].source)
|
||||||
|
})
|
||||||
|
nextItemIndex += pageSize
|
||||||
|
|
||||||
|
adapter.notifyItemRangeInserted(firstNewItemIndex, pageSize)
|
||||||
|
isLoading = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -7,9 +7,9 @@ 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.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
|
@ -20,13 +20,12 @@ import jp.wasabeef.glide.transformations.BlurTransformation
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.mosad.teapod.R
|
import org.mosad.teapod.R
|
||||||
import org.mosad.teapod.databinding.FragmentMediaBinding
|
import org.mosad.teapod.databinding.FragmentMediaBinding
|
||||||
import org.mosad.teapod.parser.crunchyroll.NoneUpNextSeriesList
|
import org.mosad.teapod.parser.crunchyroll.NoneUpNextSeriesItem
|
||||||
|
import org.mosad.teapod.ui.activity.main.MainActivity
|
||||||
import org.mosad.teapod.ui.activity.main.viewmodel.MediaFragmentViewModel
|
import org.mosad.teapod.ui.activity.main.viewmodel.MediaFragmentViewModel
|
||||||
import org.mosad.teapod.util.playerIntent
|
|
||||||
import org.mosad.teapod.util.tmdb.TMDBApiController
|
import org.mosad.teapod.util.tmdb.TMDBApiController
|
||||||
import org.mosad.teapod.util.tmdb.TMDBMovie
|
import org.mosad.teapod.util.tmdb.TMDBMovie
|
||||||
import org.mosad.teapod.util.tmdb.TMDBTVShow
|
import org.mosad.teapod.util.tmdb.TMDBTVShow
|
||||||
import org.mosad.teapod.util.toItemMediaList
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The media detail fragment.
|
* The media detail fragment.
|
||||||
|
@ -38,14 +37,12 @@ class MediaFragment(private val mediaIdStr: String) : Fragment() {
|
||||||
private lateinit var binding: FragmentMediaBinding
|
private lateinit var binding: FragmentMediaBinding
|
||||||
private lateinit var pagerAdapter: FragmentStateAdapter
|
private lateinit var pagerAdapter: FragmentStateAdapter
|
||||||
|
|
||||||
private val model: MediaFragmentViewModel by viewModels()
|
private val model: MediaFragmentViewModel by activityViewModels()
|
||||||
|
|
||||||
private val fragments = arrayListOf<Fragment>()
|
private val fragments = arrayListOf<Fragment>()
|
||||||
private var watchlistJobRunning = false
|
private var watchlistJobRunning = false
|
||||||
|
private var runOnResume = false
|
||||||
|
|
||||||
private val playerResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
|
||||||
playerFinishedCallback()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
binding = FragmentMediaBinding.inflate(inflater, container, false)
|
binding = FragmentMediaBinding.inflate(inflater, container, false)
|
||||||
|
@ -57,7 +54,7 @@ class MediaFragment(private val mediaIdStr: String) : Fragment() {
|
||||||
binding.frameLoading.visibility = View.VISIBLE
|
binding.frameLoading.visibility = View.VISIBLE
|
||||||
|
|
||||||
// tab layout and pager
|
// tab layout and pager
|
||||||
pagerAdapter = ScreenSlidePagerAdapter(this)
|
pagerAdapter = ScreenSlidePagerAdapter(requireActivity())
|
||||||
// fix material components issue #1878, if more tabs are added increase
|
// fix material components issue #1878, if more tabs are added increase
|
||||||
binding.pagerEpisodesSimilar.offscreenPageLimit = 2
|
binding.pagerEpisodesSimilar.offscreenPageLimit = 2
|
||||||
binding.pagerEpisodesSimilar.adapter = pagerAdapter
|
binding.pagerEpisodesSimilar.adapter = pagerAdapter
|
||||||
|
@ -78,6 +75,27 @@ class MediaFragment(private val mediaIdStr: String) : Fragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
|
||||||
|
if (runOnResume) {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
model.updateOnResume()
|
||||||
|
|
||||||
|
if (model.upNextSeries != NoneUpNextSeriesItem) {
|
||||||
|
binding.textTitle.text = model.upNextSeries.panel.title
|
||||||
|
}
|
||||||
|
|
||||||
|
// needs to be called after model.updateOnResume()
|
||||||
|
if (fragments.elementAtOrNull(0) is MediaFragmentEpisodes) {
|
||||||
|
(fragments[0] as MediaFragmentEpisodes).updateWatchedState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
runOnResume = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* if tmdb data is present, use it, else use the aod data
|
* if tmdb data is present, use it, else use the aod data
|
||||||
*/
|
*/
|
||||||
|
@ -90,7 +108,6 @@ class MediaFragment(private val mediaIdStr: String) : Fragment() {
|
||||||
|
|
||||||
// load poster and backdrop
|
// load poster and backdrop
|
||||||
Glide.with(requireContext()).load(posterUrl)
|
Glide.with(requireContext()).load(posterUrl)
|
||||||
.apply(RequestOptions.placeholderOf(ColorDrawable(Color.DKGRAY)))
|
|
||||||
.into(binding.imagePoster)
|
.into(binding.imagePoster)
|
||||||
Glide.with(requireContext()).load(backdropUrl)
|
Glide.with(requireContext()).load(backdropUrl)
|
||||||
.apply(RequestOptions.placeholderOf(ColorDrawable(Color.DKGRAY)))
|
.apply(RequestOptions.placeholderOf(ColorDrawable(Color.DKGRAY)))
|
||||||
|
@ -98,14 +115,14 @@ class MediaFragment(private val mediaIdStr: String) : Fragment() {
|
||||||
.into(binding.imageBackdrop)
|
.into(binding.imageBackdrop)
|
||||||
|
|
||||||
binding.textYear.text = when(tmdbResult) {
|
binding.textYear.text = when(tmdbResult) {
|
||||||
is TMDBTVShow -> (tmdbResult as TMDBTVShow).firstAirDate?.substring(0, 4)
|
is TMDBTVShow -> (tmdbResult as TMDBTVShow).firstAirDate.substring(0, 4)
|
||||||
is TMDBMovie -> (tmdbResult as TMDBMovie).releaseDate.substring(0, 4)
|
is TMDBMovie -> (tmdbResult as TMDBMovie).releaseDate.substring(0, 4)
|
||||||
else -> ""
|
else -> ""
|
||||||
}
|
}
|
||||||
binding.textAge.text = seriesCrunchy.maturityRatings.firstOrNull()
|
binding.textAge.text = seriesCrunchy.maturityRatings.firstOrNull()
|
||||||
|
|
||||||
binding.textTitle.text = if (upNextSeries != NoneUpNextSeriesList) {
|
binding.textTitle.text = if (upNextSeries != NoneUpNextSeriesItem) {
|
||||||
upNextSeries.data.first().panel.title
|
upNextSeries.panel.title
|
||||||
} else seriesCrunchy.title
|
} else seriesCrunchy.title
|
||||||
binding.textOverview.text = seriesCrunchy.description
|
binding.textOverview.text = seriesCrunchy.description
|
||||||
|
|
||||||
|
@ -113,48 +130,31 @@ class MediaFragment(private val mediaIdStr: String) : Fragment() {
|
||||||
val watchlistIcon = if (isWatchlist) R.drawable.ic_baseline_check_24 else R.drawable.ic_baseline_add_24
|
val watchlistIcon = if (isWatchlist) R.drawable.ic_baseline_check_24 else R.drawable.ic_baseline_add_24
|
||||||
Glide.with(requireContext()).load(watchlistIcon).into(binding.imageMyListAction)
|
Glide.with(requireContext()).load(watchlistIcon).into(binding.imageMyListAction)
|
||||||
|
|
||||||
/**
|
// clear fragments, since it lives in onCreate scope (don't do this in onPause/onStop -> FragmentManager transaction)
|
||||||
* clear fragments, since it lives in onCreate scope,
|
val fragmentsSize = if (fragments.lastIndex < 0) 0 else fragments.lastIndex
|
||||||
* don't do this in onPause/onStop -> FragmentManager transaction
|
|
||||||
* (will be called on similar -> new MediaFragment -> onBackPressed)
|
|
||||||
*/
|
|
||||||
val fragmentsSize = fragments.size
|
|
||||||
fragments.clear()
|
fragments.clear()
|
||||||
pagerAdapter.notifyItemRangeRemoved(0, fragmentsSize)
|
pagerAdapter.notifyItemRangeRemoved(0, fragmentsSize)
|
||||||
|
|
||||||
|
// add the episodes fragment (as tab). Note: Movies are tv shows!
|
||||||
MediaFragmentEpisodes().also {
|
MediaFragmentEpisodes().also {
|
||||||
fragments.add(it)
|
fragments.add(it)
|
||||||
pagerAdapter.notifyItemInserted(fragments.indexOf(it))
|
pagerAdapter.notifyItemInserted(fragments.indexOf(it))
|
||||||
}
|
}
|
||||||
|
|
||||||
// if has similar titles
|
|
||||||
if (model.similarTo.total > 0) {
|
|
||||||
MediaFragmentSimilar(model.similarTo.toItemMediaList()).also {
|
|
||||||
fragments.add(it)
|
|
||||||
pagerAdapter.notifyItemInserted(fragments.indexOf(it))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// disable scrolling on appbar, if no tabs where added
|
|
||||||
if(fragments.isEmpty()) {
|
|
||||||
val params = binding.linearMedia.layoutParams as AppBarLayout.LayoutParams
|
|
||||||
params.scrollFlags = 0 // clear all scroll flags
|
|
||||||
}
|
|
||||||
|
|
||||||
// specific gui (via tmdb)
|
// specific gui (via tmdb)
|
||||||
when (tmdbResult) {
|
when (tmdbResult) {
|
||||||
is TMDBTVShow -> {
|
is TMDBTVShow -> {
|
||||||
// episodes count
|
// episodes count
|
||||||
binding.textEpisodesOrRuntime.text = resources.getQuantityString(
|
binding.textEpisodesOrRuntime.text = resources.getQuantityString(
|
||||||
R.plurals.text_episodes_count,
|
R.plurals.text_episodes_count,
|
||||||
seriesCrunchy.episodeCount,
|
episodesCrunchy.total,
|
||||||
seriesCrunchy.episodeCount
|
episodesCrunchy.total
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is TMDBMovie -> {
|
is TMDBMovie -> {
|
||||||
val tmdbMovie = tmdbResult as TMDBMovie
|
val tmdbMovie = (tmdbResult as TMDBMovie?)
|
||||||
|
|
||||||
if (tmdbMovie.runtime != null) {
|
if (tmdbMovie?.runtime != null) {
|
||||||
binding.textEpisodesOrRuntime.text = resources.getQuantityString(
|
binding.textEpisodesOrRuntime.text = resources.getQuantityString(
|
||||||
R.plurals.text_runtime,
|
R.plurals.text_runtime,
|
||||||
tmdbMovie.runtime,
|
tmdbMovie.runtime,
|
||||||
|
@ -169,14 +169,28 @@ class MediaFragment(private val mediaIdStr: String) : Fragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if has similar titles
|
||||||
|
// TODO reimplement
|
||||||
|
// if (media.similar.isNotEmpty()) {
|
||||||
|
// MediaFragmentSimilar().also {
|
||||||
|
// fragments.add(it)
|
||||||
|
// pagerAdapter.notifyItemInserted(fragments.indexOf(it))
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// disable scrolling on appbar, if no tabs where added
|
||||||
|
if(fragments.isEmpty()) {
|
||||||
|
val params = binding.linearMedia.layoutParams as AppBarLayout.LayoutParams
|
||||||
|
params.scrollFlags = 0 // clear all scroll flags
|
||||||
|
}
|
||||||
|
|
||||||
binding.frameLoading.visibility = View.GONE // hide loading indicator
|
binding.frameLoading.visibility = View.GONE // hide loading indicator
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initActions() = with(model) {
|
private fun initActions() = with(model) {
|
||||||
binding.buttonPlay.setOnClickListener {
|
binding.buttonPlay.setOnClickListener {
|
||||||
if (upNextSeries != NoneUpNextSeriesList) {
|
if (upNextSeries != NoneUpNextSeriesItem) {
|
||||||
val panel = upNextSeries.data.first().panel
|
playEpisode(upNextSeries.panel.episodeMetadata.seasonId, upNextSeries.panel.id)
|
||||||
playEpisode(panel.episodeMetadata.seasonId, panel.id)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -197,31 +211,21 @@ class MediaFragment(private val mediaIdStr: String) : Fragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun playerFinishedCallback() = lifecycleScope.launch {
|
|
||||||
model.updateOnResume()
|
|
||||||
|
|
||||||
if (model.upNextSeries != NoneUpNextSeriesList) {
|
|
||||||
binding.textTitle.text = model.upNextSeries.data.first().panel.title
|
|
||||||
}
|
|
||||||
|
|
||||||
// needs to be called after model.updateOnResume()
|
|
||||||
(fragments.elementAtOrNull(0) as? MediaFragmentEpisodes)?.updateWatchedState()
|
|
||||||
|
|
||||||
Log.d(javaClass.name, "Updated model and gui after player closed")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* play a episode, also runs callback on player result return
|
* play the current episode
|
||||||
|
* TODO this is also used in MediaFragmentEpisode, we should only have on implementation
|
||||||
*/
|
*/
|
||||||
fun playEpisode(seasonId: String, episodeId: String) {
|
private fun playEpisode(seasonId: String, episodeId: String) {
|
||||||
playerResult.launch(playerIntent(seasonId, episodeId))
|
(activity as MainActivity).startPlayer(seasonId, episodeId)
|
||||||
Log.d(javaClass.name, "Started Player with episodeId: $episodeId")
|
Log.d(javaClass.name, "Started Player with episodeId: $episodeId")
|
||||||
|
|
||||||
|
//model.updateNextEpisode(episodeId) // set the correct next episode
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A simple pager adapter
|
* A simple pager adapter
|
||||||
*/
|
*/
|
||||||
private inner class ScreenSlidePagerAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) {
|
private inner class ScreenSlidePagerAdapter(fa: FragmentActivity) : FragmentStateAdapter(fa) {
|
||||||
override fun getItemCount(): Int = fragments.size
|
override fun getItemCount(): Int = fragments.size
|
||||||
|
|
||||||
override fun createFragment(position: Int): Fragment = fragments[position]
|
override fun createFragment(position: Int): Fragment = fragments[position]
|
||||||
|
|
|
@ -2,16 +2,18 @@ package org.mosad.teapod.ui.activity.main.fragments
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
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.appcompat.widget.PopupMenu
|
import androidx.appcompat.widget.PopupMenu
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.mosad.teapod.R
|
import org.mosad.teapod.R
|
||||||
import org.mosad.teapod.databinding.FragmentMediaEpisodesBinding
|
import org.mosad.teapod.databinding.FragmentMediaEpisodesBinding
|
||||||
|
import org.mosad.teapod.ui.activity.main.MainActivity
|
||||||
import org.mosad.teapod.ui.activity.main.viewmodel.MediaFragmentViewModel
|
import org.mosad.teapod.ui.activity.main.viewmodel.MediaFragmentViewModel
|
||||||
import org.mosad.teapod.util.adapter.EpisodeItemAdapter
|
import org.mosad.teapod.util.adapter.EpisodeItemAdapter
|
||||||
|
|
||||||
|
@ -20,7 +22,7 @@ class MediaFragmentEpisodes : Fragment() {
|
||||||
private lateinit var binding: FragmentMediaEpisodesBinding
|
private lateinit var binding: FragmentMediaEpisodesBinding
|
||||||
private lateinit var adapterRecEpisodes: EpisodeItemAdapter
|
private lateinit var adapterRecEpisodes: EpisodeItemAdapter
|
||||||
|
|
||||||
private val model: MediaFragmentViewModel by viewModels({requireParentFragment()})
|
private val model: MediaFragmentViewModel by activityViewModels()
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
binding = FragmentMediaEpisodesBinding.inflate(inflater, container, false)
|
binding = FragmentMediaEpisodesBinding.inflate(inflater, container, false)
|
||||||
|
@ -33,14 +35,15 @@ class MediaFragmentEpisodes : Fragment() {
|
||||||
adapterRecEpisodes = EpisodeItemAdapter(
|
adapterRecEpisodes = EpisodeItemAdapter(
|
||||||
model.currentEpisodesCrunchy,
|
model.currentEpisodesCrunchy,
|
||||||
model.tmdbTVSeason.episodes,
|
model.tmdbTVSeason.episodes,
|
||||||
model.currentPlayheads,
|
model.currentPlayheads
|
||||||
EpisodeItemAdapter.OnClickListener { episode ->
|
|
||||||
(requireParentFragment() as? MediaFragment)?.playEpisode(episode.seasonId, episode.id)
|
|
||||||
},
|
|
||||||
EpisodeItemAdapter.ViewType.MEDIA_FRAGMENT
|
|
||||||
)
|
)
|
||||||
binding.recyclerEpisodes.adapter = adapterRecEpisodes
|
binding.recyclerEpisodes.adapter = adapterRecEpisodes
|
||||||
|
|
||||||
|
// set onItemClick, adapter is initialized
|
||||||
|
adapterRecEpisodes.onImageClick = { seasonId, episodeId ->
|
||||||
|
playEpisode(seasonId, episodeId)
|
||||||
|
}
|
||||||
|
|
||||||
// don't show season selection if only one season is present
|
// don't show season selection if only one season is present
|
||||||
if (model.seasonsCrunchy.total < 2) {
|
if (model.seasonsCrunchy.total < 2) {
|
||||||
binding.buttonSeasonSelection.visibility = View.GONE
|
binding.buttonSeasonSelection.visibility = View.GONE
|
||||||
|
@ -59,15 +62,13 @@ class MediaFragmentEpisodes : Fragment() {
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
fun updateWatchedState() {
|
fun updateWatchedState() {
|
||||||
// model.currentPlayheads is a val mutable map -> notify dataset changed
|
// model.currentPlayheads is a val mutable map -> notify dataset changed
|
||||||
if (this::adapterRecEpisodes.isInitialized) {
|
adapterRecEpisodes.notifyDataSetChanged()
|
||||||
adapterRecEpisodes.notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showSeasonSelection(v: View) {
|
private fun showSeasonSelection(v: View) {
|
||||||
// TODO replace with Exposed dropdown menu: https://material.io/components/menus/android#exposed-dropdown-menus
|
// TODO replace with Exposed dropdown menu: https://material.io/components/menus/android#exposed-dropdown-menus
|
||||||
val popup = PopupMenu(requireContext(), v)
|
val popup = PopupMenu(requireContext(), v)
|
||||||
model.seasonsCrunchy.data.forEach { season ->
|
model.seasonsCrunchy.items.forEach { season ->
|
||||||
popup.menu.add(getString(
|
popup.menu.add(getString(
|
||||||
R.string.season_number_title,
|
R.string.season_number_title,
|
||||||
season.seasonNumber,
|
season.seasonNumber,
|
||||||
|
@ -104,4 +105,11 @@ class MediaFragmentEpisodes : Fragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun playEpisode(seasonId: String, episodeId: String) {
|
||||||
|
(activity as MainActivity).startPlayer(seasonId, episodeId)
|
||||||
|
Log.d(javaClass.name, "Started Player with episodeId: $episodeId")
|
||||||
|
|
||||||
|
//model.updateNextEpisode(episodeId) // set the correct next episode
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -1,25 +1,3 @@
|
||||||
/**
|
|
||||||
* Teapod
|
|
||||||
*
|
|
||||||
* Copyright 2020-2022 <seil0@mosad.xyz>
|
|
||||||
*
|
|
||||||
* This program is free software; you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation; either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program; if not, write to the Free Software
|
|
||||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
|
||||||
* MA 02110-1301, USA.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.mosad.teapod.ui.activity.main.fragments
|
package org.mosad.teapod.ui.activity.main.fragments
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
@ -27,14 +5,19 @@ import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
import org.mosad.teapod.databinding.FragmentMediaSimilarBinding
|
import org.mosad.teapod.databinding.FragmentMediaSimilarBinding
|
||||||
import org.mosad.teapod.util.ItemMedia
|
import org.mosad.teapod.ui.activity.main.viewmodel.MediaFragmentViewModel
|
||||||
import org.mosad.teapod.util.adapter.MediaItemListAdapter
|
import org.mosad.teapod.util.adapter.MediaItemAdapter
|
||||||
|
import org.mosad.teapod.util.decoration.MediaItemDecoration
|
||||||
import org.mosad.teapod.util.showFragment
|
import org.mosad.teapod.util.showFragment
|
||||||
|
|
||||||
class MediaFragmentSimilar(val items: List<ItemMedia>) : Fragment() {
|
class MediaFragmentSimilar : Fragment() {
|
||||||
|
|
||||||
private lateinit var binding: FragmentMediaSimilarBinding
|
private lateinit var binding: FragmentMediaSimilarBinding
|
||||||
|
private val model: MediaFragmentViewModel by activityViewModels()
|
||||||
|
|
||||||
|
private lateinit var adapterSimilar: MediaItemAdapter
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
binding = FragmentMediaSimilarBinding.inflate(inflater, container, false)
|
binding = FragmentMediaSimilarBinding.inflate(inflater, container, false)
|
||||||
|
@ -44,13 +27,15 @@ class MediaFragmentSimilar(val items: List<ItemMedia>) : Fragment() {
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
binding.recyclerMediaSimilar.adapter = MediaItemListAdapter(
|
adapterSimilar = MediaItemAdapter(emptyList()) //(model.media.similar)
|
||||||
MediaItemListAdapter.OnClickListener {
|
binding.recyclerMediaSimilar.adapter = adapterSimilar
|
||||||
activity?.showFragment(MediaFragment(it.id))
|
binding.recyclerMediaSimilar.addItemDecoration(MediaItemDecoration(9))
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
val adapterSimilar = binding.recyclerMediaSimilar.adapter as MediaItemListAdapter
|
// set onItemClick only in adapter is initialized
|
||||||
adapterSimilar.submitList(items)
|
if (this::adapterSimilar.isInitialized) {
|
||||||
|
adapterSimilar.onItemClick = { mediaId, _ ->
|
||||||
|
activity?.showFragment(MediaFragment("")) //(mediaId))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,90 +0,0 @@
|
||||||
package org.mosad.teapod.ui.activity.main.fragments
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.util.Log
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.fragment.app.viewModels
|
|
||||||
import androidx.lifecycle.Lifecycle
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
|
||||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
|
||||||
import com.google.android.material.tabs.TabLayoutMediator
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.mosad.teapod.R
|
|
||||||
import org.mosad.teapod.databinding.FragmentMyListsBinding
|
|
||||||
import org.mosad.teapod.ui.activity.main.viewmodel.MyListsFragmentViewModel
|
|
||||||
import org.mosad.teapod.util.toItemMediaList
|
|
||||||
|
|
||||||
class MyListsFragment : Fragment() {
|
|
||||||
|
|
||||||
private lateinit var binding: FragmentMyListsBinding
|
|
||||||
private lateinit var pagerAdapter: FragmentStateAdapter
|
|
||||||
|
|
||||||
private val model: MyListsFragmentViewModel by viewModels()
|
|
||||||
|
|
||||||
private val fragments = arrayListOf<Fragment>()
|
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
|
||||||
binding = FragmentMyListsBinding.inflate(inflater, container, false)
|
|
||||||
return binding.root
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
|
|
||||||
// tab layout and pager
|
|
||||||
pagerAdapter = ScreenSlidePagerAdapter(this)
|
|
||||||
binding.pagerMyLists.adapter = pagerAdapter
|
|
||||||
|
|
||||||
TabLayoutMediator(binding.tabMyLists, binding.pagerMyLists) { tab, position ->
|
|
||||||
tab.text = when(position) {
|
|
||||||
0 -> getString(R.string.my_list)
|
|
||||||
1 -> getString(R.string.crunchylists)
|
|
||||||
2 -> getString(R.string.downloads)
|
|
||||||
else -> ""
|
|
||||||
}
|
|
||||||
}.attach()
|
|
||||||
|
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
|
||||||
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
|
||||||
model.onUiState(viewLifecycleOwner.lifecycleScope) { uiState ->
|
|
||||||
when (uiState) {
|
|
||||||
is MyListsFragmentViewModel.UiState.Normal -> bindUiStateNormal(uiState)
|
|
||||||
is MyListsFragmentViewModel.UiState.Loading -> bindUiStateLoading()
|
|
||||||
is MyListsFragmentViewModel.UiState.Error -> bindUiStateError(uiState)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun bindUiStateNormal(uiState: MyListsFragmentViewModel.UiState.Normal) {
|
|
||||||
MediaFragmentSimilar(uiState.watchlistItems.toItemMediaList()).also {
|
|
||||||
fragments.add(it)
|
|
||||||
pagerAdapter.notifyItemInserted(fragments.indexOf(it))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun bindUiStateLoading() {
|
|
||||||
// currently not used
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun bindUiStateError(uiState: MyListsFragmentViewModel.UiState.Error) {
|
|
||||||
// currently not used
|
|
||||||
Log.e(javaClass.name, "A error occurred while loading a UiState: ${uiState.message}")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A simple pager adapter
|
|
||||||
* TODO also present in MediaFragment
|
|
||||||
*/
|
|
||||||
private inner class ScreenSlidePagerAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) {
|
|
||||||
override fun getItemCount(): Int = fragments.size
|
|
||||||
|
|
||||||
override fun createFragment(position: Int): Fragment = fragments[position]
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -0,0 +1,118 @@
|
||||||
|
package org.mosad.teapod.ui.activity.main.fragments
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.SearchView
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.mosad.teapod.databinding.FragmentSearchBinding
|
||||||
|
import org.mosad.teapod.parser.crunchyroll.Crunchyroll
|
||||||
|
import org.mosad.teapod.util.ItemMedia
|
||||||
|
import org.mosad.teapod.util.adapter.MediaItemAdapter
|
||||||
|
import org.mosad.teapod.util.decoration.MediaItemDecoration
|
||||||
|
import org.mosad.teapod.util.showFragment
|
||||||
|
|
||||||
|
class SearchFragment : Fragment() {
|
||||||
|
|
||||||
|
private lateinit var binding: FragmentSearchBinding
|
||||||
|
private lateinit var adapter: MediaItemAdapter
|
||||||
|
|
||||||
|
private val itemList = arrayListOf<ItemMedia>()
|
||||||
|
private var searchJob: Job? = null
|
||||||
|
private var oldSearchQuery = ""
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
|
binding = FragmentSearchBinding.inflate(inflater, container, false)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
// create and set the adapter, needs context
|
||||||
|
context?.let {
|
||||||
|
adapter = MediaItemAdapter(itemList)
|
||||||
|
adapter.onItemClick = { mediaIdStr, _ ->
|
||||||
|
binding.searchText.clearFocus()
|
||||||
|
activity?.showFragment(MediaFragment(mediaIdStr))
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.recyclerMediaSearch.adapter = adapter
|
||||||
|
binding.recyclerMediaSearch.addItemDecoration(MediaItemDecoration(9))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initActions()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initActions() {
|
||||||
|
binding.searchText.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||||
|
override fun onQueryTextSubmit(query: String?): Boolean {
|
||||||
|
query?.let { search(it) }
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onQueryTextChange(newText: String?): Boolean {
|
||||||
|
newText?.let { search(it) }
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun search(query: String) {
|
||||||
|
// if the query hasn't changed since the last successful search, return
|
||||||
|
if (query == oldSearchQuery) return
|
||||||
|
|
||||||
|
// cancel search job if one is already running
|
||||||
|
if (searchJob?.isActive == true) searchJob?.cancel()
|
||||||
|
|
||||||
|
searchJob = lifecycleScope.async {
|
||||||
|
// TODO maybe wait a few ms (500ms?) before searching, if the user inputs any other chars
|
||||||
|
val results = Crunchyroll.search(query, 50)
|
||||||
|
|
||||||
|
itemList.clear() // TODO needs clean up
|
||||||
|
|
||||||
|
// TODO add top results first heading
|
||||||
|
itemList.addAll(results.items[0].items.map { item ->
|
||||||
|
ItemMedia(item.id, item.title, item.images.poster_wide[0][0].source)
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO currently only tv shows are supported, hence only the first items array
|
||||||
|
// should be always present
|
||||||
|
|
||||||
|
// // TODO add tv shows heading
|
||||||
|
// if (results.items.size >= 2) {
|
||||||
|
// itemList.addAll(results.items[1].items.map { item ->
|
||||||
|
// ItemMedia(item.id, item.title, item.images.poster_wide[0][0].source)
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // TODO add movies heading
|
||||||
|
// if (results.items.size >= 3) {
|
||||||
|
// itemList.addAll(results.items[2].items.map { item ->
|
||||||
|
// ItemMedia(item.id, item.title, item.images.poster_wide[0][0].source)
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // TODO add episodes heading
|
||||||
|
// if (results.items.size >= 4) {
|
||||||
|
// itemList.addAll(results.items[3].items.map { item ->
|
||||||
|
// ItemMedia(item.id, item.title, item.images.poster_wide[0][0].source)
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
|
adapter.notifyDataSetChanged()
|
||||||
|
//adapter.notifyItemRangeInserted(0, itemList.size)
|
||||||
|
|
||||||
|
// after successfully searching the query term, add it as old query, to make sure we
|
||||||
|
// don't search again if the query hasn't changed
|
||||||
|
oldSearchQuery = query
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,145 +0,0 @@
|
||||||
/**
|
|
||||||
* Teapod
|
|
||||||
*
|
|
||||||
* Copyright 2020-2022 <seil0@mosad.xyz>
|
|
||||||
*
|
|
||||||
* This program is free software; you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation; either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program; if not, write to the Free Software
|
|
||||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
|
||||||
* MA 02110-1301, USA.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.mosad.teapod.ui.activity.main.viewmodel
|
|
||||||
|
|
||||||
import androidx.lifecycle.LifecycleCoroutineScope
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.update
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.mosad.teapod.parser.crunchyroll.*
|
|
||||||
import kotlin.random.Random
|
|
||||||
|
|
||||||
class HomeViewModel : ViewModel() {
|
|
||||||
|
|
||||||
private val WATCHLIST_LENGTH = 50
|
|
||||||
|
|
||||||
private val uiState = MutableStateFlow<UiState>(UiState.Loading)
|
|
||||||
|
|
||||||
sealed class UiState {
|
|
||||||
object Loading : UiState()
|
|
||||||
data class Normal(
|
|
||||||
val upNextItems: List<UpNextAccountItem>,
|
|
||||||
val watchlistItems: List<Item>,
|
|
||||||
val recommendationsItems: List<Item>,
|
|
||||||
val recentlyAddedItems: List<Item>,
|
|
||||||
val topTenItems: List<Item>,
|
|
||||||
val highlightItem: Item,
|
|
||||||
val highlightItemUpNext: UpNextSeriesItem,
|
|
||||||
val highlightIsWatchlist:Boolean
|
|
||||||
) : UiState()
|
|
||||||
data class Error(val message: String?) : UiState()
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
load()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onUiState(scope: LifecycleCoroutineScope, collector: (UiState) -> Unit) {
|
|
||||||
scope.launch { uiState.collect { collector(it) } }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun load() {
|
|
||||||
viewModelScope.launch {
|
|
||||||
uiState.emit(UiState.Loading)
|
|
||||||
try {
|
|
||||||
// run the loading in parallel to speed up the process
|
|
||||||
val upNextJob = viewModelScope.async { Crunchyroll.upNextAccount(n = 20).data }
|
|
||||||
val watchlistJob = viewModelScope.async { Crunchyroll.watchlist(WATCHLIST_LENGTH).data }
|
|
||||||
val recommendationsJob = viewModelScope.async {
|
|
||||||
Crunchyroll.recommendations(n = 20).data
|
|
||||||
}
|
|
||||||
val recentlyAddedJob = viewModelScope.async {
|
|
||||||
Crunchyroll.browse(sortBy = SortBy.NEWLY_ADDED, n = 50).data
|
|
||||||
}
|
|
||||||
val topTenJob = viewModelScope.async {
|
|
||||||
Crunchyroll.browse(sortBy = SortBy.POPULARITY, n = 10).data
|
|
||||||
}
|
|
||||||
|
|
||||||
val recentlyAddedItems = recentlyAddedJob.await()
|
|
||||||
// FIXME crashes on newTitles.items.size == 0
|
|
||||||
val highlightItem = recentlyAddedItems[Random.nextInt(recentlyAddedItems.size)]
|
|
||||||
val highlightItemUpNextJob = viewModelScope.async {
|
|
||||||
Crunchyroll.upNextSeries(highlightItem.id).data.first()
|
|
||||||
}
|
|
||||||
val highlightItemIsWatchlistJob = viewModelScope.async {
|
|
||||||
Crunchyroll.isWatchlist(highlightItem.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
uiState.emit(UiState.Normal(
|
|
||||||
upNextJob.await(), watchlistJob.await(), recommendationsJob.await(),
|
|
||||||
recentlyAddedJob.await(), topTenJob.await(), highlightItem,
|
|
||||||
highlightItemUpNextJob.await(), highlightItemIsWatchlistJob.await()
|
|
||||||
))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
uiState.emit(UiState.Error(e.message))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Toggle the watchlist state of the highlight media.
|
|
||||||
*/
|
|
||||||
fun toggleHighlightWatchlist() {
|
|
||||||
viewModelScope.launch {
|
|
||||||
uiState.update { currentUiState ->
|
|
||||||
if (currentUiState is UiState.Normal) {
|
|
||||||
if (currentUiState.highlightIsWatchlist) {
|
|
||||||
Crunchyroll.deleteWatchlist(currentUiState.highlightItem.id)
|
|
||||||
} else {
|
|
||||||
Crunchyroll.postWatchlist(currentUiState.highlightItem.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// update the watchlist after a item has been added/removed
|
|
||||||
val watchlistItems = Crunchyroll.watchlist(WATCHLIST_LENGTH).data
|
|
||||||
|
|
||||||
currentUiState.copy(
|
|
||||||
watchlistItems = watchlistItems,
|
|
||||||
highlightIsWatchlist = !currentUiState.highlightIsWatchlist)
|
|
||||||
} else {
|
|
||||||
currentUiState
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the up next list. To be used on player result callbacks.
|
|
||||||
*/
|
|
||||||
fun updateUpNextItems() {
|
|
||||||
viewModelScope.launch {
|
|
||||||
uiState.update { currentUiState ->
|
|
||||||
if (currentUiState is UiState.Normal) {
|
|
||||||
val upNextItems = Crunchyroll.upNextAccount(n = 20).data
|
|
||||||
currentUiState.copy(upNextItems = upNextItems)
|
|
||||||
} else {
|
|
||||||
currentUiState
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,131 +0,0 @@
|
||||||
package org.mosad.teapod.ui.activity.main.viewmodel
|
|
||||||
|
|
||||||
import androidx.lifecycle.LifecycleCoroutineScope
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.update
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.mosad.teapod.parser.crunchyroll.Crunchyroll
|
|
||||||
import org.mosad.teapod.util.ItemMedia
|
|
||||||
import org.mosad.teapod.util.toItemMediaList
|
|
||||||
|
|
||||||
class LibraryFragmentViewModel : ViewModel() {
|
|
||||||
|
|
||||||
val PAGESIZE = 50
|
|
||||||
|
|
||||||
private val uiState = MutableStateFlow<UiState>(UiState.Loading)
|
|
||||||
private var oldSearchQuery = ""
|
|
||||||
private var searchJob: Job? = null
|
|
||||||
var isLazyLoading = false
|
|
||||||
internal set
|
|
||||||
|
|
||||||
sealed class UiState {
|
|
||||||
object Loading : UiState()
|
|
||||||
data class Browse(
|
|
||||||
val itemList: MutableList<ItemMedia>
|
|
||||||
) : UiState()
|
|
||||||
data class Search(
|
|
||||||
val itemList: List<ItemMedia>
|
|
||||||
) : UiState()
|
|
||||||
data class Error(val message: String?) : UiState()
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
load()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onUiState(scope: LifecycleCoroutineScope, collector: (UiState) -> Unit) {
|
|
||||||
scope.launch { uiState.collect { collector(it) } }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* initially load the first n browsing items
|
|
||||||
*/
|
|
||||||
private fun load() {
|
|
||||||
viewModelScope.launch {
|
|
||||||
uiState.emit(UiState.Loading)
|
|
||||||
|
|
||||||
try {
|
|
||||||
initBrowse()
|
|
||||||
} catch (ex: Exception) {
|
|
||||||
uiState.emit(UiState.Error(ex.message))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Search for a query string at Crunchyroll and emit the new ui state.
|
|
||||||
*/
|
|
||||||
fun search(query: String) {
|
|
||||||
// return if nothing has changed
|
|
||||||
if (query == oldSearchQuery) return
|
|
||||||
|
|
||||||
// update the old query since it has changed
|
|
||||||
oldSearchQuery = query
|
|
||||||
|
|
||||||
viewModelScope.launch {
|
|
||||||
|
|
||||||
// always cancel a running search job
|
|
||||||
if (searchJob?.isActive == true) searchJob?.cancel()
|
|
||||||
|
|
||||||
// handle state change: browse <-> search
|
|
||||||
if (query.isEmpty()) {
|
|
||||||
// if the query is empty change back to browse state
|
|
||||||
initBrowse()
|
|
||||||
} else {
|
|
||||||
// TODO handle errors
|
|
||||||
|
|
||||||
// if the current ui state is not search, clear the recyclerview
|
|
||||||
if (uiState.value !is UiState.Search) {
|
|
||||||
uiState.emit(UiState.Search(emptyList()))
|
|
||||||
}
|
|
||||||
|
|
||||||
// create a new search job
|
|
||||||
searchJob = viewModelScope.async {
|
|
||||||
// wait for a few ms: if the user is typing the task will get canceled
|
|
||||||
delay(250)
|
|
||||||
|
|
||||||
val results = Crunchyroll.search(query, 50)
|
|
||||||
.data.firstOrNull()?.items?.toItemMediaList()
|
|
||||||
?: listOf()
|
|
||||||
uiState.emit(UiState.Search(results))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onLazyLoad() = viewModelScope.launch {
|
|
||||||
isLazyLoading = true
|
|
||||||
|
|
||||||
try {
|
|
||||||
uiState.update { currentUiState ->
|
|
||||||
if (currentUiState is UiState.Browse) {
|
|
||||||
val newBrowseItems = Crunchyroll.browse(start = currentUiState.itemList.size, n = PAGESIZE)
|
|
||||||
.toItemMediaList()
|
|
||||||
currentUiState.itemList.addAll(newBrowseItems)
|
|
||||||
}
|
|
||||||
currentUiState
|
|
||||||
}
|
|
||||||
} catch (ex: Exception) {
|
|
||||||
uiState.emit(UiState.Error(ex.message))
|
|
||||||
}
|
|
||||||
|
|
||||||
isLazyLoading = false
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun initBrowse() {
|
|
||||||
try {
|
|
||||||
val initialBrowseItems = Crunchyroll.browse(n = PAGESIZE)
|
|
||||||
.toItemMediaList()
|
|
||||||
.toMutableList()
|
|
||||||
uiState.emit(UiState.Browse(initialBrowseItems))
|
|
||||||
} catch (ex: Exception) {
|
|
||||||
uiState.emit(UiState.Error(ex.message))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -3,13 +3,13 @@ package org.mosad.teapod.ui.activity.main.viewmodel
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import androidx.lifecycle.AndroidViewModel
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.joinAll
|
import kotlinx.coroutines.joinAll
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.mosad.teapod.parser.crunchyroll.*
|
import org.mosad.teapod.parser.crunchyroll.*
|
||||||
|
import org.mosad.teapod.preferences.Preferences
|
||||||
import org.mosad.teapod.util.DataTypes.MediaType
|
import org.mosad.teapod.util.DataTypes.MediaType
|
||||||
|
import org.mosad.teapod.util.Meta
|
||||||
import org.mosad.teapod.util.tmdb.*
|
import org.mosad.teapod.util.tmdb.*
|
||||||
import org.mosad.teapod.util.toPlayheadsMap
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* handle media, next ep and tmdb
|
* handle media, next ep and tmdb
|
||||||
|
@ -17,7 +17,9 @@ import org.mosad.teapod.util.toPlayheadsMap
|
||||||
*/
|
*/
|
||||||
class MediaFragmentViewModel(application: Application) : AndroidViewModel(application) {
|
class MediaFragmentViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
|
|
||||||
var seriesCrunchy = NoneSeriesItem // movies are also series
|
// var mediaCrunchy = NoneItem
|
||||||
|
// internal set
|
||||||
|
var seriesCrunchy = NoneSeries // movies are also series
|
||||||
internal set
|
internal set
|
||||||
var seasonsCrunchy = NoneSeasons
|
var seasonsCrunchy = NoneSeasons
|
||||||
internal set
|
internal set
|
||||||
|
@ -27,15 +29,11 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic
|
||||||
internal set
|
internal set
|
||||||
val currentEpisodesCrunchy = arrayListOf<Episode>() // used for EpisodeItemAdapter (easier updates)
|
val currentEpisodesCrunchy = arrayListOf<Episode>() // used for EpisodeItemAdapter (easier updates)
|
||||||
|
|
||||||
// additional media info, might change during during user interaction
|
// additional media info
|
||||||
// use a map to update the episode adapter values
|
|
||||||
val currentPlayheads: MutableMap<String, PlayheadObject> = mutableMapOf()
|
val currentPlayheads: MutableMap<String, PlayheadObject> = mutableMapOf()
|
||||||
var isWatchlist = false
|
var isWatchlist = false
|
||||||
internal set
|
internal set
|
||||||
var upNextSeries = NoneUpNextSeriesList
|
var upNextSeries = NoneUpNextSeriesItem
|
||||||
internal set
|
|
||||||
var similarTo = NoneSimilarToResult
|
|
||||||
internal set
|
|
||||||
|
|
||||||
// TMDB stuff
|
// TMDB stuff
|
||||||
var mediaType = MediaType.OTHER
|
var mediaType = MediaType.OTHER
|
||||||
|
@ -44,6 +42,8 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic
|
||||||
internal set
|
internal set
|
||||||
var tmdbTVSeason: TMDBTVSeason = NoneTMDBTVSeason
|
var tmdbTVSeason: TMDBTVSeason = NoneTMDBTVSeason
|
||||||
internal set
|
internal set
|
||||||
|
var mediaMeta: Meta? = null
|
||||||
|
internal set
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param crunchyId the crunchyroll series id
|
* @param crunchyId the crunchyroll series id
|
||||||
|
@ -52,38 +52,41 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic
|
||||||
suspend fun loadCrunchy(crunchyId: String) {
|
suspend fun loadCrunchy(crunchyId: String) {
|
||||||
// load series and seasons info in parallel
|
// load series and seasons info in parallel
|
||||||
listOf(
|
listOf(
|
||||||
viewModelScope.launch { seriesCrunchy = Crunchyroll.series(crunchyId).data.first() },
|
viewModelScope.launch { seriesCrunchy = Crunchyroll.series(crunchyId) },
|
||||||
viewModelScope.launch { seasonsCrunchy = Crunchyroll.seasons(crunchyId) },
|
viewModelScope.launch { seasonsCrunchy = Crunchyroll.seasons(crunchyId) },
|
||||||
viewModelScope.launch { isWatchlist = Crunchyroll.isWatchlist(crunchyId) },
|
viewModelScope.launch { isWatchlist = Crunchyroll.isWatchlist(crunchyId) },
|
||||||
viewModelScope.launch { upNextSeries = Crunchyroll.upNextSeries(crunchyId) },
|
viewModelScope.launch { upNextSeries = Crunchyroll.upNextSeries(crunchyId) }
|
||||||
viewModelScope.launch { similarTo = Crunchyroll.similarTo(crunchyId) }
|
|
||||||
).joinAll()
|
).joinAll()
|
||||||
|
// println("series: $seriesCrunchy")
|
||||||
|
// println("seasons: $seasonsCrunchy")
|
||||||
|
println(upNextSeries)
|
||||||
|
|
||||||
// load the preferred season:
|
// load the preferred season (preferred language, language per season, not per stream)
|
||||||
// next episode > first season
|
currentSeasonCrunchy = seasonsCrunchy.getPreferredSeason(Preferences.preferredLocale)
|
||||||
currentSeasonCrunchy = if (upNextSeries != NoneUpNextSeriesList) {
|
|
||||||
seasonsCrunchy.data.firstOrNull{ season ->
|
|
||||||
season.id == upNextSeries.data.first().panel.episodeMetadata.seasonId
|
|
||||||
} ?: seasonsCrunchy.data.first()
|
|
||||||
} else {
|
|
||||||
seasonsCrunchy.data.first()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: if we need to query metaDB, do it now
|
|
||||||
|
|
||||||
// load episodes and metaDB in parallel (tmdb needs mediaType, which is set via episodes)
|
// load episodes and metaDB in parallel (tmdb needs mediaType, which is set via episodes)
|
||||||
viewModelScope.launch { episodesCrunchy = Crunchyroll.episodes(currentSeasonCrunchy.id) }.join()
|
listOf(
|
||||||
currentEpisodesCrunchy.clear()
|
viewModelScope.launch { episodesCrunchy = Crunchyroll.episodes(currentSeasonCrunchy.id) },
|
||||||
currentEpisodesCrunchy.addAll(episodesCrunchy.data)
|
viewModelScope.launch { mediaMeta = null }, // TODO metaDB
|
||||||
|
).joinAll()
|
||||||
|
// println("episodes: $episodesCrunchy")
|
||||||
|
|
||||||
// set media type, for movies the episode field is empty
|
currentEpisodesCrunchy.clear()
|
||||||
mediaType = episodesCrunchy.data.firstOrNull()?.let {
|
currentEpisodesCrunchy.addAll(episodesCrunchy.items)
|
||||||
if (it.episode.isNotEmpty()) MediaType.TVSHOW else MediaType.MOVIE
|
|
||||||
|
// set media type
|
||||||
|
mediaType = episodesCrunchy.items.firstOrNull()?.let {
|
||||||
|
if (it.episodeNumber != null) MediaType.TVSHOW else MediaType.MOVIE
|
||||||
} ?: MediaType.OTHER
|
} ?: MediaType.OTHER
|
||||||
|
|
||||||
// load playheads and tmdb in parallel
|
// load playheads and tmdb in parallel
|
||||||
listOf(
|
listOf(
|
||||||
updatePlayheadsAsync(),
|
viewModelScope.launch {
|
||||||
|
// get playheads (including fully watched state)
|
||||||
|
val episodeIDs = episodesCrunchy.items.map { it.id }
|
||||||
|
currentPlayheads.clear()
|
||||||
|
currentPlayheads.putAll(Crunchyroll.playheads(episodeIDs))
|
||||||
|
},
|
||||||
viewModelScope.launch { loadTmdbInfo() } // use tmdb search to get media info
|
viewModelScope.launch { loadTmdbInfo() } // use tmdb search to get media info
|
||||||
).joinAll()
|
).joinAll()
|
||||||
}
|
}
|
||||||
|
@ -100,6 +103,7 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic
|
||||||
MediaType.TVSHOW -> tmdbApiController.searchTVShow(seriesCrunchy.title)
|
MediaType.TVSHOW -> tmdbApiController.searchTVShow(seriesCrunchy.title)
|
||||||
else -> NoneTMDBSearch
|
else -> NoneTMDBSearch
|
||||||
}
|
}
|
||||||
|
println(tmdbSearchResult)
|
||||||
|
|
||||||
tmdbResult = if (tmdbSearchResult.results.isNotEmpty()) {
|
tmdbResult = if (tmdbSearchResult.results.isNotEmpty()) {
|
||||||
when (val result = tmdbSearchResult.results.first()) {
|
when (val result = tmdbSearchResult.results.first()) {
|
||||||
|
@ -109,22 +113,14 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic
|
||||||
}
|
}
|
||||||
} else NoneTMDB
|
} else NoneTMDB
|
||||||
|
|
||||||
|
println(tmdbResult)
|
||||||
|
|
||||||
// currently not used
|
// currently not used
|
||||||
// tmdbTVSeason = if (tmdbResult is TMDBTVShow) {
|
// tmdbTVSeason = if (tmdbResult is TMDBTVShow) {
|
||||||
// tmdbApiController.getTVSeasonDetails(tmdbResult.id, 0)
|
// tmdbApiController.getTVSeasonDetails(tmdbResult.id, 0)
|
||||||
// } else NoneTMDBTVSeason
|
// } else NoneTMDBTVSeason
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get current playheads for all episodes
|
|
||||||
*/
|
|
||||||
private fun updatePlayheadsAsync() = viewModelScope.async {
|
|
||||||
currentPlayheads.clear()
|
|
||||||
currentPlayheads.putAll(
|
|
||||||
Crunchyroll.playheads(episodesCrunchy.data.map { it.id }).toPlayheadsMap()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set currentSeasonCrunchy based on the season id. Also set the new seasons episodes.
|
* Set currentSeasonCrunchy based on the season id. Also set the new seasons episodes.
|
||||||
*
|
*
|
||||||
|
@ -136,16 +132,13 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic
|
||||||
|
|
||||||
// set currentSeasonCrunchy to the new season with id == seasonId, if the id isn't found,
|
// set currentSeasonCrunchy to the new season with id == seasonId, if the id isn't found,
|
||||||
// don't change the current season (this should/can never happen)
|
// don't change the current season (this should/can never happen)
|
||||||
currentSeasonCrunchy = seasonsCrunchy.data.firstOrNull {
|
currentSeasonCrunchy = seasonsCrunchy.items.firstOrNull {
|
||||||
it.id == seasonId
|
it.id == seasonId
|
||||||
} ?: currentSeasonCrunchy
|
} ?: currentSeasonCrunchy
|
||||||
|
|
||||||
episodesCrunchy = Crunchyroll.episodes(currentSeasonCrunchy.id)
|
episodesCrunchy = Crunchyroll.episodes(currentSeasonCrunchy.id)
|
||||||
currentEpisodesCrunchy.clear()
|
currentEpisodesCrunchy.clear()
|
||||||
currentEpisodesCrunchy.addAll(episodesCrunchy.data)
|
currentEpisodesCrunchy.addAll(episodesCrunchy.items)
|
||||||
|
|
||||||
// update playheads playheads (including fully watched state)
|
|
||||||
updatePlayheadsAsync().await()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun setWatchlist() {
|
suspend fun setWatchlist() {
|
||||||
|
@ -160,9 +153,25 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic
|
||||||
|
|
||||||
suspend fun updateOnResume() {
|
suspend fun updateOnResume() {
|
||||||
joinAll(
|
joinAll(
|
||||||
updatePlayheadsAsync(),
|
viewModelScope.launch {
|
||||||
|
val episodeIDs = episodesCrunchy.items.map { it.id }
|
||||||
|
currentPlayheads.clear()
|
||||||
|
currentPlayheads.putAll(Crunchyroll.playheads(episodeIDs))
|
||||||
|
},
|
||||||
viewModelScope.launch { upNextSeries = Crunchyroll.upNextSeries(seriesCrunchy.id) }
|
viewModelScope.launch { upNextSeries = Crunchyroll.upNextSeries(seriesCrunchy.id) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get the next episode based on episodeId
|
||||||
|
* if no matching is found, use first episode
|
||||||
|
*/
|
||||||
|
fun updateNextEpisode(episodeId: Int) {
|
||||||
|
// TODO reimplement if needed
|
||||||
|
// if (media.type == MediaType.MOVIE) return // return if movie
|
||||||
|
//
|
||||||
|
// nextEpisodeId = media.playlist.firstOrNull { it.index > media.getEpisodeById(episodeId).index }?.mediaId
|
||||||
|
// ?: media.playlist.first().mediaId
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,50 +0,0 @@
|
||||||
package org.mosad.teapod.ui.activity.main.viewmodel
|
|
||||||
|
|
||||||
import androidx.lifecycle.LifecycleCoroutineScope
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.mosad.teapod.parser.crunchyroll.Crunchyroll
|
|
||||||
import org.mosad.teapod.parser.crunchyroll.Item
|
|
||||||
|
|
||||||
class MyListsFragmentViewModel : ViewModel() {
|
|
||||||
|
|
||||||
private val WATCHLIST_LENGTH = 50
|
|
||||||
|
|
||||||
private val uiState = MutableStateFlow<UiState>(UiState.Loading)
|
|
||||||
|
|
||||||
sealed class UiState {
|
|
||||||
object Loading : UiState()
|
|
||||||
data class Normal(
|
|
||||||
val watchlistItems: List<Item>
|
|
||||||
) : UiState()
|
|
||||||
data class Error(val message: String?) : UiState()
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
load()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onUiState(scope: LifecycleCoroutineScope, collector: (UiState) -> Unit) {
|
|
||||||
scope.launch { uiState.collect { collector(it) } }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun load() {
|
|
||||||
viewModelScope.launch {
|
|
||||||
uiState.emit(UiState.Loading)
|
|
||||||
try {
|
|
||||||
// run the loading in parallel to speed up the process
|
|
||||||
val watchlistJob = viewModelScope.async { Crunchyroll.watchlist(WATCHLIST_LENGTH).data }
|
|
||||||
uiState.emit(
|
|
||||||
UiState.Normal(watchlistJob.await())
|
|
||||||
)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
uiState.emit(UiState.Error(e.message))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -3,14 +3,13 @@ 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.databinding.ActivityOnboardingBinding
|
|
||||||
import org.mosad.teapod.ui.activity.main.MainActivity
|
import org.mosad.teapod.ui.activity.main.MainActivity
|
||||||
|
import org.mosad.teapod.databinding.ActivityOnboardingBinding
|
||||||
|
|
||||||
class OnboardingActivity : AppCompatActivity() {
|
class OnboardingActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
@ -36,11 +35,13 @@ class OnboardingActivity : AppCompatActivity() {
|
||||||
if (fragments.size <= 1) {
|
if (fragments.size <= 1) {
|
||||||
binding.tabLayout.visibility = View.GONE
|
binding.tabLayout.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onBackPressedDispatcher.addCallback {
|
override fun onBackPressed() {
|
||||||
if (binding.viewPager.currentItem != 0) {
|
if (binding.viewPager.currentItem == 0) {
|
||||||
binding.viewPager.currentItem = binding.viewPager.currentItem - 1
|
super.onBackPressed()
|
||||||
}
|
} else {
|
||||||
|
binding.viewPager.currentItem = binding.viewPager.currentItem - 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -46,19 +46,16 @@ import androidx.lifecycle.lifecycleScope
|
||||||
import com.google.android.exoplayer2.ExoPlayer
|
import com.google.android.exoplayer2.ExoPlayer
|
||||||
import com.google.android.exoplayer2.Player
|
import com.google.android.exoplayer2.Player
|
||||||
import com.google.android.exoplayer2.ui.StyledPlayerControlView
|
import com.google.android.exoplayer2.ui.StyledPlayerControlView
|
||||||
import com.google.android.exoplayer2.ui.StyledPlayerView
|
|
||||||
import com.google.android.exoplayer2.util.Util
|
import com.google.android.exoplayer2.util.Util
|
||||||
|
import kotlinx.android.synthetic.main.activity_player.*
|
||||||
|
import kotlinx.android.synthetic.main.player_controls.*
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.mosad.teapod.R
|
import org.mosad.teapod.R
|
||||||
import org.mosad.teapod.databinding.ActivityPlayerBinding
|
|
||||||
import org.mosad.teapod.databinding.PlayerControlsBinding
|
|
||||||
import org.mosad.teapod.parser.crunchyroll.NoneEpisode
|
import org.mosad.teapod.parser.crunchyroll.NoneEpisode
|
||||||
import org.mosad.teapod.preferences.Preferences
|
import org.mosad.teapod.preferences.Preferences
|
||||||
import org.mosad.teapod.ui.activity.player.fragment.EpisodeListDialogFragment
|
import org.mosad.teapod.ui.components.EpisodesListPlayer
|
||||||
import org.mosad.teapod.ui.activity.player.fragment.LanguageSettingsDialogFragment
|
import org.mosad.teapod.ui.components.LanguageSettingsPlayer
|
||||||
import org.mosad.teapod.util.hideBars
|
import org.mosad.teapod.util.*
|
||||||
import org.mosad.teapod.util.isInPiPMode
|
|
||||||
import org.mosad.teapod.util.navToLauncherTask
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import kotlin.concurrent.scheduleAtFixedRate
|
import kotlin.concurrent.scheduleAtFixedRate
|
||||||
|
@ -66,12 +63,10 @@ import kotlin.concurrent.scheduleAtFixedRate
|
||||||
class PlayerActivity : AppCompatActivity() {
|
class PlayerActivity : AppCompatActivity() {
|
||||||
|
|
||||||
private val model: PlayerViewModel by viewModels()
|
private val model: PlayerViewModel by viewModels()
|
||||||
private lateinit var playerBinding: ActivityPlayerBinding
|
|
||||||
private lateinit var controlsBinding: PlayerControlsBinding
|
|
||||||
|
|
||||||
private lateinit var controller: StyledPlayerControlView
|
private lateinit var controller: StyledPlayerControlView
|
||||||
private lateinit var gestureDetector: GestureDetectorCompat
|
private lateinit var gestureDetector: GestureDetectorCompat
|
||||||
private lateinit var controlsUpdates: TimerTask
|
private lateinit var timerUpdates: TimerTask
|
||||||
|
|
||||||
private var wasInPiP = false
|
private var wasInPiP = false
|
||||||
private var remainingTime: Long = 0
|
private var remainingTime: Long = 0
|
||||||
|
@ -85,9 +80,6 @@ class PlayerActivity : AppCompatActivity() {
|
||||||
setContentView(R.layout.activity_player)
|
setContentView(R.layout.activity_player)
|
||||||
hideBars() // Initial hide the bars
|
hideBars() // Initial hide the bars
|
||||||
|
|
||||||
playerBinding = ActivityPlayerBinding.bind(findViewById(R.id.player_root))
|
|
||||||
controlsBinding = PlayerControlsBinding.bind(findViewById(R.id.player_controls_root))
|
|
||||||
|
|
||||||
model.loadMediaAsync(
|
model.loadMediaAsync(
|
||||||
intent.getStringExtra(getString(R.string.intent_season_id)) ?: "",
|
intent.getStringExtra(getString(R.string.intent_season_id)) ?: "",
|
||||||
intent.getStringExtra(getString(R.string.intent_episode_id)) ?: ""
|
intent.getStringExtra(getString(R.string.intent_episode_id)) ?: ""
|
||||||
|
@ -95,7 +87,7 @@ class PlayerActivity : AppCompatActivity() {
|
||||||
model.currentEpisodeChangedListener.add { onMediaChanged() }
|
model.currentEpisodeChangedListener.add { onMediaChanged() }
|
||||||
gestureDetector = GestureDetectorCompat(this, PlayerGestureListener())
|
gestureDetector = GestureDetectorCompat(this, PlayerGestureListener())
|
||||||
|
|
||||||
controller = playerBinding.videoView.findViewById(R.id.exo_controller)
|
controller = video_view.findViewById(R.id.exo_controller)
|
||||||
controller.isAnimationEnabled = false // disable controls (time-bar) animation
|
controller.isAnimationEnabled = false // disable controls (time-bar) animation
|
||||||
|
|
||||||
initExoPlayer() // call in onCreate, exoplayer lives in view model
|
initExoPlayer() // call in onCreate, exoplayer lives in view model
|
||||||
|
@ -112,7 +104,7 @@ class PlayerActivity : AppCompatActivity() {
|
||||||
super.onStart()
|
super.onStart()
|
||||||
if (Util.SDK_INT > 23) {
|
if (Util.SDK_INT > 23) {
|
||||||
initPlayer()
|
initPlayer()
|
||||||
playerBinding.videoView.onResume()
|
video_view?.onResume()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -122,7 +114,7 @@ class PlayerActivity : AppCompatActivity() {
|
||||||
|
|
||||||
if (Util.SDK_INT <= 23) {
|
if (Util.SDK_INT <= 23) {
|
||||||
initPlayer()
|
initPlayer()
|
||||||
playerBinding.videoView.onResume()
|
video_view?.onResume()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -174,7 +166,7 @@ class PlayerActivity : AppCompatActivity() {
|
||||||
} else {
|
} else {
|
||||||
val width = model.player.videoFormat?.width ?: 0
|
val width = model.player.videoFormat?.width ?: 0
|
||||||
val height = model.player.videoFormat?.height ?: 0
|
val height = model.player.videoFormat?.height ?: 0
|
||||||
val contentFrame: View = playerBinding.videoView.findViewById(R.id.exo_content_frame)
|
val contentFrame: View = video_view.findViewById(R.id.exo_content_frame)
|
||||||
val contentRect = with(contentFrame) {
|
val contentRect = with(contentFrame) {
|
||||||
val (x, y) = intArrayOf(0, 0).also(::getLocationInWindow)
|
val (x, y) = intArrayOf(0, 0).also(::getLocationInWindow)
|
||||||
Rect(x, y, x + width, y + height)
|
Rect(x, y, x + width, y + height)
|
||||||
|
@ -193,16 +185,12 @@ class PlayerActivity : AppCompatActivity() {
|
||||||
|
|
||||||
override fun onPictureInPictureModeChanged(
|
override fun onPictureInPictureModeChanged(
|
||||||
isInPictureInPictureMode: Boolean,
|
isInPictureInPictureMode: Boolean,
|
||||||
newConfig: Configuration
|
newConfig: Configuration?
|
||||||
) {
|
) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
|
||||||
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hide the full-screen UI (controls, etc.) while in picture-in-picture mode.
|
// Hide the full-screen UI (controls, etc.) while in picture-in-picture mode.
|
||||||
playerBinding.videoView.useController = !isInPictureInPictureMode
|
video_view.useController = !isInPictureInPictureMode
|
||||||
|
|
||||||
// TODO also hide language settings/episodes list
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initPlayer() {
|
private fun initPlayer() {
|
||||||
|
@ -224,16 +212,16 @@ class PlayerActivity : AppCompatActivity() {
|
||||||
override fun onPlaybackStateChanged(state: Int) {
|
override fun onPlaybackStateChanged(state: Int) {
|
||||||
super.onPlaybackStateChanged(state)
|
super.onPlaybackStateChanged(state)
|
||||||
|
|
||||||
playerBinding.loading.visibility = when (state) {
|
loading.visibility = when (state) {
|
||||||
ExoPlayer.STATE_READY -> View.GONE
|
ExoPlayer.STATE_READY -> View.GONE
|
||||||
ExoPlayer.STATE_BUFFERING -> View.VISIBLE
|
ExoPlayer.STATE_BUFFERING -> View.VISIBLE
|
||||||
else -> View.GONE
|
else -> View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
// don't use isVisible to hide exoPlayPause, as it will set the visibility to GONE
|
exo_play_pause.visibility = when (loading.visibility) {
|
||||||
controlsBinding.exoPlayPause.visibility = when(playerBinding.loading.isVisible) {
|
View.GONE -> View.VISIBLE
|
||||||
true -> View.INVISIBLE
|
View.VISIBLE -> View.INVISIBLE
|
||||||
false -> View.VISIBLE
|
else -> View.VISIBLE
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state == ExoPlayer.STATE_ENDED && hasNextEpisode() && Preferences.autoplay) {
|
if (state == ExoPlayer.STATE_ENDED && hasNextEpisode() && Preferences.autoplay) {
|
||||||
|
@ -249,10 +237,10 @@ class PlayerActivity : AppCompatActivity() {
|
||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
private fun initVideoView() {
|
private fun initVideoView() {
|
||||||
playerBinding.videoView.player = model.player
|
video_view.player = model.player
|
||||||
|
|
||||||
// when the player controls get hidden, hide the bars too
|
// when the player controls get hidden, hide the bars too
|
||||||
playerBinding.videoView.setControllerVisibilityListener(StyledPlayerView.ControllerVisibilityListener {
|
video_view.setControllerVisibilityListener {
|
||||||
when (it) {
|
when (it) {
|
||||||
View.GONE -> {
|
View.GONE -> {
|
||||||
hideBars()
|
hideBars()
|
||||||
|
@ -260,25 +248,25 @@ class PlayerActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
View.VISIBLE -> updateControls()
|
View.VISIBLE -> updateControls()
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
playerBinding.videoView.setOnTouchListener { _, event ->
|
video_view.setOnTouchListener { _, event ->
|
||||||
gestureDetector.onTouchEvent(event)
|
gestureDetector.onTouchEvent(event)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initActions() {
|
private fun initActions() {
|
||||||
controlsBinding.exoClosePlayer.setOnClickListener {
|
exo_close_player.setOnClickListener {
|
||||||
this.finish()
|
this.finish()
|
||||||
}
|
}
|
||||||
controlsBinding.rwd10.setOnButtonClickListener { rewind() }
|
rwd_10.setOnButtonClickListener { rewind() }
|
||||||
controlsBinding.ffwd10.setOnButtonClickListener { fastForward() }
|
ffwd_10.setOnButtonClickListener { fastForward() }
|
||||||
playerBinding.buttonNextEp.setOnClickListener { playNextEpisode() }
|
button_next_ep.setOnClickListener { playNextEpisode() }
|
||||||
playerBinding.buttonSkipOp.setOnClickListener { skipOpening() }
|
button_skip_op.setOnClickListener { skipOpening() }
|
||||||
controlsBinding.buttonLanguage.setOnClickListener { showLanguageSettings() }
|
button_language.setOnClickListener { showLanguageSettings() }
|
||||||
controlsBinding.buttonEpisodes.setOnClickListener { showEpisodesList() }
|
button_episodes.setOnClickListener { showEpisodesList() }
|
||||||
controlsBinding.buttonNextEpC.setOnClickListener { playNextEpisode() }
|
button_next_ep_c.setOnClickListener { playNextEpisode() }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initGUI() {
|
private fun initGUI() {
|
||||||
|
@ -289,28 +277,26 @@ class PlayerActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initTimeUpdates() {
|
private fun initTimeUpdates() {
|
||||||
if (this::controlsUpdates.isInitialized) {
|
if (this::timerUpdates.isInitialized) {
|
||||||
controlsUpdates.cancel()
|
timerUpdates.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
controlsUpdates = Timer().scheduleAtFixedRate(0, 500) {
|
timerUpdates = Timer().scheduleAtFixedRate(0, 500) {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
val currentPosition = model.player.currentPosition
|
val currentPosition = model.player.currentPosition
|
||||||
val btnNextEpIsVisible = playerBinding.buttonNextEp.isVisible
|
val btnNextEpIsVisible = button_next_ep.isVisible
|
||||||
val controlsVisible = controller.isVisible
|
val controlsVisible = controller.isVisible
|
||||||
|
|
||||||
// make sure remaining time is > 0
|
// make sure remaining time is > 0
|
||||||
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 > 1 and < 20 sec, a next ep is set, autoplay is enabled
|
// if remaining time < 20 sec, a next ep is set, autoplay is enabled and not in pip:
|
||||||
// and not in pip: show next ep button
|
// show next ep button
|
||||||
if (remainingTime in 1000..20000) {
|
if (remainingTime in 1..20000) {
|
||||||
if (!btnNextEpIsVisible && hasNextEpisode() && Preferences.autoplay && !isInPiPMode()) {
|
if (!btnNextEpIsVisible && hasNextEpisode() && Preferences.autoplay && !isInPiPMode()) {
|
||||||
showButtonNextEp()
|
showButtonNextEp()
|
||||||
}
|
}
|
||||||
|
@ -318,18 +304,17 @@ class PlayerActivity : AppCompatActivity() {
|
||||||
hideButtonNextEp()
|
hideButtonNextEp()
|
||||||
}
|
}
|
||||||
|
|
||||||
// into metadata is present and we can show the skip button
|
// if meta data is present and opening_start & opening_duration are valid, show skip opening
|
||||||
if (model.currentIntroMetadata.duration >= 10) {
|
model.currentEpisodeMeta?.let {
|
||||||
val startTime = model.currentIntroMetadata.startTime.toInt() * 1000
|
if (it.openingDuration > 0 &&
|
||||||
if (currentPosition in startTime..(startTime + 10000) && !playerBinding.buttonSkipOp.isVisible) {
|
currentPosition in it.openingStart..(it.openingStart + 10000) &&
|
||||||
showButtonSkipOp()
|
!button_skip_op.isVisible
|
||||||
} else if (playerBinding.buttonSkipOp.isVisible &&
|
|
||||||
currentPosition !in startTime..(startTime + 10000)
|
|
||||||
) {
|
) {
|
||||||
// the button should only be visible if currentEpisodeMeta != null
|
showButtonSkipOp()
|
||||||
|
} else if (button_skip_op.isVisible && currentPosition !in it.openingStart..(it.openingStart + 10000)) {
|
||||||
|
// the button should only be visible, if currentEpisodeMeta != null
|
||||||
hideButtonSkipOp()
|
hideButtonSkipOp()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// if controls are visible, update them
|
// if controls are visible, update them
|
||||||
|
@ -341,9 +326,9 @@ class PlayerActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onPauseOnStop() {
|
private fun onPauseOnStop() {
|
||||||
playerBinding.videoView.onPause()
|
video_view?.onPause()
|
||||||
model.player.pause()
|
model.player.pause()
|
||||||
controlsUpdates.cancel()
|
timerUpdates.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -356,7 +341,7 @@ class PlayerActivity : AppCompatActivity() {
|
||||||
val seconds = TimeUnit.MILLISECONDS.toSeconds(remainingTime) % 60
|
val seconds = TimeUnit.MILLISECONDS.toSeconds(remainingTime) % 60
|
||||||
|
|
||||||
// if remaining time is below 60 minutes, don't show hours
|
// if remaining time is below 60 minutes, don't show hours
|
||||||
controlsBinding.exoRemaining.text = if (TimeUnit.MILLISECONDS.toMinutes(remainingTime) < 60) {
|
exo_remaining.text = if (TimeUnit.MILLISECONDS.toMinutes(remainingTime) < 60) {
|
||||||
getString(R.string.time_min_sec, minutes, seconds)
|
getString(R.string.time_min_sec, minutes, seconds)
|
||||||
} else {
|
} else {
|
||||||
getString(R.string.time_hour_min_sec, hours, minutes, seconds)
|
getString(R.string.time_hour_min_sec, hours, minutes, seconds)
|
||||||
|
@ -374,10 +359,10 @@ class PlayerActivity : AppCompatActivity() {
|
||||||
this.finish()
|
this.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
controlsBinding.exoTextTitle.text = model.getMediaTitle()
|
exo_text_title.text = model.getMediaTitle()
|
||||||
|
|
||||||
// hide the next episode button, if there is none
|
// hide the next episode button, if there is none
|
||||||
controlsBinding.buttonNextEpC.isVisible = hasNextEpisode()
|
button_next_ep_c.visibility = if (hasNextEpisode()) View.VISIBLE else View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -397,58 +382,50 @@ class PlayerActivity : AppCompatActivity() {
|
||||||
model.seekToOffset(rwdTime)
|
model.seekToOffset(rwdTime)
|
||||||
|
|
||||||
// hide/show needed components
|
// hide/show needed components
|
||||||
playerBinding.exoDoubleTapIndicator.visibility = View.VISIBLE
|
exo_double_tap_indicator.visibility = View.VISIBLE
|
||||||
playerBinding.ffwd10Indicator.visibility = View.INVISIBLE
|
ffwd_10_indicator.visibility = View.INVISIBLE
|
||||||
controlsBinding.rwd10.visibility = View.INVISIBLE
|
rwd_10.visibility = View.INVISIBLE
|
||||||
|
|
||||||
playerBinding.rwd10Indicator.onAnimationEndCallback = {
|
rwd_10_indicator.onAnimationEndCallback = {
|
||||||
playerBinding.exoDoubleTapIndicator.visibility = View.GONE
|
exo_double_tap_indicator.visibility = View.GONE
|
||||||
playerBinding.ffwd10Indicator.visibility = View.VISIBLE
|
ffwd_10_indicator.visibility = View.VISIBLE
|
||||||
controlsBinding.rwd10.visibility = View.VISIBLE
|
rwd_10.visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
|
|
||||||
// run animation
|
// run animation
|
||||||
playerBinding.rwd10Indicator.runOnClickAnimation()
|
rwd_10_indicator.runOnClickAnimation()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fastForward() {
|
private fun fastForward() {
|
||||||
model.seekToOffset(fwdTime)
|
model.seekToOffset(fwdTime)
|
||||||
|
|
||||||
// hide/show needed components
|
// hide/show needed components
|
||||||
playerBinding.exoDoubleTapIndicator.visibility = View.VISIBLE
|
exo_double_tap_indicator.visibility = View.VISIBLE
|
||||||
playerBinding.rwd10Indicator.visibility = View.INVISIBLE
|
rwd_10_indicator.visibility = View.INVISIBLE
|
||||||
controlsBinding.ffwd10.visibility = View.INVISIBLE
|
ffwd_10.visibility = View.INVISIBLE
|
||||||
|
|
||||||
playerBinding.ffwd10Indicator.onAnimationEndCallback = {
|
ffwd_10_indicator.onAnimationEndCallback = {
|
||||||
playerBinding.exoDoubleTapIndicator.visibility = View.GONE
|
exo_double_tap_indicator.visibility = View.GONE
|
||||||
playerBinding.rwd10Indicator.visibility = View.VISIBLE
|
rwd_10_indicator.visibility = View.VISIBLE
|
||||||
controlsBinding.ffwd10.visibility = View.VISIBLE
|
ffwd_10.visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
|
|
||||||
// run animation
|
// run animation
|
||||||
playerBinding.ffwd10Indicator.runOnClickAnimation()
|
ffwd_10_indicator.runOnClickAnimation()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun playNextEpisode() {
|
private fun playNextEpisode() {
|
||||||
// disable the next episode buttons, so a user can't double click it
|
|
||||||
playerBinding.buttonNextEp.isClickable = false
|
|
||||||
controlsBinding.buttonNextEpC.isClickable = false
|
|
||||||
|
|
||||||
hideButtonNextEp()
|
|
||||||
model.playNextEpisode()
|
model.playNextEpisode()
|
||||||
|
hideButtonNextEp()
|
||||||
// enable the next episode buttons when playNextEpisode() has returned
|
|
||||||
playerBinding.buttonNextEp.isClickable = true
|
|
||||||
controlsBinding.buttonNextEpC.isClickable = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun skipOpening() {
|
private fun skipOpening() {
|
||||||
// calculate the seek time
|
// calculate the seek time
|
||||||
if (model.currentIntroMetadata.duration > 10) {
|
model.currentEpisodeMeta?.let {
|
||||||
val endTime = model.currentIntroMetadata.endTime.toInt() * 1000
|
val seekTime = (it.openingStart + it.openingDuration) - model.player.currentPosition
|
||||||
val seekTime = endTime - model.player.currentPosition
|
|
||||||
model.seekToOffset(seekTime)
|
model.seekToOffset(seekTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -456,10 +433,10 @@ class PlayerActivity : AppCompatActivity() {
|
||||||
* TODO improve the show animation
|
* TODO improve the show animation
|
||||||
*/
|
*/
|
||||||
private fun showButtonNextEp() {
|
private fun showButtonNextEp() {
|
||||||
playerBinding.buttonNextEp.isVisible = true
|
button_next_ep.isVisible = true
|
||||||
playerBinding.buttonNextEp.alpha = 0.0f
|
button_next_ep.alpha = 0.0f
|
||||||
|
|
||||||
playerBinding.buttonNextEp.animate()
|
button_next_ep.animate()
|
||||||
.alpha(1.0f)
|
.alpha(1.0f)
|
||||||
.setListener(null)
|
.setListener(null)
|
||||||
}
|
}
|
||||||
|
@ -469,45 +446,52 @@ class PlayerActivity : AppCompatActivity() {
|
||||||
* TODO improve the hide animation
|
* TODO improve the hide animation
|
||||||
*/
|
*/
|
||||||
private fun hideButtonNextEp() {
|
private fun hideButtonNextEp() {
|
||||||
playerBinding.buttonNextEp.animate()
|
button_next_ep.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
|
button_next_ep.isVisible = false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showButtonSkipOp() {
|
private fun showButtonSkipOp() {
|
||||||
playerBinding.buttonSkipOp.isVisible = true
|
button_skip_op.isVisible = true
|
||||||
playerBinding.buttonSkipOp.alpha = 0.0f
|
button_skip_op.alpha = 0.0f
|
||||||
|
|
||||||
playerBinding.buttonSkipOp.animate()
|
button_skip_op.animate()
|
||||||
.alpha(1.0f)
|
.alpha(1.0f)
|
||||||
.setListener(null)
|
.setListener(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun hideButtonSkipOp() {
|
private fun hideButtonSkipOp() {
|
||||||
playerBinding.buttonSkipOp.animate()
|
button_skip_op.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
|
button_skip_op.isVisible = false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showEpisodesList() {
|
private fun showEpisodesList() {
|
||||||
|
val episodesList = EpisodesListPlayer(this, model = model).apply {
|
||||||
|
onViewRemovedAction = { model.player.play() }
|
||||||
|
}
|
||||||
|
player_layout.addView(episodesList)
|
||||||
pauseAndHideControls()
|
pauseAndHideControls()
|
||||||
EpisodeListDialogFragment().show(supportFragmentManager, EpisodeListDialogFragment.TAG)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showLanguageSettings() {
|
private fun showLanguageSettings() {
|
||||||
|
val languageSettings = LanguageSettingsPlayer(this, model = model).apply {
|
||||||
|
onViewRemovedAction = { model.player.play() }
|
||||||
|
}
|
||||||
|
player_layout.addView(languageSettings)
|
||||||
pauseAndHideControls()
|
pauseAndHideControls()
|
||||||
LanguageSettingsDialogFragment().show(supportFragmentManager, LanguageSettingsDialogFragment.TAG)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -524,7 +508,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()
|
||||||
}
|
}
|
||||||
|
@ -535,9 +519,9 @@ 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()
|
val eventPosX = e?.x?.toInt() ?: 0
|
||||||
val viewCenterX = playerBinding.videoView.measuredWidth / 2
|
val viewCenterX = video_view.measuredWidth / 2
|
||||||
|
|
||||||
// if the event position is on the left side rewind, if it's on the right forward
|
// if the event position is on the left side rewind, if it's on the right forward
|
||||||
if (eventPosX < viewCenterX) rewind() else fastForward()
|
if (eventPosX < viewCenterX) rewind() else fastForward()
|
||||||
|
@ -548,14 +532,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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -31,18 +31,24 @@ import androidx.lifecycle.viewModelScope
|
||||||
import com.google.android.exoplayer2.ExoPlayer
|
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.SimpleExoPlayer
|
||||||
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
|
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
|
||||||
import kotlinx.coroutines.*
|
import com.google.android.exoplayer2.source.hls.HlsMediaSource
|
||||||
|
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory
|
||||||
|
import com.google.android.exoplayer2.util.Util
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
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.Crunchyroll
|
||||||
|
import org.mosad.teapod.parser.crunchyroll.NoneEpisode
|
||||||
|
import org.mosad.teapod.parser.crunchyroll.NoneEpisodes
|
||||||
|
import org.mosad.teapod.parser.crunchyroll.NonePlayback
|
||||||
import org.mosad.teapod.preferences.Preferences
|
import org.mosad.teapod.preferences.Preferences
|
||||||
import org.mosad.teapod.util.metadb.EpisodeMeta
|
import org.mosad.teapod.util.EpisodeMeta
|
||||||
import org.mosad.teapod.util.metadb.Meta
|
import org.mosad.teapod.util.tmdb.TMDBTVSeason
|
||||||
import org.mosad.teapod.util.metadb.MetaDBController
|
|
||||||
import org.mosad.teapod.util.metadb.TVShowMeta
|
|
||||||
import org.mosad.teapod.util.toPlayheadsMap
|
|
||||||
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.
|
||||||
|
@ -50,47 +56,35 @@ import kotlin.concurrent.scheduleAtFixedRate
|
||||||
* the next episode will be update and the callback is handled.
|
* the next episode will be update and the callback is handled.
|
||||||
*/
|
*/
|
||||||
class PlayerViewModel(application: Application) : AndroidViewModel(application) {
|
class PlayerViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
private val classTag = javaClass.name
|
|
||||||
|
|
||||||
val player = ExoPlayer.Builder(application).build()
|
val player = SimpleExoPlayer.Builder(application).build()
|
||||||
|
private val dataSourceFactory = DefaultDataSourceFactory(application, Util.getUserAgent(application, "Teapod"))
|
||||||
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
|
||||||
|
|
||||||
// tmdb/meta data
|
// tmdb/meta data
|
||||||
var mediaMeta: Meta? = null
|
// TODO meta data currently not implemented for cr
|
||||||
|
// var mediaMeta: Meta? = null
|
||||||
|
// internal set
|
||||||
|
var tmdbTVSeason: TMDBTVSeason? =null
|
||||||
internal set
|
internal set
|
||||||
var currentEpisodeMeta: EpisodeMeta? = null
|
var currentEpisodeMeta: EpisodeMeta? = null
|
||||||
internal set
|
internal set
|
||||||
var currentPlayheads = mapOf<String, PlayheadObject>()
|
|
||||||
internal set
|
|
||||||
var currentIntroMetadata: DatalabIntro = NoneDatalabIntro
|
|
||||||
internal set
|
|
||||||
// var tmdbTVSeason: TMDBTVSeason? =null
|
|
||||||
// internal set
|
|
||||||
|
|
||||||
// crunchyroll episodes/playback
|
// crunchyroll episodes/playback
|
||||||
var episodes = NoneEpisodes
|
var episodes = NoneEpisodes
|
||||||
internal set
|
internal set
|
||||||
var currentEpisode = NoneEpisode
|
var currentEpisode = NoneEpisode
|
||||||
internal set
|
internal set
|
||||||
var currentVersion = NoneVersion
|
var currentPlayback = NonePlayback
|
||||||
internal set
|
|
||||||
var currentStreams = NoneStreams
|
|
||||||
internal set
|
|
||||||
|
|
||||||
// current playback settings
|
// current playback settings
|
||||||
var currentAudioLocale: Locale = Preferences.preferredAudioLocale
|
var currentLanguage: Locale = Preferences.preferredLocale
|
||||||
internal set
|
|
||||||
var currentSubtitleLocale: Locale = Preferences.preferredSubtitleLocale
|
|
||||||
internal set
|
internal set
|
||||||
|
|
||||||
init {
|
init {
|
||||||
// disable platform diagnostics since they might be shared with google
|
|
||||||
ExoPlayer.Builder(application).setUsePlatformDiagnostics(false)
|
|
||||||
|
|
||||||
initMediaSession()
|
initMediaSession()
|
||||||
|
|
||||||
player.addListener(object : Player.Listener {
|
player.addListener(object : Player.Listener {
|
||||||
|
@ -106,14 +100,6 @@ 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() {
|
||||||
|
@ -122,7 +108,7 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
|
||||||
mediaSession.release()
|
mediaSession.release()
|
||||||
player.release()
|
player.release()
|
||||||
|
|
||||||
Log.d(classTag, "Released player")
|
Log.d(javaClass.name, "Released player")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -139,48 +125,30 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
|
||||||
fun loadMediaAsync(seasonId: String, episodeId: String) = viewModelScope.launch {
|
fun loadMediaAsync(seasonId: String, episodeId: String) = viewModelScope.launch {
|
||||||
episodes = Crunchyroll.episodes(seasonId)
|
episodes = Crunchyroll.episodes(seasonId)
|
||||||
|
|
||||||
listOf(
|
|
||||||
viewModelScope.launch { mediaMeta = loadMediaMeta(episodes.data.first().seriesId) },
|
|
||||||
viewModelScope.launch {
|
|
||||||
val episodeIDs = episodes.data.map { it.id }
|
|
||||||
currentPlayheads = Crunchyroll.playheads(episodeIDs).toPlayheadsMap()
|
|
||||||
}
|
|
||||||
).joinAll()
|
|
||||||
Log.d(classTag, "meta: $mediaMeta")
|
|
||||||
|
|
||||||
setCurrentEpisode(episodeId)
|
setCurrentEpisode(episodeId)
|
||||||
playCurrentMedia(currentPlayhead)
|
playCurrentMedia(currentPlayhead)
|
||||||
|
|
||||||
|
// TODO reimplement for cr
|
||||||
|
// run async as it should be loaded by the time the episodes a
|
||||||
|
// viewModelScope.launch {
|
||||||
|
// // get tmdb season info, if metaDB knows the tv show
|
||||||
|
// if (media.type == DataTypes.MediaType.TVSHOW && mediaMeta != null) {
|
||||||
|
// val tvShowMeta = mediaMeta as TVShowMeta
|
||||||
|
// tmdbTVSeason = TMDBApiController().getTVSeasonDetails(tvShowMeta.tmdbId, tvShowMeta.tmdbSeasonNumber)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// currentEpisodeMeta = getEpisodeMetaByAoDMediaId(currentEpisodeAoD.mediaId)
|
||||||
|
// currentLanguage = currentEpisodeAoD.getPreferredStream(preferredLanguage).language
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setLanguage(newAudioLocale: Locale, newSubtitleLocale: Locale) {
|
fun setLanguage(language: Locale) {
|
||||||
// TODO if the audio locale has changes update the streams, if only the subtitle locale has changed load the new stream
|
currentLanguage = language
|
||||||
if (newAudioLocale != currentAudioLocale) {
|
playCurrentMedia(player.currentPosition)
|
||||||
currentAudioLocale = newAudioLocale
|
|
||||||
|
|
||||||
currentVersion = currentEpisode.versions?.firstOrNull {
|
|
||||||
it.audioLocale == currentAudioLocale.toLanguageTag()
|
|
||||||
} ?: currentEpisode.versions?.first() ?: NoneVersion
|
|
||||||
|
|
||||||
viewModelScope.launch {
|
|
||||||
currentStreams = Crunchyroll.streamsFromMediaGUID(currentVersion.mediaGUID)
|
|
||||||
Log.d(classTag, currentVersion.toString())
|
|
||||||
|
|
||||||
playCurrentMedia(player.currentPosition)
|
|
||||||
}
|
|
||||||
} else if (newSubtitleLocale != currentSubtitleLocale) {
|
|
||||||
currentSubtitleLocale = newSubtitleLocale
|
|
||||||
playCurrentMedia(player.currentPosition)
|
|
||||||
}
|
|
||||||
|
|
||||||
// else nothing has changed so no need do do anything
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// player actions
|
// player actions
|
||||||
|
|
||||||
/**
|
|
||||||
* Seeks to a offset position specified in milliseconds in the current MediaItem.
|
|
||||||
* @param offset The offset position in the current MediaItem.
|
|
||||||
*/
|
|
||||||
fun seekToOffset(offset: Long) {
|
fun seekToOffset(offset: Long) {
|
||||||
player.seekTo(player.currentPosition + offset)
|
player.seekTo(player.currentPosition + offset)
|
||||||
}
|
}
|
||||||
|
@ -194,63 +162,42 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
|
||||||
*/
|
*/
|
||||||
fun playNextEpisode() = currentEpisode.nextEpisodeId?.let { nextEpisodeId ->
|
fun playNextEpisode() = currentEpisode.nextEpisodeId?.let { nextEpisodeId ->
|
||||||
updatePlayhead() // update playhead before switching to new episode
|
updatePlayhead() // update playhead before switching to new episode
|
||||||
viewModelScope.launch { setCurrentEpisode(nextEpisodeId, startPlayback = true) }
|
setCurrentEpisode(nextEpisodeId, startPlayback = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set currentEpisodeCr to the episode of the given ID
|
* Set currentEpisodeCr to the episode of the given ID
|
||||||
* @param episodeId The ID of the episode you want to set currentEpisodeCr to
|
* @param episodeId The ID of the episode you want to set currentEpisodeCr to
|
||||||
*/
|
*/
|
||||||
suspend fun setCurrentEpisode(episodeId: String, startPlayback: Boolean = false) {
|
fun setCurrentEpisode(episodeId: String, startPlayback: Boolean = false) {
|
||||||
currentEpisode = episodes.data.find { episode ->
|
currentEpisode = episodes.items.find { episode ->
|
||||||
episode.id == episodeId
|
episode.id == episodeId
|
||||||
} ?: NoneEpisode
|
} ?: NoneEpisode
|
||||||
|
|
||||||
// TODO improve handling of none present seasons/episodes
|
|
||||||
// update current episode meta
|
|
||||||
currentEpisodeMeta = if (mediaMeta is TVShowMeta && currentEpisode.episodeNumber != null) {
|
|
||||||
(mediaMeta as TVShowMeta)
|
|
||||||
.seasons.getOrNull(currentEpisode.seasonNumber - 1)
|
|
||||||
?.episodes?.getOrNull(currentEpisode.episodeNumber!! - 1)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
// update player gui (title, next ep button) after currentEpisode has changed
|
// update player gui (title, next ep button) after currentEpisode has changed
|
||||||
currentEpisodeChangedListener.forEach { it() }
|
currentEpisodeChangedListener.forEach { it() }
|
||||||
|
|
||||||
// needs to be blocking, currentPlayback must be present when calling playCurrentMedia()
|
// needs to be blocking, currentPlayback must be present when calling playCurrentMedia()
|
||||||
joinAll(
|
runBlocking {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
joinAll(
|
||||||
currentVersion = currentEpisode.versions?.firstOrNull {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
it.audioLocale == currentAudioLocale.toLanguageTag()
|
currentPlayback = Crunchyroll.playback(currentEpisode.playback)
|
||||||
} ?: currentEpisode.versions?.first() ?: NoneVersion
|
},
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
// get the current streams object, if no version is set, use streamsLink
|
Crunchyroll.playheads(listOf(currentEpisode.id))[currentEpisode.id]?.let {
|
||||||
currentStreams = if (currentVersion != NoneVersion) {
|
// if the episode was fully watched, start at the beginning
|
||||||
Crunchyroll.streamsFromMediaGUID(currentVersion.mediaGUID)
|
currentPlayhead = if (it.fullyWatched) {
|
||||||
} else {
|
0
|
||||||
Crunchyroll.streams(currentEpisode.streamsLink)
|
} else {
|
||||||
}
|
(it.playhead.times(1000)).toLong()
|
||||||
Log.d(classTag, currentVersion.toString())
|
}
|
||||||
},
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
|
||||||
Crunchyroll.playheads(listOf(currentEpisode.id)).data.firstOrNull {
|
|
||||||
it.contentId == currentEpisode.id
|
|
||||||
}?.let {
|
|
||||||
// if the episode was fully watched, start at the beginning
|
|
||||||
currentPlayhead = if (it.fullyWatched) {
|
|
||||||
0
|
|
||||||
} else {
|
|
||||||
(it.playhead.times(1000)).toLong()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
)
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
}
|
||||||
currentIntroMetadata = Crunchyroll.datalabIntro(currentEpisode.id)
|
println("loaded playback ${currentEpisode.playback}")
|
||||||
}
|
|
||||||
)
|
// TODO update metadata and language (it should not be needed to update the language here!)
|
||||||
Log.d(classTag, "streams: ${currentEpisode.streamsLink}")
|
|
||||||
|
|
||||||
if (startPlayback) {
|
if (startPlayback) {
|
||||||
playCurrentMedia()
|
playCurrentMedia()
|
||||||
|
@ -258,35 +205,38 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Play the current media from currentStreams.
|
* Play the current media from currentPlaybackCr.
|
||||||
*
|
*
|
||||||
* @param seekPosition The seek position for the media (default = 0).
|
* @param seekPosition The seek position for the episode (default = 0).
|
||||||
*/
|
*/
|
||||||
fun playCurrentMedia(seekPosition: Long = 0) {
|
fun playCurrentMedia(seekPosition: Long = 0) {
|
||||||
// get preferred stream url, set current language if it differs from the preferred one
|
// get preferred stream url, set current language if it differs from the preferred one
|
||||||
val preferredLocale = currentSubtitleLocale
|
val preferredLocale = currentLanguage
|
||||||
val fallbackLocal = Locale.US
|
val fallbackLocal = Locale.US
|
||||||
val url = when {
|
val url = when {
|
||||||
currentStreams.data[0].adaptive_hls.containsKey(preferredLocale.toLanguageTag()) -> {
|
currentPlayback.streams.adaptive_hls.containsKey(preferredLocale.toLanguageTag()) -> {
|
||||||
currentStreams.data[0].adaptive_hls[preferredLocale.toLanguageTag()]?.url
|
currentPlayback.streams.adaptive_hls[preferredLocale.toLanguageTag()]?.url
|
||||||
}
|
}
|
||||||
currentStreams.data[0].adaptive_hls.containsKey(fallbackLocal.toLanguageTag()) -> {
|
currentPlayback.streams.adaptive_hls.containsKey(fallbackLocal.toLanguageTag()) -> {
|
||||||
currentSubtitleLocale = fallbackLocal
|
currentLanguage = fallbackLocal
|
||||||
currentStreams.data[0].adaptive_hls[fallbackLocal.toLanguageTag()]?.url
|
currentPlayback.streams.adaptive_hls[fallbackLocal.toLanguageTag()]?.url
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
// if no language tag is present use the first entry
|
// if no language tag is present use the first entry
|
||||||
currentSubtitleLocale = Locale.ROOT
|
currentLanguage = Locale.ROOT
|
||||||
currentStreams.data[0].adaptive_hls.entries.first().value.url
|
currentPlayback.streams.adaptive_hls.entries.first().value.url
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Log.i(classTag, "stream url: $url")
|
println("stream url: $url")
|
||||||
|
|
||||||
// create the media item
|
// create the media source object
|
||||||
val mediaItem = MediaItem.fromUri(Uri.parse(url))
|
val mediaSource = HlsMediaSource.Factory(dataSourceFactory).createMediaSource(
|
||||||
player.setMediaItem(mediaItem)
|
MediaItem.fromUri(Uri.parse(url))
|
||||||
|
)
|
||||||
|
|
||||||
|
// the actual player playback code
|
||||||
|
player.setMediaSource(mediaSource)
|
||||||
player.prepare()
|
player.prepare()
|
||||||
|
|
||||||
if (seekPosition > 0) player.seekTo(seekPosition)
|
if (seekPosition > 0) player.seekTo(seekPosition)
|
||||||
player.playWhenReady = true
|
player.playWhenReady = true
|
||||||
}
|
}
|
||||||
|
@ -313,12 +263,28 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
|
||||||
* @return Boolean: true if it is the last, else false.
|
* @return Boolean: true if it is the last, else false.
|
||||||
*/
|
*/
|
||||||
fun currentEpisodeIsLastEpisode(): Boolean {
|
fun currentEpisodeIsLastEpisode(): Boolean {
|
||||||
return episodes.data.lastOrNull()?.id == currentEpisode.id
|
return episodes.items.lastOrNull()?.id == currentEpisode.id
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun loadMediaMeta(crSeriesId: String): Meta? {
|
// TODO reimplement for cr
|
||||||
return MetaDBController.getTVShowMetadata(crSeriesId)
|
// fun getEpisodeMetaByAoDMediaId(aodMediaId: Int): EpisodeMeta? {
|
||||||
}
|
// val meta = mediaMeta
|
||||||
|
// return if (meta is TVShowMeta) {
|
||||||
|
// meta.episodes.firstOrNull { it.aodMediaId == aodMediaId }
|
||||||
|
// } else {
|
||||||
|
// null
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private suspend fun loadMediaMeta(aodId: Int): Meta? {
|
||||||
|
// return if (media.type == DataTypes.MediaType.TVSHOW) {
|
||||||
|
// MetaDBController().getTVShowMetadata(aodId)
|
||||||
|
// } else {
|
||||||
|
// null
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// return null
|
||||||
|
// }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the playhead of the current episode, if currentPosition > 1000ms.
|
* Update the playhead of the current episode, if currentPosition > 1000ms.
|
||||||
|
@ -326,16 +292,10 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
|
||||||
private fun updatePlayhead() {
|
private fun updatePlayhead() {
|
||||||
val playhead = (player.currentPosition / 1000)
|
val playhead = (player.currentPosition / 1000)
|
||||||
|
|
||||||
if (playhead > 0 && Preferences.updatePlayhead) {
|
if (playhead > 0) {
|
||||||
// don't use viewModelScope here. This task may needs to finish, when ViewModel will be cleared
|
viewModelScope.launch { Crunchyroll.postPlayheads(currentEpisode.id, playhead.toInt()) }
|
||||||
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.")
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModelScope.launch {
|
|
||||||
val episodeIDs = episodes.data.map { it.id }
|
|
||||||
currentPlayheads = Crunchyroll.playheads(episodeIDs).toPlayheadsMap()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,72 +0,0 @@
|
||||||
package org.mosad.teapod.ui.activity.player.fragment
|
|
||||||
|
|
||||||
import android.content.DialogInterface
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import androidx.lifecycle.ViewModelProvider
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import org.mosad.teapod.R
|
|
||||||
import org.mosad.teapod.databinding.PlayerEpisodesListBinding
|
|
||||||
import org.mosad.teapod.ui.activity.player.PlayerViewModel
|
|
||||||
import org.mosad.teapod.util.adapter.EpisodeItemAdapter
|
|
||||||
import org.mosad.teapod.util.hideBars
|
|
||||||
|
|
||||||
class EpisodeListDialogFragment : DialogFragment() {
|
|
||||||
|
|
||||||
private lateinit var model: PlayerViewModel
|
|
||||||
private lateinit var binding: PlayerEpisodesListBinding
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val TAG = "LanguageSettingsDialogFragment"
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
setStyle(STYLE_NO_TITLE, R.style.FullScreenDialogStyle)
|
|
||||||
model = ViewModelProvider(requireActivity())[PlayerViewModel::class.java]
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
|
||||||
binding = PlayerEpisodesListBinding.inflate(inflater, container, false)
|
|
||||||
return binding.root
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
|
|
||||||
binding.buttonCloseEpisodesList.setOnClickListener {
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
|
|
||||||
val adapterRecEpisodes = EpisodeItemAdapter(
|
|
||||||
model.episodes.data,
|
|
||||||
null,
|
|
||||||
model.currentPlayheads,
|
|
||||||
EpisodeItemAdapter.OnClickListener { episode ->
|
|
||||||
dismiss()
|
|
||||||
// TODO make this none blocking, if necessary?
|
|
||||||
runBlocking {
|
|
||||||
model.setCurrentEpisode(episode.id, startPlayback = true)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
EpisodeItemAdapter.ViewType.PLAYER
|
|
||||||
)
|
|
||||||
|
|
||||||
// get the position/index of the currently playing episode
|
|
||||||
adapterRecEpisodes.currentSelected = model.episodes.data.indexOfFirst { it.id == model.currentEpisode.id }
|
|
||||||
|
|
||||||
binding.recyclerEpisodesPlayer.adapter = adapterRecEpisodes
|
|
||||||
binding.recyclerEpisodesPlayer.scrollToPosition(adapterRecEpisodes.currentSelected)
|
|
||||||
|
|
||||||
// initially hide the status and navigation bar
|
|
||||||
hideBars(requireDialog().window, binding.root)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDismiss(dialog: DialogInterface) {
|
|
||||||
super.onDismiss(dialog)
|
|
||||||
model.player.play()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,152 +0,0 @@
|
||||||
package org.mosad.teapod.ui.activity.player.fragment
|
|
||||||
|
|
||||||
import android.content.DialogInterface
|
|
||||||
import android.graphics.Color
|
|
||||||
import android.graphics.Typeface
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.util.TypedValue
|
|
||||||
import android.view.Gravity
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.LinearLayout
|
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.core.view.children
|
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import androidx.lifecycle.ViewModelProvider
|
|
||||||
import org.mosad.teapod.R
|
|
||||||
import org.mosad.teapod.databinding.PlayerLanguageSettingsBinding
|
|
||||||
import org.mosad.teapod.ui.activity.player.PlayerViewModel
|
|
||||||
import org.mosad.teapod.util.hideBars
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class LanguageSettingsDialogFragment : DialogFragment() {
|
|
||||||
|
|
||||||
private lateinit var model: PlayerViewModel
|
|
||||||
private lateinit var binding: PlayerLanguageSettingsBinding
|
|
||||||
|
|
||||||
private var selectedSubtitleLocale = Locale.ROOT
|
|
||||||
private var selectedAudioLocale = Locale.ROOT
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val TAG = "LanguageSettingsDialogFragment"
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
setStyle(STYLE_NO_TITLE, R.style.FullScreenDialogStyle)
|
|
||||||
model = ViewModelProvider(requireActivity())[PlayerViewModel::class.java]
|
|
||||||
selectedSubtitleLocale = model.currentSubtitleLocale
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
|
||||||
binding = PlayerLanguageSettingsBinding.inflate(inflater, container, false)
|
|
||||||
return binding.root
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
|
|
||||||
var selectedSubtitleView: TextView? = null
|
|
||||||
model.currentStreams.data[0].adaptive_hls.keys.forEach { languageTag ->
|
|
||||||
val locale = Locale.forLanguageTag(languageTag)
|
|
||||||
val subtitleView = addLanguage(binding.linearSubtitleLanguages, locale) { v ->
|
|
||||||
selectedSubtitleLocale = locale
|
|
||||||
updateSelectedLanguage(binding.linearSubtitleLanguages, v as TextView)
|
|
||||||
}
|
|
||||||
|
|
||||||
// if the view is the currently selected one, highlight it
|
|
||||||
if (locale == model.currentSubtitleLocale) {
|
|
||||||
selectedSubtitleView = subtitleView
|
|
||||||
updateSelectedLanguage(binding.linearSubtitleLanguages, subtitleView)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val currentAudioLocal = Locale.forLanguageTag(model.currentVersion.audioLocale)
|
|
||||||
var selectedAudioView: TextView? = null
|
|
||||||
model.currentEpisode.versions?.forEach { version ->
|
|
||||||
val locale = Locale.forLanguageTag(version.audioLocale)
|
|
||||||
val audioView = addLanguage(binding.linearAudioLanguages, locale) { v ->
|
|
||||||
selectedAudioLocale = locale
|
|
||||||
updateSelectedLanguage(binding.linearAudioLanguages, v as TextView)
|
|
||||||
}
|
|
||||||
|
|
||||||
// if the view is the currently selected one, highlight it
|
|
||||||
if (locale == currentAudioLocal) {
|
|
||||||
selectedAudioView = audioView
|
|
||||||
updateSelectedLanguage(binding.linearAudioLanguages, audioView)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.buttonCloseLanguageSettings.setOnClickListener { dismiss() }
|
|
||||||
binding.buttonCancel.setOnClickListener { dismiss() }
|
|
||||||
binding.buttonSelect.setOnClickListener {
|
|
||||||
model.setLanguage(selectedAudioLocale, selectedSubtitleLocale)
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
|
|
||||||
// initially hide the status and navigation bar
|
|
||||||
hideBars(requireDialog().window, binding.root)
|
|
||||||
|
|
||||||
// scroll to the position of the view, if it's the selected language
|
|
||||||
binding.scrollSubtitleLanguages.post {
|
|
||||||
binding.scrollSubtitleLanguages.scrollTo(0, selectedSubtitleView?.top ?: 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.scrollAudioLanguages.post {
|
|
||||||
binding.scrollSubtitleLanguages.scrollTo(0, selectedAudioView?.top ?: 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDismiss(dialog: DialogInterface) {
|
|
||||||
super.onDismiss(dialog)
|
|
||||||
model.player.play()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun addLanguage(linear: LinearLayout, locale: Locale, onClick: View.OnClickListener): TextView {
|
|
||||||
val text = TextView(context).apply {
|
|
||||||
height = 96
|
|
||||||
gravity = Gravity.CENTER_VERTICAL
|
|
||||||
text = if (locale == Locale.ROOT) context.getString(R.string.no_subtitles) else locale.displayLanguage
|
|
||||||
setTextSize(TypedValue.COMPLEX_UNIT_SP, 16f)
|
|
||||||
setTextColor(context.resources.getColor(R.color.player_text, context.theme))
|
|
||||||
setPadding(75, 0, 0, 0)
|
|
||||||
|
|
||||||
setOnClickListener(onClick)
|
|
||||||
}
|
|
||||||
|
|
||||||
linear.addView(text)
|
|
||||||
|
|
||||||
return text
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Highlights the selected audio/subtitle language
|
|
||||||
*
|
|
||||||
* @param languageLayout The audio/subtitle Layout to update
|
|
||||||
* @param selected The newly selected language TextView
|
|
||||||
*/
|
|
||||||
private fun updateSelectedLanguage(languageLayout: LinearLayout, selected: TextView) {
|
|
||||||
// rest all tf to not selected style
|
|
||||||
languageLayout.children.forEach { child ->
|
|
||||||
if (child is TextView) {
|
|
||||||
child.apply {
|
|
||||||
setTextColor(context.resources.getColor(R.color.player_text, context.theme))
|
|
||||||
setTypeface(null, Typeface.NORMAL)
|
|
||||||
setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
|
|
||||||
setPadding(75, 0, 0, 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// set selected to selected style
|
|
||||||
selected.apply {
|
|
||||||
setTextColor(context.resources.getColor(R.color.player_white, context.theme))
|
|
||||||
setTypeface(null, Typeface.BOLD)
|
|
||||||
setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_baseline_check_24, 0, 0, 0)
|
|
||||||
setPadding(0, 0, 0, 0)
|
|
||||||
compoundDrawablesRelative.getOrNull(0)?.setTint(Color.WHITE)
|
|
||||||
compoundDrawablePadding = 12
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
package org.mosad.teapod.ui.components
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.AttributeSet
|
|
||||||
import android.view.KeyEvent
|
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.appcompat.R
|
|
||||||
import androidx.appcompat.widget.SearchView
|
|
||||||
|
|
||||||
// see https://stackoverflow.com/questions/30046201/android-searchview-empty-query-doesnt-work
|
|
||||||
class EmptySubmitSearchView : SearchView {
|
|
||||||
|
|
||||||
constructor(context: Context) : super(context)
|
|
||||||
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
|
|
||||||
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
|
|
||||||
|
|
||||||
override fun setOnQueryTextListener(listener: OnQueryTextListener?) {
|
|
||||||
super.setOnQueryTextListener(listener)
|
|
||||||
|
|
||||||
findViewById<SearchAutoComplete?>(R.id.search_src_text).setOnEditorActionListener { _: TextView?, _: Int, event: KeyEvent? ->
|
|
||||||
if (event != null && event.keyCode == KeyEvent.KEYCODE_ENTER) {
|
|
||||||
listener?.onQueryTextSubmit(query.toString())
|
|
||||||
} else {
|
|
||||||
listener?.onQueryTextSubmit(query.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
package org.mosad.teapod.ui.components
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import org.mosad.teapod.databinding.PlayerEpisodesListBinding
|
||||||
|
import org.mosad.teapod.ui.activity.player.PlayerViewModel
|
||||||
|
import org.mosad.teapod.util.adapter.PlayerEpisodeItemAdapter
|
||||||
|
|
||||||
|
class EpisodesListPlayer @JvmOverloads constructor(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
defStyleAttr: Int = 0,
|
||||||
|
model: PlayerViewModel? = null
|
||||||
|
) : LinearLayout(context, attrs, defStyleAttr) {
|
||||||
|
|
||||||
|
private val binding = PlayerEpisodesListBinding.inflate(LayoutInflater.from(context), this, true)
|
||||||
|
private lateinit var adapterRecEpisodes: PlayerEpisodeItemAdapter
|
||||||
|
|
||||||
|
var onViewRemovedAction: (() -> Unit)? = null // TODO find a better solution for this
|
||||||
|
|
||||||
|
init {
|
||||||
|
binding.buttonCloseEpisodesList.setOnClickListener {
|
||||||
|
(this.parent as ViewGroup).removeView(this)
|
||||||
|
onViewRemovedAction?.invoke()
|
||||||
|
}
|
||||||
|
|
||||||
|
model?.let {
|
||||||
|
adapterRecEpisodes = PlayerEpisodeItemAdapter(model.episodes, model.tmdbTVSeason?.episodes)
|
||||||
|
adapterRecEpisodes.onImageClick = {_, episodeId ->
|
||||||
|
(this.parent as ViewGroup).removeView(this)
|
||||||
|
model.setCurrentEpisode(episodeId, startPlayback = true)
|
||||||
|
}
|
||||||
|
// episodeNumber starts at 1, we need the episode index -> - 1
|
||||||
|
adapterRecEpisodes.currentSelected = model.currentEpisode.episodeNumber?.minus(1) ?: 0
|
||||||
|
|
||||||
|
binding.recyclerEpisodesPlayer.adapter = adapterRecEpisodes
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,105 @@
|
||||||
|
package org.mosad.teapod.ui.components
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.Typeface
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.util.TypedValue
|
||||||
|
import android.view.Gravity
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.core.view.children
|
||||||
|
import org.mosad.teapod.R
|
||||||
|
import org.mosad.teapod.databinding.PlayerLanguageSettingsBinding
|
||||||
|
import org.mosad.teapod.ui.activity.player.PlayerViewModel
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
// TODO port to DialogFragment
|
||||||
|
class LanguageSettingsPlayer @JvmOverloads constructor(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
defStyleAttr: Int = 0,
|
||||||
|
model: PlayerViewModel? = null
|
||||||
|
) : LinearLayout(context, attrs, defStyleAttr) {
|
||||||
|
|
||||||
|
private val binding = PlayerLanguageSettingsBinding.inflate(LayoutInflater.from(context), this, true)
|
||||||
|
var onViewRemovedAction: (() -> Unit)? = null
|
||||||
|
|
||||||
|
private var selectedLocale = model?.currentLanguage ?: Locale.ROOT
|
||||||
|
|
||||||
|
init {
|
||||||
|
model?.let { m ->
|
||||||
|
m.currentPlayback.streams.adaptive_hls.keys.forEach { languageTag ->
|
||||||
|
val locale = Locale.forLanguageTag(languageTag)
|
||||||
|
addLanguage(locale, locale == m.currentLanguage) { v ->
|
||||||
|
selectedLocale = locale
|
||||||
|
updateSelectedLanguage(v as TextView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.buttonCloseLanguageSettings.setOnClickListener { close() }
|
||||||
|
binding.buttonCancel.setOnClickListener { close() }
|
||||||
|
binding.buttonSelect.setOnClickListener {
|
||||||
|
model?.setLanguage(selectedLocale)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addLanguage(locale: Locale, isSelected: Boolean, onClick: OnClickListener) {
|
||||||
|
val text = TextView(context).apply {
|
||||||
|
height = 96
|
||||||
|
gravity = Gravity.CENTER_VERTICAL
|
||||||
|
text = if (locale == Locale.ROOT) context.getString(R.string.no_subtitles) else locale.displayLanguage
|
||||||
|
setTextSize(TypedValue.COMPLEX_UNIT_SP, 16f)
|
||||||
|
|
||||||
|
if (isSelected) {
|
||||||
|
setTextColor(context.resources.getColor(R.color.exo_white, context.theme))
|
||||||
|
setTypeface(null, Typeface.BOLD)
|
||||||
|
setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_baseline_check_24, 0, 0, 0)
|
||||||
|
compoundDrawablesRelative.getOrNull(0)?.setTint(Color.WHITE)
|
||||||
|
compoundDrawablePadding = 12
|
||||||
|
} else {
|
||||||
|
setTextColor(context.resources.getColor(R.color.textPrimaryDark, context.theme))
|
||||||
|
setPadding(75, 0, 0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
setOnClickListener(onClick)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.linearLanguages.addView(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateSelectedLanguage(selected: TextView) {
|
||||||
|
// rest all tf to not selected style
|
||||||
|
binding.linearLanguages.children.forEach { child ->
|
||||||
|
if (child is TextView) {
|
||||||
|
child.apply {
|
||||||
|
setTextColor(context.resources.getColor(R.color.textPrimaryDark, context.theme))
|
||||||
|
setTypeface(null, Typeface.NORMAL)
|
||||||
|
setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
|
||||||
|
setPadding(75, 0, 0, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// set selected to selected style
|
||||||
|
selected.apply {
|
||||||
|
setTextColor(context.resources.getColor(R.color.exo_white, context.theme))
|
||||||
|
setTypeface(null, Typeface.BOLD)
|
||||||
|
setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_baseline_check_24, 0, 0, 0)
|
||||||
|
setPadding(0, 0, 0, 0)
|
||||||
|
compoundDrawablesRelative.getOrNull(0)?.setTint(Color.WHITE)
|
||||||
|
compoundDrawablePadding = 12
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun close() {
|
||||||
|
(this.parent as ViewGroup).removeView(this)
|
||||||
|
onViewRemovedAction?.invoke()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,94 @@
|
||||||
|
/**
|
||||||
|
* ProjectLaogai
|
||||||
|
*
|
||||||
|
* Copyright 2019-2020 <seil0@mosad.xyz>
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation; either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||||
|
* MA 02110-1301, USA.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.mosad.teapod.ui.components
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.widget.EditText
|
||||||
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
|
import com.afollestad.materialdialogs.bottomsheets.BottomSheet
|
||||||
|
import com.afollestad.materialdialogs.bottomsheets.setPeekHeight
|
||||||
|
import com.afollestad.materialdialogs.customview.customView
|
||||||
|
import com.afollestad.materialdialogs.customview.getCustomView
|
||||||
|
import org.mosad.teapod.R
|
||||||
|
|
||||||
|
// TODO rework and port away from MaterialDialog
|
||||||
|
class LoginDialog(val context: Context, firstTry: Boolean) {
|
||||||
|
|
||||||
|
private val dialog = MaterialDialog(context, BottomSheet())
|
||||||
|
|
||||||
|
private val editTextLogin: EditText
|
||||||
|
private val editTextPassword: EditText
|
||||||
|
|
||||||
|
var login = ""
|
||||||
|
var password = ""
|
||||||
|
|
||||||
|
init {
|
||||||
|
dialog.title(R.string.login)
|
||||||
|
.message(if (firstTry) R.string.login_desc else R.string.login_failed_desc)
|
||||||
|
.customView(R.layout.dialog_login)
|
||||||
|
.positiveButton(R.string.save)
|
||||||
|
.negativeButton(R.string.cancel)
|
||||||
|
.setPeekHeight(900)
|
||||||
|
|
||||||
|
editTextLogin = dialog.getCustomView().findViewById(R.id.edit_text_login)
|
||||||
|
editTextPassword = dialog.getCustomView().findViewById(R.id.edit_text_password)
|
||||||
|
|
||||||
|
// fix not working accent color
|
||||||
|
//dialog.getActionButton(WhichButton.POSITIVE).updateTextColor(Preferences.colorAccent)
|
||||||
|
//dialog.getActionButton(WhichButton.NEGATIVE).updateTextColor(Preferences.colorAccent)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun positiveButton(func: LoginDialog.() -> Unit): LoginDialog = apply {
|
||||||
|
dialog.positiveButton {
|
||||||
|
login = editTextLogin.text.toString()
|
||||||
|
password = editTextPassword.text.toString()
|
||||||
|
|
||||||
|
func()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun negativeButton(func: LoginDialog.() -> Unit): LoginDialog = apply {
|
||||||
|
dialog.negativeButton {
|
||||||
|
func()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun show() {
|
||||||
|
dialog.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun show(func: LoginDialog.() -> Unit): LoginDialog = apply {
|
||||||
|
func()
|
||||||
|
|
||||||
|
editTextLogin.setText(login)
|
||||||
|
editTextPassword.setText(password)
|
||||||
|
|
||||||
|
show()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("unused")
|
||||||
|
fun dismiss() {
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,54 +0,0 @@
|
||||||
package org.mosad.teapod.ui.components
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
|
||||||
import org.mosad.teapod.databinding.ModalBottomSheetLoginBinding
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A bottom sheet with login credential input fields.
|
|
||||||
*
|
|
||||||
* To initialize login or password values, use apply.
|
|
||||||
*/
|
|
||||||
class LoginModalBottomSheet : BottomSheetDialogFragment() {
|
|
||||||
|
|
||||||
private lateinit var binding: ModalBottomSheetLoginBinding
|
|
||||||
|
|
||||||
var login = ""
|
|
||||||
var password = ""
|
|
||||||
|
|
||||||
lateinit var positiveAction: LoginModalBottomSheet.() -> Unit
|
|
||||||
lateinit var negativeAction: LoginModalBottomSheet.() -> Unit
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val TAG = "LoginModalBottomSheet"
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View {
|
|
||||||
binding = ModalBottomSheetLoginBinding.inflate(inflater, container, false)
|
|
||||||
return binding.root
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
|
|
||||||
binding.editTextLogin.setText(login)
|
|
||||||
binding.editTextPassword.setText(password)
|
|
||||||
|
|
||||||
binding.positiveButton.setOnClickListener {
|
|
||||||
login = binding.editTextLogin.text.toString()
|
|
||||||
password = binding.editTextPassword.text.toString()
|
|
||||||
|
|
||||||
positiveAction.invoke(this)
|
|
||||||
}
|
|
||||||
binding.negativeButton.setOnClickListener {
|
|
||||||
negativeAction.invoke(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,9 @@ import android.app.ActivityManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.view.View
|
||||||
|
import android.view.WindowInsets
|
||||||
|
import android.view.WindowInsetsController
|
||||||
import androidx.fragment.app.Fragment
|
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
|
||||||
|
@ -28,7 +31,23 @@ fun FragmentActivity.showFragment(fragment: Fragment) {
|
||||||
* hide the status and navigation bar
|
* hide the status and navigation bar
|
||||||
*/
|
*/
|
||||||
fun Activity.hideBars() {
|
fun Activity.hideBars() {
|
||||||
hideBars(window, window.decorView.rootView)
|
window.apply {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
setDecorFitsSystemWindows(false)
|
||||||
|
insetsController?.apply {
|
||||||
|
hide(WindowInsets.Type.statusBars() or WindowInsets.Type.navigationBars())
|
||||||
|
systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_BARS_BY_SWIPE
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
@Suppress("deprecation")
|
||||||
|
decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_IMMERSIVE
|
||||||
|
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||||
|
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||||
|
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||||
|
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||||
|
or View.SYSTEM_UI_FLAG_FULLSCREEN)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Activity.isInPiPMode(): Boolean {
|
fun Activity.isInPiPMode(): Boolean {
|
||||||
|
|
|
@ -10,7 +10,6 @@ class DataTypes {
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class Theme(val str: String) {
|
enum class Theme(val str: String) {
|
||||||
SYSTEM("System"),
|
|
||||||
LIGHT("Light"),
|
LIGHT("Light"),
|
||||||
DARK("Dark")
|
DARK("Dark")
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,159 @@
|
||||||
|
/**
|
||||||
|
* Teapod
|
||||||
|
*
|
||||||
|
* Copyright 2020-2022 <seil0@mosad.xyz>
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation; either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||||
|
* MA 02110-1301, USA.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.mosad.teapod.util
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import java.io.FileNotFoundException
|
||||||
|
import java.net.URL
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO remove gson usage
|
||||||
|
*/
|
||||||
|
class MetaDBController {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val repoUrl = "https://gitlab.com/Seil0/teapodmetadb/-/raw/main/aod/"
|
||||||
|
|
||||||
|
var mediaList = MediaList(listOf())
|
||||||
|
private var metaCacheList = arrayListOf<Meta>()
|
||||||
|
|
||||||
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
|
suspend fun list() = withContext(Dispatchers.IO) {
|
||||||
|
val url = URL("$repoUrl/list.json")
|
||||||
|
val json = url.readText()
|
||||||
|
|
||||||
|
mediaList = Gson().fromJson(json, MediaList::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the meta data for a movie from MetaDB
|
||||||
|
* @param aodId The AoD id of the media
|
||||||
|
* @return A meta movie object, or null if not found
|
||||||
|
*/
|
||||||
|
suspend fun getMovieMetadata(aodId: Int): MovieMeta? {
|
||||||
|
return metaCacheList.firstOrNull {
|
||||||
|
it.aodId == aodId
|
||||||
|
} as MovieMeta? ?: getMovieMetadataFromDB(aodId)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the meta data for a tv show from MetaDB
|
||||||
|
* @param aodId The AoD id of the media
|
||||||
|
* @return A meta tv show object, or null if not found
|
||||||
|
*/
|
||||||
|
suspend fun getTVShowMetadata(aodId: Int): TVShowMeta? {
|
||||||
|
return metaCacheList.firstOrNull {
|
||||||
|
it.aodId == aodId
|
||||||
|
} as TVShowMeta? ?: getTVShowMetadataFromDB(aodId)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
|
private suspend fun getMovieMetadataFromDB(aodId: Int): MovieMeta? = withContext(Dispatchers.IO) {
|
||||||
|
val url = URL("$repoUrl/movie/$aodId/media.json")
|
||||||
|
return@withContext try {
|
||||||
|
val json = url.readText()
|
||||||
|
val meta = Gson().fromJson(json, MovieMeta::class.java)
|
||||||
|
metaCacheList.add(meta)
|
||||||
|
|
||||||
|
meta
|
||||||
|
} catch (ex: FileNotFoundException) {
|
||||||
|
Log.w(javaClass.name, "Waring: The requested file was not found. Requested ID: $aodId", ex)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
|
private suspend fun getTVShowMetadataFromDB(aodId: Int): TVShowMeta? = withContext(Dispatchers.IO) {
|
||||||
|
val url = URL("$repoUrl/tv/$aodId/media.json")
|
||||||
|
return@withContext try {
|
||||||
|
val json = url.readText()
|
||||||
|
val meta = Gson().fromJson(json, TVShowMeta::class.java)
|
||||||
|
metaCacheList.add(meta)
|
||||||
|
|
||||||
|
meta
|
||||||
|
} catch (ex: FileNotFoundException) {
|
||||||
|
Log.w(javaClass.name, "Waring: The requested file was not found. Requested ID: $aodId", ex)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// class representing the media list json object
|
||||||
|
data class MediaList(
|
||||||
|
val media: List<Int>
|
||||||
|
)
|
||||||
|
|
||||||
|
// abstract class used for meta data objects (tv, movie)
|
||||||
|
abstract class Meta {
|
||||||
|
abstract val id: Int
|
||||||
|
abstract val aodId: Int
|
||||||
|
abstract val tmdbId: Int
|
||||||
|
}
|
||||||
|
|
||||||
|
// class representing the movie json object
|
||||||
|
data class MovieMeta(
|
||||||
|
override val id: Int,
|
||||||
|
@SerializedName("aod_id")
|
||||||
|
override val aodId: Int,
|
||||||
|
@SerializedName("tmdb_id")
|
||||||
|
override val tmdbId: Int
|
||||||
|
): Meta()
|
||||||
|
|
||||||
|
// class representing the tv show json object
|
||||||
|
data class TVShowMeta(
|
||||||
|
override val id: Int,
|
||||||
|
@SerializedName("aod_id")
|
||||||
|
override val aodId: Int,
|
||||||
|
@SerializedName("tmdb_id")
|
||||||
|
override val tmdbId: Int,
|
||||||
|
@SerializedName("tmdb_season_id")
|
||||||
|
val tmdbSeasonId: Int,
|
||||||
|
@SerializedName("tmdb_season_number")
|
||||||
|
val tmdbSeasonNumber: Int,
|
||||||
|
@SerializedName("episodes")
|
||||||
|
val episodes: List<EpisodeMeta>
|
||||||
|
): Meta()
|
||||||
|
|
||||||
|
// class used in TVShowMeta, part of the tv show json object
|
||||||
|
data class EpisodeMeta(
|
||||||
|
val id: Int,
|
||||||
|
@SerializedName("aod_media_id")
|
||||||
|
val aodMediaId: Int,
|
||||||
|
@SerializedName("tmdb_id")
|
||||||
|
val tmdbId: Int,
|
||||||
|
@SerializedName("tmdb_number")
|
||||||
|
val tmdbNumber: Int,
|
||||||
|
@SerializedName("opening_start")
|
||||||
|
val openingStart: Long,
|
||||||
|
@SerializedName("opening_duration")
|
||||||
|
val openingDuration: Long,
|
||||||
|
@SerializedName("ending_start")
|
||||||
|
val endingStart: Long,
|
||||||
|
@SerializedName("ending_duration")
|
||||||
|
val endingDuration: Long
|
||||||
|
)
|
|
@ -1,30 +1,10 @@
|
||||||
package org.mosad.teapod.util
|
package org.mosad.teapod.util
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.view.View
|
|
||||||
import android.view.Window
|
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.core.view.WindowCompat
|
import org.mosad.teapod.parser.crunchyroll.Collection
|
||||||
import androidx.core.view.WindowInsetsCompat
|
import org.mosad.teapod.parser.crunchyroll.ContinueWatchingItem
|
||||||
import androidx.core.view.WindowInsetsControllerCompat
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import org.mosad.teapod.R
|
|
||||||
import org.mosad.teapod.parser.crunchyroll.CollectionV2
|
|
||||||
import org.mosad.teapod.parser.crunchyroll.Item
|
import org.mosad.teapod.parser.crunchyroll.Item
|
||||||
import org.mosad.teapod.parser.crunchyroll.PlayheadObject
|
import java.util.*
|
||||||
import org.mosad.teapod.ui.activity.player.PlayerActivity
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a Intent for PlayerActivity with season and episode id.
|
|
||||||
*
|
|
||||||
* @param seasonId The ID of the season the episode to be played is in
|
|
||||||
* @param episodeId The ID of the episode to play
|
|
||||||
*/
|
|
||||||
fun Fragment.playerIntent(seasonId: String, episodeId: String) = Intent(context, PlayerActivity::class.java).apply {
|
|
||||||
putExtra(getString(R.string.intent_season_id), seasonId)
|
|
||||||
putExtra(getString(R.string.intent_episode_id), episodeId)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun TextView.setDrawableTop(drawable: Int) {
|
fun TextView.setDrawableTop(drawable: Int) {
|
||||||
this.setCompoundDrawablesWithIntrinsicBounds(0, drawable, 0, 0)
|
this.setCompoundDrawablesWithIntrinsicBounds(0, drawable, 0, 0)
|
||||||
|
@ -35,16 +15,22 @@ fun <T> concatenate(vararg lists: List<T>): List<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO move to correct location
|
// TODO move to correct location
|
||||||
fun CollectionV2<Item>.toItemMediaList(): List<ItemMedia> {
|
fun Collection<Item>.toItemMediaList(): List<ItemMedia> {
|
||||||
return this.data.map {
|
return this.items.map {
|
||||||
ItemMedia(it.id, it.title, it.images.poster_wide[0][0].source)
|
ItemMedia(it.id, it.title, it.images.poster_wide[0][0].source)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmName("toItemMediaListItem")
|
@JvmName("toItemMediaListContinueWatchingItem")
|
||||||
fun List<Item>.toItemMediaList(): List<ItemMedia> {
|
fun Collection<ContinueWatchingItem>.toItemMediaList(): List<ItemMedia> {
|
||||||
|
return items.map {
|
||||||
|
ItemMedia(it.panel.episodeMetadata.seriesId, it.panel.title, it.panel.images.thumbnail[0][0].source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun List<ContinueWatchingItem>.toItemMediaList(): List<ItemMedia> {
|
||||||
return this.map {
|
return this.map {
|
||||||
ItemMedia(it.id, it.title, it.images.poster_wide[0][0].source)
|
ItemMedia(it.panel.episodeMetadata.seriesId, it.panel.title, it.panel.images.thumbnail[0][0].source)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,17 +43,3 @@ fun Locale.toDisplayString(fallback: String): String {
|
||||||
fallback
|
fallback
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun CollectionV2<PlayheadObject>.toPlayheadsMap(): Map<String, PlayheadObject> {
|
|
||||||
return this.data.associateBy { it.contentId }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun hideBars(window: Window?, root: View) {
|
|
||||||
if (window != null) {
|
|
||||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
|
||||||
WindowInsetsControllerCompat(window, root).let { controller ->
|
|
||||||
controller.hide(WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.navigationBars())
|
|
||||||
controller.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -4,7 +4,6 @@ import android.graphics.Color
|
||||||
import android.graphics.drawable.ColorDrawable
|
import android.graphics.drawable.ColorDrawable
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
@ -13,166 +12,84 @@ import com.bumptech.glide.request.RequestOptions
|
||||||
import jp.wasabeef.glide.transformations.RoundedCornersTransformation
|
import jp.wasabeef.glide.transformations.RoundedCornersTransformation
|
||||||
import org.mosad.teapod.R
|
import org.mosad.teapod.R
|
||||||
import org.mosad.teapod.databinding.ItemEpisodeBinding
|
import org.mosad.teapod.databinding.ItemEpisodeBinding
|
||||||
import org.mosad.teapod.databinding.ItemEpisodePlayerBinding
|
|
||||||
import org.mosad.teapod.parser.crunchyroll.Episode
|
import org.mosad.teapod.parser.crunchyroll.Episode
|
||||||
import org.mosad.teapod.parser.crunchyroll.PlayheadObject
|
import org.mosad.teapod.parser.crunchyroll.PlayheadsMap
|
||||||
import org.mosad.teapod.util.tmdb.TMDBTVEpisode
|
import org.mosad.teapod.util.tmdb.TMDBTVEpisode
|
||||||
|
|
||||||
class EpisodeItemAdapter(
|
class EpisodeItemAdapter(
|
||||||
private val episodes: List<Episode>,
|
private val episodes: List<Episode>,
|
||||||
private val tmdbEpisodes: List<TMDBTVEpisode>?,
|
private val tmdbEpisodes: List<TMDBTVEpisode>?,
|
||||||
private val playheads: Map<String, PlayheadObject>,
|
private val playheads: PlayheadsMap
|
||||||
private val onClickListener: OnClickListener,
|
) : RecyclerView.Adapter<EpisodeItemAdapter.EpisodeViewHolder>() {
|
||||||
private val viewType: ViewType
|
|
||||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
|
||||||
|
|
||||||
var currentSelected: Int = -1 // -1, since position should never be < 0
|
var onImageClick: ((seasonId: String, episodeId: String) -> Unit)? = null
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EpisodeViewHolder {
|
||||||
return when (viewType) {
|
return EpisodeViewHolder(ItemEpisodeBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
||||||
ViewType.PLAYER.ordinal -> {
|
|
||||||
PlayerEpisodeViewHolder((ItemEpisodePlayerBinding.inflate(LayoutInflater.from(parent.context), parent, false)))
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
// media fragment episode list is default
|
|
||||||
EpisodeViewHolder(ItemEpisodeBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: EpisodeViewHolder, position: Int) {
|
||||||
val episode = episodes[position]
|
val context = holder.binding.root.context
|
||||||
val playhead = playheads[episode.id]
|
val ep = episodes[position]
|
||||||
val tmdbEpisode = tmdbEpisodes?.getOrNull(position)
|
|
||||||
|
|
||||||
when (holder.itemViewType) {
|
val titleText = if (ep.episodeNumber != null) {
|
||||||
ViewType.MEDIA_FRAGMENT.ordinal -> {
|
// for tv shows add ep prefix and episode number
|
||||||
(holder as EpisodeViewHolder).bind(episode, playhead, tmdbEpisode)
|
if (ep.isDubbed) {
|
||||||
}
|
context.getString(R.string.component_episode_title, ep.episode, ep.title)
|
||||||
ViewType.PLAYER.ordinal -> {
|
} else {
|
||||||
(holder as PlayerEpisodeViewHolder).bind(episode, playhead, currentSelected)
|
context.getString(R.string.component_episode_title_sub, ep.episode, ep.title)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
ep.title
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemViewType(position: Int): Int {
|
holder.binding.textEpisodeTitle.text = titleText
|
||||||
return when (viewType) {
|
holder.binding.textEpisodeDesc.text = if (ep.description.isNotEmpty()) {
|
||||||
ViewType.MEDIA_FRAGMENT -> ViewType.MEDIA_FRAGMENT.ordinal
|
ep.description
|
||||||
ViewType.PLAYER -> ViewType.PLAYER.ordinal
|
} else if (tmdbEpisodes != null && position < tmdbEpisodes.size){
|
||||||
|
tmdbEpisodes[position].overview
|
||||||
|
} else {
|
||||||
|
""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO is isNotEmpty() needed? also in PlayerEpisodeItemAdapter
|
||||||
|
if (ep.images.thumbnail[0][0].source.isNotEmpty()) {
|
||||||
|
Glide.with(context).load(ep.images.thumbnail[0][0].source)
|
||||||
|
.apply(RequestOptions.placeholderOf(ColorDrawable(Color.DKGRAY)))
|
||||||
|
.apply(RequestOptions.bitmapTransform(RoundedCornersTransformation(10, 0)))
|
||||||
|
.into(holder.binding.imageEpisode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// add watched icon to episode, if the episode id is present in playheads and fullyWatched
|
||||||
|
val watchedImage: Drawable? = if (playheads[ep.id]?.fullyWatched == true) {
|
||||||
|
ContextCompat.getDrawable(context, R.drawable.ic_baseline_check_circle_24)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
holder.binding.imageWatched.setImageDrawable(watchedImage)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemCount(): Int {
|
override fun getItemCount(): Int {
|
||||||
return episodes.size
|
return episodes.size
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun updateWatchedState(watched: Boolean, position: Int) {
|
||||||
|
// use getOrNull as there could be a index out of bound when running this in onResume()
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
//episodes.getOrNull(position)?.watched = watched
|
||||||
|
}
|
||||||
|
|
||||||
inner class EpisodeViewHolder(val binding: ItemEpisodeBinding) :
|
inner class EpisodeViewHolder(val binding: ItemEpisodeBinding) :
|
||||||
RecyclerView.ViewHolder(binding.root) {
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
|
init {
|
||||||
fun bind(episode: Episode, playhead: PlayheadObject?, tmdbEpisode: TMDBTVEpisode?) {
|
// on image click return the episode id and index (within the adapter)
|
||||||
val context = binding.root.context
|
|
||||||
|
|
||||||
val titleText = if (episode.episodeNumber != null) {
|
|
||||||
// for tv shows add ep prefix and episode number
|
|
||||||
if (episode.isDubbed) {
|
|
||||||
context.getString(R.string.component_episode_title, episode.episode, episode.title)
|
|
||||||
} else {
|
|
||||||
context.getString(R.string.component_episode_title_sub, episode.episode, episode.title)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
episode.title
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.textEpisodeTitle.text = titleText
|
|
||||||
binding.textEpisodeDesc.text = episode.description.ifEmpty {
|
|
||||||
tmdbEpisode?.overview ?: ""
|
|
||||||
}
|
|
||||||
|
|
||||||
if (episode.images.thumbnail[0][0].source.isNotEmpty()) {
|
|
||||||
Glide.with(context).load(episode.images.thumbnail[0][0].source)
|
|
||||||
.apply(RequestOptions.placeholderOf(ColorDrawable(Color.DKGRAY)))
|
|
||||||
.apply(RequestOptions.bitmapTransform(RoundedCornersTransformation(10, 0)))
|
|
||||||
.into(binding.imageEpisode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// add watched progress
|
|
||||||
val playheadProgress = playhead?.playhead?.let {
|
|
||||||
((it.toFloat() / (episode.durationMs / 1000)) * 100).toInt()
|
|
||||||
} ?: 0
|
|
||||||
binding.progressPlayhead.setProgressCompat(playheadProgress, false)
|
|
||||||
binding.progressPlayhead.visibility = if (playheadProgress <= 0)
|
|
||||||
View.GONE else View.VISIBLE
|
|
||||||
|
|
||||||
// add watched icon to episode, if the episode id is present in playheads and fullyWatched
|
|
||||||
val watchedImage: Drawable? = if (playhead?.fullyWatched == true) {
|
|
||||||
ContextCompat.getDrawable(context, R.drawable.ic_baseline_check_circle_24)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
binding.imageWatched.setImageDrawable(watchedImage)
|
|
||||||
|
|
||||||
binding.imageEpisode.setOnClickListener {
|
binding.imageEpisode.setOnClickListener {
|
||||||
onClickListener.onClick(episode)
|
onImageClick?.invoke(
|
||||||
|
episodes[bindingAdapterPosition].seasonId,
|
||||||
|
episodes[bindingAdapterPosition].id
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class PlayerEpisodeViewHolder(val binding: ItemEpisodePlayerBinding) :
|
|
||||||
RecyclerView.ViewHolder(binding.root) {
|
|
||||||
|
|
||||||
// -1, since position should never be < 0
|
|
||||||
fun bind(episode: Episode, playhead: PlayheadObject?, currentSelected: Int) {
|
|
||||||
val context = binding.root.context
|
|
||||||
|
|
||||||
val titleText = if (episode.episodeNumber != null) {
|
|
||||||
// for tv shows add ep prefix and episode number
|
|
||||||
if (episode.isDubbed) {
|
|
||||||
context.getString(R.string.component_episode_title, episode.episode, episode.title)
|
|
||||||
} else {
|
|
||||||
context.getString(R.string.component_episode_title_sub, episode.episode, episode.title)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
episode.title
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.textEpisodeTitle2.text = titleText
|
|
||||||
binding.textEpisodeDesc2.text = episode.description.ifEmpty { "" }
|
|
||||||
|
|
||||||
if (episode.images.thumbnail[0][0].source.isNotEmpty()) {
|
|
||||||
Glide.with(context).load(episode.images.thumbnail[0][0].source)
|
|
||||||
.apply(RequestOptions.bitmapTransform(RoundedCornersTransformation(10, 0)))
|
|
||||||
.into(binding.imageEpisode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// add watched progress
|
|
||||||
val playheadProgress = playhead?.playhead?.let {
|
|
||||||
((it.toFloat() / (episode.durationMs / 1000)) * 100).toInt()
|
|
||||||
} ?: 0
|
|
||||||
binding.progressPlayhead.setProgressCompat(playheadProgress, false)
|
|
||||||
binding.progressPlayhead.visibility = if (playheadProgress <= 0)
|
|
||||||
View.GONE else View.VISIBLE
|
|
||||||
|
|
||||||
// hide the play icon, if it's the current episode
|
|
||||||
binding.imageEpisodePlay.visibility = if (currentSelected == bindingAdapterPosition) {
|
|
||||||
View.GONE
|
|
||||||
} else {
|
|
||||||
View.VISIBLE
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentSelected != bindingAdapterPosition) {
|
|
||||||
binding.imageEpisode.setOnClickListener {
|
|
||||||
onClickListener.onClick(episode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class OnClickListener(val clickListener: (episode: Episode) -> Unit) {
|
|
||||||
fun onClick(episode: Episode) = clickListener(episode)
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class ViewType {
|
|
||||||
MEDIA_FRAGMENT,
|
|
||||||
PLAYER
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -1,73 +0,0 @@
|
||||||
package org.mosad.teapod.util.adapter
|
|
||||||
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
|
||||||
import androidx.recyclerview.widget.ListAdapter
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import com.bumptech.glide.Glide
|
|
||||||
import org.mosad.teapod.R
|
|
||||||
import org.mosad.teapod.databinding.ItemMediaBinding
|
|
||||||
import org.mosad.teapod.parser.crunchyroll.UpNextAccountItem
|
|
||||||
|
|
||||||
class MediaEpisodeListAdapter(private val onClickListener: OnClickListener, private val itemOffset: Int = 0) : ListAdapter<UpNextAccountItem, MediaEpisodeListAdapter.MediaViewHolder>(DiffCallback) {
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MediaViewHolder {
|
|
||||||
val binding = ItemMediaBinding.inflate(
|
|
||||||
LayoutInflater.from(parent.context),
|
|
||||||
parent,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
binding.root.layoutParams.apply {
|
|
||||||
width = (parent.measuredWidth / parent.context.resources.getInteger(R.integer.item_media_columns)) - itemOffset
|
|
||||||
}
|
|
||||||
|
|
||||||
return MediaViewHolder(binding)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: MediaViewHolder, position: Int) {
|
|
||||||
val item = getItem(position)
|
|
||||||
holder.binding.root.setOnClickListener {
|
|
||||||
onClickListener.onClick(item)
|
|
||||||
}
|
|
||||||
holder.bind(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class MediaViewHolder(val binding: ItemMediaBinding) :
|
|
||||||
RecyclerView.ViewHolder(binding.root) {
|
|
||||||
|
|
||||||
fun bind(item: UpNextAccountItem) {
|
|
||||||
val metadata = item.panel.episodeMetadata
|
|
||||||
|
|
||||||
binding.textTitle.text = binding.root.context.getString(R.string.season_episode_title,
|
|
||||||
metadata.seasonNumber, metadata.episodeNumber, metadata.seriesTitle
|
|
||||||
)
|
|
||||||
|
|
||||||
Glide.with(binding.imagePoster)
|
|
||||||
.load(item.panel.images.thumbnail[0][0].source)
|
|
||||||
.into(binding.imagePoster)
|
|
||||||
|
|
||||||
// add watched progress
|
|
||||||
val playheadProgress = ((item.playhead.toFloat() / (metadata.durationMs / 1000)) * 100)
|
|
||||||
.toInt()
|
|
||||||
binding.progressPlayhead.setProgressCompat(playheadProgress, false)
|
|
||||||
binding.progressPlayhead.visibility = if (playheadProgress <= 0)
|
|
||||||
View.GONE else View.VISIBLE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object DiffCallback : DiffUtil.ItemCallback<UpNextAccountItem>() {
|
|
||||||
override fun areItemsTheSame(oldItem: UpNextAccountItem, newItem: UpNextAccountItem): Boolean {
|
|
||||||
return oldItem.panel.id == newItem.panel.id
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItem: UpNextAccountItem, newItem: UpNextAccountItem): Boolean {
|
|
||||||
return oldItem == newItem
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class OnClickListener(val clickListener: (item: UpNextAccountItem) -> Unit) {
|
|
||||||
fun onClick(item: UpNextAccountItem) = clickListener(item)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
package org.mosad.teapod.util.adapter
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import org.mosad.teapod.databinding.ItemMediaBinding
|
||||||
|
import org.mosad.teapod.util.ItemMedia
|
||||||
|
|
||||||
|
class MediaItemAdapter(private val items: List<ItemMedia>) : RecyclerView.Adapter<MediaItemAdapter.MediaViewHolder>() {
|
||||||
|
|
||||||
|
var onItemClick: ((id: String, position: Int) -> Unit)? = null
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MediaItemAdapter.MediaViewHolder {
|
||||||
|
return MediaViewHolder(ItemMediaBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: MediaItemAdapter.MediaViewHolder, position: Int) {
|
||||||
|
holder.binding.root.apply {
|
||||||
|
holder.binding.textTitle.text = items[position].title
|
||||||
|
Glide.with(context).load(items[position].posterUrl).into(holder.binding.imagePoster)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int {
|
||||||
|
return items.size
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class MediaViewHolder(val binding: ItemMediaBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
|
init {
|
||||||
|
binding.root.setOnClickListener {
|
||||||
|
onItemClick?.invoke(
|
||||||
|
items[bindingAdapterPosition].id,
|
||||||
|
bindingAdapterPosition
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,65 +0,0 @@
|
||||||
package org.mosad.teapod.util.adapter
|
|
||||||
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
|
||||||
import androidx.recyclerview.widget.ListAdapter
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import com.bumptech.glide.Glide
|
|
||||||
import org.mosad.teapod.R
|
|
||||||
import org.mosad.teapod.databinding.ItemMediaBinding
|
|
||||||
import org.mosad.teapod.util.ItemMedia
|
|
||||||
|
|
||||||
class MediaItemListAdapter(private val onClickListener: OnClickListener, private val itemOffset: Int = 0) : ListAdapter<ItemMedia, MediaItemListAdapter.MediaViewHolder>(DiffCallback) {
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MediaViewHolder {
|
|
||||||
val binding = ItemMediaBinding.inflate(
|
|
||||||
LayoutInflater.from(parent.context),
|
|
||||||
parent,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
binding.root.layoutParams.apply {
|
|
||||||
width = (parent.measuredWidth / parent.context.resources.getInteger(R.integer.item_media_columns)) - itemOffset
|
|
||||||
}
|
|
||||||
|
|
||||||
return MediaViewHolder(binding)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: MediaViewHolder, position: Int) {
|
|
||||||
val item = getItem(position)
|
|
||||||
holder.binding.root.setOnClickListener {
|
|
||||||
onClickListener.onClick(item)
|
|
||||||
}
|
|
||||||
holder.bind(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class MediaViewHolder(val binding: ItemMediaBinding) :
|
|
||||||
RecyclerView.ViewHolder(binding.root) {
|
|
||||||
|
|
||||||
fun bind(item: ItemMedia) {
|
|
||||||
binding.textTitle.text = item.title
|
|
||||||
|
|
||||||
Glide.with(binding.root.context)
|
|
||||||
.load(item.posterUrl)
|
|
||||||
.into(binding.imagePoster)
|
|
||||||
|
|
||||||
binding.imageEpisodePlay.isVisible = false
|
|
||||||
binding.progressPlayhead.isVisible = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object DiffCallback : DiffUtil.ItemCallback<ItemMedia>() {
|
|
||||||
override fun areItemsTheSame(oldItem: ItemMedia, newItem: ItemMedia): Boolean {
|
|
||||||
return oldItem.id == newItem.id
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItem: ItemMedia, newItem: ItemMedia): Boolean {
|
|
||||||
return oldItem == newItem
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class OnClickListener(val clickListener: (item: ItemMedia) -> Unit) {
|
|
||||||
fun onClick(item: ItemMedia) = clickListener(item)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,79 @@
|
||||||
|
package org.mosad.teapod.util.adapter
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import com.bumptech.glide.request.RequestOptions
|
||||||
|
import jp.wasabeef.glide.transformations.RoundedCornersTransformation
|
||||||
|
import org.mosad.teapod.R
|
||||||
|
import org.mosad.teapod.databinding.ItemEpisodePlayerBinding
|
||||||
|
import org.mosad.teapod.parser.crunchyroll.Episodes
|
||||||
|
import org.mosad.teapod.util.tmdb.TMDBTVEpisode
|
||||||
|
|
||||||
|
class PlayerEpisodeItemAdapter(private val episodes: Episodes, private val tmdbEpisodes: List<TMDBTVEpisode>?) : RecyclerView.Adapter<PlayerEpisodeItemAdapter.EpisodeViewHolder>() {
|
||||||
|
|
||||||
|
var onImageClick: ((seasonId: String, episodeId: String) -> Unit)? = null
|
||||||
|
var currentSelected: Int = -1 // -1, since position should never be < 0
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EpisodeViewHolder {
|
||||||
|
return EpisodeViewHolder(ItemEpisodePlayerBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: EpisodeViewHolder, position: Int) {
|
||||||
|
val context = holder.binding.root.context
|
||||||
|
val ep = episodes.items[position]
|
||||||
|
|
||||||
|
val titleText = if (ep.episodeNumber != null) {
|
||||||
|
// for tv shows add ep prefix and episode number
|
||||||
|
if (ep.isDubbed) {
|
||||||
|
context.getString(R.string.component_episode_title, ep.episode, ep.title)
|
||||||
|
} else {
|
||||||
|
context.getString(R.string.component_episode_title_sub, ep.episode, ep.title)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ep.title
|
||||||
|
}
|
||||||
|
|
||||||
|
holder.binding.textEpisodeTitle2.text = titleText
|
||||||
|
holder.binding.textEpisodeDesc2.text = if (ep.description.isNotEmpty()) {
|
||||||
|
ep.description
|
||||||
|
} else if (tmdbEpisodes != null && position < tmdbEpisodes.size){
|
||||||
|
tmdbEpisodes[position].overview
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ep.images.thumbnail[0][0].source.isNotEmpty()) {
|
||||||
|
Glide.with(context).load(ep.images.thumbnail[0][0].source)
|
||||||
|
.apply(RequestOptions.bitmapTransform(RoundedCornersTransformation(10, 0)))
|
||||||
|
.into(holder.binding.imageEpisode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// hide the play icon, if it's the current episode
|
||||||
|
holder.binding.imageEpisodePlay.visibility = if (currentSelected == position) {
|
||||||
|
View.GONE
|
||||||
|
} else {
|
||||||
|
View.VISIBLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int {
|
||||||
|
return episodes.items.size
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class EpisodeViewHolder(val binding: ItemEpisodePlayerBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||||
|
init {
|
||||||
|
binding.imageEpisode.setOnClickListener {
|
||||||
|
// don't execute, if it's the current episode
|
||||||
|
if (currentSelected != bindingAdapterPosition) {
|
||||||
|
onImageClick?.invoke(
|
||||||
|
episodes.items[bindingAdapterPosition].seasonId,
|
||||||
|
episodes.items[bindingAdapterPosition].id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,57 +0,0 @@
|
||||||
package org.mosad.teapod.util.metadb
|
|
||||||
|
|
||||||
import kotlinx.serialization.SerialName
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
|
|
||||||
// class representing the media list json object
|
|
||||||
@Serializable
|
|
||||||
data class MediaList(
|
|
||||||
@SerialName("media") val media: List<String>
|
|
||||||
)
|
|
||||||
|
|
||||||
// abstract class used for meta data objects (tv, movie)
|
|
||||||
abstract class Meta {
|
|
||||||
abstract val id: Int
|
|
||||||
abstract val tmdbId: Int
|
|
||||||
abstract val crSeriesId: String
|
|
||||||
}
|
|
||||||
|
|
||||||
// class representing the movie json object
|
|
||||||
@Serializable
|
|
||||||
data class MovieMeta(
|
|
||||||
@SerialName("id") override val id: Int,
|
|
||||||
@SerialName("tmdb_id") override val tmdbId: Int,
|
|
||||||
@SerialName("cr_series_id") override val crSeriesId: String,
|
|
||||||
): Meta()
|
|
||||||
|
|
||||||
// class representing the tv show json object
|
|
||||||
@Serializable
|
|
||||||
data class TVShowMeta(
|
|
||||||
@SerialName("id") override val id: Int,
|
|
||||||
@SerialName("tmdb_id") override val tmdbId: Int,
|
|
||||||
@SerialName("cr_series_id") override val crSeriesId: String,
|
|
||||||
@SerialName("seasons") val seasons: List<SeasonMeta>,
|
|
||||||
): Meta()
|
|
||||||
|
|
||||||
// class used in TVShowMeta, part of the tv show json object
|
|
||||||
@Serializable
|
|
||||||
data class SeasonMeta(
|
|
||||||
@SerialName("id") val id: Int,
|
|
||||||
@SerialName("tmdb_season_id") val tmdbSeasonId: Int,
|
|
||||||
@SerialName("tmdb_season_number") val tmdbSeasonNumber: Int,
|
|
||||||
@SerialName("cr_season_ids") val crSeasonIds: List<String>,
|
|
||||||
@SerialName("episodes") val episodes: List<EpisodeMeta>,
|
|
||||||
)
|
|
||||||
|
|
||||||
// class used in TVShowMeta, part of the tv show json object
|
|
||||||
@Serializable
|
|
||||||
data class EpisodeMeta(
|
|
||||||
@SerialName("id") val id: Int,
|
|
||||||
@SerialName("tmdb_episode_id") val tmdbEpisodeId: Int,
|
|
||||||
@SerialName("tmdb_episode_number") val tmdbEpisodeNumber: Int,
|
|
||||||
@SerialName("cr_episode_ids") val crEpisodeIds: List<String>,
|
|
||||||
@SerialName("opening_start") val openingStart: Long,
|
|
||||||
@SerialName("opening_duration") val openingDuration: Long,
|
|
||||||
@SerialName("ending_start") val endingStart: Long,
|
|
||||||
@SerialName("ending_duration") val endingDuration: Long
|
|
||||||
)
|
|
|
@ -1,89 +0,0 @@
|
||||||
/**
|
|
||||||
* Teapod
|
|
||||||
*
|
|
||||||
* Copyright 2020-2022 <seil0@mosad.xyz>
|
|
||||||
*
|
|
||||||
* This program is free software; you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation; either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program; if not, write to the Free Software
|
|
||||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
|
||||||
* MA 02110-1301, USA.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.mosad.teapod.util.metadb
|
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import io.ktor.client.*
|
|
||||||
import io.ktor.client.call.*
|
|
||||||
import io.ktor.client.plugins.*
|
|
||||||
import io.ktor.client.plugins.contentnegotiation.*
|
|
||||||
import io.ktor.client.request.*
|
|
||||||
import io.ktor.http.*
|
|
||||||
import io.ktor.serialization.kotlinx.json.*
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import kotlinx.serialization.decodeFromString
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
|
|
||||||
object MetaDBController {
|
|
||||||
private val TAG = javaClass.name
|
|
||||||
|
|
||||||
private const val repoUrl = "https://gitlab.com/Seil0/teapodmetadb/-/raw/main/crunchy/"
|
|
||||||
|
|
||||||
private val client = HttpClient {
|
|
||||||
install(ContentNegotiation) {
|
|
||||||
json()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var mediaList = MediaList(listOf())
|
|
||||||
private var metaCacheList = arrayListOf<Meta>()
|
|
||||||
|
|
||||||
suspend fun list() = withContext(Dispatchers.IO) {
|
|
||||||
val raw: String = client.get("$repoUrl/list.json").body()
|
|
||||||
mediaList = Json.decodeFromString(raw)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the meta data for a movie from MetaDB
|
|
||||||
* @param crSeriesId The crunchyroll media id
|
|
||||||
* @return A meta object, or null if not found
|
|
||||||
*/
|
|
||||||
suspend fun getTVShowMetadata(crSeriesId: String): TVShowMeta? {
|
|
||||||
return if (mediaList.media.contains(crSeriesId)) {
|
|
||||||
metaCacheList.firstOrNull {
|
|
||||||
it.crSeriesId == crSeriesId
|
|
||||||
} as TVShowMeta? ?: getTVShowMetadataFromDB(crSeriesId)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun getTVShowMetadataFromDB(crSeriesId: String): TVShowMeta? = withContext(Dispatchers.IO) {
|
|
||||||
return@withContext try {
|
|
||||||
val raw: String = client.get("$repoUrl/tv/$crSeriesId/media.json").body()
|
|
||||||
val meta: TVShowMeta = Json.decodeFromString(raw)
|
|
||||||
metaCacheList.add(meta)
|
|
||||||
|
|
||||||
meta
|
|
||||||
} catch (ex: ClientRequestException) {
|
|
||||||
when (ex.response.status) {
|
|
||||||
HttpStatusCode.NotFound -> Log.w(TAG, "The requested file was not found. Series ID: $crSeriesId", ex)
|
|
||||||
else -> Log.e(TAG, "Error while requesting meta data. Series ID: $crSeriesId", ex)
|
|
||||||
}
|
|
||||||
|
|
||||||
null // todo return none object
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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.plugins.contentnegotiation.*
|
import io.ktor.client.features.json.*
|
||||||
|
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,11 +46,10 @@ 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(ContentNegotiation) {
|
install(JsonFeature) {
|
||||||
json(Json {
|
serializer = KotlinxSerializer(json)
|
||||||
ignoreUnknownKeys = true
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,7 +66,7 @@ class TMDBApiController {
|
||||||
): T = coroutineScope {
|
): T = coroutineScope {
|
||||||
val path = "$apiUrl$endpoint"
|
val path = "$apiUrl$endpoint"
|
||||||
val params = concatenate(
|
val params = concatenate(
|
||||||
listOf("api_key" to apiKey, "language" to Preferences.preferredSubtitleLocale.toLanguageTag()),
|
listOf("api_key" to apiKey, "language" to Preferences.preferredLocale.language),
|
||||||
parameters
|
parameters
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -79,7 +78,7 @@ class TMDBApiController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
response.body<T>()
|
response.receive<T>()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,7 +89,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/movie"
|
val searchEndpoint = "/search/multi"
|
||||||
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? // for movies tmdb return string or null
|
val name: String
|
||||||
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 genres
|
// TODO generes
|
||||||
) : TMDBResult
|
) : TMDBResult
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
@ -102,10 +102,10 @@ data class TMDBTVShow(
|
||||||
@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?,
|
||||||
@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 genres
|
// TODO generes
|
||||||
) : 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
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:color="?attr/colorPrimary" android:state_checked="true"/>
|
||||||
|
<item android:color="?attr/iconColor"/>
|
||||||
|
</selector>
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<item android:drawable="@android:color/black"/>
|
||||||
|
|
||||||
|
<item android:gravity="center" android:width="144dp" android:height="144dp">
|
||||||
|
<bitmap
|
||||||
|
android:gravity="fill_horizontal|fill_vertical"
|
||||||
|
android:src="@drawable/ic_splash_logo"/>
|
||||||
|
</item>
|
||||||
|
|
||||||
|
</layer-list>
|
|
@ -6,7 +6,7 @@
|
||||||
android:shape="ring"
|
android:shape="ring"
|
||||||
android:thickness="4dp"
|
android:thickness="4dp"
|
||||||
android:useLevel="false">
|
android:useLevel="false">
|
||||||
<solid android:color="?colorOutline"/>
|
<solid android:color="?iconColor"/>
|
||||||
</shape>
|
</shape>
|
||||||
</item>
|
</item>
|
||||||
</layer-list>
|
</layer-list>
|
|
@ -6,7 +6,7 @@
|
||||||
android:shape="ring"
|
android:shape="ring"
|
||||||
android:thickness="4dp"
|
android:thickness="4dp"
|
||||||
android:useLevel="false">
|
android:useLevel="false">
|
||||||
<solid android:color="?colorSecondary" />
|
<solid android:color="@color/colorAccent" />
|
||||||
</shape>
|
</shape>
|
||||||
</item>
|
</item>
|
||||||
</layer-list>
|
</layer-list>
|
|
@ -1,13 +1,6 @@
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||||
android:width="24dp"
|
android:viewportHeight="24" android:viewportWidth="24"
|
||||||
android:height="24dp"
|
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
android:viewportWidth="24"
|
<path android:fillColor="@android:color/white" android:pathData="M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z"/>
|
||||||
android:viewportHeight="24"
|
<path android:fillColor="@android:color/white" android:pathData="M12.5,7H11v6l5.25,3.15 0.75,-1.23 -4.5,-2.67z"/>
|
||||||
android:tint="?attr/colorControlNormal">
|
|
||||||
<path
|
|
||||||
android:fillColor="@android:color/white"
|
|
||||||
android:pathData="M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z"/>
|
|
||||||
<path
|
|
||||||
android:fillColor="@android:color/white"
|
|
||||||
android:pathData="M12.5,7H11v6l5.25,3.15 0.75,-1.23 -4.5,-2.67z" />
|
|
||||||
</vector>
|
</vector>
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
android:viewportWidth="24"
|
android:viewportWidth="24"
|
||||||
android:viewportHeight="24"
|
android:viewportHeight="24"
|
||||||
android:tint="?attr/colorControlNormal">
|
android:tint="?attr/colorControlNormal">
|
||||||
<path
|
<path
|
||||||
android:fillColor="@android:color/white"
|
android:fillColor="@android:color/white"
|
||||||
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
|
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
|
||||||
</vector>
|
</vector>
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24"
|
|
||||||
android:tint="?attr/colorControlNormal">
|
|
||||||
<path
|
|
||||||
android:fillColor="@android:color/white"
|
|
||||||
android:pathData="M12,3v9.28c-0.47,-0.17 -0.97,-0.28 -1.5,-0.28C8.01,12 6,14.01 6,16.5S8.01,21 10.5,21c2.31,0 4.2,-1.75 4.45,-4H15V6h4V3h-7z"/>
|
|
||||||
</vector>
|
|
|
@ -1,5 +0,0 @@
|
||||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
|
||||||
android:viewportHeight="24" android:viewportWidth="24"
|
|
||||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<path android:fillColor="@android:color/white" android:pathData="M17,3L7,3c-1.1,0 -1.99,0.9 -1.99,2L5,21l7,-3 7,3L19,5c0,-1.1 -0.9,-2 -2,-2zM17,18l-5,-2.18L7,18L7,5h10v13z"/>
|
|
||||||
</vector>
|
|
|
@ -1,10 +1,5 @@
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||||
android:width="24dp"
|
android:viewportHeight="24" android:viewportWidth="24"
|
||||||
android:height="24dp"
|
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
android:viewportWidth="24"
|
<path android:fillColor="@android:color/white" android:pathData="M9.4,16.6L4.8,12l4.6,-4.6L8,6l-6,6 6,6 1.4,-1.4zM14.6,16.6l4.6,-4.6 -4.6,-4.6L16,6l6,6 -6,6 -1.4,-1.4z"/>
|
||||||
android:viewportHeight="24"
|
|
||||||
android:tint="?attr/colorControlNormal">
|
|
||||||
<path
|
|
||||||
android:fillColor="@android:color/white"
|
|
||||||
android:pathData="M9.4,16.6L4.8,12l4.6,-4.6L8,6l-6,6 6,6 1.4,-1.4zM14.6,16.6l4.6,-4.6 -4.6,-4.6L16,6l6,6 -6,6 -1.4,-1.4z"/>
|
|
||||||
</vector>
|
</vector>
|
||||||
|
|
|
@ -1,10 +1,5 @@
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||||
android:width="24dp"
|
android:viewportHeight="24" android:viewportWidth="24"
|
||||||
android:height="24dp"
|
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
android:viewportWidth="24"
|
<path android:fillColor="@android:color/white" android:pathData="M14,2L6,2c-1.1,0 -1.99,0.9 -1.99,2L4,20c0,1.1 0.89,2 1.99,2L18,22c1.1,0 2,-0.9 2,-2L20,8l-6,-6zM16,18L8,18v-2h8v2zM16,14L8,14v-2h8v2zM13,9L13,3.5L18.5,9L13,9z"/>
|
||||||
android:viewportHeight="24"
|
|
||||||
android:tint="?attr/colorControlNormal">
|
|
||||||
<path
|
|
||||||
android:fillColor="@android:color/white"
|
|
||||||
android:pathData="M14,2L6,2c-1.1,0 -1.99,0.9 -1.99,2L4,20c0,1.1 0.89,2 1.99,2L18,22c1.1,0 2,-0.9 2,-2L20,8l-6,-6zM16,18L8,18v-2h8v2zM16,14L8,14v-2h8v2zM13,9L13,3.5L18.5,9L13,9z"/>
|
|
||||||
</vector>
|
</vector>
|
||||||
|
|
|
@ -1,10 +1,5 @@
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||||
android:width="24dp"
|
android:viewportHeight="24" android:viewportWidth="24"
|
||||||
android:height="24dp"
|
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
android:viewportWidth="24"
|
<path android:fillColor="@android:color/white" android:pathData="M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM18.92,8h-2.95c-0.32,-1.25 -0.78,-2.45 -1.38,-3.56 1.84,0.63 3.37,1.91 4.33,3.56zM12,4.04c0.83,1.2 1.48,2.53 1.91,3.96h-3.82c0.43,-1.43 1.08,-2.76 1.91,-3.96zM4.26,14C4.1,13.36 4,12.69 4,12s0.1,-1.36 0.26,-2h3.38c-0.08,0.66 -0.14,1.32 -0.14,2 0,0.68 0.06,1.34 0.14,2L4.26,14zM5.08,16h2.95c0.32,1.25 0.78,2.45 1.38,3.56 -1.84,-0.63 -3.37,-1.9 -4.33,-3.56zM8.03,8L5.08,8c0.96,-1.66 2.49,-2.93 4.33,-3.56C8.81,5.55 8.35,6.75 8.03,8zM12,19.96c-0.83,-1.2 -1.48,-2.53 -1.91,-3.96h3.82c-0.43,1.43 -1.08,2.76 -1.91,3.96zM14.34,14L9.66,14c-0.09,-0.66 -0.16,-1.32 -0.16,-2 0,-0.68 0.07,-1.35 0.16,-2h4.68c0.09,0.65 0.16,1.32 0.16,2 0,0.68 -0.07,1.34 -0.16,2zM14.59,19.56c0.6,-1.11 1.06,-2.31 1.38,-3.56h2.95c-0.96,1.65 -2.49,2.93 -4.33,3.56zM16.36,14c0.08,-0.66 0.14,-1.32 0.14,-2 0,-0.68 -0.06,-1.34 -0.14,-2h3.38c0.16,0.64 0.26,1.31 0.26,2s-0.1,1.36 -0.26,2h-3.38z"/>
|
||||||
android:viewportHeight="24"
|
|
||||||
android:tint="?attr/colorControlNormal">
|
|
||||||
<path
|
|
||||||
android:fillColor="@android:color/white"
|
|
||||||
android:pathData="M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM18.92,8h-2.95c-0.32,-1.25 -0.78,-2.45 -1.38,-3.56 1.84,0.63 3.37,1.91 4.33,3.56zM12,4.04c0.83,1.2 1.48,2.53 1.91,3.96h-3.82c0.43,-1.43 1.08,-2.76 1.91,-3.96zM4.26,14C4.1,13.36 4,12.69 4,12s0.1,-1.36 0.26,-2h3.38c-0.08,0.66 -0.14,1.32 -0.14,2 0,0.68 0.06,1.34 0.14,2L4.26,14zM5.08,16h2.95c0.32,1.25 0.78,2.45 1.38,3.56 -1.84,-0.63 -3.37,-1.9 -4.33,-3.56zM8.03,8L5.08,8c0.96,-1.66 2.49,-2.93 4.33,-3.56C8.81,5.55 8.35,6.75 8.03,8zM12,19.96c-0.83,-1.2 -1.48,-2.53 -1.91,-3.96h3.82c-0.43,1.43 -1.08,2.76 -1.91,3.96zM14.34,14L9.66,14c-0.09,-0.66 -0.16,-1.32 -0.16,-2 0,-0.68 0.07,-1.35 0.16,-2h4.68c0.09,0.65 0.16,1.32 0.16,2 0,0.68 -0.07,1.34 -0.16,2zM14.59,19.56c0.6,-1.11 1.06,-2.31 1.38,-3.56h2.95c-0.96,1.65 -2.49,2.93 -4.33,3.56zM16.36,14c0.08,-0.66 0.14,-1.32 0.14,-2 0,-0.68 -0.06,-1.34 -0.14,-2h3.38c0.16,0.64 0.26,1.31 0.26,2s-0.1,1.36 -0.26,2h-3.38z"/>
|
|
||||||
</vector>
|
</vector>
|
||||||
|
|
|
@ -1,8 +1,5 @@
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||||
android:width="24dp"
|
android:viewportHeight="24" android:viewportWidth="24"
|
||||||
android:height="24dp"
|
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24"
|
|
||||||
android:tint="?attr/colorControlNormal">
|
|
||||||
<path android:fillColor="@android:color/white" android:pathData="M16,11c1.66,0 2.99,-1.34 2.99,-3S17.66,5 16,5c-1.66,0 -3,1.34 -3,3s1.34,3 3,3zM8,11c1.66,0 2.99,-1.34 2.99,-3S9.66,5 8,5C6.34,5 5,6.34 5,8s1.34,3 3,3zM8,13c-2.33,0 -7,1.17 -7,3.5L1,19h14v-2.5c0,-2.33 -4.67,-3.5 -7,-3.5zM16,13c-0.29,0 -0.62,0.02 -0.97,0.05 1.16,0.84 1.97,1.97 1.97,3.45L17,19h6v-2.5c0,-2.33 -4.67,-3.5 -7,-3.5z"/>
|
<path android:fillColor="@android:color/white" android:pathData="M16,11c1.66,0 2.99,-1.34 2.99,-3S17.66,5 16,5c-1.66,0 -3,1.34 -3,3s1.34,3 3,3zM8,11c1.66,0 2.99,-1.34 2.99,-3S9.66,5 8,5C6.34,5 5,6.34 5,8s1.34,3 3,3zM8,13c-2.33,0 -7,1.17 -7,3.5L1,19h14v-2.5c0,-2.33 -4.67,-3.5 -7,-3.5zM16,13c-0.29,0 -0.62,0.02 -0.97,0.05 1.16,0.84 1.97,1.97 1.97,3.45L17,19h6v-2.5c0,-2.33 -4.67,-3.5 -7,-3.5z"/>
|
||||||
</vector>
|
</vector>
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
android:height="24dp"
|
android:height="24dp"
|
||||||
android:viewportWidth="24"
|
android:viewportWidth="24"
|
||||||
android:viewportHeight="24"
|
android:viewportHeight="24"
|
||||||
android:tint="?attr/colorControlNormal">
|
android:tint="?attr/iconColor">
|
||||||
<path
|
<path
|
||||||
android:fillColor="@android:color/white"
|
android:fillColor="@android:color/white"
|
||||||
android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z"/>
|
android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z"/>
|
||||||
|
|
|
@ -1,10 +1,5 @@
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||||
android:width="24dp"
|
android:viewportHeight="24" android:viewportWidth="24"
|
||||||
android:height="24dp"
|
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
android:viewportWidth="24"
|
<path android:fillColor="@android:color/white" android:pathData="M2.53,19.65l1.34,0.56v-9.03l-2.43,5.86c-0.41,1.02 0.08,2.19 1.09,2.61zM22.03,15.95L17.07,3.98c-0.31,-0.75 -1.04,-1.21 -1.81,-1.23 -0.26,0 -0.53,0.04 -0.79,0.15L7.1,5.95c-0.75,0.31 -1.21,1.03 -1.23,1.8 -0.01,0.27 0.04,0.54 0.15,0.8l4.96,11.97c0.31,0.76 1.05,1.22 1.83,1.23 0.26,0 0.52,-0.05 0.77,-0.15l7.36,-3.05c1.02,-0.42 1.51,-1.59 1.09,-2.6zM7.88,8.75c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1 1,0.45 1,1 -0.45,1 -1,1zM5.88,19.75c0,1.1 0.9,2 2,2h1.45l-3.45,-8.34v6.34z"/>
|
||||||
android:viewportHeight="24"
|
|
||||||
android:tint="?attr/colorControlNormal">
|
|
||||||
<path
|
|
||||||
android:fillColor="@android:color/white"
|
|
||||||
android:pathData="M2.53,19.65l1.34,0.56v-9.03l-2.43,5.86c-0.41,1.02 0.08,2.19 1.09,2.61zM22.03,15.95L17.07,3.98c-0.31,-0.75 -1.04,-1.21 -1.81,-1.23 -0.26,0 -0.53,0.04 -0.79,0.15L7.1,5.95c-0.75,0.31 -1.21,1.03 -1.23,1.8 -0.01,0.27 0.04,0.54 0.15,0.8l4.96,11.97c0.31,0.76 1.05,1.22 1.83,1.23 0.26,0 0.52,-0.05 0.77,-0.15l7.36,-3.05c1.02,-0.42 1.51,-1.59 1.09,-2.6zM7.88,8.75c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1 1,0.45 1,1 -0.45,1 -1,1zM5.88,19.75c0,1.1 0.9,2 2,2h1.45l-3.45,-8.34v6.34z"/>
|
|
||||||
</vector>
|
</vector>
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:width="24dp"
|
android:width="24dp"
|
||||||
android:height="24dp"
|
android:height="24dp"
|
||||||
|
android:tint="#FFFFFF"
|
||||||
android:viewportWidth="24"
|
android:viewportWidth="24"
|
||||||
android:viewportHeight="24"
|
android:viewportHeight="24">
|
||||||
android:tint="?attr/colorControlNormal">
|
|
||||||
<path
|
<path
|
||||||
android:fillColor="@android:color/white"
|
android:fillColor="@android:color/white"
|
||||||
android:pathData="M11,7h2v2h-2zM11,11h2v6h-2zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8z" />
|
android:pathData="M11,7h2v2h-2zM11,11h2v6h-2zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8z" />
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="108dp"
|
|
||||||
android:height="108dp"
|
|
||||||
android:viewportWidth="108"
|
|
||||||
android:viewportHeight="108">
|
|
||||||
<group android:scaleX="0.03158203"
|
|
||||||
android:scaleY="0.03158203"
|
|
||||||
android:translateX="37.83"
|
|
||||||
android:translateY="44.778053">
|
|
||||||
<path
|
|
||||||
android:pathData="m850.19,372.71c87.88,-11.01 119.04,-84.97 123.1,-99.87 4.06,-14.89 24.91,-80.57 11.92,-129.36 -12.99,-48.79 -34.36,-72.36 -58.62,-77.25 -24.25,-4.9 -50.59,10.51 -65,32.81 -14.41,22.3 -14.68,45.14 -14.78,55.29 -0.11,10.15 0.76,23.2 -3.37,33.29 -4.13,10.09 3.23,25.71 6.04,35.23 2.81,9.52 9.67,82.62 5.78,115.57 -3.89,32.95 -5.07,34.29 -5.07,34.29zM0.4,23.58C55.81,77.29 56.45,120.86 56.08,132.92c-0.36,12.06 4.77,130.59 11.47,150.76 4.42,13.3 12.11,50.16 41.78,74.48 25.51,20.91 58.65,31.38 58.65,31.38 0,0 36.42,78.46 78.83,108.64 31.56,22.46 39.61,23.74 46.5,35.55 6.18,10.6 93.56,62.62 275.1,47.23 127.29,-10.79 138.56,-44.3 138.56,-44.3 0,0 49.41,-21.9 101.15,-80.43 12.87,-14.56 4.41,-13.21 28.57,-17.79 24.16,-4.58 138.01,-45.58 170.66,-154.36C1039.99,175.32 1017.81,96.01 994.52,69.12 971.23,42.22 931.6,24.18 912.25,24.93c-18.47,0.71 -44.78,4.24 -80.21,46.87 -35.43,42.62 -28.94,37.4 -39.36,41.73 -6.82,2.83 -5.68,3.91 -26.75,-11.65 -20.23,-14.93 -28.9,-21.24 -43.38,-27.24 -7.96,-3.3 2.05,-5.55 2.59,-19.48 0.54,-13.93 2.4,-23.51 -17.32,-23.77 -19.72,-0.26 -408.02,0.21 -408.02,0.21 0,0 -18.8,-1.29 -7.79,24.82 4.2,9.94 -1.45,6.43 -33.27,25.85 -31.82,19.42 -55.58,34.4 -72.28,66.09 -8.43,16 -22.91,23.02 -27.97,8.05C153.44,141.43 125.2,48.96 105.17,23.22 85.56,-1.97 77.8,0.26 77.8,0.26Z"
|
|
||||||
android:strokeLineJoin="miter"
|
|
||||||
android:strokeWidth="0.41878"
|
|
||||||
android:fillColor="#000000"
|
|
||||||
android:strokeColor="#000000"
|
|
||||||
android:fillType="evenOdd"
|
|
||||||
android:strokeLineCap="butt"/>
|
|
||||||
</group>
|
|
||||||
</vector>
|
|
After Width: | Height: | Size: 10 KiB |
|
@ -1,7 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<solid android:color="?colorSurfaceVariant"/>
|
|
||||||
<size
|
|
||||||
android:width="1920px"
|
|
||||||
android:height="1080px"/>
|
|
||||||
</shape>
|
|
|
@ -1,7 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<solid android:color="?colorSurfaceVariant"/>
|
|
||||||
<size
|
|
||||||
android:width="400px"
|
|
||||||
android:height="600px"/>
|
|
||||||
</shape>
|
|
|
@ -1,5 +1,5 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<solid android:color="?colorSurfaceVariant"/>
|
<solid android:color="?attr/shapeTextBackground"/>
|
||||||
<corners android:radius="3dp"/>
|
<corners android:radius="3dp"/>
|
||||||
</shape>
|
</shape>
|
|
@ -9,6 +9,8 @@
|
||||||
android:id="@+id/nav_view"
|
android:id="@+id/nav_view"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?themeSecondary"
|
||||||
|
app:itemIconTint="@color/bottom_nav_item_tint"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
app:layout_constraintRight_toRightOf="parent"
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:id="@+id/player_root"
|
android:id="@+id/player_layout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:background="#000000"
|
android:background="#000000"
|
||||||
|
@ -24,7 +24,7 @@
|
||||||
android:layout_height="70dp"
|
android:layout_height="70dp"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
android:indeterminate="true"
|
android:indeterminate="true"
|
||||||
app:indicatorColor="@color/player_white"
|
app:indicatorColor="@color/exo_white"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
@ -77,14 +77,14 @@
|
||||||
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="72dp"
|
android:layout_marginBottom="70dp"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:text="@string/next_episode"
|
android:text="@string/next_episode"
|
||||||
android:textAllCaps="false"
|
android:textAllCaps="false"
|
||||||
android:textColor="@android:color/primary_text_light"
|
android:textColor="@android:color/primary_text_light"
|
||||||
android:textSize="16sp"
|
android:textSize="16sp"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:backgroundTint="@color/player_white"
|
app:backgroundTint="@color/exo_white"
|
||||||
app:iconGravity="textStart" />
|
app:iconGravity="textStart" />
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
|
@ -93,14 +93,14 @@
|
||||||
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="72dp"
|
android:layout_marginBottom="70dp"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:text="@string/skip_opening"
|
android:text="@string/skip_opening"
|
||||||
android:textAllCaps="false"
|
android:textAllCaps="false"
|
||||||
android:textColor="@android:color/primary_text_light"
|
android:textColor="@android:color/primary_text_light"
|
||||||
android:textSize="16sp"
|
android:textSize="16sp"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:backgroundTint="@color/player_white"
|
app:backgroundTint="@color/exo_white"
|
||||||
app:iconGravity="textStart" />
|
app:iconGravity="textStart" />
|
||||||
|
|
||||||
</FrameLayout>
|
</FrameLayout>
|
|
@ -19,6 +19,6 @@
|
||||||
android:layout_centerInParent="true"
|
android:layout_centerInParent="true"
|
||||||
android:layout_marginStart="42dp"
|
android:layout_marginStart="42dp"
|
||||||
android:text="@string/fwd_10_s"
|
android:text="@string/fwd_10_s"
|
||||||
android:textColor="@color/player_white"
|
android:textColor="@color/exo_white"
|
||||||
android:visibility="gone" />
|
android:visibility="gone" />
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
|
@ -20,7 +20,7 @@
|
||||||
android:layout_centerInParent="true"
|
android:layout_centerInParent="true"
|
||||||
android:layout_marginEnd="42dp"
|
android:layout_marginEnd="42dp"
|
||||||
android:text="@string/rwd_10_s"
|
android:text="@string/rwd_10_s"
|
||||||
android:textColor="@color/player_white"
|
android:textColor="@color/exo_white"
|
||||||
android:visibility="gone" />
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:id="@+id/linLayout_login"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingStart="24dp"
|
||||||
|
android:paddingEnd="24dp">
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/edit_text_login"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="7dp"
|
||||||
|
android:ems="10"
|
||||||
|
android:hint="@string/login"
|
||||||
|
android:importantForAutofill="no"
|
||||||
|
android:inputType="textEmailAddress" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/edit_text_password"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="7dp"
|
||||||
|
android:ems="10"
|
||||||
|
android:hint="@string/password"
|
||||||
|
android:importantForAutofill="no"
|
||||||
|
android:inputType="textPassword" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
|
@ -1,8 +1,11 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.core.widget.NestedScrollView
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns: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="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
android:background="?themePrimary"
|
||||||
tools:context=".ui.activity.main.fragments.AboutFragment">
|
tools:context=".ui.activity.main.fragments.AboutFragment">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
@ -64,7 +67,8 @@
|
||||||
android:minHeight="48dp"
|
android:minHeight="48dp"
|
||||||
android:padding="9dp"
|
android:padding="9dp"
|
||||||
android:scaleType="fitXY"
|
android:scaleType="fitXY"
|
||||||
android:src="@drawable/ic_outline_info_24" />
|
android:src="@drawable/ic_outline_info_24"
|
||||||
|
app:tint="?iconColor" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
@ -85,7 +89,8 @@
|
||||||
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/version_desc" />
|
android:text="@string/version_desc"
|
||||||
|
android:textColor="?textSecondary" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
@ -107,7 +112,8 @@
|
||||||
android:minHeight="48dp"
|
android:minHeight="48dp"
|
||||||
android:padding="9dp"
|
android:padding="9dp"
|
||||||
android:scaleType="fitXY"
|
android:scaleType="fitXY"
|
||||||
android:src="@drawable/ic_baseline_people_24" />
|
android:src="@drawable/ic_baseline_people_24"
|
||||||
|
app:tint="?iconColor" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
@ -128,7 +134,8 @@
|
||||||
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/author_desc" />
|
android:text="@string/author_desc"
|
||||||
|
android:textColor="?textSecondary" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
@ -150,7 +157,8 @@
|
||||||
android:minHeight="48dp"
|
android:minHeight="48dp"
|
||||||
android:padding="9dp"
|
android:padding="9dp"
|
||||||
android:scaleType="fitXY"
|
android:scaleType="fitXY"
|
||||||
android:src="@drawable/ic_baseline_code_24" />
|
android:src="@drawable/ic_baseline_code_24"
|
||||||
|
app:tint="?iconColor" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
@ -171,7 +179,8 @@
|
||||||
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/teapod_repo" />
|
android:text="@string/teapod_repo"
|
||||||
|
android:textColor="?textSecondary" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
@ -193,7 +202,8 @@
|
||||||
android:minHeight="48dp"
|
android:minHeight="48dp"
|
||||||
android:padding="9dp"
|
android:padding="9dp"
|
||||||
android:scaleType="fitXY"
|
android:scaleType="fitXY"
|
||||||
android:src="@drawable/ic_baseline_description_24" />
|
android:src="@drawable/ic_baseline_description_24"
|
||||||
|
app:tint="?iconColor" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
@ -214,7 +224,8 @@
|
||||||
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/license_desc" />
|
android:text="@string/license_desc"
|
||||||
|
android:textColor="?textSecondary" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
@ -256,7 +267,8 @@
|
||||||
android:layout_marginEnd="7dp"
|
android:layout_marginEnd="7dp"
|
||||||
android:paddingBottom="5dp"
|
android:paddingBottom="5dp"
|
||||||
android:text="@string/tmdb_notice"
|
android:text="@string/tmdb_notice"
|
||||||
android:textAlignment="center" />
|
android:textAlignment="center"
|
||||||
|
android:textColor="?textSecondary" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</androidx.core.widget.NestedScrollView>
|
</androidx.core.widget.NestedScrollView>
|
|
@ -4,12 +4,12 @@
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
android:background="?themePrimary"
|
||||||
tools:context=".ui.activity.main.fragments.AccountFragment">
|
tools:context=".ui.activity.main.fragments.AccountFragment">
|
||||||
|
|
||||||
<ScrollView
|
<ScrollView
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent">
|
||||||
android:scrollbars="none">
|
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
@ -23,6 +23,7 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="12dp"
|
android:layout_marginTop="12dp"
|
||||||
|
android:background="?themeSecondary"
|
||||||
android:elevation="5dp"
|
android:elevation="5dp"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
@ -33,7 +34,7 @@
|
||||||
android:paddingStart="7dp"
|
android:paddingStart="7dp"
|
||||||
android:paddingEnd="7dp"
|
android:paddingEnd="7dp"
|
||||||
android:text="@string/account"
|
android:text="@string/account"
|
||||||
android:textAppearance="@style/TextAppearance.Material3.TitleMedium"
|
android:textSize="16sp"
|
||||||
android:textStyle="bold" />
|
android:textStyle="bold" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
@ -54,7 +55,8 @@
|
||||||
android:minHeight="48dp"
|
android:minHeight="48dp"
|
||||||
android:padding="9dp"
|
android:padding="9dp"
|
||||||
android:scaleType="fitXY"
|
android:scaleType="fitXY"
|
||||||
android:src="@drawable/ic_baseline_account_box_24" />
|
android:src="@drawable/ic_baseline_account_box_24"
|
||||||
|
app:tint="?iconColor" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
@ -67,14 +69,15 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:text="@string/account_login_ex"
|
android:text="@string/account_login_ex"
|
||||||
android:textAppearance="@style/TextAppearance.Material3.BodyLarge" />
|
android:textSize="16sp" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/text_account_login_desc"
|
android:id="@+id/text_account_login_desc"
|
||||||
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_login_desc" />
|
android:text="@string/account_login_desc"
|
||||||
|
android:textColor="?textSecondary" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
@ -96,7 +99,8 @@
|
||||||
android:minHeight="48dp"
|
android:minHeight="48dp"
|
||||||
android:padding="9dp"
|
android:padding="9dp"
|
||||||
android:scaleType="fitXY"
|
android:scaleType="fitXY"
|
||||||
android:src="@drawable/ic_baseline_access_time_24" />
|
android:src="@drawable/ic_baseline_access_time_24"
|
||||||
|
app:tint="?iconColor" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
@ -108,15 +112,16 @@
|
||||||
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/loading"
|
android:text="@string/account_subscription"
|
||||||
android:textAppearance="@style/TextAppearance.Material3.BodyLarge" />
|
android:textSize="16sp" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/text_account_subscription_desc"
|
android:id="@+id/text_account_subscription_desc"
|
||||||
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_tier" />
|
android:text="@string/account_subscription_desc"
|
||||||
|
android:textColor="?textSecondary" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
@ -127,6 +132,7 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="12dp"
|
android:layout_marginTop="12dp"
|
||||||
|
android:background="?themeSecondary"
|
||||||
android:elevation="5dp"
|
android:elevation="5dp"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
@ -137,11 +143,11 @@
|
||||||
android:paddingStart="7dp"
|
android:paddingStart="7dp"
|
||||||
android:paddingEnd="7dp"
|
android:paddingEnd="7dp"
|
||||||
android:text="@string/settings"
|
android:text="@string/settings"
|
||||||
android:textAppearance="@style/TextAppearance.Material3.TitleMedium"
|
android:textSize="16sp"
|
||||||
android:textStyle="bold" />
|
android:textStyle="bold" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/linear_settings_audio_language"
|
android:id="@+id/linear_settings_content_language"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
|
@ -151,12 +157,13 @@
|
||||||
android:id="@+id/imageView4"
|
android:id="@+id/imageView4"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:contentDescription="@string/settings_audio_language"
|
android:contentDescription="@string/settings_content_language"
|
||||||
android:minWidth="48dp"
|
android:minWidth="48dp"
|
||||||
android:minHeight="48dp"
|
android:minHeight="48dp"
|
||||||
android:padding="9dp"
|
android:padding="9dp"
|
||||||
android:scaleType="fitXY"
|
android:scaleType="fitXY"
|
||||||
android:src="@drawable/ic_baseline_audiotrack_24" />
|
android:src="@drawable/ic_baseline_language_24"
|
||||||
|
app:tint="?iconColor" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
@ -167,53 +174,81 @@
|
||||||
android:id="@+id/text_settings_content_language"
|
android:id="@+id/text_settings_content_language"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/settings_audio_language"
|
android:text="@string/settings_content_language"
|
||||||
android:textAppearance="@style/TextAppearance.Material3.BodyLarge" />
|
android:textSize="16sp" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/text_settings_audio_language_desc"
|
android:id="@+id/text_settings_content_language_desc"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/settings_content_language_desc" />
|
android:text="@string/settings_content_language_desc"
|
||||||
|
android:textColor="?textSecondary" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/linear_settings_subtitle_language"
|
android:id="@+id/linear_settings_secondary"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="center"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:padding="7dp">
|
android:padding="7dp">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/imageView7"
|
android:id="@+id/imageView3"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:contentDescription="@string/settings_subtitle_language"
|
android:contentDescription="@string/settings_prefer_subbed"
|
||||||
android:minWidth="48dp"
|
android:minWidth="48dp"
|
||||||
android:minHeight="48dp"
|
android:minHeight="48dp"
|
||||||
android:padding="9dp"
|
android:padding="9dp"
|
||||||
android:scaleType="fitXY"
|
android:scaleType="fitXY"
|
||||||
android:src="@drawable/ic_baseline_subtitles_24" />
|
android:src="@drawable/ic_baseline_subtitles_24"
|
||||||
|
app:tint="?iconColor" />
|
||||||
|
|
||||||
<LinearLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="wrap_content">
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<TextView
|
<LinearLayout
|
||||||
android:id="@+id/text_settings_subtitle_language"
|
android:id="@+id/linearLayout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/settings_subtitle_language"
|
android:orientation="vertical"
|
||||||
android:textAppearance="@style/TextAppearance.Material3.BodyLarge" />
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/switch_secondary"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/text_settings_subtitle_language_desc"
|
android:id="@+id/text_settings_secondary"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/settings_prefer_subbed"
|
||||||
|
android:textSize="16sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_settings_secondary_desc"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:maxLines="2"
|
||||||
|
android:text="@string/settings_prefer_subbed_desc"
|
||||||
|
android:textColor="?textSecondary" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||||
|
android:id="@+id/switch_secondary"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/settings_content_language_desc" />
|
android:checked="true"
|
||||||
</LinearLayout>
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
@ -232,7 +267,8 @@
|
||||||
android:minWidth="48dp"
|
android:minWidth="48dp"
|
||||||
android:minHeight="48dp"
|
android:minHeight="48dp"
|
||||||
android:padding="9dp"
|
android:padding="9dp"
|
||||||
android:src="@drawable/ic_baseline_autorenew_24" />
|
android:src="@drawable/ic_baseline_autorenew_24"
|
||||||
|
app:tint="?iconColor" />
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
@ -253,13 +289,14 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/settings_autoplay"
|
android:text="@string/settings_autoplay"
|
||||||
android:textAppearance="@style/TextAppearance.Material3.BodyLarge" />
|
android:textSize="16sp" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/text_settings_auoplay_desc"
|
android:id="@+id/text_settings_auoplay_desc"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/settings_autoplay_desc" />
|
android:text="@string/settings_autoplay_desc"
|
||||||
|
android:textColor="?textSecondary" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||||
|
@ -267,7 +304,6 @@
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:checked="true"
|
android:checked="true"
|
||||||
android:contentDescription="@string/settings_autoplay"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
@ -293,7 +329,8 @@
|
||||||
android:minHeight="48dp"
|
android:minHeight="48dp"
|
||||||
android:padding="9dp"
|
android:padding="9dp"
|
||||||
android:scaleType="fitXY"
|
android:scaleType="fitXY"
|
||||||
android:src="@drawable/ic_baseline_style_24" />
|
android:src="@drawable/ic_baseline_style_24"
|
||||||
|
app:tint="?iconColor" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
@ -306,14 +343,15 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:text="@string/theme"
|
android:text="@string/theme"
|
||||||
android:textAppearance="@style/TextAppearance.Material3.BodyLarge" />
|
android:textSize="16sp" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/text_theme_selected"
|
android:id="@+id/text_theme_selected"
|
||||||
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/theme_light" />
|
android:text="@string/theme_light"
|
||||||
|
android:textColor="?textSecondary" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
@ -325,6 +363,7 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="12dp"
|
android:layout_marginTop="12dp"
|
||||||
|
android:background="?themeSecondary"
|
||||||
android:clipToPadding="false"
|
android:clipToPadding="false"
|
||||||
android:elevation="5dp"
|
android:elevation="5dp"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
@ -336,70 +375,9 @@
|
||||||
android:paddingStart="7dp"
|
android:paddingStart="7dp"
|
||||||
android:paddingEnd="7dp"
|
android:paddingEnd="7dp"
|
||||||
android:text="@string/dev_settings"
|
android:text="@string/dev_settings"
|
||||||
android:textAppearance="@style/TextAppearance.Material3.TitleMedium"
|
android:textSize="16sp"
|
||||||
android:textStyle="bold" />
|
android:textStyle="bold" />
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/linear_update_playhead"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:gravity="center"
|
|
||||||
android:orientation="horizontal"
|
|
||||||
android:padding="7dp">
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/imageView5"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:contentDescription="@string/update_playhead"
|
|
||||||
android:minWidth="48dp"
|
|
||||||
android:minHeight="48dp"
|
|
||||||
android:padding="9dp"
|
|
||||||
android:scaleType="fitXY"
|
|
||||||
android:src="@drawable/ic_baseline_access_time_24" />
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content">
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/linearLayout4"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:orientation="vertical"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toStartOf="@+id/switch_update_playhead"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/text_update_playhead"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/update_playhead"
|
|
||||||
android:textAppearance="@style/TextAppearance.Material3.BodyLarge" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/text_update_playhead_desc"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/update_playhead_desc" />
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
|
||||||
android:id="@+id/switch_update_playhead"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:checked="true"
|
|
||||||
android:contentDescription="@string/update_playhead"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/linear_export_data"
|
android:id="@+id/linear_export_data"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
@ -407,8 +385,7 @@
|
||||||
android:foreground="?android:selectableItemBackground"
|
android:foreground="?android:selectableItemBackground"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:padding="7dp"
|
android:padding="7dp">
|
||||||
android:visibility="gone">
|
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/image_export_data"
|
android:id="@+id/image_export_data"
|
||||||
|
@ -419,7 +396,8 @@
|
||||||
android:minHeight="48dp"
|
android:minHeight="48dp"
|
||||||
android:padding="9dp"
|
android:padding="9dp"
|
||||||
android:scaleType="fitXY"
|
android:scaleType="fitXY"
|
||||||
app:srcCompat="@drawable/ic_outline_upload_24" />
|
app:srcCompat="@drawable/ic_outline_upload_24"
|
||||||
|
app:tint="?iconColor" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
@ -439,7 +417,8 @@
|
||||||
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/export_data_desc" />
|
android:text="@string/export_data_desc"
|
||||||
|
android:textColor="?textSecondary" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
@ -451,8 +430,7 @@
|
||||||
android:foreground="?android:selectableItemBackground"
|
android:foreground="?android:selectableItemBackground"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:padding="7dp"
|
android:padding="7dp">
|
||||||
android:visibility="gone">
|
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/image_import_data"
|
android:id="@+id/image_import_data"
|
||||||
|
@ -463,7 +441,8 @@
|
||||||
android:minHeight="48dp"
|
android:minHeight="48dp"
|
||||||
android:padding="9dp"
|
android:padding="9dp"
|
||||||
android:scaleType="fitXY"
|
android:scaleType="fitXY"
|
||||||
app:srcCompat="@drawable/ic_outline_download_24" />
|
app:srcCompat="@drawable/ic_outline_download_24"
|
||||||
|
app:tint="?iconColor" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
@ -483,7 +462,8 @@
|
||||||
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/import_data_desc" />
|
android:text="@string/import_data_desc"
|
||||||
|
android:textColor="?textSecondary" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
@ -495,6 +475,7 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="12dp"
|
android:layout_marginTop="12dp"
|
||||||
|
android:background="?themeSecondary"
|
||||||
android:clipToPadding="false"
|
android:clipToPadding="false"
|
||||||
android:elevation="5dp"
|
android:elevation="5dp"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
@ -506,7 +487,7 @@
|
||||||
android:paddingStart="7dp"
|
android:paddingStart="7dp"
|
||||||
android:paddingEnd="7dp"
|
android:paddingEnd="7dp"
|
||||||
android:text="@string/info"
|
android:text="@string/info"
|
||||||
android:textAppearance="@style/TextAppearance.Material3.TitleMedium"
|
android:textSize="16sp"
|
||||||
android:textStyle="bold" />
|
android:textStyle="bold" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
@ -527,7 +508,8 @@
|
||||||
android:minHeight="48dp"
|
android:minHeight="48dp"
|
||||||
android:padding="9dp"
|
android:padding="9dp"
|
||||||
android:scaleType="fitXY"
|
android:scaleType="fitXY"
|
||||||
app:srcCompat="@drawable/ic_outline_info_24" />
|
app:srcCompat="@drawable/ic_outline_info_24"
|
||||||
|
app:tint="?iconColor" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
@ -540,14 +522,15 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:text="@string/info_about"
|
android:text="@string/info_about"
|
||||||
android:textAppearance="@style/TextAppearance.Material3.BodyLarge" />
|
android:textSize="16sp" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/text_info_about_desc"
|
android:id="@+id/text_info_about_desc"
|
||||||
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/info_about_desc" />
|
android:text="@string/info_about_desc"
|
||||||
|
android:textColor="?textSecondary" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
|
@ -5,29 +5,18 @@
|
||||||
android:id="@+id/ff_test"
|
android:id="@+id/ff_test"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
android:background="?themePrimary"
|
||||||
tools:context=".ui.activity.main.fragments.HomeFragment">
|
tools:context=".ui.activity.main.fragments.HomeFragment">
|
||||||
|
|
||||||
<ScrollView
|
<ScrollView
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent">
|
||||||
android:scrollbars="none">
|
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginBottom="7dp"
|
|
||||||
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"
|
||||||
|
@ -70,7 +59,9 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:text="@string/my_list"
|
android:text="@string/my_list"
|
||||||
|
android:textColor="?textSecondary"
|
||||||
android:textSize="12sp"
|
android:textSize="12sp"
|
||||||
|
app:drawableTint="?buttonBackground"
|
||||||
app:drawableTopCompat="@drawable/ic_baseline_add_24" />
|
app:drawableTopCompat="@drawable/ic_baseline_add_24" />
|
||||||
|
|
||||||
<Space
|
<Space
|
||||||
|
@ -85,9 +76,12 @@
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:text="@string/button_play"
|
android:text="@string/button_play"
|
||||||
android:textAllCaps="false"
|
android:textAllCaps="false"
|
||||||
|
android:textColor="?themePrimary"
|
||||||
android:textSize="16sp"
|
android:textSize="16sp"
|
||||||
|
app:backgroundTint="?buttonBackground"
|
||||||
app:icon="@drawable/ic_baseline_play_arrow_24"
|
app:icon="@drawable/ic_baseline_play_arrow_24"
|
||||||
app:iconGravity="textStart" />
|
app:iconGravity="textStart"
|
||||||
|
app:iconTint="?themePrimary" />
|
||||||
|
|
||||||
<Space
|
<Space
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
|
@ -100,7 +94,9 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:text="@string/info"
|
android:text="@string/info"
|
||||||
|
android:textColor="?textSecondary"
|
||||||
android:textSize="12sp"
|
android:textSize="12sp"
|
||||||
|
app:drawableTint="?buttonBackground"
|
||||||
app:drawableTopCompat="@drawable/ic_outline_info_24" />
|
app:drawableTopCompat="@drawable/ic_outline_info_24" />
|
||||||
|
|
||||||
<Space
|
<Space
|
||||||
|
@ -114,11 +110,12 @@
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/linear_up_next"
|
android:id="@+id/linear_up_next"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="match_parent"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical"
|
||||||
|
android:paddingBottom="7dp">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/text_up_next"
|
android:id="@+id/text_new_episodes"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:paddingStart="10dp"
|
android:paddingStart="10dp"
|
||||||
|
@ -129,30 +126,10 @@
|
||||||
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="wrap_content"
|
|
||||||
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" />
|
|
||||||
<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_new_episodes"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="match_parent"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||||
tools:listitem="@layout/item_media" />
|
tools:listitem="@layout/item_media" />
|
||||||
|
@ -162,7 +139,8 @@
|
||||||
android:id="@+id/linear_watchlist"
|
android:id="@+id/linear_watchlist"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical"
|
||||||
|
android:paddingBottom="7dp">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/text_watchlist"
|
android:id="@+id/text_watchlist"
|
||||||
|
@ -176,77 +154,9 @@
|
||||||
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" />
|
|
||||||
<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"
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="horizontal"
|
|
||||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
|
||||||
tools:listitem="@layout/item_media" />
|
|
||||||
</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" />
|
|
||||||
<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:layout_height="match_parent"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||||
|
@ -257,7 +167,8 @@
|
||||||
android:id="@+id/linear_new_titles"
|
android:id="@+id/linear_new_titles"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical"
|
||||||
|
android:paddingBottom="7dp">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/text_new_titles"
|
android:id="@+id/text_new_titles"
|
||||||
|
@ -271,26 +182,6 @@
|
||||||
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" />
|
|
||||||
<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"
|
||||||
|
@ -304,7 +195,8 @@
|
||||||
android:id="@+id/linear_top_ten"
|
android:id="@+id/linear_top_ten"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical"
|
||||||
|
android:paddingBottom="7dp">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/text_top_ten"
|
android:id="@+id/text_top_ten"
|
||||||
|
@ -318,26 +210,6 @@
|
||||||
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" />
|
|
||||||
<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"
|
||||||
|
|
|
@ -4,35 +4,22 @@
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
android:background="?themePrimary"
|
||||||
tools:context=".ui.activity.main.fragments.LibraryFragment">
|
tools:context=".ui.activity.main.fragments.LibraryFragment">
|
||||||
|
|
||||||
<org.mosad.teapod.ui.components.EmptySubmitSearchView
|
|
||||||
android:id="@+id/search_text"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:elevation="8dp"
|
|
||||||
android:iconifiedByDefault="false"
|
|
||||||
android:paddingBottom="5dp"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent">
|
|
||||||
</org.mosad.teapod.ui.components.EmptySubmitSearchView>
|
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/recycler_media_search"
|
android:id="@+id/recycler_media_library"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="0dp"
|
android:layout_height="match_parent"
|
||||||
android:clipToPadding="false"
|
android:clipToPadding="false"
|
||||||
android:orientation="vertical"
|
|
||||||
android:padding="3dp"
|
android:padding="3dp"
|
||||||
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
|
android:orientation="vertical"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/search_text"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:spanCount="@integer/item_media_columns"
|
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
|
||||||
tools:listitem="@layout/item_media">
|
app:spanCount="2"
|
||||||
|
tools:listitem="@layout/item_media" />
|
||||||
</androidx.recyclerview.widget.RecyclerView>
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -4,6 +4,7 @@
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
android:background="?themePrimary"
|
||||||
tools:context=".ui.activity.main.fragments.MediaFragment">
|
tools:context=".ui.activity.main.fragments.MediaFragment">
|
||||||
|
|
||||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
@ -13,7 +14,8 @@
|
||||||
<com.google.android.material.appbar.AppBarLayout
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
android:id="@+id/app_layout"
|
android:id="@+id/app_layout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?themePrimary">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/linear_media"
|
android:id="@+id/linear_media"
|
||||||
|
@ -22,42 +24,29 @@
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
app:layout_scrollFlags="scroll">
|
app:layout_scrollFlags="scroll">
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<RelativeLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
<FrameLayout
|
<ImageView
|
||||||
android:id="@+id/frame_image_progress"
|
android:id="@+id/image_backdrop"
|
||||||
android:layout_width="0dp"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="0dp"
|
android:layout_height="wrap_content"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
android:adjustViewBounds="false"
|
||||||
app:layout_constraintDimensionRatio="H,16:9"
|
android:contentDescription="@string/media_poster_backdrop_desc"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
android:maxHeight="231dp"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
android:minHeight="220dp"
|
||||||
app:layout_constraintTop_toTopOf="parent">
|
android:scaleType="centerCrop" />
|
||||||
|
|
||||||
<ImageView
|
<com.google.android.material.imageview.ShapeableImageView
|
||||||
android:id="@+id/image_backdrop"
|
android:id="@+id/image_poster"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="200dp"
|
||||||
android:contentDescription="@string/media_poster_backdrop_desc"
|
android:layout_centerInParent="true"
|
||||||
android:scaleType="fitCenter"
|
app:shapeAppearance="@style/ShapeAppearance.Teapod.RoundedPoster"
|
||||||
tools:srcCompat="@android:color/darker_gray" />
|
tools:src="@drawable/ic_launcher_background" />
|
||||||
|
|
||||||
<com.google.android.material.imageview.ShapeableImageView
|
</RelativeLayout>
|
||||||
android:id="@+id/image_poster"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layout_gravity="center_horizontal"
|
|
||||||
android:layout_marginTop="7dp"
|
|
||||||
android:layout_marginBottom="7dp"
|
|
||||||
android:scaleType="fitCenter"
|
|
||||||
app:shapeAppearance="@style/ShapeAppearance.Teapod.RoundedPoster"
|
|
||||||
tools:src="@drawable/placeholder_image_2_3" />
|
|
||||||
|
|
||||||
</FrameLayout>
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/linear_media_info"
|
android:id="@+id/linear_media_info"
|
||||||
|
@ -106,9 +95,12 @@
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:text="@string/button_play"
|
android:text="@string/button_play"
|
||||||
android:textAllCaps="false"
|
android:textAllCaps="false"
|
||||||
|
android:textColor="?themePrimary"
|
||||||
android:textSize="16sp"
|
android:textSize="16sp"
|
||||||
|
app:backgroundTint="?buttonBackground"
|
||||||
app:icon="@drawable/ic_baseline_play_arrow_24"
|
app:icon="@drawable/ic_baseline_play_arrow_24"
|
||||||
app:iconGravity="textStart" />
|
app:iconGravity="textStart"
|
||||||
|
app:iconTint="?themePrimary" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/text_title"
|
android:id="@+id/text_title"
|
||||||
|
@ -158,13 +150,15 @@
|
||||||
android:paddingTop="11dp"
|
android:paddingTop="11dp"
|
||||||
android:paddingEnd="11dp"
|
android:paddingEnd="11dp"
|
||||||
android:paddingBottom="7dp"
|
android:paddingBottom="7dp"
|
||||||
android:src="@drawable/ic_baseline_add_24" />
|
android:src="@drawable/ic_baseline_add_24"
|
||||||
|
app:tint="?buttonBackground" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/text_my_list_action"
|
android:id="@+id/text_my_list_action"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/my_list"
|
android:text="@string/my_list"
|
||||||
|
android:textColor="?textSecondary"
|
||||||
android:textSize="12sp" />
|
android:textSize="12sp" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
@ -179,7 +173,9 @@
|
||||||
android:layout_marginEnd="7dp"
|
android:layout_marginEnd="7dp"
|
||||||
android:background="@android:color/transparent"
|
android:background="@android:color/transparent"
|
||||||
app:tabGravity="start"
|
app:tabGravity="start"
|
||||||
app:tabMode="scrollable" />
|
app:tabMode="scrollable"
|
||||||
|
app:tabSelectedTextColor="?textPrimary"
|
||||||
|
app:tabTextColor="?textSecondary" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</com.google.android.material.appbar.AppBarLayout>
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
@ -198,7 +194,7 @@
|
||||||
android:id="@+id/frame_loading"
|
android:id="@+id/frame_loading"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:background="?android:colorBackground"
|
android:background="?themePrimary"
|
||||||
android:visibility="gone">
|
android:visibility="gone">
|
||||||
|
|
||||||
<com.google.android.material.progressindicator.CircularProgressIndicator
|
<com.google.android.material.progressindicator.CircularProgressIndicator
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<LinearLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:id="@+id/linear_episodes"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
android:paddingEnd="3dp"
|
android:paddingEnd="3dp"
|
||||||
android:paddingBottom="3dp"
|
android:paddingBottom="3dp"
|
||||||
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
|
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
|
||||||
app:spanCount="@integer/item_media_columns"
|
app:spanCount="2"
|
||||||
tools:listitem="@layout/item_media" />
|
tools:listitem="@layout/item_media" />
|
||||||
|
|
||||||
</FrameLayout>
|
</FrameLayout>
|
|
@ -1,44 +0,0 @@
|
||||||
<?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="match_parent"
|
|
||||||
tools:context=".ui.activity.main.fragments.MyListsFragment">
|
|
||||||
|
|
||||||
<com.google.android.material.tabs.TabLayout
|
|
||||||
android:id="@+id/tab_my_lists"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:background="@android:color/transparent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:tabMode="fixed">
|
|
||||||
<!-- TODO app:tabTextColor="?colorOnPrimary" -->
|
|
||||||
|
|
||||||
<com.google.android.material.tabs.TabItem
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/my_list" />
|
|
||||||
|
|
||||||
<com.google.android.material.tabs.TabItem
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/crunchylists" />
|
|
||||||
|
|
||||||
<com.google.android.material.tabs.TabItem
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/downloads" />
|
|
||||||
</com.google.android.material.tabs.TabLayout>
|
|
||||||
|
|
||||||
<androidx.viewpager2.widget.ViewPager2
|
|
||||||
android:id="@+id/pager_my_lists"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/tab_my_lists" />
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
|
@ -2,7 +2,8 @@
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.constraintlayout.widget.ConstraintLayout 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"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent"
|
||||||
|
android:background="?themePrimary">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/image_login"
|
android:id="@+id/image_login"
|
||||||
|
@ -10,12 +11,12 @@
|
||||||
android:layout_height="128dp"
|
android:layout_height="128dp"
|
||||||
android:contentDescription="@string/app_name"
|
android:contentDescription="@string/app_name"
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
android:tint="?colorTeapodIcon"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintHorizontal_bias="0.5"
|
app:layout_constraintHorizontal_bias="0.5"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:srcCompat="@drawable/ic_launcher_foreground" />
|
app:srcCompat="@drawable/ic_launcher_foreground"
|
||||||
|
app:tint="?buttonBackground" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/linear_login"
|
android:id="@+id/linear_login"
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<LinearLayout 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"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent"
|
||||||
|
android:background="?themePrimary">
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
@ -15,12 +17,12 @@
|
||||||
android:layout_height="128dp"
|
android:layout_height="128dp"
|
||||||
android:contentDescription="@string/app_name"
|
android:contentDescription="@string/app_name"
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
android:tint="?colorTeapodIcon"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintHorizontal_bias="0.5"
|
app:layout_constraintHorizontal_bias="0.5"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:srcCompat="@drawable/ic_launcher_foreground" />
|
app:srcCompat="@drawable/ic_launcher_foreground"
|
||||||
|
app:tint="?buttonBackground" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/linearLayout3"
|
android:id="@+id/linearLayout3"
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
<?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="match_parent"
|
||||||
|
android:background="?themePrimary"
|
||||||
|
tools:context=".ui.activity.main.fragments.SearchFragment">
|
||||||
|
|
||||||
|
<SearchView
|
||||||
|
android:id="@+id/search_text"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:background="?themeSecondary"
|
||||||
|
android:elevation="8dp"
|
||||||
|
android:iconifiedByDefault="false"
|
||||||
|
android:paddingBottom="5dp"
|
||||||
|
android:queryHint="@string/search_hint"
|
||||||
|
android:searchIcon="@drawable/ic_baseline_search_24"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
</SearchView>
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/recycler_media_search"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="3dp"
|
||||||
|
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/search_text"
|
||||||
|
app:spanCount="2"
|
||||||
|
tools:listitem="@layout/item_media">
|
||||||
|
|
||||||
|
</androidx.recyclerview.widget.RecyclerView>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -10,22 +10,21 @@
|
||||||
android:paddingBottom="7dp">
|
android:paddingBottom="7dp">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/linear_episode"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="horizontal">
|
android:orientation="horizontal">
|
||||||
|
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
android:layout_width="128dp"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="72dp">
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
<com.google.android.material.imageview.ShapeableImageView
|
<com.google.android.material.imageview.ShapeableImageView
|
||||||
android:id="@+id/image_episode"
|
android:id="@+id/image_episode"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="128dp"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="72dp"
|
||||||
android:contentDescription="@string/component_poster_desc"
|
android:contentDescription="@string/component_poster_desc"
|
||||||
android:src="@drawable/placeholder_image"
|
app:shapeAppearance="@style/ShapeAppearance.Teapod.RoundedPoster"
|
||||||
app:shapeAppearance="@style/ShapeAppearance.Teapod.RoundedPoster" />
|
app:srcCompat="@color/md_disabled_text_dark_theme" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/image_episode_play"
|
android:id="@+id/image_episode_play"
|
||||||
|
@ -36,15 +35,6 @@
|
||||||
android:contentDescription="@string/button_play"
|
android:contentDescription="@string/button_play"
|
||||||
app:srcCompat="@drawable/ic_baseline_play_arrow_24"
|
app:srcCompat="@drawable/ic_baseline_play_arrow_24"
|
||||||
app:tint="#FFFFFF" />
|
app:tint="#FFFFFF" />
|
||||||
|
|
||||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
|
||||||
android:id="@+id/progress_playhead"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="bottom"
|
|
||||||
android:max="100"
|
|
||||||
app:trackColor="#00FFFFFF"
|
|
||||||
app:trackThickness="2dp" />
|
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
@ -53,9 +43,8 @@
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_marginStart="7dp"
|
android:layout_marginStart="7dp"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:ellipsize="end"
|
|
||||||
android:maxLines="3"
|
|
||||||
android:text="@string/component_episode_title"
|
android:text="@string/component_episode_title"
|
||||||
|
android:textColor="?textPrimary"
|
||||||
android:textSize="16sp" />
|
android:textSize="16sp" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
|
@ -64,7 +53,8 @@
|
||||||
android:layout_height="30dp"
|
android:layout_height="30dp"
|
||||||
android:layout_margin="2dp"
|
android:layout_margin="2dp"
|
||||||
android:contentDescription="@string/component_watched_desc"
|
android:contentDescription="@string/component_watched_desc"
|
||||||
app:srcCompat="@drawable/ic_baseline_check_circle_24" />
|
app:srcCompat="@drawable/ic_baseline_check_circle_24"
|
||||||
|
app:tint="?iconColor" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
@ -72,6 +62,6 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:ellipsize="end"
|
android:ellipsize="end"
|
||||||
android:maxLines="3" />
|
android:maxLines="3"
|
||||||
<!-- TODO android:textColor="?textSecondary" -->
|
android:textColor="?textSecondary" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
|
@ -7,16 +7,16 @@
|
||||||
android:padding="7dp">
|
android:padding="7dp">
|
||||||
|
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
android:layout_width="192dp"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="108dp">
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
<com.google.android.material.imageview.ShapeableImageView
|
<com.google.android.material.imageview.ShapeableImageView
|
||||||
android:id="@+id/image_episode"
|
android:id="@+id/image_episode"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="192dp"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="108dp"
|
||||||
android:contentDescription="@string/component_poster_desc"
|
android:contentDescription="@string/component_poster_desc"
|
||||||
android:src="@drawable/placeholder_image"
|
app:shapeAppearance="@style/ShapeAppearance.Teapod.RoundedPoster"
|
||||||
app:shapeAppearance="@style/ShapeAppearance.Teapod.RoundedPoster" />
|
app:srcCompat="@color/md_disabled_text_dark_theme" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/image_episode_play"
|
android:id="@+id/image_episode_play"
|
||||||
|
@ -26,16 +26,7 @@
|
||||||
android:background="@drawable/bg_circle__black_transparent_24dp"
|
android:background="@drawable/bg_circle__black_transparent_24dp"
|
||||||
android:contentDescription="@string/button_play"
|
android:contentDescription="@string/button_play"
|
||||||
app:srcCompat="@drawable/ic_baseline_play_arrow_24"
|
app:srcCompat="@drawable/ic_baseline_play_arrow_24"
|
||||||
app:tint="@color/player_white" />
|
app:tint="#FFFFFF" />
|
||||||
|
|
||||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
|
||||||
android:id="@+id/progress_playhead"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="bottom"
|
|
||||||
android:max="100"
|
|
||||||
app:trackColor="#00FFFFFF"
|
|
||||||
app:trackThickness="2dp" />
|
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
@ -44,7 +35,7 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="7dp"
|
android:layout_marginTop="7dp"
|
||||||
android:text="@string/component_episode_title"
|
android:text="@string/component_episode_title"
|
||||||
android:textColor="@color/player_text"
|
android:textColor="@color/textPrimaryDark"
|
||||||
android:textSize="16sp" />
|
android:textSize="16sp" />
|
||||||
|
|
||||||
<View
|
<View
|
||||||
|
@ -53,7 +44,7 @@
|
||||||
android:layout_height="1dp"
|
android:layout_height="1dp"
|
||||||
android:layout_marginTop="5dp"
|
android:layout_marginTop="5dp"
|
||||||
android:layout_marginBottom="5dp"
|
android:layout_marginBottom="5dp"
|
||||||
android:background="@color/player_text_secondary" />
|
android:background="@color/textSecondaryDark" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/text_episode_desc2"
|
android:id="@+id/text_episode_desc2"
|
||||||
|
@ -62,6 +53,6 @@
|
||||||
android:layout_marginTop="5dp"
|
android:layout_marginTop="5dp"
|
||||||
android:maxLines="10"
|
android:maxLines="10"
|
||||||
android:text="@string/text_overview_ex"
|
android:text="@string/text_overview_ex"
|
||||||
android:textColor="@color/player_text" />
|
android:textColor="@color/textPrimaryDark" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
|
@ -1,91 +0,0 @@
|
||||||
<?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">
|
|
||||||
|
|
||||||
<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: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: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" />
|
|
||||||
|
|
||||||
<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: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>
|
|
|
@ -1,79 +1,43 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout 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="match_parent"
|
android:layout_width="195dp"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content"
|
||||||
|
android:backgroundTint="?themeSecondary"
|
||||||
|
android:visibility="visible"
|
||||||
|
app:cardCornerRadius="7dp"
|
||||||
|
app:cardElevation="4dp">
|
||||||
|
|
||||||
<com.google.android.material.card.MaterialCardView
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="match_parent">
|
||||||
app:cardCornerRadius="7dp"
|
|
||||||
app:cardElevation="4dp"
|
|
||||||
app:cardUseCompatPadding="true"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent">
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<ImageView
|
||||||
|
android:id="@+id/image_poster"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:contentDescription="@string/media_poster_desc"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
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"
|
||||||
|
tools:srcCompat="@color/md_disabled_text_dark_theme" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_title"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center"
|
||||||
|
android:lines="2"
|
||||||
|
android:maxLines="2"
|
||||||
|
android:padding="3dp"
|
||||||
|
android:text="@string/text_title_ex"
|
||||||
|
android:textAlignment="center"
|
||||||
|
android:textSize="15sp"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/image_poster" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
<FrameLayout
|
</com.google.android.material.card.MaterialCardView>
|
||||||
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">
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/image_poster"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:contentDescription="@string/media_poster_desc"
|
|
||||||
android:scaleType="fitCenter"
|
|
||||||
tools:srcCompat="@drawable/placeholder_image" />
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/image_episode_play"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
android:background="@drawable/bg_circle__black_transparent_24dp"
|
|
||||||
android:contentDescription="@string/button_play"
|
|
||||||
app:srcCompat="@drawable/ic_baseline_play_arrow_24"
|
|
||||||
app:tint="#FFFFFF" />
|
|
||||||
|
|
||||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
|
||||||
android:id="@+id/progress_playhead"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="bottom"
|
|
||||||
android:max="100"
|
|
||||||
app:trackColor="#00FFFFFF"
|
|
||||||
app:trackThickness="2dp" />
|
|
||||||
</FrameLayout>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/text_title"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:gravity="center"
|
|
||||||
android:lines="2"
|
|
||||||
android:maxLines="2"
|
|
||||||
android:padding="3dp"
|
|
||||||
android:text="@string/text_title_ex"
|
|
||||||
android:textAlignment="center"
|
|
||||||
android:textSize="15sp"
|
|
||||||
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>
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
|
@ -1,56 +0,0 @@
|
||||||
<?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">
|
|
||||||
|
|
||||||
<com.google.android.material.card.MaterialCardView
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
app:cardCornerRadius="7dp"
|
|
||||||
app:cardElevation="4dp"
|
|
||||||
app:cardUseCompatPadding="true"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent">
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
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">
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/image_poster"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
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>
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
|
@ -1,74 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
android:id="@+id/standard_bottom_sheet"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:paddingTop="24dp"
|
|
||||||
android:paddingStart="24dp"
|
|
||||||
android:paddingEnd="24dp"
|
|
||||||
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/text_title"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:paddingBottom="7dp"
|
|
||||||
android:text="@string/edit_login_credentials"
|
|
||||||
android:textSize="20sp"
|
|
||||||
android:textStyle="bold" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/text_supporting_desc"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:paddingBottom="5dp"
|
|
||||||
android:text="@string/edit_login_credentials_desc" />
|
|
||||||
|
|
||||||
<EditText
|
|
||||||
android:id="@+id/edit_text_login"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_margin="7dp"
|
|
||||||
android:ems="10"
|
|
||||||
android:hint="@string/login"
|
|
||||||
android:importantForAutofill="no"
|
|
||||||
android:inputType="textEmailAddress"
|
|
||||||
android:minHeight="48dp" />
|
|
||||||
|
|
||||||
<EditText
|
|
||||||
android:id="@+id/edit_text_password"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_margin="7dp"
|
|
||||||
android:ems="10"
|
|
||||||
android:hint="@string/password"
|
|
||||||
android:importantForAutofill="no"
|
|
||||||
android:inputType="textPassword"
|
|
||||||
android:minHeight="48dp" />
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:gravity="end"
|
|
||||||
android:orientation="horizontal">
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/negative_button"
|
|
||||||
style="@android:style/Widget.Material.Button.Borderless.Small"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginEnd="24dp"
|
|
||||||
android:text="@string/cancel" />
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/positive_button"
|
|
||||||
style="@android:style/Widget.Material.Button.Borderless.Small"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginEnd="24dp"
|
|
||||||
android:text="@string/save" />
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
|
@ -1,8 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:id="@+id/player_controls_root"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:background="#73000000">
|
android:background="#73000000">
|
||||||
|
@ -19,12 +17,12 @@
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/exo_close_player"
|
android:id="@+id/exo_close_player"
|
||||||
android:layout_width="48dp"
|
|
||||||
android:layout_height="48dp"
|
|
||||||
android:background="@android:color/transparent"
|
android:background="@android:color/transparent"
|
||||||
|
android:scaleType="fitXY"
|
||||||
|
android:layout_width="44dp"
|
||||||
|
android:layout_height="44dp"
|
||||||
android:contentDescription="@string/close_player"
|
android:contentDescription="@string/close_player"
|
||||||
android:padding="10dp"
|
android:padding="10dp"
|
||||||
android:scaleType="fitXY"
|
|
||||||
app:srcCompat="@drawable/ic_baseline_arrow_back_24" />
|
app:srcCompat="@drawable/ic_baseline_arrow_back_24" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
@ -34,9 +32,8 @@
|
||||||
android:layout_marginEnd="44dp"
|
android:layout_marginEnd="44dp"
|
||||||
android:text="@string/text_title_ex"
|
android:text="@string/text_title_ex"
|
||||||
android:textAlignment="center"
|
android:textAlignment="center"
|
||||||
android:textColor="@color/player_white"
|
android:textColor="@color/exo_white"
|
||||||
android:textSize="16sp"
|
android:textSize="16sp" />
|
||||||
tools:ignore="TextContrastCheck" />
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
@ -93,15 +90,13 @@
|
||||||
android:layout_gravity="bottom"
|
android:layout_gravity="bottom"
|
||||||
android:layout_marginStart="12dp"
|
android:layout_marginStart="12dp"
|
||||||
android:layout_marginEnd="12dp"
|
android:layout_marginEnd="12dp"
|
||||||
android:layout_marginBottom="@dimen/player_styled_progress_margin_bottom">
|
android:layout_marginBottom="@dimen/exo_styled_progress_margin_bottom">
|
||||||
|
|
||||||
<com.google.android.exoplayer2.ui.DefaultTimeBar
|
<View
|
||||||
android:id="@id/exo_progress"
|
android:id="@+id/exo_progress_placeholder"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="@dimen/player_styled_progress_layout_height"
|
android:layout_height="@dimen/exo_styled_progress_layout_height"
|
||||||
android:contentDescription="@string/desc_time_bar"
|
android:layout_marginBottom="2dp"
|
||||||
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"
|
||||||
|
@ -110,10 +105,9 @@
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/exo_remaining"
|
android:id="@+id/exo_remaining"
|
||||||
style="@style/ExoStyledControls.TimeText.Position"
|
style="@style/ExoStyledControls.TimeText.Position"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="0dp"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent" />
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
@ -131,7 +125,7 @@
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginEnd="7dp"
|
android:layout_marginEnd="7dp"
|
||||||
android:text="@string/language"
|
android:text="@string/subtitles"
|
||||||
android:textAllCaps="false"
|
android:textAllCaps="false"
|
||||||
app:icon="@drawable/ic_baseline_subtitles_24"
|
app:icon="@drawable/ic_baseline_subtitles_24"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
|
|
@ -22,12 +22,12 @@
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/button_close_episodes_list"
|
android:id="@+id/button_close_episodes_list"
|
||||||
android:layout_width="48dp"
|
|
||||||
android:layout_height="48dp"
|
|
||||||
android:background="@android:color/transparent"
|
android:background="@android:color/transparent"
|
||||||
|
android:scaleType="fitXY"
|
||||||
|
android:layout_width="44dp"
|
||||||
|
android:layout_height="44dp"
|
||||||
android:contentDescription="@string/close_player"
|
android:contentDescription="@string/close_player"
|
||||||
android:padding="10dp"
|
android:padding="10dp"
|
||||||
android:scaleType="fitXY"
|
|
||||||
app:srcCompat="@drawable/ic_baseline_arrow_back_24" />
|
app:srcCompat="@drawable/ic_baseline_arrow_back_24" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.constraintlayout.widget.ConstraintLayout 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"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:background="#73000000"
|
android:background="#73000000"
|
||||||
|
@ -23,12 +22,12 @@
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/button_close_language_settings"
|
android:id="@+id/button_close_language_settings"
|
||||||
android:layout_width="48dp"
|
|
||||||
android:layout_height="48dp"
|
|
||||||
android:background="@android:color/transparent"
|
android:background="@android:color/transparent"
|
||||||
|
android:scaleType="fitXY"
|
||||||
|
android:layout_width="44dp"
|
||||||
|
android:layout_height="44dp"
|
||||||
android:contentDescription="@string/close_player"
|
android:contentDescription="@string/close_player"
|
||||||
android:padding="10dp"
|
android:padding="10dp"
|
||||||
android:scaleType="fitXY"
|
|
||||||
app:srcCompat="@drawable/ic_baseline_arrow_back_24" />
|
app:srcCompat="@drawable/ic_baseline_arrow_back_24" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
@ -36,87 +35,25 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginEnd="44dp"
|
android:layout_marginEnd="44dp"
|
||||||
|
android:text="@string/subtitles"
|
||||||
android:textAlignment="center"
|
android:textAlignment="center"
|
||||||
android:textColor="@color/player_white"
|
android:textColor="@color/exo_white"
|
||||||
android:textSize="18sp"
|
android:textSize="16sp"
|
||||||
android:textStyle="bold" />
|
android:textStyle="bold" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
android:id="@+id/linear_languages"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:orientation="horizontal"
|
android:layout_marginStart="56dp"
|
||||||
|
android:layout_marginEnd="56dp"
|
||||||
|
android:orientation="vertical"
|
||||||
app:layout_constraintBottom_toTopOf="@+id/linear_bottom"
|
app:layout_constraintBottom_toTopOf="@+id/linear_bottom"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/linear_top">
|
app:layout_constraintTop_toBottomOf="@+id/linear_top" />
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/text_audio"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/audio"
|
|
||||||
android:textAlignment="center"
|
|
||||||
android:textColor="@color/player_white"
|
|
||||||
android:textSize="18sp"
|
|
||||||
android:textStyle="bold" />
|
|
||||||
|
|
||||||
<ScrollView
|
|
||||||
android:id="@+id/scroll_audio_languages"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:contentDescription="@string/audio">
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/linear_audio_languages"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="56dp"
|
|
||||||
android:layout_marginEnd="56dp"
|
|
||||||
android:orientation="vertical" />
|
|
||||||
</ScrollView>
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/text_subtitles"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/subtitles"
|
|
||||||
android:textAlignment="center"
|
|
||||||
android:textColor="@color/player_white"
|
|
||||||
android:textSize="18sp"
|
|
||||||
android:textStyle="bold" />
|
|
||||||
|
|
||||||
<ScrollView
|
|
||||||
android:id="@+id/scroll_subtitle_languages"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:contentDescription="@string/subtitles">
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/linear_subtitle_languages"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="56dp"
|
|
||||||
android:layout_marginEnd="56dp"
|
|
||||||
android:orientation="vertical" />
|
|
||||||
</ScrollView>
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/linear_bottom"
|
android:id="@+id/linear_bottom"
|
||||||
|
@ -138,9 +75,9 @@
|
||||||
android:layout_marginEnd="7dp"
|
android:layout_marginEnd="7dp"
|
||||||
android:text="@string/cancel"
|
android:text="@string/cancel"
|
||||||
android:textAllCaps="false"
|
android:textAllCaps="false"
|
||||||
android:textColor="@color/button_text_color_light"
|
android:textColor="@color/exo_white"
|
||||||
android:textSize="16sp"
|
android:textSize="16sp"
|
||||||
app:backgroundTint="@color/button_background_light"
|
app:backgroundTint="@color/buttonBackgroundLight"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
@ -151,13 +88,12 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/apply"
|
android:text="@string/apply"
|
||||||
android:textAllCaps="false"
|
android:textAllCaps="false"
|
||||||
android:textColor="@color/button_text_color_dark"
|
android:textColor="@color/themePrimaryDark"
|
||||||
android:textSize="16sp"
|
android:textSize="16sp"
|
||||||
app:backgroundTint="@color/button_background_dark"
|
app:backgroundTint="@color/buttonBackgroundDark"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
tools:ignore="TextContrastCheck" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -6,15 +6,15 @@
|
||||||
android:icon="@drawable/ic_home_black_24dp"
|
android:icon="@drawable/ic_home_black_24dp"
|
||||||
android:title="@string/title_home" />
|
android:title="@string/title_home" />
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/navigation_my_lists"
|
|
||||||
android:icon="@drawable/ic_baseline_bookmark_border_24"
|
|
||||||
android:title="@string/title_my_lists" />
|
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/navigation_library"
|
android:id="@+id/navigation_library"
|
||||||
android:icon="@drawable/ic_baseline_video_library_24"
|
android:icon="@drawable/ic_baseline_video_library_24"
|
||||||
android:title="@string/title_library" />
|
android:title="@string/title_library" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/navigation_search"
|
||||||
|
android:icon="@drawable/ic_baseline_search_24"
|
||||||
|
android:title="@string/title_search" />
|
||||||
<item
|
<item
|
||||||
android:id="@+id/navigation_account"
|
android:id="@+id/navigation_account"
|
||||||
android:icon="@drawable/ic_baseline_account_box_24"
|
android:icon="@drawable/ic_baseline_account_box_24"
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@color/ic_splash_background"/>
|
|
||||||
<foreground android:drawable="@drawable/ic_splash_foreground"/>
|
|
||||||
</adaptive-icon>
|
|
Before Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 7.1 KiB |
Before Width: | Height: | Size: 10 KiB |
|
@ -11,18 +11,18 @@
|
||||||
android:label="@string/title_home"
|
android:label="@string/title_home"
|
||||||
tools:layout="@layout/fragment_home" />
|
tools:layout="@layout/fragment_home" />
|
||||||
|
|
||||||
<fragment
|
|
||||||
android:id="@+id/navigation_my_lists"
|
|
||||||
android:name="org.mosad.teapod.ui.activity.main.fragments.MyListsFragment"
|
|
||||||
android:label="@string/title_my_lists"
|
|
||||||
tools:layout="@layout/fragment_my_lists" />
|
|
||||||
|
|
||||||
<fragment
|
<fragment
|
||||||
android:id="@+id/navigation_library"
|
android:id="@+id/navigation_library"
|
||||||
android:name="org.mosad.teapod.ui.activity.main.fragments.LibraryFragment"
|
android:name="org.mosad.teapod.ui.activity.main.fragments.LibraryFragment"
|
||||||
android:label="@string/title_library"
|
android:label="@string/title_library"
|
||||||
tools:layout="@layout/fragment_library" />
|
tools:layout="@layout/fragment_library" />
|
||||||
|
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/navigation_search"
|
||||||
|
android:name="org.mosad.teapod.ui.activity.main.fragments.SearchFragment"
|
||||||
|
android:label="@string/title_search"
|
||||||
|
tools:layout="@layout/fragment_search" />
|
||||||
|
|
||||||
<fragment
|
<fragment
|
||||||
android:id="@+id/navigation_account"
|
android:id="@+id/navigation_account"
|
||||||
android:name="org.mosad.teapod.ui.activity.main.fragments.AccountFragment"
|
android:name="org.mosad.teapod.ui.activity.main.fragments.AccountFragment"
|
||||||
|
|