Compare commits
61 Commits
1.0.0-beta
...
develop
Author | SHA1 | Date | |
---|---|---|---|
0fd7cc964f | |||
b07a6fd407 | |||
7d661712f7 | |||
8fcf047e99 | |||
17dbe945e5 | |||
5f609d4c33 | |||
6515f657d0 | |||
c448b44fc4 | |||
88ebc378d3 | |||
1a012cba7d | |||
59a457430e | |||
0662d656ac | |||
3549a3d2a7 | |||
c89ae54929 | |||
3aa03783a9 | |||
4bceacf75c | |||
cf02bee7d4 | |||
01d026cc7f | |||
7580093649 | |||
f266731115 | |||
a6a23c8560 | |||
2cb05de810 | |||
5cf4527a92 | |||
14ad34138c | |||
47e1f6bd49 | |||
fdcb76e26e | |||
7004d73b9f | |||
a13eb15adf | |||
d40ab9519c | |||
2e7db26d1d | |||
8b7fb3ac5f | |||
097383a082 | |||
9380f98098 | |||
e0f05169f5 | |||
e113a9c795 | |||
8e397e13d2 | |||
31e7adac03 | |||
63f5e69094 | |||
bf6f2d916e | |||
81a20e0aa9 | |||
ed8f3fdcda | |||
fffbeaeb49 | |||
21caa8eb1b | |||
bbc819551b | |||
2004a3f483 | |||
0a31c2fd88 | |||
f49b5a2730 | |||
a95813e91e | |||
8bdaa8122b | |||
e2ea0a364e | |||
777c6e0212 | |||
71d5c58653 | |||
6624e71228 | |||
d33de371d1 | |||
1ecd25bb06 | |||
fa28eb35ab | |||
d3fe81224b | |||
34c7f9d081 | |||
19552d3950 | |||
49e0b1ec29 | |||
af66d968cc |
@ -26,4 +26,4 @@ Currently you need to have an Crunchyroll account to contribute to Teapod. Contr
|
||||
#### 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 © 2020-2022 [@Seil0](https://git.mosad.xyz/Seil0)
|
||||
Teapod © 2020-2023 [@Seil0](https://git.mosad.xyz/Seil0)
|
||||
|
@ -4,16 +4,23 @@ plugins {
|
||||
id 'org.jetbrains.kotlin.plugin.serialization' version "$kotlin_version"
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain 17
|
||||
sourceSets.configureEach {
|
||||
languageSettings.optIn("kotlin.RequiresOptIn")
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 33
|
||||
buildToolsVersion "30.0.3"
|
||||
compileSdk 34
|
||||
buildToolsVersion = '34.0.0'
|
||||
|
||||
defaultConfig {
|
||||
applicationId "org.mosad.teapod"
|
||||
minSdkVersion 23
|
||||
targetSdkVersion 32
|
||||
versionCode 9020 //00.09.020
|
||||
versionName "1.0.0-beta3"
|
||||
minSdk 23
|
||||
targetSdk 33
|
||||
versionCode 100992 //01.00.000
|
||||
versionName "1.1.0-beta3"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
resValue "string", "build_time", buildTime()
|
||||
@ -22,6 +29,7 @@ android {
|
||||
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
buildConfig true
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
@ -32,37 +40,28 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
kotlin.sourceSets.all {
|
||||
languageSettings.optIn("kotlin.RequiresOptIn")
|
||||
}
|
||||
}
|
||||
namespace 'org.mosad.teapod'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: "libs", include: ["*.jar"])
|
||||
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.3'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3'
|
||||
|
||||
implementation 'androidx.core:core-ktx:1.9.0'
|
||||
implementation 'androidx.core:core-splashscreen:1.0.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.5.1'
|
||||
implementation 'androidx.core:core-ktx:1.13.1'
|
||||
implementation 'androidx.core:core-splashscreen:1.0.1'
|
||||
implementation 'androidx.appcompat:appcompat:1.7.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.5.2'
|
||||
implementation 'androidx.navigation:navigation-ui-ktx:2.5.2'
|
||||
implementation 'androidx.security:security-crypto:1.1.0-alpha03'
|
||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.8.3'
|
||||
implementation 'androidx.navigation:navigation-ui-ktx:2.8.3'
|
||||
implementation 'androidx.security:security-crypto:1.1.0-alpha06'
|
||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.8.6'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.6'
|
||||
implementation "androidx.paging:paging-runtime-ktx:3.3.2"
|
||||
|
||||
implementation 'com.google.android.material:material:1.6.1'
|
||||
implementation 'com.google.android.material:material:1.12.0'
|
||||
implementation "com.google.android.exoplayer:exoplayer-core:$exo_version"
|
||||
implementation "com.google.android.exoplayer:exoplayer-hls:$exo_version"
|
||||
implementation "com.google.android.exoplayer:exoplayer-dash:$exo_version"
|
||||
@ -71,7 +70,7 @@ dependencies {
|
||||
|
||||
implementation 'com.facebook.shimmer:shimmer:0.5.0'
|
||||
|
||||
implementation 'com.github.bumptech.glide:glide:4.13.2'
|
||||
implementation 'com.github.bumptech.glide:glide:4.16.0'
|
||||
implementation 'jp.wasabeef:glide-transformations:4.3.0'
|
||||
|
||||
implementation "io.ktor:ktor-client-core:$ktor_version"
|
||||
@ -80,8 +79,8 @@ dependencies {
|
||||
implementation "io.ktor:ktor-serialization-kotlinx-json:$ktor_version"
|
||||
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
|
||||
|
||||
}
|
||||
|
||||
|
3
app/proguard-rules.pro
vendored
3
app/proguard-rules.pro
vendored
@ -52,6 +52,9 @@
|
||||
# @Serializable and @Polymorphic are used at runtime for polymorphic serialization.
|
||||
-keepattributes RuntimeVisibleAnnotations,AnnotationDefault
|
||||
|
||||
# This is generated automatically by the Android Gradle plugin.
|
||||
-dontwarn org.slf4j.impl.StaticLoggerBinder
|
||||
|
||||
#misc
|
||||
-dontwarn java.lang.instrument.ClassFileTransformer
|
||||
-dontwarn java.lang.ClassValue
|
||||
|
@ -10,7 +10,7 @@
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme.Dark">
|
||||
android:theme="@style/AppTheme">
|
||||
<activity
|
||||
android:exported="true"
|
||||
android:name="org.mosad.teapod.ui.activity.main.MainActivity"
|
||||
|
@ -31,9 +31,9 @@ import io.ktor.client.request.*
|
||||
import io.ktor.client.request.forms.*
|
||||
import io.ktor.client.statement.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.serialization.*
|
||||
import io.ktor.serialization.kotlinx.json.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
@ -52,21 +52,18 @@ object Crunchyroll {
|
||||
}
|
||||
}
|
||||
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 var basicApiToken: String = ""
|
||||
|
||||
private lateinit var token: Token
|
||||
private var tokenValidUntil: Long = 0
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
@OptIn(DelicateCoroutinesApi::class, ExperimentalCoroutinesApi::class)
|
||||
private val tokenRefreshContext = newSingleThreadContext("TokenRefreshContext")
|
||||
|
||||
private var accountID = ""
|
||||
private var externalID = ""
|
||||
|
||||
private var policy = ""
|
||||
private var signature = ""
|
||||
private var keyPairID = ""
|
||||
|
||||
private val browsingCache = hashMapOf<String, BrowseResult>()
|
||||
|
||||
/**
|
||||
@ -146,7 +143,7 @@ object Crunchyroll {
|
||||
}
|
||||
|
||||
return@coroutineScope (Dispatchers.IO) {
|
||||
val response: T = client.request(url) {
|
||||
val response = client.request(url) {
|
||||
method = httpMethod
|
||||
header("Authorization", "${token.tokenType} ${token.accessToken}")
|
||||
params.forEach {
|
||||
@ -158,18 +155,21 @@ object Crunchyroll {
|
||||
setBody(bodyObject)
|
||||
contentType(ContentType.Application.Json)
|
||||
}
|
||||
}.body()
|
||||
}
|
||||
|
||||
response
|
||||
response.body<T>()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(
|
||||
endpoint: String,
|
||||
params: List<Pair<String, Any?>> = listOf(),
|
||||
url: String = ""
|
||||
): T {
|
||||
val path = url.ifEmpty { "$baseUrl$endpoint" }
|
||||
val path = url.ifEmpty { baseUrl }.plus(endpoint)
|
||||
|
||||
return request(path, HttpMethod.Get, params)
|
||||
}
|
||||
@ -208,27 +208,10 @@ object Crunchyroll {
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic functions: index, account
|
||||
* Basic functions: account
|
||||
* 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.
|
||||
* The account id is needed for other calls.
|
||||
@ -240,7 +223,7 @@ object Crunchyroll {
|
||||
|
||||
val account: Account = try {
|
||||
requestGet(indexEndpoint)
|
||||
} catch (ex: SerializationException) {
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "SerializationException in account(). This is bad!", ex)
|
||||
NoneAccount
|
||||
}
|
||||
@ -256,24 +239,30 @@ object Crunchyroll {
|
||||
/**
|
||||
* Browse the media available on crunchyroll.
|
||||
*
|
||||
* @param sortBy
|
||||
* @param n Number of items to return, defaults to 10
|
||||
*
|
||||
* @param start start of the item list, used for pagination, default = 0
|
||||
* @param n number of items to return, default = 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.
|
||||
*/
|
||||
suspend fun browse(
|
||||
categories: List<Categories> = emptyList(),
|
||||
sortBy: SortBy = SortBy.ALPHABETICAL,
|
||||
seasonTag: String = "",
|
||||
start: Int = 0,
|
||||
n: Int = 10
|
||||
n: Int = 10,
|
||||
sortBy: SortBy = SortBy.ALPHABETICAL,
|
||||
ratings: Boolean = false,
|
||||
seasonTag: String = "",
|
||||
categories: List<Categories> = emptyList()
|
||||
): BrowseResult {
|
||||
val browseEndpoint = "/content/v1/browse"
|
||||
val browseEndpoint = "/content/v2/discover/browse"
|
||||
val parameters = mutableListOf(
|
||||
"locale" to Preferences.preferredLocale.toLanguageTag(),
|
||||
"sort_by" to sortBy.str,
|
||||
"start" to start,
|
||||
"n" to n
|
||||
"n" to n,
|
||||
"sort_by" to sortBy.str,
|
||||
"ratings" to ratings,
|
||||
"preferred_audio_language" to Preferences.preferredSubtitleLocale.toLanguageTag(),
|
||||
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag(),
|
||||
)
|
||||
|
||||
// if a season tag is present add it to the parameters
|
||||
@ -293,14 +282,16 @@ object Crunchyroll {
|
||||
Log.d(TAG, "browse result not cached, fetching: $parameters")
|
||||
val browseResult: BrowseResult = try {
|
||||
requestGet(browseEndpoint, parameters)
|
||||
}catch (ex: SerializationException) {
|
||||
}catch (ex: Exception) {
|
||||
Log.e(TAG, "SerializationException in browse().", ex)
|
||||
NoneBrowseResult
|
||||
}
|
||||
|
||||
// if the cache has more than 100 entries clear it, so it doesn't become a memory problem
|
||||
|
||||
|
||||
// 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 > 100) {
|
||||
if (browsingCache.size > 10) {
|
||||
browsingCache.clear()
|
||||
}
|
||||
|
||||
@ -317,15 +308,18 @@ object Crunchyroll {
|
||||
*
|
||||
* @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): SearchResult {
|
||||
val searchEndpoint = "/content/v1/search"
|
||||
suspend fun search(query: String, n: Int = 10, ratings: Boolean = false): SearchResult {
|
||||
val searchEndpoint = "/content/v2/discover/search"
|
||||
val parameters = listOf(
|
||||
"locale" to Preferences.preferredLocale.toLanguageTag(),
|
||||
"q" to query,
|
||||
"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,
|
||||
@ -333,8 +327,8 @@ object Crunchyroll {
|
||||
|
||||
return try {
|
||||
requestGet(searchEndpoint, parameters)
|
||||
} catch (ex: SerializationException) {
|
||||
Log.e(TAG, "SerializationException in search(), with query = \"$query\".", ex)
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "Exception in search(), with query = \"$query\".", ex)
|
||||
NoneSearchResult
|
||||
}
|
||||
}
|
||||
@ -344,38 +338,22 @@ object Crunchyroll {
|
||||
* Note: episode objects are currently not supported
|
||||
*
|
||||
* @param objects The object IDs as list of Strings
|
||||
* @param ratings add user rating to the objects
|
||||
* @return A **[Collection]** of Panels
|
||||
*/
|
||||
suspend fun objects(objects: List<String>): Collection<Item> {
|
||||
val episodesEndpoint = "/cms/v2/DE/M3/crunchyroll/objects/${objects.joinToString(",")}"
|
||||
suspend fun objects(objects: List<String>, ratings: Boolean = false): CollectionV2<Item> {
|
||||
val episodesEndpoint = "/content/v2/cms/objects/${objects.joinToString(",")}"
|
||||
val parameters = listOf(
|
||||
"locale" to Preferences.preferredLocale.toLanguageTag(),
|
||||
"Signature" to signature,
|
||||
"Policy" to policy,
|
||||
"Key-Pair-Id" to keyPairID
|
||||
"ratings" to ratings,
|
||||
"preferred_audio_language" to Preferences.preferredAudioLocale.toLanguageTag(),
|
||||
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag()
|
||||
)
|
||||
|
||||
return try {
|
||||
requestGet(episodesEndpoint, parameters)
|
||||
} catch (ex: SerializationException) {
|
||||
Log.e(TAG, "SerializationException in objects().", ex)
|
||||
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
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "Exception in objects().", ex)
|
||||
NoneCollectionV2
|
||||
}
|
||||
}
|
||||
|
||||
@ -387,18 +365,16 @@ object Crunchyroll {
|
||||
* series id == crunchyroll id?
|
||||
*/
|
||||
suspend fun series(seriesId: String): Series {
|
||||
val seriesEndpoint = "/cms/v2/${token.country}/M3/crunchyroll/series/$seriesId"
|
||||
val seriesEndpoint = "/content/v2/cms/series/$seriesId"
|
||||
val parameters = listOf(
|
||||
"locale" to Preferences.preferredLocale.toLanguageTag(),
|
||||
"Signature" to signature,
|
||||
"Policy" to policy,
|
||||
"Key-Pair-Id" to keyPairID
|
||||
"preferred_audio_language" to Preferences.preferredAudioLocale.toLanguageTag(),
|
||||
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag()
|
||||
)
|
||||
|
||||
return try {
|
||||
requestGet(seriesEndpoint, parameters)
|
||||
} catch (ex: SerializationException) {
|
||||
Log.e(TAG, "SerializationException in series().", ex)
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "Exception in series() for id $seriesId.", ex)
|
||||
NoneSeries
|
||||
}
|
||||
}
|
||||
@ -406,21 +382,29 @@ object Crunchyroll {
|
||||
/**
|
||||
* Get the next episode for a series.
|
||||
*
|
||||
* 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): UpNextSeriesItem {
|
||||
val upNextSeriesEndpoint = "/content/v1/up_next_series"
|
||||
suspend fun upNextSeries(seriesId: String): UpNextSeriesList {
|
||||
val upNextSeriesEndpoint = "/content/v2/discover/up_next/$seriesId"
|
||||
val parameters = listOf(
|
||||
"series_id" to seriesId,
|
||||
"locale" to Preferences.preferredLocale.toLanguageTag()
|
||||
"preferred_audio_language" to Preferences.preferredAudioLocale.toLanguageTag(),
|
||||
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag()
|
||||
)
|
||||
|
||||
return try {
|
||||
requestGet(upNextSeriesEndpoint, parameters)
|
||||
} catch (ex: SerializationException) {
|
||||
Log.e(TAG, "SerializationException in upNextSeries().", ex)
|
||||
NoneUpNextSeriesItem
|
||||
} catch (ex: NoTransformationFoundException) {
|
||||
// should be 204 No Content
|
||||
NoneUpNextSeriesList
|
||||
} 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
|
||||
}
|
||||
}
|
||||
|
||||
@ -431,19 +415,16 @@ object Crunchyroll {
|
||||
* @return A **[Seasons]** object with a list of **[Season]**
|
||||
*/
|
||||
suspend fun seasons(seriesId: String): Seasons {
|
||||
val seasonsEndpoint = "/cms/v2/${token.country}/M3/crunchyroll/seasons"
|
||||
val seasonsEndpoint = "/content/v2/cms/series/$seriesId/seasons"
|
||||
val parameters = listOf(
|
||||
"series_id" to seriesId,
|
||||
"locale" to Preferences.preferredLocale.toLanguageTag(),
|
||||
"Signature" to signature,
|
||||
"Policy" to policy,
|
||||
"Key-Pair-Id" to keyPairID
|
||||
"preferred_audio_language" to Preferences.preferredSubtitleLocale.toLanguageTag(),
|
||||
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag()
|
||||
)
|
||||
|
||||
return try {
|
||||
requestGet(seasonsEndpoint, parameters)
|
||||
} catch (ex: SerializationException) {
|
||||
Log.e(TAG, "SerializationException in seasons().", ex)
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "Exception in seasons() for seriesId $seriesId.", ex)
|
||||
NoneSeasons
|
||||
}
|
||||
}
|
||||
@ -455,19 +436,16 @@ object Crunchyroll {
|
||||
* @return A **[Episodes]** object with a list of **[Episode]**
|
||||
*/
|
||||
suspend fun episodes(seasonId: String): Episodes {
|
||||
val episodesEndpoint = "/cms/v2/${token.country}/M3/crunchyroll/episodes"
|
||||
val episodesEndpoint = "/content/v2/cms/seasons/$seasonId/episodes"
|
||||
val parameters = listOf(
|
||||
"season_id" to seasonId,
|
||||
"locale" to Preferences.preferredLocale.toLanguageTag(),
|
||||
"Signature" to signature,
|
||||
"Policy" to policy,
|
||||
"Key-Pair-Id" to keyPairID
|
||||
"preferred_audio_language" to Preferences.preferredSubtitleLocale.toLanguageTag(),
|
||||
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag()
|
||||
)
|
||||
|
||||
return try {
|
||||
requestGet(episodesEndpoint, parameters)
|
||||
} catch (ex: SerializationException) {
|
||||
Log.e(TAG, "SerializationException in episodes().", ex)
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "Exception in episodes() for seasonId $seasonId.", ex)
|
||||
NoneEpisodes
|
||||
}
|
||||
}
|
||||
@ -475,18 +453,28 @@ object Crunchyroll {
|
||||
/**
|
||||
* Get all available subtitles and streams of a episode.
|
||||
*
|
||||
* @param url The playback url of a episode
|
||||
* @return A **[Playback]** object
|
||||
* @param url The streams url of a episode
|
||||
* @return A **[Streams]** object
|
||||
*/
|
||||
suspend fun playback(url: String): Playback {
|
||||
suspend fun streams(url: String): Streams {
|
||||
val parameters = listOf(
|
||||
"preferred_audio_language" to Preferences.preferredSubtitleLocale.toLanguageTag(),
|
||||
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag()
|
||||
)
|
||||
|
||||
return try {
|
||||
requestGet("", url = url)
|
||||
} catch (ex: SerializationException) {
|
||||
Log.e(TAG, "SerializationException in playback(), with url = $url.", ex)
|
||||
NonePlayback
|
||||
requestGet(url, parameters)
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "Exception in streams() with url $url.", ex)
|
||||
NoneStreams
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
*/
|
||||
@ -498,14 +486,18 @@ object Crunchyroll {
|
||||
* @return **[Boolean]**: ture if it was found, else false
|
||||
*/
|
||||
suspend fun isWatchlist(seriesId: String): Boolean {
|
||||
val watchlistSeriesEndpoint = "/content/v1/watchlist/$accountID/$seriesId"
|
||||
val parameters = listOf("locale" to Preferences.preferredLocale.toLanguageTag())
|
||||
val watchlistSeriesEndpoint = "/content/v2/$accountID/watchlist"
|
||||
val parameters = listOf(
|
||||
"content_ids" to seriesId,
|
||||
"preferred_audio_language" to Preferences.preferredAudioLocale.toLanguageTag(),
|
||||
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag()
|
||||
)
|
||||
|
||||
return try {
|
||||
(requestGet(watchlistSeriesEndpoint, parameters) as JsonObject)
|
||||
.containsKey(seriesId)
|
||||
} catch (ex: SerializationException) {
|
||||
Log.e(TAG, "SerializationException in isWatchlist() with seriesId = $seriesId", ex)
|
||||
(requestGet(watchlistSeriesEndpoint, parameters) as CollectionV2<IsWatchlistItem>)
|
||||
.total == 1
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "Exception in isWatchlist() with seriesId $seriesId", ex)
|
||||
false
|
||||
}
|
||||
}
|
||||
@ -516,14 +508,21 @@ object Crunchyroll {
|
||||
* @param seriesId The crunchyroll series id of the media to check
|
||||
*/
|
||||
suspend fun postWatchlist(seriesId: String) {
|
||||
val watchlistPostEndpoint = "/content/v1/watchlist/$accountID"
|
||||
val parameters = listOf("locale" to Preferences.preferredLocale.toLanguageTag())
|
||||
val watchlistPostEndpoint = "/content/v2/$accountID/watchlist"
|
||||
val parameters = listOf(
|
||||
"preferred_audio_language" to Preferences.preferredAudioLocale.toLanguageTag(),
|
||||
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag()
|
||||
)
|
||||
|
||||
val json = buildJsonObject {
|
||||
put("content_id", seriesId)
|
||||
}
|
||||
|
||||
requestPost(watchlistPostEndpoint, parameters, json)
|
||||
try {
|
||||
requestPost(watchlistPostEndpoint, parameters, json)
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "Exception in postWatchlist() with seriesId $seriesId", ex)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -532,10 +531,17 @@ object Crunchyroll {
|
||||
* @param seriesId The crunchyroll series id of the media to check
|
||||
*/
|
||||
suspend fun deleteWatchlist(seriesId: String) {
|
||||
val watchlistDeleteEndpoint = "/content/v1/watchlist/$accountID/$seriesId"
|
||||
val parameters = listOf("locale" to Preferences.preferredLocale.toLanguageTag())
|
||||
val watchlistDeleteEndpoint = "/content/v2/$accountID/watchlist/$seriesId"
|
||||
val parameters = listOf(
|
||||
"preferred_audio_language" to Preferences.preferredAudioLocale.toLanguageTag(),
|
||||
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag()
|
||||
)
|
||||
|
||||
requestDelete(watchlistDeleteEndpoint, parameters)
|
||||
try {
|
||||
requestDelete(watchlistDeleteEndpoint, parameters)
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "Exception in deleteWatchlist() with seriesId $seriesId", ex)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -546,18 +552,19 @@ object Crunchyroll {
|
||||
* @param episodeIDs A **[List]** of episodes IDs as strings.
|
||||
* @return A **[Map]**<String, **[PlayheadObject]**> containing playback info.
|
||||
*/
|
||||
suspend fun playheads(episodeIDs: List<String>): PlayheadsMap {
|
||||
val playheadsEndpoint = "/content/v1/playheads/$accountID/${episodeIDs.joinToString(",")}"
|
||||
val parameters = listOf("locale" to Preferences.preferredLocale.toLanguageTag())
|
||||
suspend fun playheads(episodeIDs: List<String>): Playheads {
|
||||
val playheadsEndpoint = "/content/v2/$accountID/playheads"
|
||||
val parameters = listOf(
|
||||
"content_ids" to episodeIDs.joinToString(","),
|
||||
"preferred_audio_language" to Preferences.preferredAudioLocale.toLanguageTag(),
|
||||
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag()
|
||||
)
|
||||
|
||||
return try {
|
||||
requestGet(playheadsEndpoint, parameters)
|
||||
} catch (ex: SerializationException) {
|
||||
Log.e(TAG, "SerializationException in playheads().", ex)
|
||||
emptyMap()
|
||||
} catch (ex: Throwable) {
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "Exception in playheads().", ex.cause)
|
||||
emptyMap()
|
||||
NonePlayheads
|
||||
}
|
||||
}
|
||||
|
||||
@ -569,7 +576,7 @@ object Crunchyroll {
|
||||
*/
|
||||
suspend fun postPlayheads(episodeId: String, playhead: Int) {
|
||||
val playheadsEndpoint = "/content/v1/playheads/$accountID"
|
||||
val parameters = listOf("locale" to Preferences.preferredLocale.toLanguageTag())
|
||||
val parameters = listOf("locale" to Preferences.preferredSubtitleLocale.toLanguageTag())
|
||||
|
||||
val json = buildJsonObject {
|
||||
put("content_id", episodeId)
|
||||
@ -578,30 +585,53 @@ object Crunchyroll {
|
||||
|
||||
try {
|
||||
requestPost(playheadsEndpoint, parameters, json)
|
||||
} catch (ex: Throwable) {
|
||||
} 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): SimilarToResult {
|
||||
val similarToEndpoint = "/content/v1/$accountID/similar_to"
|
||||
suspend fun similarTo(seriesId: String, n: Int = 10, ratings: Boolean = false): SimilarToResult {
|
||||
val similarToEndpoint = "/content/v2/discover/$accountID/similar_to/$seriesId"
|
||||
val parameters = listOf(
|
||||
"guid" to seriesId,
|
||||
"locale" to Preferences.preferredLocale.toLanguageTag(),
|
||||
"n" to n
|
||||
"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: SerializationException) {
|
||||
Log.e(TAG, "SerializationException in similarTo().", ex)
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "Exception in similarTo().", ex)
|
||||
NoneSimilarToResult
|
||||
}
|
||||
}
|
||||
@ -614,60 +644,69 @@ object Crunchyroll {
|
||||
* List items present in the watchlist.
|
||||
*
|
||||
* @param n Number of items to return, defaults to 20.
|
||||
* @return A **[Watchlist]** containing up to n **[Item]**.
|
||||
* @return A **[Collection]** containing up to n **[Item]**.
|
||||
*/
|
||||
suspend fun watchlist(n: Int = 20): Watchlist {
|
||||
val watchlistEndpoint = "/content/v1/$accountID/watchlist"
|
||||
suspend fun watchlist(n: Int = 20): CollectionV2<Item> {
|
||||
val watchlistEndpoint = "/content/v2/discover/$accountID/watchlist"
|
||||
val parameters = listOf(
|
||||
"locale" to Preferences.preferredLocale.toLanguageTag(),
|
||||
"n" to n
|
||||
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag(),
|
||||
"n" to n,
|
||||
"preferred_audio_language" to Preferences.preferredAudioLocale.toLanguageTag()
|
||||
)
|
||||
|
||||
val list: ContinueWatchingList = try {
|
||||
val list: Watchlist = try {
|
||||
requestGet(watchlistEndpoint, parameters)
|
||||
} catch (ex: SerializationException) {
|
||||
Log.e(TAG, "SerializationException in watchlist().", ex)
|
||||
NoneContinueWatchingList
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "Exception in watchlist().", ex)
|
||||
NoneWatchlist
|
||||
}
|
||||
|
||||
val objects = list.items.map{ it.panel.episodeMetadata.seriesId }
|
||||
val objects = list.data.map{ it.panel.episodeMetadata.seriesId }
|
||||
return objects(objects)
|
||||
}
|
||||
|
||||
/**
|
||||
* List the next up episodes for the logged in account.
|
||||
*
|
||||
* @param n Number of items to return, defaults to 20.
|
||||
* @return A **[ContinueWatchingList]** containing up to n **[ContinueWatchingItem]**.
|
||||
* @param n Number of items to return, default = 20
|
||||
* @return A **[HistoryList]** containing up to n **[UpNextAccountItem]**.
|
||||
*/
|
||||
suspend fun upNextAccount(n: Int = 20): ContinueWatchingList {
|
||||
val watchlistEndpoint = "/content/v1/$accountID/up_next_account"
|
||||
suspend fun upNextAccount(n: Int = 10): HistoryList {
|
||||
val watchlistEndpoint = "/content/v2/discover/$accountID/history"
|
||||
val parameters = listOf(
|
||||
"locale" to Preferences.preferredLocale.toLanguageTag(),
|
||||
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag(),
|
||||
"n" to n
|
||||
)
|
||||
|
||||
return try {
|
||||
requestGet(watchlistEndpoint, parameters)
|
||||
} catch (ex: SerializationException) {
|
||||
Log.e(TAG, "SerializationException in upNextAccount().", ex)
|
||||
NoneContinueWatchingList
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "Exception in upNextAccount().", ex)
|
||||
NoneHistoryList
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun recommendations(n: Int = 20, start: Int = 0): RecommendationsList {
|
||||
val recommendationsEndpoint = "/content/v1/$accountID/recommendations"
|
||||
/**
|
||||
* 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(
|
||||
"locale" to Preferences.preferredLocale.toLanguageTag(),
|
||||
"n" to n,
|
||||
"start" to start,
|
||||
"variant_id" to 0
|
||||
"n" to n,
|
||||
"ratings" to ratings,
|
||||
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag(),
|
||||
)
|
||||
|
||||
return try {
|
||||
requestGet(recommendationsEndpoint, parameters)
|
||||
} catch (ex: SerializationException) {
|
||||
Log.e(TAG, "SerializationException in recommendations().", ex)
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "Exception in recommendations().", ex)
|
||||
NoneRecommendationsList
|
||||
}
|
||||
}
|
||||
@ -686,8 +725,8 @@ object Crunchyroll {
|
||||
|
||||
return try {
|
||||
requestGet(profileEndpoint)
|
||||
} catch (ex: SerializationException) {
|
||||
Log.e(TAG, "SerializationException in profile().", ex)
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "Exception in profile().", ex)
|
||||
NoneProfile
|
||||
}
|
||||
}
|
||||
@ -697,7 +736,7 @@ object Crunchyroll {
|
||||
*
|
||||
* @param languageTag the preferred language as language tag
|
||||
*/
|
||||
suspend fun postPrefSubLanguage(languageTag: String) {
|
||||
suspend fun setPreferredSubtitleLanguage(languageTag: String) {
|
||||
val profileEndpoint = "/accounts/v1/me/profile"
|
||||
val json = buildJsonObject {
|
||||
put("preferred_content_subtitle_language", languageTag)
|
||||
@ -706,6 +745,20 @@ object Crunchyroll {
|
||||
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.
|
||||
*
|
||||
@ -716,8 +769,8 @@ object Crunchyroll {
|
||||
|
||||
return try {
|
||||
requestGet(profileEndpoint)
|
||||
} catch (ex: SerializationException) {
|
||||
Log.e(TAG, "SerializationException in benefits().", ex)
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "Exception in benefits().", ex)
|
||||
NoneBenefits
|
||||
}
|
||||
}
|
||||
|
@ -24,19 +24,47 @@ package org.mosad.teapod.parser.crunchyroll
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.util.*
|
||||
import java.util.Locale
|
||||
|
||||
val supportedLocals = listOf(
|
||||
val supportedAudioLocals = listOf(
|
||||
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("en-US"),
|
||||
Locale.forLanguageTag("es-419"),
|
||||
Locale.forLanguageTag("es-ES"),
|
||||
Locale.forLanguageTag("fr-FR"),
|
||||
Locale.forLanguageTag("hi-IN"),
|
||||
Locale.forLanguageTag("it-IT"),
|
||||
Locale.forLanguageTag("ms-MY"),
|
||||
Locale.forLanguageTag("pl-PL"),
|
||||
Locale.forLanguageTag("pt-BR"),
|
||||
Locale.forLanguageTag("pt-PT"),
|
||||
Locale.forLanguageTag("ru-RU"),
|
||||
Locale.forLanguageTag("tr-TR"),
|
||||
Locale.ROOT
|
||||
)
|
||||
|
||||
@ -44,6 +72,10 @@ val supportedLocals = listOf(
|
||||
* data classes for browse
|
||||
* TODO make class names more clear/possibly overlapping for now
|
||||
*/
|
||||
|
||||
/**
|
||||
* Enum of all supported sorting orders.
|
||||
*/
|
||||
enum class SortBy(val str: String) {
|
||||
ALPHABETICAL("alphabetical"),
|
||||
NEWLY_ADDED("newly_added"),
|
||||
@ -112,29 +144,23 @@ val NoneAccount = Account("", "", false, "")
|
||||
*/
|
||||
|
||||
@Serializable
|
||||
data class Collection<T>(
|
||||
data class CollectionV1<T>(
|
||||
@SerialName("total") val total: Int,
|
||||
@SerialName("items") val items: List<T>
|
||||
)
|
||||
|
||||
typealias SearchResult = Collection<SearchCollection>
|
||||
typealias SearchCollection = Collection<Item>
|
||||
typealias BrowseResult = Collection<Item>
|
||||
typealias SimilarToResult = Collection<Item>
|
||||
typealias DiscSeasonList = Collection<SeasonListItem>
|
||||
typealias Watchlist = Collection<Item>
|
||||
typealias ContinueWatchingList = Collection<ContinueWatchingItem>
|
||||
typealias RecommendationsList = Collection<Item>
|
||||
typealias Benefits = Collection<Benefit>
|
||||
|
||||
@Serializable
|
||||
data class UpNextSeriesItem(
|
||||
@SerialName("playhead") val playhead: Int,
|
||||
@SerialName("fully_watched") val fullyWatched: Boolean,
|
||||
@SerialName("never_watched") val neverWatched: Boolean,
|
||||
@SerialName("panel") val panel: EpisodePanel,
|
||||
data class CollectionV2<T>(
|
||||
@SerialName("total") val total: Int,
|
||||
@SerialName("data") val data: List<T>
|
||||
)
|
||||
|
||||
typealias SearchResult = CollectionV2<SearchTypedList<Item>>
|
||||
typealias BrowseResult = CollectionV2<Item>
|
||||
typealias SimilarToResult = CollectionV2<Item>
|
||||
typealias RecommendationsList = CollectionV2<Item>
|
||||
typealias Benefits = CollectionV1<Benefit>
|
||||
|
||||
/**
|
||||
* panel data classes
|
||||
*/
|
||||
@ -161,35 +187,45 @@ 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)
|
||||
|
||||
/**
|
||||
* season list data classes
|
||||
* up next & watchlist data classes
|
||||
*/
|
||||
@Serializable
|
||||
data class SeasonListItem(
|
||||
@SerialName("id") val id: String,
|
||||
@SerialName("localization") val localization: SeasonListLocalization
|
||||
)
|
||||
|
||||
typealias Watchlist = CollectionV2<WatchlistItem>
|
||||
typealias HistoryList = CollectionV2<UpNextAccountItem>
|
||||
typealias UpNextSeriesList = CollectionV2<UpNextSeriesItem>
|
||||
|
||||
@Serializable
|
||||
data class SeasonListLocalization(
|
||||
@SerialName("title") val title: String,
|
||||
@SerialName("description") val description: String,
|
||||
)
|
||||
|
||||
/**
|
||||
* continue_watching_item data classes
|
||||
*/
|
||||
@Serializable
|
||||
data class ContinueWatchingItem(
|
||||
data class WatchlistItem(
|
||||
@SerialName("panel") val panel: EpisodePanel,
|
||||
@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,
|
||||
// not present in watchlist -> continue_watching_item
|
||||
@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("is_favorite") val isFavorite: Boolean,
|
||||
@SerialName("date_added") val dateAdded: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class UpNextAccountItem(
|
||||
@SerialName("panel") val panel: EpisodePanel,
|
||||
@SerialName("new") val new: Boolean,
|
||||
@SerialName("playhead") val playhead: Int,
|
||||
@SerialName("fully_watched") val fullyWatched: Boolean = false,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
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
|
||||
@ -202,7 +238,7 @@ data class EpisodePanel(
|
||||
@SerialName("description") val description: String,
|
||||
@SerialName("episode_metadata") val episodeMetadata: EpisodeMetadata,
|
||||
@SerialName("images") val images: Thumbnail,
|
||||
@SerialName("playback") val playback: String,
|
||||
// @SerialName("streams_link") val streamsLink: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@ -216,38 +252,36 @@ data class EpisodeMetadata(
|
||||
@SerialName("series_title") val seriesTitle: String,
|
||||
)
|
||||
|
||||
val NoneItem = Item("", "", "", "", "", Images(emptyList(), emptyList()))
|
||||
val NoneEpisodeMetadata = EpisodeMetadata(0, 0, "", 0, "", "", "")
|
||||
val NoneEpisodePanel = EpisodePanel("", "", "", "", "", NoneEpisodeMetadata, Thumbnail(listOf()), "")
|
||||
|
||||
val NoneCollection = Collection<Item>(0, emptyList())
|
||||
val NoneCollectionV2 = CollectionV2<Item>(0, emptyList())
|
||||
val NoneSearchResult = SearchResult(0, emptyList())
|
||||
val NoneBrowseResult = BrowseResult(0, emptyList())
|
||||
val NoneSimilarToResult = SimilarToResult(0, emptyList())
|
||||
val NoneDiscSeasonList = DiscSeasonList(0, emptyList())
|
||||
val NoneContinueWatchingList = ContinueWatchingList(0, emptyList())
|
||||
val NoneWatchlist = Watchlist(0, emptyList())
|
||||
val NoneHistoryList = HistoryList(0, emptyList())
|
||||
val NoneUpNextSeriesList = UpNextSeriesList(0, emptyList())
|
||||
val NoneRecommendationsList = RecommendationsList(0, emptyList())
|
||||
val NoneBenefits = Benefits(0, emptyList())
|
||||
|
||||
val NoneUpNextSeriesItem = UpNextSeriesItem(
|
||||
playhead = 0,
|
||||
fullyWatched = false,
|
||||
neverWatched = false,
|
||||
panel = NoneEpisodePanel
|
||||
)
|
||||
|
||||
/**
|
||||
* series data class
|
||||
*/
|
||||
|
||||
typealias Series = CollectionV2<SeriesItem>
|
||||
|
||||
@Serializable
|
||||
data class Series(
|
||||
data class SeriesItem(
|
||||
@SerialName("id") val id: String,
|
||||
@SerialName("title") val title: String,
|
||||
@SerialName("description") val description: String,
|
||||
@SerialName("images") val images: Images,
|
||||
@SerialName("maturity_ratings") val maturityRatings: List<String>
|
||||
@SerialName("is_simulcast") val isSimulcast: Boolean,
|
||||
@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
|
||||
@ -255,18 +289,8 @@ val NoneSeries = Series("", "", "", Images(emptyList(), emptyList()), emptyList(
|
||||
@Serializable
|
||||
data class Seasons(
|
||||
@SerialName("total") val total: Int,
|
||||
@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
|
||||
}
|
||||
}
|
||||
@SerialName("data") val data: List<Season>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Season(
|
||||
@ -289,7 +313,7 @@ val NoneSeason = Season("", "", "", "", 0, isSubbed = false, isDubbed = false)
|
||||
@Serializable
|
||||
data class Episodes(
|
||||
@SerialName("total") val total: Int,
|
||||
@SerialName("items") val items: List<Episode>
|
||||
@SerialName("data") val data: List<Episode>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@ -309,7 +333,8 @@ data class Episode(
|
||||
@SerialName("is_dubbed") val isDubbed: Boolean,
|
||||
@SerialName("images") val images: Thumbnail,
|
||||
@SerialName("duration_ms") val durationMs: Int,
|
||||
@SerialName("playback") val playback: String,
|
||||
@SerialName("versions") val versions: List<Version>? = null,
|
||||
@SerialName("streams_link") val streamsLink: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@ -317,6 +342,17 @@ data class Thumbnail(
|
||||
@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 NoneEpisode = Episode(
|
||||
id = "",
|
||||
@ -334,10 +370,21 @@ val NoneEpisode = Episode(
|
||||
isDubbed = false,
|
||||
images = Thumbnail(listOf()),
|
||||
durationMs = 0,
|
||||
playback = ""
|
||||
versions = emptyList(),
|
||||
streamsLink = ""
|
||||
)
|
||||
|
||||
typealias PlayheadsMap = Map<String, PlayheadObject>
|
||||
val NoneVersion = Version(
|
||||
audioLocale = "",
|
||||
guid = "",
|
||||
isPremiumOnly = false,
|
||||
mediaGUID = "",
|
||||
original = true,
|
||||
seasonGUID = "",
|
||||
variant = ""
|
||||
)
|
||||
|
||||
typealias Playheads = CollectionV2<PlayheadObject>
|
||||
|
||||
@Serializable
|
||||
data class PlayheadObject(
|
||||
@ -347,37 +394,47 @@ data class PlayheadObject(
|
||||
@SerialName("last_modified") val lastModified: String,
|
||||
)
|
||||
|
||||
val NonePlayheads = Playheads(0, emptyList())
|
||||
|
||||
/**
|
||||
* Meta data for a episode intro. All time values are in seconds.
|
||||
*/
|
||||
@Serializable
|
||||
data class DatalabIntro(
|
||||
@SerialName("media_id") val mediaId: String,
|
||||
@SerialName("startTime") val startTime: Float,
|
||||
@SerialName("endTime") val endTime: Float,
|
||||
@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, "", "", "")
|
||||
|
||||
/**
|
||||
* playback/stream data classes
|
||||
*/
|
||||
@Serializable
|
||||
data class Playback(
|
||||
@SerialName("audio_locale") val audioLocale: String,
|
||||
@SerialName("subtitles") val subtitles: Map<String, Subtitle>,
|
||||
@SerialName("streams") val streams: Streams,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Subtitle(
|
||||
@SerialName("locale") val locale: String,
|
||||
@SerialName("url") val url: String,
|
||||
@SerialName("format") val format: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
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_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("drm_adaptive_dash") val drm_adaptive_dash: Map<String, Stream>,
|
||||
@SerialName("drm_adaptive_hls") val drm_adaptive_hls: Map<String, Stream>,
|
||||
@SerialName("drm_download_hls") val drm_download_hls: Map<String, Stream>,
|
||||
@SerialName("trailer_dash") val trailer_dash: Map<String, Stream>,
|
||||
@SerialName("trailer_hls") val trailer_hls: Map<String, Stream>,
|
||||
@SerialName("vo_adaptive_dash") val vo_adaptive_dash: Map<String, Stream>,
|
||||
@SerialName("vo_adaptive_hls") val vo_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>,
|
||||
// @SerialName("drm_adaptive_dash") val drmAdaptiveDash: Map<String, Stream>,
|
||||
// @SerialName("drm_adaptive_hls") val drmAdaptiveHls: Map<String, Stream>,
|
||||
// @SerialName("drm_download_dash") val drmDownloadDash: Map<String, Stream>,
|
||||
// @SerialName("drm_download_hls") val drmDownloadHls: Map<String, Stream>,
|
||||
// @SerialName("vo_adaptive_dash") val vo_adaptive_dash: Map<String, Stream>,
|
||||
// @SerialName("vo_adaptive_hls") val vo_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
|
||||
@ -387,13 +444,11 @@ data class Stream(
|
||||
@SerialName("vcodec") val vcodec: String = "", // default/nullable value since optional
|
||||
)
|
||||
|
||||
val NonePlayback = Playback(
|
||||
"",
|
||||
mapOf(),
|
||||
Streams(
|
||||
mapOf(), mapOf(), mapOf(), mapOf(), mapOf(), mapOf(),
|
||||
mapOf(), mapOf(), mapOf(), mapOf(), mapOf(), mapOf(),
|
||||
)
|
||||
val NoneStreams = Streams(
|
||||
0,
|
||||
arrayListOf(StreamList(
|
||||
mapOf(), mapOf(), mapOf(), mapOf()
|
||||
))
|
||||
)
|
||||
|
||||
/**
|
||||
@ -404,6 +459,7 @@ data class Profile(
|
||||
@SerialName("avatar") val avatar: String,
|
||||
@SerialName("email") val email: String,
|
||||
@SerialName("maturity_rating") val maturityRating: String,
|
||||
@SerialName("preferred_content_audio_language") val preferredContentAudioLanguage: String,
|
||||
@SerialName("preferred_content_subtitle_language") val preferredContentSubtitleLanguage: String,
|
||||
@SerialName("username") val username: String,
|
||||
)
|
||||
@ -411,6 +467,7 @@ val NoneProfile = Profile(
|
||||
avatar = "",
|
||||
email = "",
|
||||
maturityRating = "",
|
||||
preferredContentAudioLanguage = "",
|
||||
preferredContentSubtitleLanguage = "",
|
||||
username = ""
|
||||
)
|
||||
@ -423,7 +480,18 @@ 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,15 +8,15 @@ import java.util.*
|
||||
|
||||
object Preferences {
|
||||
|
||||
var preferredLocale: Locale = Locale.forLanguageTag("en-US") // TODO this should be saved (potential offline usage) but fetched on start
|
||||
var preferredAudioLocale: Locale = Locale.forLanguageTag("en-US")
|
||||
internal set
|
||||
var preferSubbed = false
|
||||
var preferredSubtitleLocale: Locale = Locale.forLanguageTag("en-US")
|
||||
internal set
|
||||
var autoplay = true
|
||||
internal set
|
||||
var devSettings = false
|
||||
internal set
|
||||
var theme = DataTypes.Theme.DARK
|
||||
var theme = DataTypes.Theme.SYSTEM
|
||||
internal set
|
||||
|
||||
// dev settings
|
||||
@ -30,22 +30,22 @@ object Preferences {
|
||||
)
|
||||
}
|
||||
|
||||
fun savePreferredLocal(context: Context, preferredLocale: Locale) {
|
||||
fun savePreferredAudioLocal(context: Context, preferredLocale: Locale) {
|
||||
with(getSharedPref(context).edit()) {
|
||||
putString(context.getString(R.string.save_key_preferred_local), preferredLocale.toLanguageTag())
|
||||
apply()
|
||||
}
|
||||
|
||||
this.preferredLocale = preferredLocale
|
||||
this.preferredAudioLocale = preferredLocale
|
||||
}
|
||||
|
||||
fun savePreferSecondary(context: Context, preferSubbed: Boolean) {
|
||||
fun savePreferredSubtitleLocal(context: Context, preferredLocale: Locale) {
|
||||
with(getSharedPref(context).edit()) {
|
||||
putBoolean(context.getString(R.string.save_key_prefer_secondary), preferSubbed)
|
||||
putString(context.getString(R.string.save_key_preferred_local), preferredLocale.toLanguageTag())
|
||||
apply()
|
||||
}
|
||||
|
||||
this.preferSubbed = preferSubbed
|
||||
this.preferredSubtitleLocale = preferredLocale
|
||||
}
|
||||
|
||||
fun saveAutoplay(context: Context, autoplay: Boolean) {
|
||||
@ -90,14 +90,16 @@ object Preferences {
|
||||
fun load(context: Context) {
|
||||
val sharedPref = getSharedPref(context)
|
||||
|
||||
preferredLocale = Locale.forLanguageTag(
|
||||
preferredAudioLocale = Locale.forLanguageTag(
|
||||
sharedPref.getString(
|
||||
context.getString(R.string.save_key_preferred_audio_local), "en-US"
|
||||
) ?: "en-US"
|
||||
)
|
||||
preferredSubtitleLocale = Locale.forLanguageTag(
|
||||
sharedPref.getString(
|
||||
context.getString(R.string.save_key_preferred_local), "en-US"
|
||||
) ?: "en-US"
|
||||
)
|
||||
preferSubbed = sharedPref.getBoolean(
|
||||
context.getString(R.string.save_key_prefer_secondary), false
|
||||
)
|
||||
autoplay = sharedPref.getBoolean(
|
||||
context.getString(R.string.save_key_autoplay), true
|
||||
)
|
||||
@ -106,8 +108,8 @@ object Preferences {
|
||||
)
|
||||
theme = DataTypes.Theme.valueOf(
|
||||
sharedPref.getString(
|
||||
context.getString(R.string.save_key_theme), DataTypes.Theme.DARK.toString()
|
||||
) ?: DataTypes.Theme.DARK.toString()
|
||||
context.getString(R.string.save_key_theme), DataTypes.Theme.SYSTEM.toString()
|
||||
) ?: DataTypes.Theme.SYSTEM.toString()
|
||||
)
|
||||
|
||||
// dev settings
|
||||
|
@ -28,6 +28,7 @@ import android.util.Log
|
||||
import android.view.MenuItem
|
||||
import androidx.activity.addCallback
|
||||
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.commit
|
||||
@ -40,10 +41,9 @@ import org.mosad.teapod.preferences.EncryptedPreferences
|
||||
import org.mosad.teapod.preferences.Preferences
|
||||
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.MyListsFragment
|
||||
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.player.PlayerActivity
|
||||
import org.mosad.teapod.util.DataTypes
|
||||
import org.mosad.teapod.util.metadb.MetaDBController
|
||||
import java.util.*
|
||||
@ -70,7 +70,14 @@ class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListen
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
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.navView.setOnItemSelectedListener(this)
|
||||
@ -101,12 +108,12 @@ class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListen
|
||||
activeBaseFragment = HomeFragment()
|
||||
true
|
||||
}
|
||||
R.id.navigation_library -> {
|
||||
activeBaseFragment = LibraryFragment()
|
||||
R.id.navigation_my_lists -> {
|
||||
activeBaseFragment = MyListsFragment()
|
||||
true
|
||||
}
|
||||
R.id.navigation_search -> {
|
||||
activeBaseFragment = SearchFragment()
|
||||
R.id.navigation_library -> {
|
||||
activeBaseFragment = LibraryFragment()
|
||||
true
|
||||
}
|
||||
R.id.navigation_account -> {
|
||||
@ -123,12 +130,12 @@ class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListen
|
||||
return ret
|
||||
}
|
||||
|
||||
private fun getThemeResource(): Int {
|
||||
return when (Preferences.theme) {
|
||||
DataTypes.Theme.LIGHT -> R.style.AppTheme_Light
|
||||
else -> R.style.AppTheme_Dark
|
||||
}
|
||||
}
|
||||
// private fun getThemeResource(): Int {
|
||||
// return when (Preferences.theme) {
|
||||
// DataTypes.Theme.LIGHT -> R.style.AppTheme_Light
|
||||
// else -> R.style.AppTheme_Dark
|
||||
// }
|
||||
// }
|
||||
|
||||
/**
|
||||
* initial loading and login are run in parallel, as initial loading doesn't require
|
||||
@ -166,13 +173,15 @@ class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListen
|
||||
private fun initCrunchyroll(): List<Job> {
|
||||
val scope = CoroutineScope(Dispatchers.IO + CoroutineName("InitialCrunchyLoading"))
|
||||
return listOf(
|
||||
scope.launch { Crunchyroll.index() },
|
||||
scope.launch { Crunchyroll.account() },
|
||||
scope.launch {
|
||||
// update the local preferred content language, since it may have changed
|
||||
val locale = Locale.forLanguageTag(Crunchyroll.profile().preferredContentSubtitleLanguage)
|
||||
Preferences.savePreferredLocal(this@MainActivity, locale)
|
||||
val profile = Crunchyroll.profile()
|
||||
|
||||
val audioLocale = Locale.forLanguageTag(profile.preferredContentAudioLanguage)
|
||||
val subtitleLocale = Locale.forLanguageTag(profile.preferredContentSubtitleLanguage)
|
||||
Preferences.savePreferredAudioLocal(this@MainActivity, audioLocale)
|
||||
Preferences.savePreferredSubtitleLocal(this@MainActivity, subtitleLocale)
|
||||
}
|
||||
)
|
||||
}
|
||||
@ -190,17 +199,6 @@ class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListen
|
||||
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
|
||||
*/
|
||||
|
@ -8,17 +8,15 @@ import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.*
|
||||
import org.mosad.teapod.BuildConfig
|
||||
import org.mosad.teapod.R
|
||||
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.Profile
|
||||
import org.mosad.teapod.parser.crunchyroll.supportedLocals
|
||||
import org.mosad.teapod.parser.crunchyroll.supportedAudioLocals
|
||||
import org.mosad.teapod.parser.crunchyroll.supportedSubtitleLocals
|
||||
import org.mosad.teapod.preferences.EncryptedPreferences
|
||||
import org.mosad.teapod.preferences.Preferences
|
||||
import org.mosad.teapod.ui.activity.main.MainActivity
|
||||
@ -64,15 +62,18 @@ class AccountFragment : Fragment() {
|
||||
|
||||
// add preferred subtitles
|
||||
lifecycleScope.launch {
|
||||
binding.textSettingsContentLanguageDesc.text = Locale.forLanguageTag(
|
||||
binding.textSettingsAudioLanguageDesc.text = Locale.forLanguageTag(
|
||||
profile.await().preferredContentAudioLanguage
|
||||
).displayLanguage
|
||||
binding.textSettingsSubtitleLanguageDesc.text = Locale.forLanguageTag(
|
||||
profile.await().preferredContentSubtitleLanguage
|
||||
).displayLanguage
|
||||
}
|
||||
binding.switchSecondary.isChecked = Preferences.preferSubbed
|
||||
binding.switchAutoplay.isChecked = Preferences.autoplay
|
||||
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)
|
||||
else -> getString(R.string.theme_light)
|
||||
}
|
||||
|
||||
binding.linearDevSettings.isVisible = Preferences.devSettings
|
||||
@ -88,12 +89,12 @@ class AccountFragment : Fragment() {
|
||||
showLoginDialog()
|
||||
}
|
||||
|
||||
binding.linearSettingsContentLanguage.setOnClickListener {
|
||||
showContentLanguageSelection()
|
||||
binding.linearSettingsAudioLanguage.setOnClickListener {
|
||||
showAudioLanguageSelection()
|
||||
}
|
||||
|
||||
binding.switchSecondary.setOnClickListener {
|
||||
Preferences.savePreferSecondary(requireContext(), binding.switchSecondary.isChecked)
|
||||
binding.linearSettingsSubtitleLanguage.setOnClickListener {
|
||||
showSubtitleLanguageSelection()
|
||||
}
|
||||
|
||||
binding.switchAutoplay.setOnClickListener {
|
||||
@ -138,43 +139,86 @@ class AccountFragment : Fragment() {
|
||||
activity?.let { loginModal.show(it.supportFragmentManager, LoginModalBottomSheet.TAG) }
|
||||
}
|
||||
|
||||
private fun showContentLanguageSelection() {
|
||||
private fun showAudioLanguageSelection() {
|
||||
// we should be able to use the index of supportedLocals for language selection, items is GUI only
|
||||
val items = supportedLocals.map {
|
||||
val items = supportedAudioLocals.map {
|
||||
it.toDisplayString(getString(R.string.settings_content_language_none))
|
||||
}.toTypedArray()
|
||||
|
||||
var initialSelection: Int
|
||||
// profile should be completed here, therefore blocking
|
||||
runBlocking {
|
||||
initialSelection = supportedLocals.indexOf(Locale.forLanguageTag(
|
||||
profile.await().preferredContentSubtitleLanguage))
|
||||
if (initialSelection < 0) initialSelection = supportedLocals.lastIndex
|
||||
initialSelection = supportedAudioLocals.indexOf(Locale.forLanguageTag(
|
||||
profile.await().preferredContentAudioLanguage))
|
||||
if (initialSelection < 0) initialSelection = supportedAudioLocals.lastIndex
|
||||
}
|
||||
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.settings_content_language)
|
||||
.setTitle(R.string.settings_audio_language)
|
||||
.setSingleChoiceItems(items, initialSelection){ dialog, which ->
|
||||
updatePrefContentLanguage(supportedLocals[which])
|
||||
updateAudioLanguage(supportedAudioLocals[which])
|
||||
dialog.dismiss()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
@kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
private fun updatePrefContentLanguage(preferredLocale: Locale) {
|
||||
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))
|
||||
if (initialSelection < 0) initialSelection = supportedSubtitleLocals.lastIndex
|
||||
}
|
||||
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.settings_audio_language)
|
||||
.setSingleChoiceItems(items, initialSelection){ dialog, which ->
|
||||
updateSubtitleLanguage(supportedSubtitleLocals[which])
|
||||
dialog.dismiss()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private fun updateAudioLanguage(preferredLocale: Locale) {
|
||||
lifecycleScope.launch {
|
||||
Crunchyroll.postPrefSubLanguage(preferredLocale.toLanguageTag())
|
||||
Crunchyroll.setPreferredAudioLanguage(preferredLocale.toLanguageTag())
|
||||
|
||||
}.invokeOnCompletion {
|
||||
// update the local preferred content language
|
||||
Preferences.savePreferredLocal(requireContext(), preferredLocale)
|
||||
// update the local preferred audio language
|
||||
Preferences.savePreferredAudioLocal(requireContext(), preferredLocale)
|
||||
|
||||
// update profile since the language selection might have changed
|
||||
profile = lifecycleScope.async { Crunchyroll.profile() }
|
||||
profile.invokeOnCompletion {
|
||||
// update language once loading profile is completed
|
||||
binding.textSettingsContentLanguageDesc.text = Locale.forLanguageTag(
|
||||
binding.textSettingsAudioLanguageDesc.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
|
||||
).displayLanguage
|
||||
}
|
||||
@ -183,17 +227,19 @@ class AccountFragment : Fragment() {
|
||||
|
||||
private fun showThemeDialog() {
|
||||
val items = arrayOf(
|
||||
resources.getString(R.string.theme_system),
|
||||
resources.getString(R.string.theme_light),
|
||||
resources.getString(R.string.theme_dark)
|
||||
)
|
||||
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.settings_content_language)
|
||||
.setTitle(R.string.theme)
|
||||
.setSingleChoiceItems(items, Preferences.theme.ordinal){ _, which ->
|
||||
when(which) {
|
||||
0 -> Preferences.saveTheme(requireContext(), Theme.LIGHT)
|
||||
1 -> Preferences.saveTheme(requireContext(), Theme.DARK)
|
||||
else -> Preferences.saveTheme(requireContext(), Theme.DARK)
|
||||
0 -> Preferences.saveTheme(requireContext(), Theme.SYSTEM)
|
||||
1 -> Preferences.saveTheme(requireContext(), Theme.LIGHT)
|
||||
2 -> Preferences.saveTheme(requireContext(), Theme.DARK)
|
||||
else -> Preferences.saveTheme(requireContext(), Theme.SYSTEM)
|
||||
}
|
||||
|
||||
(activity as MainActivity).restart()
|
||||
|
@ -27,6 +27,8 @@ import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
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
|
||||
@ -39,11 +41,10 @@ import com.facebook.shimmer.ShimmerFrameLayout
|
||||
import kotlinx.coroutines.launch
|
||||
import org.mosad.teapod.R
|
||||
import org.mosad.teapod.databinding.FragmentHomeBinding
|
||||
import org.mosad.teapod.ui.activity.main.MainActivity
|
||||
import org.mosad.teapod.ui.activity.main.viewmodel.HomeViewModel
|
||||
import org.mosad.teapod.util.adapter.MediaEpisodeListAdapter
|
||||
import org.mosad.teapod.util.adapter.MediaItemListAdapter
|
||||
import org.mosad.teapod.util.decoration.MediaItemDecoration
|
||||
import org.mosad.teapod.util.playerIntent
|
||||
import org.mosad.teapod.util.setDrawableTop
|
||||
import org.mosad.teapod.util.showFragment
|
||||
import org.mosad.teapod.util.toItemMediaList
|
||||
@ -54,6 +55,12 @@ class HomeFragment : Fragment() {
|
||||
private val model: HomeViewModel by viewModels()
|
||||
private lateinit var binding: FragmentHomeBinding
|
||||
|
||||
private val itemOffset = 21
|
||||
|
||||
private val playerResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
model.updateUpNextItems()
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
binding = FragmentHomeBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
@ -62,43 +69,39 @@ class HomeFragment : Fragment() {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.recyclerUpNext.addItemDecoration(MediaItemDecoration(9))
|
||||
binding.recyclerWatchlist.addItemDecoration(MediaItemDecoration(9))
|
||||
binding.recyclerRecommendations.addItemDecoration(MediaItemDecoration(9))
|
||||
binding.recyclerNewTitles.addItemDecoration(MediaItemDecoration(9))
|
||||
binding.recyclerTopTen.addItemDecoration(MediaItemDecoration(9))
|
||||
|
||||
binding.recyclerUpNext.adapter = MediaEpisodeListAdapter(
|
||||
MediaEpisodeListAdapter.OnClickListener {
|
||||
val activity = activity
|
||||
if (activity is MainActivity) {
|
||||
activity.startPlayer(it.panel.episodeMetadata.seasonId, it.panel.id)
|
||||
}
|
||||
}
|
||||
playerResult.launch(playerIntent(it.panel.episodeMetadata.seasonId, it.panel.id))
|
||||
},
|
||||
itemOffset
|
||||
)
|
||||
|
||||
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 {
|
||||
@ -109,15 +112,12 @@ class HomeFragment : Fragment() {
|
||||
// TODO since this might take a few seconds show a loading animation for the watchlist button
|
||||
}
|
||||
|
||||
binding.buttonPlayHighlight.setOnClickListener {
|
||||
// TODO implement
|
||||
lifecycleScope.launch {
|
||||
//val media = AoDParser.getMediaById(0)
|
||||
|
||||
// Log.d(javaClass.name, "Starting Player with mediaId: ${media.aodId}")
|
||||
//(activity as MainActivity).startPlayer(media.aodId, media.playlist.first().mediaId)
|
||||
}
|
||||
}
|
||||
// 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) {
|
||||
@ -165,7 +165,46 @@ class HomeFragment : Fragment() {
|
||||
activity?.showFragment(MediaFragment(uiState.highlightItem.id))
|
||||
}
|
||||
|
||||
// disable the shimmer effect and hide the shimmer layouts
|
||||
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.
|
||||
*/
|
||||
private fun disableShimmer() {
|
||||
binding.shimmerLayoutHighlight.apply {
|
||||
stopShimmer()
|
||||
isVisible = false
|
||||
@ -190,23 +229,6 @@ class HomeFragment : Fragment() {
|
||||
stopShimmer()
|
||||
isVisible = false
|
||||
}
|
||||
|
||||
// make highlights layout visible again
|
||||
binding.linearHighlight.isVisible = true
|
||||
}
|
||||
|
||||
private fun bindUiStateLoading() {
|
||||
// hide highlights layout
|
||||
binding.linearHighlight.isVisible = false
|
||||
binding.root.children.filter { it is ShimmerFrameLayout }.forEach {
|
||||
it as ShimmerFrameLayout
|
||||
it.startShimmer()
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindUiStateError(uiState: HomeViewModel.UiState.Error) {
|
||||
// currently not used
|
||||
Log.e(classTag, "A error occurred while loading a UiState: ${uiState.message}")
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,29 +1,30 @@
|
||||
package org.mosad.teapod.ui.activity.main.fragments
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.coroutines.launch
|
||||
import org.mosad.teapod.databinding.FragmentLibraryBinding
|
||||
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.ui.activity.main.viewmodel.LibraryFragmentViewModel
|
||||
import org.mosad.teapod.util.adapter.MediaItemListAdapter
|
||||
import org.mosad.teapod.util.showFragment
|
||||
|
||||
class LibraryFragment : Fragment() {
|
||||
|
||||
private lateinit var binding: FragmentLibraryBinding
|
||||
private lateinit var adapter: MediaItemAdapter
|
||||
|
||||
private val itemList = arrayListOf<ItemMedia>()
|
||||
private val pageSize = 30
|
||||
private var nextItemIndex = 0
|
||||
private lateinit var adapter: MediaItemListAdapter
|
||||
private val model: LibraryFragmentViewModel by viewModels()
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
binding = FragmentLibraryBinding.inflate(inflater, container, false)
|
||||
@ -33,57 +34,79 @@ class LibraryFragment : Fragment() {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
// init async
|
||||
lifecycleScope.launch {
|
||||
// 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
|
||||
// TODO replace with pagination3
|
||||
// https://medium.com/swlh/paging3-recyclerview-pagination-made-easy-333c7dfa8797
|
||||
binding.recyclerMediaSearch.addOnScrollListener(PaginationScrollListener())
|
||||
|
||||
adapter = MediaItemAdapter(itemList)
|
||||
adapter.onItemClick = { mediaIdStr, _ ->
|
||||
activity?.showFragment(MediaFragment(mediaIdStr))
|
||||
}
|
||||
adapter = MediaItemListAdapter(MediaItemListAdapter.OnClickListener {
|
||||
binding.searchText.clearFocus()
|
||||
activity?.showFragment(MediaFragment(it.id))
|
||||
})
|
||||
binding.recyclerMediaSearch.adapter = adapter
|
||||
|
||||
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())
|
||||
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
|
||||
}
|
||||
})
|
||||
|
||||
inner class PaginationScrollListener: RecyclerView.OnScrollListener() {
|
||||
private var isLoading = false
|
||||
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
super.onScrolled(recyclerView, dx, dy)
|
||||
val layoutManager = recyclerView.layoutManager as GridLayoutManager?
|
||||
|
||||
if (!isLoading) layoutManager?.let {
|
||||
// itemList.size - 5 to start loading a bit earlier than the actual end
|
||||
if (layoutManager.findLastCompletelyVisibleItemPosition() >= (itemList.size - 5)) {
|
||||
// load new browse results async
|
||||
isLoading = true
|
||||
lifecycleScope.launch {
|
||||
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
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
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() {
|
||||
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
super.onScrolled(recyclerView, dx, dy)
|
||||
|
||||
if (!model.isLazyLoading) {
|
||||
val layoutManager = recyclerView.layoutManager as? GridLayoutManager
|
||||
layoutManager?.let {
|
||||
// adapter.itemCount - 10 to start loading a bit earlier than the actual end
|
||||
if (layoutManager.findLastCompletelyVisibleItemPosition() >= (adapter.itemCount - 10)) {
|
||||
model.onLazyLoad().invokeOnCompletion {
|
||||
adapter.notifyItemRangeInserted(adapter.itemCount, model.PAGESIZE)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
@ -19,12 +20,13 @@ import jp.wasabeef.glide.transformations.BlurTransformation
|
||||
import kotlinx.coroutines.launch
|
||||
import org.mosad.teapod.R
|
||||
import org.mosad.teapod.databinding.FragmentMediaBinding
|
||||
import org.mosad.teapod.parser.crunchyroll.NoneUpNextSeriesItem
|
||||
import org.mosad.teapod.ui.activity.main.MainActivity
|
||||
import org.mosad.teapod.parser.crunchyroll.NoneUpNextSeriesList
|
||||
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.TMDBMovie
|
||||
import org.mosad.teapod.util.tmdb.TMDBTVShow
|
||||
import org.mosad.teapod.util.toItemMediaList
|
||||
|
||||
/**
|
||||
* The media detail fragment.
|
||||
@ -40,8 +42,10 @@ class MediaFragment(private val mediaIdStr: String) : Fragment() {
|
||||
|
||||
private val fragments = arrayListOf<Fragment>()
|
||||
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 {
|
||||
binding = FragmentMediaBinding.inflate(inflater, container, false)
|
||||
@ -74,33 +78,6 @@ class MediaFragment(private val mediaIdStr: String) : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
if (runOnResume) {
|
||||
/**
|
||||
* FIXME
|
||||
* this is currently also run on back press when multiple MediaFragments have
|
||||
* been open and closed via similar tab
|
||||
*/
|
||||
|
||||
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
|
||||
*/
|
||||
@ -113,6 +90,7 @@ class MediaFragment(private val mediaIdStr: String) : Fragment() {
|
||||
|
||||
// load poster and backdrop
|
||||
Glide.with(requireContext()).load(posterUrl)
|
||||
.apply(RequestOptions.placeholderOf(ColorDrawable(Color.DKGRAY)))
|
||||
.into(binding.imagePoster)
|
||||
Glide.with(requireContext()).load(backdropUrl)
|
||||
.apply(RequestOptions.placeholderOf(ColorDrawable(Color.DKGRAY)))
|
||||
@ -120,14 +98,14 @@ class MediaFragment(private val mediaIdStr: String) : Fragment() {
|
||||
.into(binding.imageBackdrop)
|
||||
|
||||
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)
|
||||
else -> ""
|
||||
}
|
||||
binding.textAge.text = seriesCrunchy.maturityRatings.firstOrNull()
|
||||
|
||||
binding.textTitle.text = if (upNextSeries != NoneUpNextSeriesItem) {
|
||||
upNextSeries.panel.title
|
||||
binding.textTitle.text = if (upNextSeries != NoneUpNextSeriesList) {
|
||||
upNextSeries.data.first().panel.title
|
||||
} else seriesCrunchy.title
|
||||
binding.textOverview.text = seriesCrunchy.description
|
||||
|
||||
@ -149,20 +127,34 @@ class MediaFragment(private val mediaIdStr: String) : Fragment() {
|
||||
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)
|
||||
when (tmdbResult) {
|
||||
is TMDBTVShow -> {
|
||||
// episodes count
|
||||
binding.textEpisodesOrRuntime.text = resources.getQuantityString(
|
||||
R.plurals.text_episodes_count,
|
||||
episodesCrunchy.total,
|
||||
episodesCrunchy.total
|
||||
seriesCrunchy.episodeCount,
|
||||
seriesCrunchy.episodeCount
|
||||
)
|
||||
}
|
||||
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(
|
||||
R.plurals.text_runtime,
|
||||
tmdbMovie.runtime,
|
||||
@ -177,27 +169,14 @@ class MediaFragment(private val mediaIdStr: String) : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
// if has similar titles
|
||||
if (model.similarTo.total > 0) {
|
||||
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
|
||||
}
|
||||
|
||||
private fun initActions() = with(model) {
|
||||
binding.buttonPlay.setOnClickListener {
|
||||
if (upNextSeries != NoneUpNextSeriesItem) {
|
||||
playEpisode(upNextSeries.panel.episodeMetadata.seasonId, upNextSeries.panel.id)
|
||||
if (upNextSeries != NoneUpNextSeriesList) {
|
||||
val panel = upNextSeries.data.first().panel
|
||||
playEpisode(panel.episodeMetadata.seasonId, panel.id)
|
||||
}
|
||||
}
|
||||
|
||||
@ -218,15 +197,25 @@ class MediaFragment(private val mediaIdStr: String) : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* play the current episode
|
||||
* TODO this is also used in MediaFragmentEpisode, we should only have on implementation
|
||||
*/
|
||||
private fun playEpisode(seasonId: String, episodeId: String) {
|
||||
(activity as MainActivity).startPlayer(seasonId, episodeId)
|
||||
Log.d(javaClass.name, "Started Player with episodeId: $episodeId")
|
||||
private fun playerFinishedCallback() = lifecycleScope.launch {
|
||||
model.updateOnResume()
|
||||
|
||||
//model.updateNextEpisode(episodeId) // set the correct next episode
|
||||
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
|
||||
*/
|
||||
fun playEpisode(seasonId: String, episodeId: String) {
|
||||
playerResult.launch(playerIntent(seasonId, episodeId))
|
||||
Log.d(javaClass.name, "Started Player with episodeId: $episodeId")
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -2,7 +2,6 @@ package org.mosad.teapod.ui.activity.main.fragments
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@ -13,7 +12,6 @@ import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.mosad.teapod.R
|
||||
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.util.adapter.EpisodeItemAdapter
|
||||
|
||||
@ -37,7 +35,7 @@ class MediaFragmentEpisodes : Fragment() {
|
||||
model.tmdbTVSeason.episodes,
|
||||
model.currentPlayheads,
|
||||
EpisodeItemAdapter.OnClickListener { episode ->
|
||||
playEpisode(episode.seasonId, episode.id)
|
||||
(requireParentFragment() as? MediaFragment)?.playEpisode(episode.seasonId, episode.id)
|
||||
},
|
||||
EpisodeItemAdapter.ViewType.MEDIA_FRAGMENT
|
||||
)
|
||||
@ -69,7 +67,7 @@ class MediaFragmentEpisodes : Fragment() {
|
||||
private fun showSeasonSelection(v: View) {
|
||||
// TODO replace with Exposed dropdown menu: https://material.io/components/menus/android#exposed-dropdown-menus
|
||||
val popup = PopupMenu(requireContext(), v)
|
||||
model.seasonsCrunchy.items.forEach { season ->
|
||||
model.seasonsCrunchy.data.forEach { season ->
|
||||
popup.menu.add(getString(
|
||||
R.string.season_number_title,
|
||||
season.seasonNumber,
|
||||
@ -106,11 +104,4 @@ 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
|
||||
}
|
||||
|
||||
}
|
@ -27,17 +27,13 @@ import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import org.mosad.teapod.databinding.FragmentMediaSimilarBinding
|
||||
import org.mosad.teapod.ui.activity.main.viewmodel.MediaFragmentViewModel
|
||||
import org.mosad.teapod.util.ItemMedia
|
||||
import org.mosad.teapod.util.adapter.MediaItemListAdapter
|
||||
import org.mosad.teapod.util.decoration.MediaItemDecoration
|
||||
import org.mosad.teapod.util.showFragment
|
||||
import org.mosad.teapod.util.toItemMediaList
|
||||
|
||||
class MediaFragmentSimilar : Fragment() {
|
||||
class MediaFragmentSimilar(val items: List<ItemMedia>) : Fragment() {
|
||||
|
||||
private val model: MediaFragmentViewModel by viewModels({requireParentFragment()})
|
||||
private lateinit var binding: FragmentMediaSimilarBinding
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
@ -48,7 +44,6 @@ class MediaFragmentSimilar : Fragment() {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.recyclerMediaSimilar.addItemDecoration(MediaItemDecoration(9))
|
||||
binding.recyclerMediaSimilar.adapter = MediaItemListAdapter(
|
||||
MediaItemListAdapter.OnClickListener {
|
||||
activity?.showFragment(MediaFragment(it.id))
|
||||
@ -56,6 +51,6 @@ class MediaFragmentSimilar : Fragment() {
|
||||
)
|
||||
|
||||
val adapterSimilar = binding.recyclerMediaSimilar.adapter as MediaItemListAdapter
|
||||
adapterSimilar.submitList(model.similarTo.toItemMediaList())
|
||||
adapterSimilar.submitList(items)
|
||||
}
|
||||
}
|
@ -0,0 +1,90 @@
|
||||
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]
|
||||
}
|
||||
|
||||
}
|
@ -1,118 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
@ -26,24 +26,28 @@ import androidx.lifecycle.LifecycleCoroutineScope
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.flow.*
|
||||
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() {
|
||||
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<ContinueWatchingItem>,
|
||||
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()
|
||||
@ -62,27 +66,32 @@ class HomeViewModel : ViewModel() {
|
||||
uiState.emit(UiState.Loading)
|
||||
try {
|
||||
// run the loading in parallel to speed up the process
|
||||
val upNextJob = viewModelScope.async { Crunchyroll.upNextAccount().items }
|
||||
val watchlistJob = viewModelScope.async { Crunchyroll.watchlist(50).items }
|
||||
val upNextJob = viewModelScope.async { Crunchyroll.upNextAccount(n = 20).data }
|
||||
val watchlistJob = viewModelScope.async { Crunchyroll.watchlist(WATCHLIST_LENGTH).data }
|
||||
val recommendationsJob = viewModelScope.async {
|
||||
Crunchyroll.recommendations(20).items
|
||||
Crunchyroll.recommendations(n = 20).data
|
||||
}
|
||||
val recentlyAddedJob = viewModelScope.async {
|
||||
Crunchyroll.browse(sortBy = SortBy.NEWLY_ADDED, n = 50).items
|
||||
Crunchyroll.browse(sortBy = SortBy.NEWLY_ADDED, n = 50).data
|
||||
}
|
||||
val topTenJob = viewModelScope.async {
|
||||
Crunchyroll.browse(sortBy = SortBy.POPULARITY, n = 10).items
|
||||
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 highlightItemIsWatchlist = Crunchyroll.isWatchlist(highlightItem.id)
|
||||
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,
|
||||
highlightItemIsWatchlist
|
||||
highlightItemUpNextJob.await(), highlightItemIsWatchlistJob.await()
|
||||
))
|
||||
} catch (e: Exception) {
|
||||
uiState.emit(UiState.Error(e.message))
|
||||
@ -105,7 +114,7 @@ class HomeViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
// update the watchlist after a item has been added/removed
|
||||
val watchlistItems = Crunchyroll.watchlist(50).items
|
||||
val watchlistItems = Crunchyroll.watchlist(WATCHLIST_LENGTH).data
|
||||
|
||||
currentUiState.copy(
|
||||
watchlistItems = watchlistItems,
|
||||
@ -115,9 +124,22 @@ class HomeViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,131 @@
|
||||
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,12 +3,13 @@ package org.mosad.teapod.ui.activity.main.viewmodel
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.joinAll
|
||||
import kotlinx.coroutines.launch
|
||||
import org.mosad.teapod.parser.crunchyroll.*
|
||||
import org.mosad.teapod.preferences.Preferences
|
||||
import org.mosad.teapod.util.DataTypes.MediaType
|
||||
import org.mosad.teapod.util.tmdb.*
|
||||
import org.mosad.teapod.util.toPlayheadsMap
|
||||
|
||||
/**
|
||||
* handle media, next ep and tmdb
|
||||
@ -16,7 +17,7 @@ import org.mosad.teapod.util.tmdb.*
|
||||
*/
|
||||
class MediaFragmentViewModel(application: Application) : AndroidViewModel(application) {
|
||||
|
||||
var seriesCrunchy = NoneSeries // movies are also series
|
||||
var seriesCrunchy = NoneSeriesItem // movies are also series
|
||||
internal set
|
||||
var seasonsCrunchy = NoneSeasons
|
||||
internal set
|
||||
@ -26,11 +27,12 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic
|
||||
internal set
|
||||
val currentEpisodesCrunchy = arrayListOf<Episode>() // used for EpisodeItemAdapter (easier updates)
|
||||
|
||||
// additional media info
|
||||
// additional media info, might change during during user interaction
|
||||
// use a map to update the episode adapter values
|
||||
val currentPlayheads: MutableMap<String, PlayheadObject> = mutableMapOf()
|
||||
var isWatchlist = false
|
||||
internal set
|
||||
var upNextSeries = NoneUpNextSeriesItem
|
||||
var upNextSeries = NoneUpNextSeriesList
|
||||
internal set
|
||||
var similarTo = NoneSimilarToResult
|
||||
internal set
|
||||
@ -50,36 +52,38 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic
|
||||
suspend fun loadCrunchy(crunchyId: String) {
|
||||
// load series and seasons info in parallel
|
||||
listOf(
|
||||
viewModelScope.launch { seriesCrunchy = Crunchyroll.series(crunchyId) },
|
||||
viewModelScope.launch { seriesCrunchy = Crunchyroll.series(crunchyId).data.first() },
|
||||
viewModelScope.launch { seasonsCrunchy = Crunchyroll.seasons(crunchyId) },
|
||||
viewModelScope.launch { isWatchlist = Crunchyroll.isWatchlist(crunchyId) },
|
||||
viewModelScope.launch { upNextSeries = Crunchyroll.upNextSeries(crunchyId) },
|
||||
viewModelScope.launch { similarTo = Crunchyroll.similarTo(crunchyId) }
|
||||
).joinAll()
|
||||
|
||||
// load the preferred season (preferred language, language per season, not per stream)
|
||||
currentSeasonCrunchy = seasonsCrunchy.getPreferredSeason(Preferences.preferredLocale)
|
||||
// load the preferred season:
|
||||
// next episode > first season
|
||||
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)
|
||||
viewModelScope.launch { episodesCrunchy = Crunchyroll.episodes(currentSeasonCrunchy.id) }.join()
|
||||
currentEpisodesCrunchy.clear()
|
||||
currentEpisodesCrunchy.addAll(episodesCrunchy.items)
|
||||
currentEpisodesCrunchy.addAll(episodesCrunchy.data)
|
||||
|
||||
// set media type
|
||||
mediaType = episodesCrunchy.items.firstOrNull()?.let {
|
||||
if (it.episodeNumber != null) MediaType.TVSHOW else MediaType.MOVIE
|
||||
// set media type, for movies the episode field is empty
|
||||
mediaType = episodesCrunchy.data.firstOrNull()?.let {
|
||||
if (it.episode.isNotEmpty()) MediaType.TVSHOW else MediaType.MOVIE
|
||||
} ?: MediaType.OTHER
|
||||
|
||||
// load playheads and tmdb in parallel
|
||||
listOf(
|
||||
viewModelScope.launch {
|
||||
// get playheads (including fully watched state)
|
||||
val episodeIDs = episodesCrunchy.items.map { it.id }
|
||||
currentPlayheads.clear()
|
||||
currentPlayheads.putAll(Crunchyroll.playheads(episodeIDs))
|
||||
},
|
||||
updatePlayheadsAsync(),
|
||||
viewModelScope.launch { loadTmdbInfo() } // use tmdb search to get media info
|
||||
).joinAll()
|
||||
}
|
||||
@ -96,7 +100,6 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic
|
||||
MediaType.TVSHOW -> tmdbApiController.searchTVShow(seriesCrunchy.title)
|
||||
else -> NoneTMDBSearch
|
||||
}
|
||||
// println(tmdbSearchResult)
|
||||
|
||||
tmdbResult = if (tmdbSearchResult.results.isNotEmpty()) {
|
||||
when (val result = tmdbSearchResult.results.first()) {
|
||||
@ -105,7 +108,6 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic
|
||||
else -> NoneTMDB
|
||||
}
|
||||
} else NoneTMDB
|
||||
// println(tmdbResult)
|
||||
|
||||
// currently not used
|
||||
// tmdbTVSeason = if (tmdbResult is TMDBTVShow) {
|
||||
@ -113,6 +115,16 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic
|
||||
// } 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.
|
||||
*
|
||||
@ -124,18 +136,16 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic
|
||||
|
||||
// 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)
|
||||
currentSeasonCrunchy = seasonsCrunchy.items.firstOrNull {
|
||||
currentSeasonCrunchy = seasonsCrunchy.data.firstOrNull {
|
||||
it.id == seasonId
|
||||
} ?: currentSeasonCrunchy
|
||||
|
||||
episodesCrunchy = Crunchyroll.episodes(currentSeasonCrunchy.id)
|
||||
currentEpisodesCrunchy.clear()
|
||||
currentEpisodesCrunchy.addAll(episodesCrunchy.items)
|
||||
currentEpisodesCrunchy.addAll(episodesCrunchy.data)
|
||||
|
||||
// update playheads playheads (including fully watched state)
|
||||
val episodeIDs = episodesCrunchy.items.map { it.id }
|
||||
currentPlayheads.clear()
|
||||
currentPlayheads.putAll(Crunchyroll.playheads(episodeIDs))
|
||||
updatePlayheadsAsync().await()
|
||||
}
|
||||
|
||||
suspend fun setWatchlist() {
|
||||
@ -150,11 +160,7 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic
|
||||
|
||||
suspend fun updateOnResume() {
|
||||
joinAll(
|
||||
viewModelScope.launch {
|
||||
val episodeIDs = episodesCrunchy.items.map { it.id }
|
||||
currentPlayheads.clear()
|
||||
currentPlayheads.putAll(Crunchyroll.playheads(episodeIDs))
|
||||
},
|
||||
updatePlayheadsAsync(),
|
||||
viewModelScope.launch { upNextSeries = Crunchyroll.upNextSeries(seriesCrunchy.id) }
|
||||
)
|
||||
}
|
||||
|
@ -0,0 +1,50 @@
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -46,6 +46,7 @@ import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.exoplayer2.ExoPlayer
|
||||
import com.google.android.exoplayer2.Player
|
||||
import com.google.android.exoplayer2.ui.StyledPlayerControlView
|
||||
import com.google.android.exoplayer2.ui.StyledPlayerView
|
||||
import com.google.android.exoplayer2.util.Util
|
||||
import kotlinx.coroutines.launch
|
||||
import org.mosad.teapod.R
|
||||
@ -251,7 +252,7 @@ class PlayerActivity : AppCompatActivity() {
|
||||
playerBinding.videoView.player = model.player
|
||||
|
||||
// when the player controls get hidden, hide the bars too
|
||||
playerBinding.videoView.setControllerVisibilityListener {
|
||||
playerBinding.videoView.setControllerVisibilityListener(StyledPlayerView.ControllerVisibilityListener {
|
||||
when (it) {
|
||||
View.GONE -> {
|
||||
hideBars()
|
||||
@ -259,7 +260,7 @@ class PlayerActivity : AppCompatActivity() {
|
||||
}
|
||||
View.VISIBLE -> updateControls()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
playerBinding.videoView.setOnTouchListener { _, event ->
|
||||
gestureDetector.onTouchEvent(event)
|
||||
@ -317,19 +318,18 @@ class PlayerActivity : AppCompatActivity() {
|
||||
hideButtonNextEp()
|
||||
}
|
||||
|
||||
// if meta data is present and opening_start & opening_duration are valid, show skip opening
|
||||
model.currentEpisodeMeta?.let {
|
||||
if (it.openingDuration > 0 &&
|
||||
currentPosition in it.openingStart..(it.openingStart + 10000) &&
|
||||
!playerBinding.buttonSkipOp.isVisible
|
||||
) {
|
||||
// into metadata is present and we can show the skip button
|
||||
if (model.currentIntroMetadata.duration >= 10) {
|
||||
val startTime = model.currentIntroMetadata.startTime.toInt() * 1000
|
||||
if (currentPosition in startTime..(startTime + 10000) && !playerBinding.buttonSkipOp.isVisible) {
|
||||
showButtonSkipOp()
|
||||
} else if (playerBinding.buttonSkipOp.isVisible &&
|
||||
currentPosition !in it.openingStart..(it.openingStart + 10000)
|
||||
currentPosition !in startTime..(startTime + 10000)
|
||||
) {
|
||||
// the button should only be visible, if currentEpisodeMeta != null
|
||||
// the button should only be visible if currentEpisodeMeta != null
|
||||
hideButtonSkipOp()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// if controls are visible, update them
|
||||
@ -444,8 +444,9 @@ class PlayerActivity : AppCompatActivity() {
|
||||
|
||||
private fun skipOpening() {
|
||||
// calculate the seek time
|
||||
model.currentEpisodeMeta?.let {
|
||||
val seekTime = (it.openingStart + it.openingDuration) - model.player.currentPosition
|
||||
if (model.currentIntroMetadata.duration > 10) {
|
||||
val endTime = model.currentIntroMetadata.endTime.toInt() * 1000
|
||||
val seekTime = endTime - model.player.currentPosition
|
||||
model.seekToOffset(seekTime)
|
||||
}
|
||||
}
|
||||
|
@ -40,6 +40,7 @@ import org.mosad.teapod.util.metadb.EpisodeMeta
|
||||
import org.mosad.teapod.util.metadb.Meta
|
||||
import org.mosad.teapod.util.metadb.MetaDBController
|
||||
import org.mosad.teapod.util.metadb.TVShowMeta
|
||||
import org.mosad.teapod.util.toPlayheadsMap
|
||||
import java.util.*
|
||||
import kotlin.concurrent.scheduleAtFixedRate
|
||||
|
||||
@ -63,7 +64,9 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
|
||||
internal set
|
||||
var currentEpisodeMeta: EpisodeMeta? = null
|
||||
internal set
|
||||
var currentPlayheads: PlayheadsMap = mutableMapOf()
|
||||
var currentPlayheads = mapOf<String, PlayheadObject>()
|
||||
internal set
|
||||
var currentIntroMetadata: DatalabIntro = NoneDatalabIntro
|
||||
internal set
|
||||
// var tmdbTVSeason: TMDBTVSeason? =null
|
||||
// internal set
|
||||
@ -73,13 +76,21 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
|
||||
internal set
|
||||
var currentEpisode = NoneEpisode
|
||||
internal set
|
||||
var currentPlayback = NonePlayback
|
||||
var currentVersion = NoneVersion
|
||||
internal set
|
||||
var currentStreams = NoneStreams
|
||||
internal set
|
||||
|
||||
// current playback settings
|
||||
var currentLanguage: Locale = Preferences.preferredLocale
|
||||
var currentAudioLocale: Locale = Preferences.preferredAudioLocale
|
||||
internal set
|
||||
var currentSubtitleLocale: Locale = Preferences.preferredSubtitleLocale
|
||||
internal set
|
||||
|
||||
init {
|
||||
// disable platform diagnostics since they might be shared with google
|
||||
ExoPlayer.Builder(application).setUsePlatformDiagnostics(false)
|
||||
|
||||
initMediaSession()
|
||||
|
||||
player.addListener(object : Player.Listener {
|
||||
@ -129,10 +140,10 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
|
||||
episodes = Crunchyroll.episodes(seasonId)
|
||||
|
||||
listOf(
|
||||
viewModelScope.launch { mediaMeta = loadMediaMeta(episodes.items.first().seriesId) },
|
||||
viewModelScope.launch { mediaMeta = loadMediaMeta(episodes.data.first().seriesId) },
|
||||
viewModelScope.launch {
|
||||
val episodeIDs = episodes.items.map { it.id }
|
||||
currentPlayheads = Crunchyroll.playheads(episodeIDs)
|
||||
val episodeIDs = episodes.data.map { it.id }
|
||||
currentPlayheads = Crunchyroll.playheads(episodeIDs).toPlayheadsMap()
|
||||
}
|
||||
).joinAll()
|
||||
Log.d(classTag, "meta: $mediaMeta")
|
||||
@ -141,13 +152,35 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
|
||||
playCurrentMedia(currentPlayhead)
|
||||
}
|
||||
|
||||
fun setLanguage(language: Locale) {
|
||||
currentLanguage = language
|
||||
playCurrentMedia(player.currentPosition)
|
||||
fun setLanguage(newAudioLocale: Locale, newSubtitleLocale: Locale) {
|
||||
// TODO if the audio locale has changes update the streams, if only the subtitle locale has changed load the new stream
|
||||
if (newAudioLocale != currentAudioLocale) {
|
||||
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
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
player.seekTo(player.currentPosition + offset)
|
||||
}
|
||||
@ -161,15 +194,15 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
|
||||
*/
|
||||
fun playNextEpisode() = currentEpisode.nextEpisodeId?.let { nextEpisodeId ->
|
||||
updatePlayhead() // update playhead before switching to new episode
|
||||
setCurrentEpisode(nextEpisodeId, startPlayback = true)
|
||||
viewModelScope.launch { setCurrentEpisode(nextEpisodeId, startPlayback = true) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Set currentEpisodeCr to the episode of the given ID
|
||||
* @param episodeId The ID of the episode you want to set currentEpisodeCr to
|
||||
*/
|
||||
fun setCurrentEpisode(episodeId: String, startPlayback: Boolean = false) {
|
||||
currentEpisode = episodes.items.find { episode ->
|
||||
suspend fun setCurrentEpisode(episodeId: String, startPlayback: Boolean = false) {
|
||||
currentEpisode = episodes.data.find { episode ->
|
||||
episode.id == episodeId
|
||||
} ?: NoneEpisode
|
||||
|
||||
@ -187,24 +220,37 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
|
||||
currentEpisodeChangedListener.forEach { it() }
|
||||
|
||||
// needs to be blocking, currentPlayback must be present when calling playCurrentMedia()
|
||||
runBlocking {
|
||||
joinAll(
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
currentPlayback = Crunchyroll.playback(currentEpisode.playback)
|
||||
},
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
Crunchyroll.playheads(listOf(currentEpisode.id))[currentEpisode.id]?.let {
|
||||
// if the episode was fully watched, start at the beginning
|
||||
currentPlayhead = if (it.fullyWatched) {
|
||||
0
|
||||
} else {
|
||||
(it.playhead.times(1000)).toLong()
|
||||
}
|
||||
joinAll(
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
currentVersion = currentEpisode.versions?.firstOrNull {
|
||||
it.audioLocale == currentAudioLocale.toLanguageTag()
|
||||
} ?: currentEpisode.versions?.first() ?: NoneVersion
|
||||
|
||||
// get the current streams object, if no version is set, use streamsLink
|
||||
currentStreams = if (currentVersion != NoneVersion) {
|
||||
Crunchyroll.streamsFromMediaGUID(currentVersion.mediaGUID)
|
||||
} else {
|
||||
Crunchyroll.streams(currentEpisode.streamsLink)
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
Log.d(classTag, "playback: ${currentEpisode.playback}")
|
||||
},
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
currentIntroMetadata = Crunchyroll.datalabIntro(currentEpisode.id)
|
||||
}
|
||||
)
|
||||
Log.d(classTag, "streams: ${currentEpisode.streamsLink}")
|
||||
|
||||
if (startPlayback) {
|
||||
playCurrentMedia()
|
||||
@ -212,26 +258,26 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
|
||||
}
|
||||
|
||||
/**
|
||||
* Play the current media from currentPlaybackCr.
|
||||
* Play the current media from currentStreams.
|
||||
*
|
||||
* @param seekPosition The seek position for the episode (default = 0).
|
||||
* @param seekPosition The seek position for the media (default = 0).
|
||||
*/
|
||||
fun playCurrentMedia(seekPosition: Long = 0) {
|
||||
// get preferred stream url, set current language if it differs from the preferred one
|
||||
val preferredLocale = currentLanguage
|
||||
val preferredLocale = currentSubtitleLocale
|
||||
val fallbackLocal = Locale.US
|
||||
val url = when {
|
||||
currentPlayback.streams.adaptive_hls.containsKey(preferredLocale.toLanguageTag()) -> {
|
||||
currentPlayback.streams.adaptive_hls[preferredLocale.toLanguageTag()]?.url
|
||||
currentStreams.data[0].adaptive_hls.containsKey(preferredLocale.toLanguageTag()) -> {
|
||||
currentStreams.data[0].adaptive_hls[preferredLocale.toLanguageTag()]?.url
|
||||
}
|
||||
currentPlayback.streams.adaptive_hls.containsKey(fallbackLocal.toLanguageTag()) -> {
|
||||
currentLanguage = fallbackLocal
|
||||
currentPlayback.streams.adaptive_hls[fallbackLocal.toLanguageTag()]?.url
|
||||
currentStreams.data[0].adaptive_hls.containsKey(fallbackLocal.toLanguageTag()) -> {
|
||||
currentSubtitleLocale = fallbackLocal
|
||||
currentStreams.data[0].adaptive_hls[fallbackLocal.toLanguageTag()]?.url
|
||||
}
|
||||
else -> {
|
||||
// if no language tag is present use the first entry
|
||||
currentLanguage = Locale.ROOT
|
||||
currentPlayback.streams.adaptive_hls.entries.first().value.url
|
||||
currentSubtitleLocale = Locale.ROOT
|
||||
currentStreams.data[0].adaptive_hls.entries.first().value.url
|
||||
}
|
||||
}
|
||||
Log.i(classTag, "stream url: $url")
|
||||
@ -267,7 +313,7 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
|
||||
* @return Boolean: true if it is the last, else false.
|
||||
*/
|
||||
fun currentEpisodeIsLastEpisode(): Boolean {
|
||||
return episodes.items.lastOrNull()?.id == currentEpisode.id
|
||||
return episodes.data.lastOrNull()?.id == currentEpisode.id
|
||||
}
|
||||
|
||||
private suspend fun loadMediaMeta(crSeriesId: String): Meta? {
|
||||
@ -287,8 +333,8 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
val episodeIDs = episodes.items.map { it.id }
|
||||
currentPlayheads = Crunchyroll.playheads(episodeIDs)
|
||||
val episodeIDs = episodes.data.map { it.id }
|
||||
currentPlayheads = Crunchyroll.playheads(episodeIDs).toPlayheadsMap()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,7 @@ 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
|
||||
@ -41,18 +42,21 @@ class EpisodeListDialogFragment : DialogFragment() {
|
||||
}
|
||||
|
||||
val adapterRecEpisodes = EpisodeItemAdapter(
|
||||
model.episodes.items,
|
||||
model.episodes.data,
|
||||
null,
|
||||
model.currentPlayheads.toMap(),
|
||||
model.currentPlayheads,
|
||||
EpisodeItemAdapter.OnClickListener { episode ->
|
||||
dismiss()
|
||||
model.setCurrentEpisode(episode.id, startPlayback = true)
|
||||
// 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.items.indexOfFirst { it.id == model.currentEpisode.id }
|
||||
adapterRecEpisodes.currentSelected = model.episodes.data.indexOfFirst { it.id == model.currentEpisode.id }
|
||||
|
||||
binding.recyclerEpisodesPlayer.adapter = adapterRecEpisodes
|
||||
binding.recyclerEpisodesPlayer.scrollToPosition(adapterRecEpisodes.currentSelected)
|
||||
|
@ -9,10 +9,13 @@ 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 androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.mosad.teapod.R
|
||||
import org.mosad.teapod.databinding.PlayerLanguageSettingsBinding
|
||||
import org.mosad.teapod.ui.activity.player.PlayerViewModel
|
||||
@ -24,7 +27,8 @@ class LanguageSettingsDialogFragment : DialogFragment() {
|
||||
private lateinit var model: PlayerViewModel
|
||||
private lateinit var binding: PlayerLanguageSettingsBinding
|
||||
|
||||
private var selectedLocale = Locale.ROOT
|
||||
private var selectedSubtitleLocale = Locale.ROOT
|
||||
private var selectedAudioLocale = Locale.ROOT
|
||||
|
||||
companion object {
|
||||
const val TAG = "LanguageSettingsDialogFragment"
|
||||
@ -34,7 +38,7 @@ class LanguageSettingsDialogFragment : DialogFragment() {
|
||||
super.onCreate(savedInstanceState)
|
||||
setStyle(STYLE_NO_TITLE, R.style.FullScreenDialogStyle)
|
||||
model = ViewModelProvider(requireActivity())[PlayerViewModel::class.java]
|
||||
selectedLocale = model.currentLanguage
|
||||
selectedSubtitleLocale = model.currentSubtitleLocale
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
@ -45,23 +49,57 @@ class LanguageSettingsDialogFragment : DialogFragment() {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
model.currentPlayback.streams.adaptive_hls.keys.forEach { languageTag ->
|
||||
var selectedSubtitleView: TextView? = null
|
||||
model.currentStreams.data[0].adaptive_hls.keys.forEach { languageTag ->
|
||||
val locale = Locale.forLanguageTag(languageTag)
|
||||
addLanguage(locale, locale == model.currentLanguage) { v ->
|
||||
selectedLocale = locale
|
||||
updateSelectedLanguage(v as TextView)
|
||||
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(selectedLocale)
|
||||
dismiss()
|
||||
lifecycleScope.launch {
|
||||
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) {
|
||||
@ -69,36 +107,35 @@ class LanguageSettingsDialogFragment : DialogFragment() {
|
||||
model.player.play()
|
||||
}
|
||||
|
||||
private fun addLanguage(locale: Locale, isSelected: Boolean, onClick: View.OnClickListener) {
|
||||
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)
|
||||
|
||||
if (isSelected) {
|
||||
setTextColor(context.resources.getColor(R.color.textPrimaryDark, 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.textSecondaryDark, context.theme))
|
||||
setPadding(75, 0, 0, 0)
|
||||
}
|
||||
setTextColor(context.resources.getColor(R.color.player_text, context.theme))
|
||||
setPadding(75, 0, 0, 0)
|
||||
|
||||
setOnClickListener(onClick)
|
||||
}
|
||||
|
||||
binding.linearLanguages.addView(text)
|
||||
linear.addView(text)
|
||||
|
||||
return text
|
||||
}
|
||||
|
||||
private fun updateSelectedLanguage(selected: TextView) {
|
||||
/**
|
||||
* 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
|
||||
binding.linearLanguages.children.forEach { child ->
|
||||
languageLayout.children.forEach { child ->
|
||||
if (child is TextView) {
|
||||
child.apply {
|
||||
setTextColor(context.resources.getColor(R.color.textPrimaryDark, context.theme))
|
||||
setTextColor(context.resources.getColor(R.color.player_text, context.theme))
|
||||
setTypeface(null, Typeface.NORMAL)
|
||||
setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
|
||||
setPadding(75, 0, 0, 0)
|
||||
|
@ -0,0 +1,31 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -10,6 +10,7 @@ class DataTypes {
|
||||
}
|
||||
|
||||
enum class Theme(val str: String) {
|
||||
SYSTEM("System"),
|
||||
LIGHT("Light"),
|
||||
DARK("Dark")
|
||||
}
|
||||
|
@ -1,15 +1,30 @@
|
||||
package org.mosad.teapod.util
|
||||
|
||||
import android.content.Intent
|
||||
import android.view.View
|
||||
import android.view.Window
|
||||
import android.widget.TextView
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import org.mosad.teapod.parser.crunchyroll.Collection
|
||||
import org.mosad.teapod.parser.crunchyroll.ContinueWatchingItem
|
||||
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 java.util.*
|
||||
import org.mosad.teapod.parser.crunchyroll.PlayheadObject
|
||||
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) {
|
||||
this.setCompoundDrawablesWithIntrinsicBounds(0, drawable, 0, 0)
|
||||
@ -20,8 +35,8 @@ fun <T> concatenate(vararg lists: List<T>): List<T> {
|
||||
}
|
||||
|
||||
// TODO move to correct location
|
||||
fun Collection<Item>.toItemMediaList(): List<ItemMedia> {
|
||||
return this.items.map {
|
||||
fun CollectionV2<Item>.toItemMediaList(): List<ItemMedia> {
|
||||
return this.data.map {
|
||||
ItemMedia(it.id, it.title, it.images.poster_wide[0][0].source)
|
||||
}
|
||||
}
|
||||
@ -33,19 +48,6 @@ fun List<Item>.toItemMediaList(): List<ItemMedia> {
|
||||
}
|
||||
}
|
||||
|
||||
@JvmName("toItemMediaListContinueWatchingItem")
|
||||
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 {
|
||||
ItemMedia(it.panel.episodeMetadata.seriesId, it.panel.title, it.panel.images.thumbnail[0][0].source)
|
||||
}
|
||||
}
|
||||
|
||||
fun Locale.toDisplayString(fallback: String): String {
|
||||
return if (this.displayLanguage.isNotEmpty() && this.displayCountry.isNotEmpty()) {
|
||||
"${this.displayLanguage} (${this.displayCountry})"
|
||||
@ -56,6 +58,10 @@ fun Locale.toDisplayString(fallback: String): String {
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
@ -16,13 +16,12 @@ 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.PlayheadObject
|
||||
import org.mosad.teapod.parser.crunchyroll.PlayheadsMap
|
||||
import org.mosad.teapod.util.tmdb.TMDBTVEpisode
|
||||
|
||||
class EpisodeItemAdapter(
|
||||
private val episodes: List<Episode>,
|
||||
private val tmdbEpisodes: List<TMDBTVEpisode>?,
|
||||
private val playheads: PlayheadsMap,
|
||||
private val playheads: Map<String, PlayheadObject>,
|
||||
private val onClickListener: OnClickListener,
|
||||
private val viewType: ViewType
|
||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
|
@ -9,18 +9,21 @@ 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.ContinueWatchingItem
|
||||
import org.mosad.teapod.parser.crunchyroll.UpNextAccountItem
|
||||
|
||||
class MediaEpisodeListAdapter(private val onClickListener: OnClickListener) : ListAdapter<ContinueWatchingItem, MediaEpisodeListAdapter.MediaViewHolder>(DiffCallback) {
|
||||
class MediaEpisodeListAdapter(private val onClickListener: OnClickListener, private val itemOffset: Int = 0) : ListAdapter<UpNextAccountItem, MediaEpisodeListAdapter.MediaViewHolder>(DiffCallback) {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MediaViewHolder {
|
||||
return MediaViewHolder(
|
||||
ItemMediaBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
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) {
|
||||
@ -34,7 +37,7 @@ class MediaEpisodeListAdapter(private val onClickListener: OnClickListener) : Li
|
||||
inner class MediaViewHolder(val binding: ItemMediaBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
fun bind(item: ContinueWatchingItem) {
|
||||
fun bind(item: UpNextAccountItem) {
|
||||
val metadata = item.panel.episodeMetadata
|
||||
|
||||
binding.textTitle.text = binding.root.context.getString(R.string.season_episode_title,
|
||||
@ -54,17 +57,17 @@ class MediaEpisodeListAdapter(private val onClickListener: OnClickListener) : Li
|
||||
}
|
||||
}
|
||||
|
||||
companion object DiffCallback : DiffUtil.ItemCallback<ContinueWatchingItem>() {
|
||||
override fun areItemsTheSame(oldItem: ContinueWatchingItem, newItem: ContinueWatchingItem): Boolean {
|
||||
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: ContinueWatchingItem, newItem: ContinueWatchingItem): Boolean {
|
||||
override fun areContentsTheSame(oldItem: UpNextAccountItem, newItem: UpNextAccountItem): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
}
|
||||
|
||||
class OnClickListener(val clickListener: (item: ContinueWatchingItem) -> Unit) {
|
||||
fun onClick(item: ContinueWatchingItem) = clickListener(item)
|
||||
class OnClickListener(val clickListener: (item: UpNextAccountItem) -> Unit) {
|
||||
fun onClick(item: UpNextAccountItem) = clickListener(item)
|
||||
}
|
||||
}
|
@ -1,44 +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.RecyclerView
|
||||
import com.bumptech.glide.Glide
|
||||
import org.mosad.teapod.databinding.ItemMediaBinding
|
||||
import org.mosad.teapod.util.ItemMedia
|
||||
|
||||
@Deprecated("Use MediaItemListAdapter instead")
|
||||
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.imageEpisodePlay.isVisible = false // hide the play button for media items
|
||||
binding.root.setOnClickListener {
|
||||
onItemClick?.invoke(
|
||||
items[bindingAdapterPosition].id,
|
||||
bindingAdapterPosition
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -7,19 +7,23 @@ 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) : ListAdapter<ItemMedia, MediaItemListAdapter.MediaViewHolder>(DiffCallback) {
|
||||
class MediaItemListAdapter(private val onClickListener: OnClickListener, private val itemOffset: Int = 0) : ListAdapter<ItemMedia, MediaItemListAdapter.MediaViewHolder>(DiffCallback) {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MediaViewHolder {
|
||||
return MediaViewHolder(
|
||||
ItemMediaBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
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) {
|
||||
@ -36,7 +40,7 @@ class MediaItemListAdapter(private val onClickListener: OnClickListener) : ListA
|
||||
fun bind(item: ItemMedia) {
|
||||
binding.textTitle.text = item.title
|
||||
|
||||
Glide.with(binding.imagePoster)
|
||||
Glide.with(binding.root.context)
|
||||
.load(item.posterUrl)
|
||||
.into(binding.imagePoster)
|
||||
|
||||
|
@ -67,7 +67,7 @@ class TMDBApiController {
|
||||
): T = coroutineScope {
|
||||
val path = "$apiUrl$endpoint"
|
||||
val params = concatenate(
|
||||
listOf("api_key" to apiKey, "language" to Preferences.preferredLocale.language),
|
||||
listOf("api_key" to apiKey, "language" to Preferences.preferredSubtitleLocale.toLanguageTag()),
|
||||
parameters
|
||||
)
|
||||
|
||||
@ -90,7 +90,7 @@ class TMDBApiController {
|
||||
* NoneTMDBSearchMovie if nothing was found
|
||||
*/
|
||||
suspend fun searchMovie(query: String): TMDBSearch<TMDBSearchResultMovie> {
|
||||
val searchEndpoint = "/search/multi"
|
||||
val searchEndpoint = "/search/movie"
|
||||
val parameters = listOf("query" to query, "include_adult" to false)
|
||||
|
||||
return try {
|
||||
|
@ -32,7 +32,7 @@ import kotlinx.serialization.Serializable
|
||||
|
||||
interface TMDBResult {
|
||||
val id: Int
|
||||
val name: String
|
||||
val name: String? // for movies tmdb return string or null
|
||||
val overview: String? // for movies tmdb return string or null
|
||||
val posterPath: String?
|
||||
val backdropPath: String?
|
||||
@ -40,7 +40,7 @@ interface TMDBResult {
|
||||
|
||||
data class TMDBBase(
|
||||
override val id: Int,
|
||||
override val name: String,
|
||||
override val name: String?,
|
||||
override val overview: String?,
|
||||
override val posterPath: String?,
|
||||
override val backdropPath: String?
|
||||
@ -59,7 +59,7 @@ data class TMDBSearch<T>(
|
||||
@Serializable
|
||||
data class TMDBSearchResultMovie(
|
||||
@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("poster_path") override val posterPath: String?,
|
||||
@SerialName("backdrop_path") override val backdropPath: String?,
|
||||
@ -68,7 +68,7 @@ data class TMDBSearchResultMovie(
|
||||
@Serializable
|
||||
data class TMDBSearchResultTVShow(
|
||||
@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("poster_path") override val posterPath: String?,
|
||||
@SerialName("backdrop_path") override val backdropPath: String?,
|
||||
@ -92,7 +92,7 @@ data class TMDBMovie(
|
||||
@SerialName("release_date") val releaseDate: String,
|
||||
@SerialName("runtime") val runtime: Int?,
|
||||
@SerialName("status") val status: String,
|
||||
// TODO generes
|
||||
// TODO genres
|
||||
) : TMDBResult
|
||||
|
||||
@Serializable
|
||||
@ -102,10 +102,10 @@ data class TMDBTVShow(
|
||||
@SerialName("overview")override val overview: String,
|
||||
@SerialName("poster_path") override val posterPath: String?,
|
||||
@SerialName("backdrop_path") override val backdropPath: String?,
|
||||
@SerialName("first_air_date") val firstAirDate: String,
|
||||
@SerialName("last_air_date") val lastAirDate: String,
|
||||
@SerialName("status") val status: String,
|
||||
// TODO generes
|
||||
@SerialName("first_air_date") val firstAirDate: String?,
|
||||
@SerialName("last_air_date") val lastAirDate: String?,
|
||||
@SerialName("status") val status: String?,
|
||||
// TODO genres
|
||||
) : TMDBResult
|
||||
|
||||
// use null for nullable types, the gui needs to handle/implement a fallback for null values
|
||||
|
@ -1,5 +0,0 @@
|
||||
<?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>
|
@ -6,7 +6,7 @@
|
||||
android:shape="ring"
|
||||
android:thickness="4dp"
|
||||
android:useLevel="false">
|
||||
<solid android:color="?iconColor"/>
|
||||
<solid android:color="?colorOutline"/>
|
||||
</shape>
|
||||
</item>
|
||||
</layer-list>
|
@ -6,7 +6,7 @@
|
||||
android:shape="ring"
|
||||
android:thickness="4dp"
|
||||
android:useLevel="false">
|
||||
<solid android:color="@color/colorAccent" />
|
||||
<solid android:color="?colorSecondary" />
|
||||
</shape>
|
||||
</item>
|
||||
</layer-list>
|
@ -1,6 +1,13 @@
|
||||
<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="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 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="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>
|
||||
|
@ -4,7 +4,7 @@
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
|
||||
</vector>
|
||||
|
10
app/src/main/res/drawable/ic_baseline_audiotrack_24.xml
Normal file
10
app/src/main/res/drawable/ic_baseline_audiotrack_24.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<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>
|
@ -0,0 +1,5 @@
|
||||
<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,5 +1,10 @@
|
||||
<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="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 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="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>
|
||||
|
@ -1,5 +1,10 @@
|
||||
<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="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 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="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>
|
||||
|
@ -1,5 +1,10 @@
|
||||
<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="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 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="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>
|
||||
|
@ -1,5 +1,8 @@
|
||||
<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">
|
||||
<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="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>
|
||||
|
@ -3,7 +3,7 @@
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/iconColor">
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
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"/>
|
||||
|
@ -1,5 +1,10 @@
|
||||
<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="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 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="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>
|
||||
|
@ -1,9 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="#FFFFFF"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
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" />
|
||||
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="?shapeTextBackground"/>
|
||||
<solid android:color="?colorSurfaceVariant"/>
|
||||
<size
|
||||
android:width="1920px"
|
||||
android:height="1080px"/>
|
||||
|
7
app/src/main/res/drawable/placeholder_image_2_3.xml
Normal file
7
app/src/main/res/drawable/placeholder_image_2_3.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="?colorSurfaceVariant"/>
|
||||
<size
|
||||
android:width="400px"
|
||||
android:height="600px"/>
|
||||
</shape>
|
@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="?attr/shapeTextBackground"/>
|
||||
<solid android:color="?colorSurfaceVariant"/>
|
||||
<corners android:radius="3dp"/>
|
||||
</shape>
|
@ -9,8 +9,6 @@
|
||||
android:id="@+id/nav_view"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?themeSecondary"
|
||||
app:itemIconTint="@color/bottom_nav_item_tint"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
|
@ -19,6 +19,6 @@
|
||||
android:layout_centerInParent="true"
|
||||
android:layout_marginStart="42dp"
|
||||
android:text="@string/fwd_10_s"
|
||||
android:textColor="@color/exo_white"
|
||||
android:textColor="@color/player_white"
|
||||
android:visibility="gone" />
|
||||
</RelativeLayout>
|
@ -20,7 +20,7 @@
|
||||
android:layout_centerInParent="true"
|
||||
android:layout_marginEnd="42dp"
|
||||
android:text="@string/rwd_10_s"
|
||||
android:textColor="@color/exo_white"
|
||||
android:textColor="@color/player_white"
|
||||
android:visibility="gone" />
|
||||
|
||||
|
||||
|
@ -1,11 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.core.widget.NestedScrollView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
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.AboutFragment">
|
||||
|
||||
<LinearLayout
|
||||
@ -67,8 +64,7 @@
|
||||
android:minHeight="48dp"
|
||||
android:padding="9dp"
|
||||
android:scaleType="fitXY"
|
||||
android:src="@drawable/ic_outline_info_24"
|
||||
app:tint="?iconColor" />
|
||||
android:src="@drawable/ic_outline_info_24" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
@ -89,8 +85,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/version_desc"
|
||||
android:textColor="?textSecondary" />
|
||||
android:text="@string/version_desc" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
@ -112,8 +107,7 @@
|
||||
android:minHeight="48dp"
|
||||
android:padding="9dp"
|
||||
android:scaleType="fitXY"
|
||||
android:src="@drawable/ic_baseline_people_24"
|
||||
app:tint="?iconColor" />
|
||||
android:src="@drawable/ic_baseline_people_24" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
@ -134,8 +128,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/author_desc"
|
||||
android:textColor="?textSecondary" />
|
||||
android:text="@string/author_desc" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
@ -157,8 +150,7 @@
|
||||
android:minHeight="48dp"
|
||||
android:padding="9dp"
|
||||
android:scaleType="fitXY"
|
||||
android:src="@drawable/ic_baseline_code_24"
|
||||
app:tint="?iconColor" />
|
||||
android:src="@drawable/ic_baseline_code_24" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
@ -179,8 +171,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/teapod_repo"
|
||||
android:textColor="?textSecondary" />
|
||||
android:text="@string/teapod_repo" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
@ -202,8 +193,7 @@
|
||||
android:minHeight="48dp"
|
||||
android:padding="9dp"
|
||||
android:scaleType="fitXY"
|
||||
android:src="@drawable/ic_baseline_description_24"
|
||||
app:tint="?iconColor" />
|
||||
android:src="@drawable/ic_baseline_description_24" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
@ -224,8 +214,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/license_desc"
|
||||
android:textColor="?textSecondary" />
|
||||
android:text="@string/license_desc" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
@ -267,8 +256,7 @@
|
||||
android:layout_marginEnd="7dp"
|
||||
android:paddingBottom="5dp"
|
||||
android:text="@string/tmdb_notice"
|
||||
android:textAlignment="center"
|
||||
android:textColor="?textSecondary" />
|
||||
android:textAlignment="center" />
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
@ -4,12 +4,12 @@
|
||||
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.AccountFragment">
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="match_parent"
|
||||
android:scrollbars="none">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
@ -23,7 +23,6 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:background="?themeSecondary"
|
||||
android:elevation="5dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
@ -34,7 +33,7 @@
|
||||
android:paddingStart="7dp"
|
||||
android:paddingEnd="7dp"
|
||||
android:text="@string/account"
|
||||
android:textSize="16sp"
|
||||
android:textAppearance="@style/TextAppearance.Material3.TitleMedium"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<LinearLayout
|
||||
@ -55,8 +54,7 @@
|
||||
android:minHeight="48dp"
|
||||
android:padding="9dp"
|
||||
android:scaleType="fitXY"
|
||||
android:src="@drawable/ic_baseline_account_box_24"
|
||||
app:tint="?iconColor" />
|
||||
android:src="@drawable/ic_baseline_account_box_24" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
@ -69,15 +67,14 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/account_login_ex"
|
||||
android:textSize="16sp" />
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodyLarge" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_account_login_desc"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/account_login_desc"
|
||||
android:textColor="?textSecondary" />
|
||||
android:text="@string/account_login_desc" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
@ -99,8 +96,7 @@
|
||||
android:minHeight="48dp"
|
||||
android:padding="9dp"
|
||||
android:scaleType="fitXY"
|
||||
android:src="@drawable/ic_baseline_access_time_24"
|
||||
app:tint="?iconColor" />
|
||||
android:src="@drawable/ic_baseline_access_time_24" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
@ -113,15 +109,14 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/loading"
|
||||
android:textSize="16sp" />
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodyLarge" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_account_subscription_desc"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/account_tier"
|
||||
android:textColor="?textSecondary" />
|
||||
android:text="@string/account_tier" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
@ -132,7 +127,6 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:background="?themeSecondary"
|
||||
android:elevation="5dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
@ -143,11 +137,11 @@
|
||||
android:paddingStart="7dp"
|
||||
android:paddingEnd="7dp"
|
||||
android:text="@string/settings"
|
||||
android:textSize="16sp"
|
||||
android:textAppearance="@style/TextAppearance.Material3.TitleMedium"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/linear_settings_content_language"
|
||||
android:id="@+id/linear_settings_audio_language"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="horizontal"
|
||||
@ -157,13 +151,12 @@
|
||||
android:id="@+id/imageView4"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/settings_content_language"
|
||||
android:contentDescription="@string/settings_audio_language"
|
||||
android:minWidth="48dp"
|
||||
android:minHeight="48dp"
|
||||
android:padding="9dp"
|
||||
android:scaleType="fitXY"
|
||||
android:src="@drawable/ic_baseline_language_24"
|
||||
app:tint="?iconColor" />
|
||||
android:src="@drawable/ic_baseline_audiotrack_24" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
@ -174,82 +167,53 @@
|
||||
android:id="@+id/text_settings_content_language"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/settings_content_language"
|
||||
android:textSize="16sp" />
|
||||
android:text="@string/settings_audio_language"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodyLarge" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_settings_content_language_desc"
|
||||
android:id="@+id/text_settings_audio_language_desc"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/settings_content_language_desc"
|
||||
android:textColor="?textSecondary" />
|
||||
android:text="@string/settings_content_language_desc" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/linear_settings_secondary"
|
||||
android:id="@+id/linear_settings_subtitle_language"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal"
|
||||
android:padding="7dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imageView3"
|
||||
android:id="@+id/imageView7"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/settings_prefer_subbed"
|
||||
android:contentDescription="@string/settings_subtitle_language"
|
||||
android:minWidth="48dp"
|
||||
android:minHeight="48dp"
|
||||
android:padding="9dp"
|
||||
android:scaleType="fitXY"
|
||||
android:src="@drawable/ic_baseline_subtitles_24"
|
||||
app:tint="?iconColor" />
|
||||
android:src="@drawable/ic_baseline_subtitles_24" />
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/linearLayout"
|
||||
android:layout_width="0dp"
|
||||
<TextView
|
||||
android:id="@+id/text_settings_subtitle_language"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/switch_secondary"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
android:text="@string/settings_subtitle_language"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodyLarge" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_settings_secondary"
|
||||
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"
|
||||
<TextView
|
||||
android:id="@+id/text_settings_subtitle_language_desc"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:checked="true"
|
||||
android:contentDescription="@string/settings_prefer_subbed"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
|
||||
android:text="@string/settings_content_language_desc" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
@ -268,8 +232,7 @@
|
||||
android:minWidth="48dp"
|
||||
android:minHeight="48dp"
|
||||
android:padding="9dp"
|
||||
android:src="@drawable/ic_baseline_autorenew_24"
|
||||
app:tint="?iconColor" />
|
||||
android:src="@drawable/ic_baseline_autorenew_24" />
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
@ -290,14 +253,13 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/settings_autoplay"
|
||||
android:textSize="16sp" />
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodyLarge" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_settings_auoplay_desc"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/settings_autoplay_desc"
|
||||
android:textColor="?textSecondary" />
|
||||
android:text="@string/settings_autoplay_desc" />
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
@ -331,8 +293,7 @@
|
||||
android:minHeight="48dp"
|
||||
android:padding="9dp"
|
||||
android:scaleType="fitXY"
|
||||
android:src="@drawable/ic_baseline_style_24"
|
||||
app:tint="?iconColor" />
|
||||
android:src="@drawable/ic_baseline_style_24" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
@ -345,15 +306,14 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/theme"
|
||||
android:textSize="16sp" />
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodyLarge" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_theme_selected"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/theme_light"
|
||||
android:textColor="?textSecondary" />
|
||||
android:text="@string/theme_light" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
@ -365,7 +325,6 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:background="?themeSecondary"
|
||||
android:clipToPadding="false"
|
||||
android:elevation="5dp"
|
||||
android:orientation="vertical">
|
||||
@ -377,7 +336,7 @@
|
||||
android:paddingStart="7dp"
|
||||
android:paddingEnd="7dp"
|
||||
android:text="@string/dev_settings"
|
||||
android:textSize="16sp"
|
||||
android:textAppearance="@style/TextAppearance.Material3.TitleMedium"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<LinearLayout
|
||||
@ -397,8 +356,7 @@
|
||||
android:minHeight="48dp"
|
||||
android:padding="9dp"
|
||||
android:scaleType="fitXY"
|
||||
android:src="@drawable/ic_baseline_access_time_24"
|
||||
app:tint="?iconColor" />
|
||||
android:src="@drawable/ic_baseline_access_time_24" />
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
@ -419,14 +377,13 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/update_playhead"
|
||||
android:textSize="16sp" />
|
||||
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"
|
||||
android:textColor="?textSecondary" />
|
||||
android:text="@string/update_playhead_desc" />
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
@ -462,8 +419,7 @@
|
||||
android:minHeight="48dp"
|
||||
android:padding="9dp"
|
||||
android:scaleType="fitXY"
|
||||
app:srcCompat="@drawable/ic_outline_upload_24"
|
||||
app:tint="?iconColor" />
|
||||
app:srcCompat="@drawable/ic_outline_upload_24" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
@ -483,8 +439,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/export_data_desc"
|
||||
android:textColor="?textSecondary" />
|
||||
android:text="@string/export_data_desc" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
@ -508,8 +463,7 @@
|
||||
android:minHeight="48dp"
|
||||
android:padding="9dp"
|
||||
android:scaleType="fitXY"
|
||||
app:srcCompat="@drawable/ic_outline_download_24"
|
||||
app:tint="?iconColor" />
|
||||
app:srcCompat="@drawable/ic_outline_download_24" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
@ -529,8 +483,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/import_data_desc"
|
||||
android:textColor="?textSecondary" />
|
||||
android:text="@string/import_data_desc" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
@ -542,7 +495,6 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:background="?themeSecondary"
|
||||
android:clipToPadding="false"
|
||||
android:elevation="5dp"
|
||||
android:orientation="vertical">
|
||||
@ -554,7 +506,7 @@
|
||||
android:paddingStart="7dp"
|
||||
android:paddingEnd="7dp"
|
||||
android:text="@string/info"
|
||||
android:textSize="16sp"
|
||||
android:textAppearance="@style/TextAppearance.Material3.TitleMedium"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<LinearLayout
|
||||
@ -575,8 +527,7 @@
|
||||
android:minHeight="48dp"
|
||||
android:padding="9dp"
|
||||
android:scaleType="fitXY"
|
||||
app:srcCompat="@drawable/ic_outline_info_24"
|
||||
app:tint="?iconColor" />
|
||||
app:srcCompat="@drawable/ic_outline_info_24" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
@ -589,15 +540,14 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/info_about"
|
||||
android:textSize="16sp" />
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodyLarge" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_info_about_desc"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/info_about_desc"
|
||||
android:textColor="?textSecondary" />
|
||||
android:text="@string/info_about_desc" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
@ -5,16 +5,17 @@
|
||||
android:id="@+id/ff_test"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?themePrimary"
|
||||
tools:context=".ui.activity.main.fragments.HomeFragment">
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="match_parent"
|
||||
android:scrollbars="none">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="7dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.facebook.shimmer.ShimmerFrameLayout
|
||||
@ -69,9 +70,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:text="@string/my_list"
|
||||
android:textColor="?textSecondary"
|
||||
android:textSize="12sp"
|
||||
app:drawableTint="?buttonBackground"
|
||||
app:drawableTopCompat="@drawable/ic_baseline_add_24" />
|
||||
|
||||
<Space
|
||||
@ -86,12 +85,9 @@
|
||||
android:gravity="center"
|
||||
android:text="@string/button_play"
|
||||
android:textAllCaps="false"
|
||||
android:textColor="?themePrimary"
|
||||
android:textSize="16sp"
|
||||
app:backgroundTint="?buttonBackground"
|
||||
app:icon="@drawable/ic_baseline_play_arrow_24"
|
||||
app:iconGravity="textStart"
|
||||
app:iconTint="?themePrimary" />
|
||||
app:iconGravity="textStart" />
|
||||
|
||||
<Space
|
||||
android:layout_width="0dp"
|
||||
@ -104,9 +100,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:text="@string/info"
|
||||
android:textColor="?textSecondary"
|
||||
android:textSize="12sp"
|
||||
app:drawableTint="?buttonBackground"
|
||||
app:drawableTopCompat="@drawable/ic_outline_info_24" />
|
||||
|
||||
<Space
|
||||
@ -120,9 +114,8 @@
|
||||
<LinearLayout
|
||||
android:id="@+id/linear_up_next"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="7dp">
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_up_next"
|
||||
@ -139,7 +132,7 @@
|
||||
<com.facebook.shimmer.ShimmerFrameLayout
|
||||
android:id="@+id/shimmer_layout_up_next"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
tools:visibility="gone">
|
||||
|
||||
<LinearLayout
|
||||
@ -149,6 +142,9 @@
|
||||
<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>
|
||||
@ -156,7 +152,7 @@
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler_up_next"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:listitem="@layout/item_media" />
|
||||
@ -166,8 +162,7 @@
|
||||
android:id="@+id/linear_watchlist"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="7dp">
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_watchlist"
|
||||
@ -194,6 +189,9 @@
|
||||
<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>
|
||||
@ -201,7 +199,7 @@
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler_watchlist"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:listitem="@layout/item_media" />
|
||||
@ -239,6 +237,9 @@
|
||||
<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>
|
||||
@ -256,8 +257,7 @@
|
||||
android:id="@+id/linear_new_titles"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="7dp">
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_new_titles"
|
||||
@ -284,6 +284,9 @@
|
||||
<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>
|
||||
@ -301,8 +304,7 @@
|
||||
android:id="@+id/linear_top_ten"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="7dp">
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_top_ten"
|
||||
@ -329,6 +331,9 @@
|
||||
<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>
|
||||
|
@ -4,22 +4,35 @@
|
||||
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.LibraryFragment">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler_media_library"
|
||||
<org.mosad.teapod.ui.components.EmptySubmitSearchView
|
||||
android:id="@+id/search_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="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
|
||||
android:id="@+id/recycler_media_search"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:clipToPadding="false"
|
||||
android:padding="3dp"
|
||||
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_toTopOf="parent"
|
||||
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
|
||||
app:spanCount="2"
|
||||
tools:listitem="@layout/item_media" />
|
||||
app:layout_constraintTop_toBottomOf="@+id/search_text"
|
||||
app:spanCount="@integer/item_media_columns"
|
||||
tools:listitem="@layout/item_media">
|
||||
|
||||
</androidx.recyclerview.widget.RecyclerView>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -4,7 +4,6 @@
|
||||
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.MediaFragment">
|
||||
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
@ -14,8 +13,7 @@
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/app_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?themePrimary">
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/linear_media"
|
||||
@ -24,29 +22,42 @@
|
||||
android:orientation="vertical"
|
||||
app:layout_scrollFlags="scroll">
|
||||
|
||||
<RelativeLayout
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/image_backdrop"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:adjustViewBounds="false"
|
||||
android:contentDescription="@string/media_poster_backdrop_desc"
|
||||
android:maxHeight="231dp"
|
||||
android:minHeight="220dp"
|
||||
android:scaleType="centerCrop" />
|
||||
<FrameLayout
|
||||
android:id="@+id/frame_image_progress"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintDimensionRatio="H,16:9"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<com.google.android.material.imageview.ShapeableImageView
|
||||
android:id="@+id/image_poster"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="200dp"
|
||||
android:layout_centerInParent="true"
|
||||
app:shapeAppearance="@style/ShapeAppearance.Teapod.RoundedPoster"
|
||||
tools:src="@drawable/ic_launcher_background" />
|
||||
<ImageView
|
||||
android:id="@+id/image_backdrop"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:contentDescription="@string/media_poster_backdrop_desc"
|
||||
android:scaleType="fitCenter"
|
||||
tools:srcCompat="@android:color/darker_gray" />
|
||||
|
||||
</RelativeLayout>
|
||||
<com.google.android.material.imageview.ShapeableImageView
|
||||
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
|
||||
android:id="@+id/linear_media_info"
|
||||
@ -95,12 +106,9 @@
|
||||
android:gravity="center"
|
||||
android:text="@string/button_play"
|
||||
android:textAllCaps="false"
|
||||
android:textColor="?themePrimary"
|
||||
android:textSize="16sp"
|
||||
app:backgroundTint="?buttonBackground"
|
||||
app:icon="@drawable/ic_baseline_play_arrow_24"
|
||||
app:iconGravity="textStart"
|
||||
app:iconTint="?themePrimary" />
|
||||
app:iconGravity="textStart" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_title"
|
||||
@ -150,15 +158,13 @@
|
||||
android:paddingTop="11dp"
|
||||
android:paddingEnd="11dp"
|
||||
android:paddingBottom="7dp"
|
||||
android:src="@drawable/ic_baseline_add_24"
|
||||
app:tint="?buttonBackground" />
|
||||
android:src="@drawable/ic_baseline_add_24" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_my_list_action"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/my_list"
|
||||
android:textColor="?textSecondary"
|
||||
android:textSize="12sp" />
|
||||
|
||||
</LinearLayout>
|
||||
@ -173,9 +179,7 @@
|
||||
android:layout_marginEnd="7dp"
|
||||
android:background="@android:color/transparent"
|
||||
app:tabGravity="start"
|
||||
app:tabMode="scrollable"
|
||||
app:tabSelectedTextColor="?textPrimary"
|
||||
app:tabTextColor="?textSecondary" />
|
||||
app:tabMode="scrollable" />
|
||||
|
||||
</LinearLayout>
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
@ -194,7 +198,7 @@
|
||||
android:id="@+id/frame_loading"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?themePrimary"
|
||||
android:background="?android:colorBackground"
|
||||
android:visibility="gone">
|
||||
|
||||
<com.google.android.material.progressindicator.CircularProgressIndicator
|
||||
|
@ -16,7 +16,7 @@
|
||||
android:paddingEnd="3dp"
|
||||
android:paddingBottom="3dp"
|
||||
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
|
||||
app:spanCount="2"
|
||||
app:spanCount="@integer/item_media_columns"
|
||||
tools:listitem="@layout/item_media" />
|
||||
|
||||
</FrameLayout>
|
44
app/src/main/res/layout/fragment_my_lists.xml
Normal file
44
app/src/main/res/layout/fragment_my_lists.xml
Normal file
@ -0,0 +1,44 @@
|
||||
<?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,8 +2,7 @@
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?themePrimary">
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/image_login"
|
||||
@ -11,12 +10,12 @@
|
||||
android:layout_height="128dp"
|
||||
android:contentDescription="@string/app_name"
|
||||
android:scaleType="fitCenter"
|
||||
android:tint="?colorTeapodIcon"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/ic_launcher_foreground"
|
||||
app:tint="?buttonBackground" />
|
||||
app:srcCompat="@drawable/ic_launcher_foreground" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/linear_login"
|
||||
|
@ -1,11 +1,9 @@
|
||||
<?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"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?themePrimary">
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
@ -17,12 +15,12 @@
|
||||
android:layout_height="128dp"
|
||||
android:contentDescription="@string/app_name"
|
||||
android:scaleType="fitCenter"
|
||||
android:tint="?colorTeapodIcon"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/ic_launcher_foreground"
|
||||
app:tint="?buttonBackground" />
|
||||
app:srcCompat="@drawable/ic_launcher_foreground" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/linearLayout3"
|
||||
|
@ -1,43 +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"
|
||||
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>
|
@ -24,8 +24,8 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:contentDescription="@string/component_poster_desc"
|
||||
app:shapeAppearance="@style/ShapeAppearance.Teapod.RoundedPoster"
|
||||
app:srcCompat="@color/imagePlaceholder" />
|
||||
android:src="@drawable/placeholder_image"
|
||||
app:shapeAppearance="@style/ShapeAppearance.Teapod.RoundedPoster" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/image_episode_play"
|
||||
@ -56,7 +56,6 @@
|
||||
android:ellipsize="end"
|
||||
android:maxLines="3"
|
||||
android:text="@string/component_episode_title"
|
||||
android:textColor="?textPrimary"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<ImageView
|
||||
@ -65,8 +64,7 @@
|
||||
android:layout_height="30dp"
|
||||
android:layout_margin="2dp"
|
||||
android:contentDescription="@string/component_watched_desc"
|
||||
app:srcCompat="@drawable/ic_baseline_check_circle_24"
|
||||
app:tint="?iconColor" />
|
||||
app:srcCompat="@drawable/ic_baseline_check_circle_24" />
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
@ -74,6 +72,6 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="3"
|
||||
android:textColor="?textSecondary" />
|
||||
android:maxLines="3" />
|
||||
<!-- TODO android:textColor="?textSecondary" -->
|
||||
</LinearLayout>
|
@ -15,8 +15,8 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:contentDescription="@string/component_poster_desc"
|
||||
app:shapeAppearance="@style/ShapeAppearance.Teapod.RoundedPoster"
|
||||
app:srcCompat="@color/imagePlaceholder" />
|
||||
android:src="@drawable/placeholder_image"
|
||||
app:shapeAppearance="@style/ShapeAppearance.Teapod.RoundedPoster" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/image_episode_play"
|
||||
@ -44,7 +44,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="7dp"
|
||||
android:text="@string/component_episode_title"
|
||||
android:textColor="@color/textPrimaryDark"
|
||||
android:textColor="@color/player_text"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<View
|
||||
@ -53,7 +53,7 @@
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginTop="5dp"
|
||||
android:layout_marginBottom="5dp"
|
||||
android:background="@color/textSecondaryDark" />
|
||||
android:background="@color/player_text_secondary" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_episode_desc2"
|
||||
@ -62,6 +62,6 @@
|
||||
android:layout_marginTop="5dp"
|
||||
android:maxLines="10"
|
||||
android:text="@string/text_overview_ex"
|
||||
android:textColor="@color/textPrimaryDark" />
|
||||
android:textColor="@color/player_text" />
|
||||
|
||||
</LinearLayout>
|
@ -3,8 +3,7 @@
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?themePrimary">
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/shimmer_image_highlight"
|
||||
@ -21,7 +20,6 @@
|
||||
android:id="@+id/shimmer_linear_highlight"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?themePrimary"
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="7dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
@ -29,14 +27,14 @@
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/shimmer_image_highlight">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/shimmer_text_highlight_title"
|
||||
android:layout_width="120dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
<ImageView
|
||||
android:id="@+id/image_dummy_text"
|
||||
android:layout_width="128dp"
|
||||
android:layout_height="21dp"
|
||||
android:layout_marginTop="7dp"
|
||||
android:background="?shapeTextBackground"
|
||||
android:textSize="16sp" />
|
||||
android:layout_gravity="center"
|
||||
app:srcCompat="@drawable/shape_rounded_corner"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
@ -56,7 +54,6 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:textSize="12sp"
|
||||
app:drawableTint="?shapeTextBackground"
|
||||
app:drawableTopCompat="@drawable/ic_baseline_add_24" />
|
||||
|
||||
<Space
|
||||
@ -69,8 +66,7 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:textSize="16sp"
|
||||
app:backgroundTint="?shapeTextBackground" />
|
||||
android:textSize="16sp" />
|
||||
|
||||
<Space
|
||||
android:layout_width="0dp"
|
||||
@ -82,7 +78,6 @@
|
||||
android:layout_width="64dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
app:drawableTint="?shapeTextBackground"
|
||||
app:drawableTopCompat="@drawable/ic_outline_info_24" />
|
||||
|
||||
<Space
|
||||
|
@ -1,71 +1,79 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.card.MaterialCardView 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:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:backgroundTint="?themeSecondary"
|
||||
app:cardCornerRadius="7dp"
|
||||
app:cardElevation="4dp">
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="wrap_content"
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintWidth_max="195dp">
|
||||
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">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/frame_image_progress"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_title"
|
||||
app:layout_constraintDimensionRatio="H,16:9"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintWidth="195dp">
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/image_poster"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:contentDescription="@string/media_poster_desc"
|
||||
android:scaleType="centerCrop"
|
||||
tools:srcCompat="@color/imagePlaceholder" />
|
||||
<FrameLayout
|
||||
android:id="@+id/frame_image_progress"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_title"
|
||||
app:layout_constraintDimensionRatio="H,16:9"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/image_episode_play"
|
||||
android:layout_width="wrap_content"
|
||||
<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: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" />
|
||||
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.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>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<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,51 +1,56 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.card.MaterialCardView 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:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="3dp"
|
||||
android:backgroundTint="?themeSecondary"
|
||||
app:cardCornerRadius="7dp"
|
||||
app:cardElevation="4dp">
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_constraintWidth_max="195dp">
|
||||
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">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/frame_image_progress"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_title"
|
||||
app:layout_constraintDimensionRatio="H,16:9"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintWidth="195dp">
|
||||
<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_poster"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?shapeTextBackground"
|
||||
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>
|
||||
|
||||
</FrameLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_title"
|
||||
android:layout_width="128dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="11dp"
|
||||
android:background="?shapeTextBackground"
|
||||
android:textSize="15sp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/frame_image_progress" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
@ -4,7 +4,6 @@
|
||||
android:id="@+id/standard_bottom_sheet"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?themeSecondary"
|
||||
android:orientation="vertical"
|
||||
android:paddingTop="24dp"
|
||||
android:paddingStart="24dp"
|
||||
@ -61,8 +60,7 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:text="@string/cancel"
|
||||
android:textColor="?colorPrimary" />
|
||||
android:text="@string/cancel" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/positive_button"
|
||||
@ -70,8 +68,7 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:text="@string/save"
|
||||
android:textColor="?colorPrimary" />
|
||||
android:text="@string/save" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
@ -131,7 +131,7 @@
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="7dp"
|
||||
android:text="@string/subtitles"
|
||||
android:text="@string/language"
|
||||
android:textAllCaps="false"
|
||||
app:icon="@drawable/ic_baseline_subtitles_24"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
|
@ -36,7 +36,6 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="44dp"
|
||||
android:text="@string/subtitles"
|
||||
android:textAlignment="center"
|
||||
android:textColor="@color/player_white"
|
||||
android:textSize="18sp"
|
||||
@ -45,16 +44,79 @@
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/linear_languages"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginStart="56dp"
|
||||
android:layout_marginEnd="56dp"
|
||||
android:orientation="vertical"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintBottom_toTopOf="@+id/linear_bottom"
|
||||
app:layout_constraintEnd_toEndOf="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
|
||||
android:id="@+id/linear_bottom"
|
||||
@ -76,9 +138,9 @@
|
||||
android:layout_marginEnd="7dp"
|
||||
android:text="@string/cancel"
|
||||
android:textAllCaps="false"
|
||||
android:textColor="@color/player_white"
|
||||
android:textColor="@color/button_text_color_light"
|
||||
android:textSize="16sp"
|
||||
app:backgroundTint="@color/buttonBackgroundLight"
|
||||
app:backgroundTint="@color/button_background_light"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
@ -89,9 +151,9 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/apply"
|
||||
android:textAllCaps="false"
|
||||
android:textColor="@color/themePrimaryDark"
|
||||
android:textColor="@color/button_text_color_dark"
|
||||
android:textSize="16sp"
|
||||
app:backgroundTint="@color/buttonBackgroundDark"
|
||||
app:backgroundTint="@color/button_background_dark"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
|
@ -6,15 +6,15 @@
|
||||
android:icon="@drawable/ic_home_black_24dp"
|
||||
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
|
||||
android:id="@+id/navigation_library"
|
||||
android:icon="@drawable/ic_baseline_video_library_24"
|
||||
android:title="@string/title_library" />
|
||||
|
||||
<item
|
||||
android:id="@+id/navigation_search"
|
||||
android:icon="@drawable/ic_baseline_search_24"
|
||||
android:title="@string/title_search" />
|
||||
<item
|
||||
android:id="@+id/navigation_account"
|
||||
android:icon="@drawable/ic_baseline_account_box_24"
|
||||
|
@ -11,18 +11,18 @@
|
||||
android:label="@string/title_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
|
||||
android:id="@+id/navigation_library"
|
||||
android:name="org.mosad.teapod.ui.activity.main.fragments.LibraryFragment"
|
||||
android:label="@string/title_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
|
||||
android:id="@+id/navigation_account"
|
||||
android:name="org.mosad.teapod.ui.activity.main.fragments.AccountFragment"
|
||||
|
@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="title_home">Startseite</string>
|
||||
<string name="title_my_lists">Meine Listen</string>
|
||||
<string name="title_library">Übersicht</string>
|
||||
<string name="title_search">Suche</string>
|
||||
<string name="title_account">Account</string>
|
||||
|
||||
<!-- home fragment -->
|
||||
@ -18,6 +18,9 @@
|
||||
<!-- search fragment -->
|
||||
<string name="search_hint">Suche nach Filmen und Serien</string>
|
||||
|
||||
<!-- my lists fragment -->
|
||||
<string name="downloads">Downloads</string>
|
||||
|
||||
<!-- media fragment -->
|
||||
<string name="button_play">Abspielen</string>
|
||||
<plurals name="text_episodes_count">
|
||||
@ -42,7 +45,8 @@
|
||||
<string name="info">Info</string>
|
||||
<string name="info_about_desc">Version %1$s (%2$s)</string>
|
||||
<string name="settings">Einstellungen</string>
|
||||
<string name="settings_content_language">Bevorzuge Inhaltssprache</string>
|
||||
<string name="settings_audio_language">Audio Sprache</string>
|
||||
<string name="settings_subtitle_language">Untertielsprache</string>
|
||||
<string name="settings_content_language_desc">Englisch</string>
|
||||
<string name="settings_content_language_none">Keine</string>
|
||||
<string name="settings_prefer_subbed">Bevorzuge OmU</string>
|
||||
@ -52,6 +56,7 @@
|
||||
<string name="theme">Design</string>
|
||||
<string name="theme_light">Hell</string>
|
||||
<string name="theme_dark">Dunkel</string>
|
||||
<string name="theme_system">System</string>
|
||||
<string name="dev_settings">Entwickler Einstellungen</string>
|
||||
<string name="update_playhead">Playhead Updates</string>
|
||||
<string name="update_playhead_desc">Fortschritt bei Episoden auf cr updaten</string>
|
||||
@ -83,6 +88,7 @@
|
||||
<string name="next_episode">Nächste Folge</string>
|
||||
<string name="skip_opening">Intro überspringen</string>
|
||||
<string name="language">Sprache</string>
|
||||
<string name="audio">Audio</string>
|
||||
<string name="subtitles">Untertitel</string>
|
||||
<string name="episodes">Folgen</string>
|
||||
<string name="episode">Folge</string>
|
||||
|
44
app/src/main/res/values-night/themes.xml
Normal file
44
app/src/main/res/values-night/themes.xml
Normal file
@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<style name="Widget.App.Button" parent="Widget.Material3.Button">
|
||||
<item name="backgroundTint">@color/button_background_dark</item>
|
||||
<item name="android:textColor">@color/button_text_color_dark</item>
|
||||
<item name="iconTint">@color/button_text_color_dark</item>
|
||||
</style>
|
||||
|
||||
<style name="AppTheme" parent="Theme.Material3.Dark.NoActionBar">
|
||||
<!-- <item name="materialButtonStyle">@style/Widget.App.Button</item>-->
|
||||
<item name="searchViewStyle">@style/SearchViewStyle</item>
|
||||
<item name="materialCardViewStyle">?attr/materialCardViewElevatedStyle</item>
|
||||
|
||||
<item name="colorPrimary">@color/seed</item>
|
||||
<item name="colorOnPrimary">@color/md_theme_light_onPrimary</item>
|
||||
<item name="colorPrimaryContainer">@color/md_theme_dark_primaryContainer</item>
|
||||
<item name="colorOnPrimaryContainer">@color/md_theme_dark_onPrimaryContainer</item>
|
||||
<item name="colorSecondary">@color/md_theme_dark_secondary</item>
|
||||
<item name="colorOnSecondary">@color/md_theme_dark_onSecondary</item>
|
||||
<item name="colorSecondaryContainer">@color/md_theme_dark_secondaryContainer</item>
|
||||
<item name="colorOnSecondaryContainer">@color/md_theme_dark_onSecondaryContainer</item>
|
||||
<item name="colorTertiary">@color/md_theme_dark_tertiary</item>
|
||||
<item name="colorOnTertiary">@color/md_theme_dark_onTertiary</item>
|
||||
<item name="colorTertiaryContainer">@color/md_theme_dark_tertiaryContainer</item>
|
||||
<item name="colorOnTertiaryContainer">@color/md_theme_dark_onTertiaryContainer</item>
|
||||
<item name="colorError">@color/md_theme_dark_error</item>
|
||||
<item name="colorErrorContainer">@color/md_theme_dark_errorContainer</item>
|
||||
<item name="colorOnError">@color/md_theme_dark_onError</item>
|
||||
<item name="colorOnErrorContainer">@color/md_theme_dark_onErrorContainer</item>
|
||||
<item name="android:colorBackground">@color/md_theme_dark_background</item>
|
||||
<item name="colorOnBackground">@color/md_theme_dark_onBackground</item>
|
||||
<item name="colorSurface">@color/md_theme_dark_surface</item>
|
||||
<item name="colorOnSurface">@color/md_theme_dark_onSurface</item>
|
||||
<item name="colorSurfaceVariant">@color/md_theme_dark_surfaceVariant</item>
|
||||
<item name="colorOnSurfaceVariant">@color/md_theme_dark_onSurfaceVariant</item>
|
||||
<item name="colorOutline">@color/md_theme_dark_outline</item>
|
||||
<item name="colorOnSurfaceInverse">@color/md_theme_dark_inverseOnSurface</item>
|
||||
<item name="colorSurfaceInverse">@color/md_theme_dark_inverseSurface</item>
|
||||
<item name="colorPrimaryInverse">@color/md_theme_dark_inversePrimary</item>
|
||||
<item name="colorTeapodIcon">@color/button_background_dark</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
4
app/src/main/res/values-sw600dp/dimens.xml
Normal file
4
app/src/main/res/values-sw600dp/dimens.xml
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<item name="item_media_columns" type="integer">3</item>
|
||||
</resources>
|
4
app/src/main/res/values-sw720dp/dimens.xml
Normal file
4
app/src/main/res/values-sw720dp/dimens.xml
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<item name="item_media_columns" type="integer">4</item>
|
||||
</resources>
|
4
app/src/main/res/values-sw840dp/dimens.xml
Normal file
4
app/src/main/res/values-sw840dp/dimens.xml
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<item name="item_media_columns" type="integer">5</item>
|
||||
</resources>
|
@ -1,10 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<attr format="color" name="themePrimary"/>
|
||||
<attr format="color" name="themeSecondary"/>
|
||||
<attr format="color" name="textPrimary"/>
|
||||
<attr format="color" name="textSecondary"/>
|
||||
<attr format="color" name="iconColor"/>
|
||||
<attr format="color" name="buttonBackground"/>
|
||||
<attr format="color" name="shapeTextBackground"/>
|
||||
<attr format="color" name="colorTeapodIcon"/>
|
||||
</resources>
|
@ -1,34 +1,83 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- base theme colors -->
|
||||
<color name="colorPrimary">#66aa00</color>
|
||||
<color name="colorPrimaryLight">#99dc45</color>
|
||||
<color name="colorPrimaryDark">#317a00</color>
|
||||
<color name="colorAccent">#607d8b</color>
|
||||
<color name="imagePlaceholder">#c2c2c2</color>
|
||||
|
||||
<!-- light theme colors -->
|
||||
<color name="themePrimaryLight">#ffffff</color>
|
||||
<color name="themeSecondaryLight">#ffffff</color>
|
||||
<color name="textPrimaryLight">#de000000</color>
|
||||
<color name="textSecondaryLight">#99000000</color>
|
||||
<color name="textBackgroundLight">#55000000</color>
|
||||
<color name="iconColorLight">#99000000</color>
|
||||
<color name="buttonBackgroundLight">#000000</color>
|
||||
<color name="button_background_light">#000000</color>
|
||||
<color name="button_text_color_light">#ffffff</color>
|
||||
|
||||
<!-- dark theme colors -->
|
||||
<color name="themePrimaryDark">#121212</color>
|
||||
<color name="themeSecondaryDark">#202020</color>
|
||||
<color name="textPrimaryDark">#deffffff</color>
|
||||
<color name="textSecondaryDark">#99ffffff</color>
|
||||
<color name="textBackgroundDark">#55ffffff</color>
|
||||
<color name="iconColorDark">#99ffffff</color>
|
||||
<color name="buttonBackgroundDark">#ffffff</color>
|
||||
<color name="controlHighlightDark">#11ffffff</color>
|
||||
<color name="button_background_dark">#ffffff</color>
|
||||
<color name="button_text_color_dark">#000000</color>
|
||||
|
||||
<!-- material3 colors -->
|
||||
<color name="seed">#66aa00</color> <!-- base/primary color -->
|
||||
<color name="md_theme_light_primary">#3E6A00</color>
|
||||
<color name="md_theme_light_onPrimary">#FFFFFF</color>
|
||||
<color name="md_theme_light_primaryContainer">#99d853</color>
|
||||
<color name="md_theme_light_onPrimaryContainer">#0F2000</color>
|
||||
<color name="md_theme_light_secondary">#416916</color>
|
||||
<color name="md_theme_light_onSecondary">#FFFFFF</color>
|
||||
<color name="md_theme_light_secondaryContainer">#C1F18E</color>
|
||||
<color name="md_theme_light_onSecondaryContainer">#0E2000</color>
|
||||
<color name="md_theme_light_tertiary">#006783</color>
|
||||
<color name="md_theme_light_onTertiary">#FFFFFF</color>
|
||||
<color name="md_theme_light_tertiaryContainer">#BDE9FF</color>
|
||||
<color name="md_theme_light_onTertiaryContainer">#001F2A</color>
|
||||
<color name="md_theme_light_error">#BA1A1A</color>
|
||||
<color name="md_theme_light_errorContainer">#FFDAD6</color>
|
||||
<color name="md_theme_light_onError">#FFFFFF</color>
|
||||
<color name="md_theme_light_onErrorContainer">#410002</color>
|
||||
<color name="md_theme_light_background">#FDFCF5</color>
|
||||
<color name="md_theme_light_onBackground">#1B1C18</color>
|
||||
<color name="md_theme_light_surface">#FDFCF5</color>
|
||||
<color name="md_theme_light_onSurface">#1B1C18</color>
|
||||
<color name="md_theme_light_surfaceVariant">#E1E4D5</color>
|
||||
<color name="md_theme_light_onSurfaceVariant">#44483D</color>
|
||||
<color name="md_theme_light_outline">#75796C</color>
|
||||
<color name="md_theme_light_inverseOnSurface">#F2F1E9</color>
|
||||
<color name="md_theme_light_inverseSurface">#30312C</color>
|
||||
<color name="md_theme_light_inversePrimary">#92DA3E</color>
|
||||
<color name="md_theme_light_shadow">#000000</color>
|
||||
<color name="md_theme_light_surfaceTint">#3E6A00</color>
|
||||
<color name="md_theme_light_outlineVariant">#C5C8BA</color>
|
||||
<color name="md_theme_light_scrim">#000000</color>
|
||||
<color name="md_theme_dark_primary">#92DA3E</color>
|
||||
<color name="md_theme_dark_onPrimary">#1E3700</color>
|
||||
<color name="md_theme_dark_primaryContainer">#2D5000</color>
|
||||
<color name="md_theme_dark_onPrimaryContainer">#ACF758</color>
|
||||
<color name="md_theme_dark_secondary">#A6D475</color>
|
||||
<color name="md_theme_dark_onSecondary">#1D3700</color>
|
||||
<color name="md_theme_dark_secondaryContainer">#2C5000</color>
|
||||
<color name="md_theme_dark_onSecondaryContainer">#C1F18E</color>
|
||||
<color name="md_theme_dark_tertiary">#65D3FF</color>
|
||||
<color name="md_theme_dark_onTertiary">#003546</color>
|
||||
<color name="md_theme_dark_tertiaryContainer">#004D64</color>
|
||||
<color name="md_theme_dark_onTertiaryContainer">#BDE9FF</color>
|
||||
<color name="md_theme_dark_error">#FFB4AB</color>
|
||||
<color name="md_theme_dark_errorContainer">#93000A</color>
|
||||
<color name="md_theme_dark_onError">#690005</color>
|
||||
<color name="md_theme_dark_onErrorContainer">#FFDAD6</color>
|
||||
<color name="md_theme_dark_background">#1B1C18</color>
|
||||
<color name="md_theme_dark_onBackground">#E3E3DB</color>
|
||||
<color name="md_theme_dark_surface">#1B1C18</color>
|
||||
<color name="md_theme_dark_onSurface">#E3E3DB</color>
|
||||
<color name="md_theme_dark_surfaceVariant">#44483D</color>
|
||||
<color name="md_theme_dark_onSurfaceVariant">#C5C8BA</color>
|
||||
<color name="md_theme_dark_outline">#8E9285</color>
|
||||
<color name="md_theme_dark_inverseOnSurface">#1B1C18</color>
|
||||
<color name="md_theme_dark_inverseSurface">#E3E3DB</color>
|
||||
<color name="md_theme_dark_inversePrimary">#3E6A00</color>
|
||||
<color name="md_theme_dark_shadow">#000000</color>
|
||||
<color name="md_theme_dark_surfaceTint">#92DA3E</color>
|
||||
<color name="md_theme_dark_outlineVariant">#44483D</color>
|
||||
<color name="md_theme_dark_scrim">#000000</color>
|
||||
|
||||
<!-- player colors -->
|
||||
<color name="player_white">#ffffff</color>
|
||||
<color name="player_text">#deffffff</color>
|
||||
<color name="player_text_secondary">#99ffffff</color>
|
||||
|
||||
<!-- launcher/splash screen colors -->
|
||||
<color name="ic_launcher_background">#ffffff</color>
|
||||
<color name="ic_splash_background">#ffffff</color>
|
||||
|
||||
</resources>
|
@ -2,4 +2,5 @@
|
||||
<resources>
|
||||
<dimen name="player_styled_progress_layout_height">28dp</dimen>
|
||||
<dimen name="player_styled_progress_margin_bottom">52dp</dimen>
|
||||
<item name="item_media_columns" type="integer">2</item>
|
||||
</resources>
|
||||
|
@ -1,8 +1,8 @@
|
||||
<resources>
|
||||
<string name="app_name" translatable="false">Teapod</string>
|
||||
<string name="title_home">Home</string>
|
||||
<string name="title_my_lists">My Lists</string>
|
||||
<string name="title_library">Library</string>
|
||||
<string name="title_search">Search</string>
|
||||
<string name="title_account">Account</string>
|
||||
|
||||
<!-- home fragment -->
|
||||
@ -21,6 +21,10 @@
|
||||
<string name="media_poster_desc" translatable="false">poster</string>
|
||||
<string name="media_poster_backdrop_desc" translatable="false">poster backdrop</string>
|
||||
|
||||
<!-- my lists fragment -->
|
||||
<string name="crunchylists" translatable="false">Crunchylists</string>
|
||||
<string name="downloads">Downloads</string>
|
||||
|
||||
<!-- media fragment -->
|
||||
<string name="button_play">Play</string>
|
||||
<string name="text_title_ex" translatable="false">A Silent Voice</string>
|
||||
@ -55,7 +59,8 @@
|
||||
<string name="account_tier_mega_fan" translatable="false">Mega Fan</string>
|
||||
<string name="account_tier_ultimate_fan" translatable="false">Ultimate Fan</string>
|
||||
<string name="settings">Settings</string>
|
||||
<string name="settings_content_language">Preferred content language</string>
|
||||
<string name="settings_audio_language">Audio language</string>
|
||||
<string name="settings_subtitle_language">Subtitle language</string>
|
||||
<string name="settings_content_language_desc">English</string>
|
||||
<string name="settings_content_language_none">None</string>
|
||||
<string name="settings_prefer_subbed">Prefer subbed</string>
|
||||
@ -65,6 +70,7 @@
|
||||
<string name="theme">Theme</string>
|
||||
<string name="theme_light">Light</string>
|
||||
<string name="theme_dark">Dark</string>
|
||||
<string name="theme_system">System</string>
|
||||
<string name="dev_settings">Developer Settings</string>
|
||||
<string name="update_playhead">Playhead updates</string>
|
||||
<string name="update_playhead_desc">Update episode playhead on cr</string>
|
||||
@ -108,6 +114,7 @@
|
||||
<string name="time_min_sec" translatable="false">%1$02d:%2$02d</string>
|
||||
<string name="time_hour_min_sec" translatable="false">%1$d:%2$02d:%3$02d</string>
|
||||
<string name="language">Language</string>
|
||||
<string name="audio">Audio</string>
|
||||
<string name="subtitles">Subtitles</string>
|
||||
<string name="episodes">Episodes</string>
|
||||
<string name="episode">Episode</string>
|
||||
@ -146,6 +153,7 @@
|
||||
<string name="save_key_user_password" translatable="false">org.mosad.teapod.user_password</string>
|
||||
<!-- for legacy reasons the prefer subbed key is called prefer_secondary-->
|
||||
<string name="save_key_prefer_secondary" translatable="false">org.mosad.teapod.prefer_secondary</string>
|
||||
<string name="save_key_preferred_audio_local" translatable="false">org.mosad.teapod.preferred_audio_local</string>
|
||||
<string name="save_key_preferred_local" translatable="false">org.mosad.teapod.preferred_local</string>
|
||||
<string name="save_key_autoplay" translatable="false">org.mosad.teapod.autoplay</string>
|
||||
<string name="save_key_dev_settings" translatable="false">org.mosad.teapod.dev.settings</string>
|
||||
|
@ -1,53 +1,10 @@
|
||||
<resources>
|
||||
<!-- application themes -->
|
||||
<style name="AppTheme" parent="Theme.MaterialComponents.Light.NoActionBar">
|
||||
<item name="colorPrimary">@color/colorPrimary</item>
|
||||
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||
<item name="colorAccent">@color/colorAccent</item>
|
||||
<item name="popupMenuStyle">@style/Widget.App.PopupMenu</item>
|
||||
</style>
|
||||
|
||||
<style name="AppTheme.Light" parent="AppTheme">
|
||||
<item name="themePrimary">@color/themePrimaryLight</item>
|
||||
<item name="themeSecondary">@color/themeSecondaryLight</item>
|
||||
<item name="textPrimary">@color/textPrimaryLight</item>
|
||||
<item name="textSecondary">@color/textSecondaryLight</item>
|
||||
<item name="android:textColor">@color/textPrimaryLight</item>
|
||||
<item name="android:textColorPrimary">@color/textPrimaryLight</item>
|
||||
<item name="android:textColorHint">@color/textSecondaryLight</item>
|
||||
<item name="shapeTextBackground">@color/textBackgroundLight</item>
|
||||
<item name="iconColor">@color/iconColorLight</item>
|
||||
<item name="buttonBackground">@color/buttonBackgroundLight</item>
|
||||
</style>
|
||||
|
||||
<style name="AppTheme.Dark" parent="AppTheme">
|
||||
<item name="themePrimary">@color/themePrimaryDark</item>
|
||||
<item name="themeSecondary">@color/themeSecondaryDark</item>
|
||||
<item name="textPrimary">@color/textPrimaryDark</item>
|
||||
<item name="textSecondary">@color/textSecondaryDark</item>
|
||||
<item name="android:textColor">@color/textPrimaryDark</item>
|
||||
<item name="android:textColorPrimary">@color/textPrimaryDark</item>
|
||||
<item name="android:textColorHint">@color/textSecondaryDark</item>
|
||||
<item name="shapeTextBackground">@color/textBackgroundDark</item>
|
||||
<item name="iconColor">@color/iconColorDark</item>
|
||||
<item name="buttonBackground">@color/buttonBackgroundDark</item>
|
||||
|
||||
<item name="materialAlertDialogTheme">@style/ThemeOverlay.App.MaterialAlertDialog.Dark</item>
|
||||
<!-- change on click indicator color for manually set components -->
|
||||
<item name="colorControlHighlight">@color/controlHighlightDark</item>
|
||||
</style>
|
||||
|
||||
<!-- dialog themes -->
|
||||
<style name="ThemeOverlay.App.MaterialAlertDialog.Dark" parent="ThemeOverlay.MaterialComponents.MaterialAlertDialog">
|
||||
<item name="colorPrimary">@color/colorPrimary</item>
|
||||
<item name="colorSurface">@color/themeSecondaryDark</item>
|
||||
<item name="colorOnSurface">@color/textPrimaryDark</item>
|
||||
<item name="android:colorControlNormal">@color/textSecondaryDark</item> <!-- Radio button unchecked-->
|
||||
<item name="materialAlertDialogTitleTextStyle">@style/MaterialAlertDialog.App.Title.Text</item>
|
||||
</style>
|
||||
|
||||
<style name="MaterialAlertDialog.App.Title.Text" parent="MaterialAlertDialog.MaterialComponents.Title.Text">
|
||||
<item name="android:textColor">?textPrimary</item>
|
||||
<!-- search view style -->
|
||||
<style name="SearchViewStyle" parent="Widget.AppCompat.SearchView.ActionBar">
|
||||
<item name="iconifiedByDefault">false</item>
|
||||
<item name="searchIcon">@drawable/ic_baseline_search_24</item>
|
||||
<item name="queryHint">@string/search_hint</item>
|
||||
</style>
|
||||
|
||||
<!-- player theme -->
|
||||
@ -71,21 +28,15 @@
|
||||
<item name="windowSplashScreenAnimationDuration">200</item>
|
||||
|
||||
<!-- Set the theme of the Activity that directly follows your splash screen. -->
|
||||
<item name="postSplashScreenTheme">@style/AppTheme.Dark</item> # Required.
|
||||
<item name="postSplashScreenTheme">@style/AppTheme</item> <!-- Required -->
|
||||
</style>
|
||||
|
||||
|
||||
<!-- shapes -->
|
||||
<style name="ShapeAppearance.Teapod.RoundedPoster" parent="ShapeAppearance.MaterialComponents.LargeComponent">
|
||||
<item name="cornerFamily">rounded</item>
|
||||
<item name="cornerSize">5dp</item>
|
||||
</style>
|
||||
|
||||
<!-- popup menus -->
|
||||
<style name="Widget.App.PopupMenu" parent="Widget.MaterialComponents.PopupMenu">
|
||||
<item name="android:popupBackground">?themeSecondary</item>
|
||||
</style>
|
||||
|
||||
<!-- fullscreen dialog fragments -->
|
||||
<style name="FullScreenDialogStyle" parent="AppTheme">
|
||||
<item name="android:windowFullscreen">true</item>
|
||||
@ -95,5 +46,4 @@
|
||||
<item name="android:windowTranslucentNavigation">true</item>
|
||||
</style>
|
||||
|
||||
|
||||
</resources>
|
42
app/src/main/res/values/themes.xml
Normal file
42
app/src/main/res/values/themes.xml
Normal file
@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="Widget.App.Button" parent="Widget.Material3.Button">
|
||||
<item name="backgroundTint">@color/button_background_light</item>
|
||||
<item name="android:textColor">@color/button_text_color_light</item>
|
||||
<item name="iconTint">@color/button_text_color_light</item>
|
||||
</style>
|
||||
|
||||
<style name="AppTheme" parent="Theme.Material3.Light.NoActionBar">
|
||||
<!-- <item name="materialButtonStyle">@style/Widget.App.Button</item>-->
|
||||
<item name="searchViewStyle">@style/SearchViewStyle</item>
|
||||
<item name="materialCardViewStyle">?attr/materialCardViewElevatedStyle</item>
|
||||
|
||||
<item name="colorPrimary">@color/seed</item>
|
||||
<item name="colorOnPrimary">@color/md_theme_light_onPrimary</item>
|
||||
<item name="colorPrimaryContainer">@color/md_theme_light_primaryContainer</item>
|
||||
<item name="colorOnPrimaryContainer">@color/md_theme_light_onPrimaryContainer</item>
|
||||
<item name="colorSecondary">@color/md_theme_light_secondary</item>
|
||||
<item name="colorOnSecondary">@color/md_theme_light_onSecondary</item>
|
||||
<item name="colorSecondaryContainer">@color/md_theme_light_secondaryContainer</item>
|
||||
<item name="colorOnSecondaryContainer">@color/md_theme_light_onSecondaryContainer</item>
|
||||
<item name="colorTertiary">@color/md_theme_light_tertiary</item>
|
||||
<item name="colorOnTertiary">@color/md_theme_light_onTertiary</item>
|
||||
<item name="colorTertiaryContainer">@color/md_theme_light_tertiaryContainer</item>
|
||||
<item name="colorOnTertiaryContainer">@color/md_theme_light_onTertiaryContainer</item>
|
||||
<item name="colorError">@color/md_theme_light_error</item>
|
||||
<item name="colorErrorContainer">@color/md_theme_light_errorContainer</item>
|
||||
<item name="colorOnError">@color/md_theme_light_onError</item>
|
||||
<item name="colorOnErrorContainer">@color/md_theme_light_onErrorContainer</item>
|
||||
<item name="android:colorBackground">@color/md_theme_light_background</item>
|
||||
<item name="colorOnBackground">@color/md_theme_light_onBackground</item>
|
||||
<item name="colorSurface">@color/md_theme_light_surface</item>
|
||||
<item name="colorOnSurface">@color/md_theme_light_onSurface</item>
|
||||
<item name="colorSurfaceVariant">@color/md_theme_light_surfaceVariant</item>
|
||||
<item name="colorOnSurfaceVariant">@color/md_theme_light_onSurfaceVariant</item>
|
||||
<item name="colorOutline">@color/md_theme_light_outline</item>
|
||||
<item name="colorOnSurfaceInverse">@color/md_theme_light_inverseOnSurface</item>
|
||||
<item name="colorSurfaceInverse">@color/md_theme_light_inverseSurface</item>
|
||||
<item name="colorPrimaryInverse">@color/md_theme_light_inversePrimary</item>
|
||||
<item name="colorTeapodIcon">@color/button_background_light</item>
|
||||
</style>
|
||||
</resources>
|
14
build.gradle
14
build.gradle
@ -1,14 +1,14 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
buildscript {
|
||||
ext.kotlin_version = "1.7.10"
|
||||
ext.ktor_version = "2.1.1"
|
||||
ext.exo_version = "2.17.1"
|
||||
ext.kotlin_version = "2.0.20"
|
||||
ext.ktor_version = "3.0.0"
|
||||
ext.exo_version = "2.18.7"
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.3.0'
|
||||
classpath 'com.android.tools.build:gradle:8.7.1'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
@ -23,6 +23,6 @@ allprojects {
|
||||
}
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
||||
tasks.register('clean', Delete) {
|
||||
delete rootProject.layout.buildDirectory
|
||||
}
|
||||
|
6
fastlane/metadata/android/de/changelogs/100000.txt
Normal file
6
fastlane/metadata/android/de/changelogs/100000.txt
Normal file
@ -0,0 +1,6 @@
|
||||
Dies ist der erste stabile Release von Teapod mit Unterstützung für Cunchyroll.
|
||||
|
||||
* Unterstützung für Crunchyroll hinzugefügt (Ein premium Account wird benötigt)
|
||||
* Diverse UI/UX Verbesserungen
|
||||
|
||||
Alle Änderungen: https://git.mosad.xyz/Seil0/teapod/compare/0.4.2...1.0.0
|
9
fastlane/metadata/android/de/changelogs/100990.txt
Normal file
9
fastlane/metadata/android/de/changelogs/100990.txt
Normal file
@ -0,0 +1,9 @@
|
||||
Dies ist der erste beta Release von Teapod 1.1.
|
||||
|
||||
* Unterstützung für Crunchyroll v2 API
|
||||
* Intro überspringen hinzugefügt
|
||||
* Seperaten Screen für Meine Liste
|
||||
* Dynamische Spaltenanzahl für alle Screens um große Bildschirme besser zu unterstützen
|
||||
* Kleine UI/UX Verbesserungen
|
||||
|
||||
Alle Änderungen: https://git.mosad.xyz/Seil0/teapod/compare/1.0.0...1.1.0-beta1
|
10
fastlane/metadata/android/de/changelogs/100991.txt
Normal file
10
fastlane/metadata/android/de/changelogs/100991.txt
Normal file
@ -0,0 +1,10 @@
|
||||
Dies ist der zweite beta Release von Teapod 1.1.
|
||||
|
||||
* Neues App Design (Material Design 3)
|
||||
* Unterstützung für Crunchyroll v2 API
|
||||
* Intro überspringen hinzugefügt
|
||||
* Seperaten Screen für "Meine Liste"
|
||||
* Dynamische Spaltenanzahl für alle Screens um große Bildschirme besser zu unterstützen
|
||||
* Kleine UI/UX Verbesserungen
|
||||
|
||||
Alle Änderungen: https://git.mosad.xyz/Seil0/teapod/compare/1.0.0...1.1.0-beta2
|
10
fastlane/metadata/android/de/changelogs/100992.txt
Normal file
10
fastlane/metadata/android/de/changelogs/100992.txt
Normal file
@ -0,0 +1,10 @@
|
||||
Dies ist der dritte beta Release von Teapod 1.1.
|
||||
|
||||
* Neues App Design (Material Design 3)
|
||||
* Unterstützung für Crunchyroll v2 API
|
||||
* Intro überspringen hinzugefügt
|
||||
* Seperaten Screen für "Meine Liste"
|
||||
* Dynamische Spaltenanzahl für alle Screens um große Bildschirme besser zu unterstützen
|
||||
* Kleine UI/UX Verbesserungen
|
||||
|
||||
Alle Änderungen: https://git.mosad.xyz/Seil0/teapod/compare/1.0.0...1.1.0-beta3
|
6
fastlane/metadata/android/en-US/changelogs/100000.txt
Normal file
6
fastlane/metadata/android/en-US/changelogs/100000.txt
Normal file
@ -0,0 +1,6 @@
|
||||
This is the first stable release of Teapod with support for crunchyroll.
|
||||
|
||||
* Support for crunchyroll (a premium account is needed)
|
||||
* UI/UX improvements
|
||||
|
||||
Full changelog: https://git.mosad.xyz/Seil0/teapod/compare/0.4.2...1.0.0
|
9
fastlane/metadata/android/en-US/changelogs/100990.txt
Normal file
9
fastlane/metadata/android/en-US/changelogs/100990.txt
Normal file
@ -0,0 +1,9 @@
|
||||
This is the first beta release of Teapod 1.1.
|
||||
|
||||
* Migrate crunchyroll parser to v2 (fixes crunchyroll)
|
||||
* Add skip intro function
|
||||
* Add a separate Watchlist fragment
|
||||
* Dynamically set column count based on the display size
|
||||
* Minor UI/UX improvements
|
||||
|
||||
Full changelog: https://git.mosad.xyz/Seil0/teapod/compare/1.0.0...1.1.0-beta1
|
10
fastlane/metadata/android/en-US/changelogs/100991.txt
Normal file
10
fastlane/metadata/android/en-US/changelogs/100991.txt
Normal file
@ -0,0 +1,10 @@
|
||||
This is the second beta release of Teapod 1.1.
|
||||
|
||||
* Migrate to material design 3
|
||||
* Migrate crunchyroll parser to v2 (fixes crunchyroll)
|
||||
* Add skip intro function
|
||||
* Add a separate Watchlist fragment
|
||||
* Dynamically set column count based on the display size
|
||||
* Minor UI/UX improvements
|
||||
|
||||
Full changelog: https://git.mosad.xyz/Seil0/teapod/compare/1.0.0...1.1.0-beta2
|
10
fastlane/metadata/android/en-US/changelogs/100992.txt
Normal file
10
fastlane/metadata/android/en-US/changelogs/100992.txt
Normal file
@ -0,0 +1,10 @@
|
||||
This is the third beta release of Teapod 1.1.
|
||||
|
||||
* Migrate to material design 3
|
||||
* Migrate crunchyroll parser to v2 (fixes crunchyroll)
|
||||
* Add skip intro function
|
||||
* Add a separate Watchlist fragment
|
||||
* Dynamically set column count based on the display size
|
||||
* Minor UI/UX improvements
|
||||
|
||||
Full changelog: https://git.mosad.xyz/Seil0/teapod/compare/1.0.0...1.1.0-beta3
|
@ -16,6 +16,8 @@ org.gradle.jvmargs=-Xmx2048m
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
android.useAndroidX=true
|
||||
# Automatically convert third-party libraries to use AndroidX
|
||||
android.enableJetifier=true
|
||||
android.enableJetifier=false
|
||||
# Kotlin code style for this project: "official" or "obsolete":
|
||||
kotlin.code.style=official
|
||||
kotlin.code.style=official
|
||||
android.nonTransitiveRClass=false
|
||||
android.nonFinalResIds=false
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,5 +1,7 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
44
gradlew
vendored
44
gradlew
vendored
@ -15,6 +15,8 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
@ -55,7 +57,7 @@
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
@ -80,13 +82,12 @@ do
|
||||
esac
|
||||
done
|
||||
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
|
||||
APP_NAME="Gradle"
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
|
||||
' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
@ -133,22 +134,29 @@ location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
@ -193,11 +201,15 @@ if "$cygwin" || "$msys" ; then
|
||||
done
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command;
|
||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||
# shell script including quotes and variable substitutions, so put them in
|
||||
# double quotes to make sure that they get re-expanded; and
|
||||
# * put everything else in single quotes, so that it's not re-expanded.
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
@ -205,6 +217,12 @@ set -- \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
|
37
gradlew.bat
vendored
37
gradlew.bat
vendored
@ -13,8 +13,10 @@
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@ -25,7 +27,8 @@
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@ -40,13 +43,13 @@ if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto execute
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
@ -56,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
@ -75,13 +78,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
Loading…
Reference in New Issue
Block a user