Compare commits
	
		
			53 Commits
		
	
	
		
	
	| 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 | 
| @ -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 100000 //01.00.000 | ||||
|         versionName "1.0.0" | ||||
|         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 | ||||
| @ -42,10 +44,9 @@ import org.mosad.teapod.databinding.FragmentHomeBinding | ||||
| 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.startPlayer | ||||
| import org.mosad.teapod.util.toItemMediaList | ||||
|  | ||||
| class HomeFragment : Fragment() { | ||||
| @ -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,40 +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 { | ||||
|                 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 { | ||||
| @ -106,6 +112,13 @@ class HomeFragment : Fragment() { | ||||
|             // TODO since this might take a few seconds show a loading animation for the watchlist button | ||||
|         } | ||||
|  | ||||
|         // set the shimmer items size as it's depending on the screen size | ||||
|         setShimmerLayoutItemSize(binding.shimmerLayoutUpNext) | ||||
|         setShimmerLayoutItemSize(binding.shimmerLayoutWatchlist) | ||||
|         setShimmerLayoutItemSize(binding.shimmerLayoutRecommendations) | ||||
|         setShimmerLayoutItemSize(binding.shimmerLayoutNewTitles) | ||||
|         setShimmerLayoutItemSize(binding.shimmerLayoutTopTen) | ||||
|  | ||||
|         viewLifecycleOwner.lifecycleScope.launch { | ||||
|             viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { | ||||
|                 model.onUiState(viewLifecycleOwner.lifecycleScope) { uiState -> | ||||
| @ -154,7 +167,7 @@ class HomeFragment : Fragment() { | ||||
|  | ||||
|         binding.buttonPlayHighlight.setOnClickListener { | ||||
|             val panel = uiState.highlightItemUpNext.panel | ||||
|             activity?.startPlayer(panel.episodeMetadata.seasonId, panel.id) | ||||
|             playerResult.launch(playerIntent(panel.episodeMetadata.seasonId, panel.id)) | ||||
|         } | ||||
|  | ||||
|         // disable the shimmer effect | ||||
| @ -167,10 +180,19 @@ class HomeFragment : Fragment() { | ||||
|     private fun bindUiStateLoading() { | ||||
|         // hide highlights layout | ||||
|         binding.linearHighlight.isVisible = false | ||||
|         println(binding.root.childCount) | ||||
|         binding.root.children.filter { it is ShimmerFrameLayout }.forEach { | ||||
|             it as ShimmerFrameLayout | ||||
|             it.startShimmer() | ||||
|  | ||||
|         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 | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|  | ||||
| @ -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,19 +26,22 @@ 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>, | ||||
| @ -63,23 +66,23 @@ 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 highlightItemUpNextJob = viewModelScope.async { | ||||
|                     Crunchyroll.upNextSeries(highlightItem.id) | ||||
|                     Crunchyroll.upNextSeries(highlightItem.id).data.first() | ||||
|                 } | ||||
|                 val highlightItemIsWatchlistJob = viewModelScope.async { | ||||
|                     Crunchyroll.isWatchlist(highlightItem.id) | ||||
| @ -111,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, | ||||
| @ -123,4 +126,20 @@ 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 | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
| @ -9,7 +9,6 @@ import androidx.fragment.app.Fragment | ||||
| import androidx.fragment.app.FragmentActivity | ||||
| import androidx.fragment.app.commit | ||||
| import org.mosad.teapod.R | ||||
| import org.mosad.teapod.ui.activity.player.PlayerActivity | ||||
| import kotlin.system.exitProcess | ||||
|  | ||||
| /** | ||||
| @ -25,20 +24,6 @@ fun FragmentActivity.showFragment(fragment: Fragment) { | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Start the player as new activity. | ||||
|  * | ||||
|  * @param seasonId The ID of the season the episode to be played is in | ||||
|  * @param episodeId The ID of the episode to play | ||||
|  */ | ||||
| fun Activity.startPlayer(seasonId: String, episodeId: String) { | ||||
|     val intent = Intent(this, PlayerActivity::class.java).apply { | ||||
|         putExtra(getString(R.string.intent_season_id), seasonId) | ||||
|         putExtra(getString(R.string.intent_episode_id), episodeId) | ||||
|     } | ||||
|     startActivity(intent) | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * hide the status and navigation bar | ||||
|  */ | ||||
|  | ||||
| @ -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 | ||||
|         ) | ||||
|  | ||||
|  | ||||
| @ -102,9 +102,9 @@ 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, | ||||
|     @SerialName("first_air_date") val firstAirDate: String?, | ||||
|     @SerialName("last_air_date") val lastAirDate: String?, | ||||
|     @SerialName("status") val status: String?, | ||||
|     // TODO genres | ||||
| ) : TMDBResult | ||||
|  | ||||
|  | ||||
| @ -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" | ||||
| @ -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,50 +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_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> | ||||
|  | ||||
|         <ImageView | ||||
|             android:id="@+id/image_dummy_text" | ||||
|             android:layout_width="128dp" | ||||
|             android:layout_height="19dp" | ||||
|             android:layout_margin="11dp" | ||||
|             app:layout_constraintBottom_toBottomOf="parent" | ||||
|             app:layout_constraintEnd_toEndOf="parent" | ||||
|             app:layout_constraintStart_toStartOf="parent" | ||||
|             app:layout_constraintTop_toBottomOf="@+id/frame_image_progress" | ||||
|             app:srcCompat="@drawable/shape_rounded_corner" | ||||
|             tools:ignore="ContentDescription" /> | ||||
|     </androidx.constraintlayout.widget.ConstraintLayout> | ||||
|  | ||||
| </com.google.android.material.card.MaterialCardView> | ||||
| </androidx.constraintlayout.widget.ConstraintLayout> | ||||
|  | ||||
| @ -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> | ||||
							
								
								
									
										12
									
								
								build.gradle
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								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 | ||||
| } | ||||
							
								
								
									
										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 | ||||
							
								
								
									
										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 | ||||
| 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 | ||||
|  | ||||
		Reference in New Issue
	
	Block a user