Compare commits
12 Commits
1.1.0-beta
...
develop
Author | SHA1 | Date |
---|---|---|
Jannik | b07a6fd407 | |
Jannik | 7d661712f7 | |
Jannik | 8fcf047e99 | |
Jannik | 17dbe945e5 | |
Jannik | 5f609d4c33 | |
Jannik | 6515f657d0 | |
Jannik | c448b44fc4 | |
Jannik | 88ebc378d3 | |
Jannik | 1a012cba7d | |
Jannik | 59a457430e | |
Jannik | 0662d656ac | |
Jannik | 3549a3d2a7 |
|
@ -4,16 +4,23 @@ plugins {
|
||||||
id 'org.jetbrains.kotlin.plugin.serialization' version "$kotlin_version"
|
id 'org.jetbrains.kotlin.plugin.serialization' version "$kotlin_version"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvmToolchain 11
|
||||||
|
sourceSets.configureEach {
|
||||||
|
languageSettings.optIn("kotlin.RequiresOptIn")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 33
|
compileSdk 34
|
||||||
buildToolsVersion "30.0.3"
|
buildToolsVersion = '34.0.0'
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "org.mosad.teapod"
|
applicationId "org.mosad.teapod"
|
||||||
minSdkVersion 23
|
minSdk 23
|
||||||
targetSdkVersion 33
|
targetSdk 33
|
||||||
versionCode 100991 //01.00.000
|
versionCode 100992 //01.00.000
|
||||||
versionName "1.1.0-beta2"
|
versionName "1.1.0-beta3"
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
resValue "string", "build_time", buildTime()
|
resValue "string", "build_time", buildTime()
|
||||||
|
@ -22,6 +29,7 @@ android {
|
||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
viewBinding true
|
viewBinding true
|
||||||
|
buildConfig true
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
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'
|
namespace 'org.mosad.teapod'
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation fileTree(dir: "libs", include: ["*.jar"])
|
implementation fileTree(dir: "libs", include: ["*.jar"])
|
||||||
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
|
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
|
||||||
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0'
|
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0'
|
||||||
|
|
||||||
implementation 'androidx.core:core-ktx:1.10.0'
|
implementation 'androidx.core:core-ktx:1.12.0'
|
||||||
implementation 'androidx.core:core-splashscreen:1.0.0'
|
implementation 'androidx.core:core-splashscreen:1.0.1'
|
||||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.5.3'
|
implementation 'androidx.navigation:navigation-fragment-ktx:2.7.7'
|
||||||
implementation 'androidx.navigation:navigation-ui-ktx:2.5.3'
|
implementation 'androidx.navigation:navigation-ui-ktx:2.7.7'
|
||||||
implementation 'androidx.security:security-crypto:1.1.0-alpha05'
|
implementation 'androidx.security:security-crypto:1.1.0-alpha06'
|
||||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1'
|
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0'
|
||||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1'
|
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0'
|
||||||
|
implementation "androidx.paging:paging-runtime-ktx:3.2.1"
|
||||||
|
|
||||||
implementation 'com.google.android.material:material:1.8.0'
|
implementation 'com.google.android.material:material:1.11.0'
|
||||||
implementation "com.google.android.exoplayer:exoplayer-core:$exo_version"
|
implementation "com.google.android.exoplayer:exoplayer-core:$exo_version"
|
||||||
implementation "com.google.android.exoplayer:exoplayer-hls:$exo_version"
|
implementation "com.google.android.exoplayer:exoplayer-hls:$exo_version"
|
||||||
implementation "com.google.android.exoplayer:exoplayer-dash:$exo_version"
|
implementation "com.google.android.exoplayer:exoplayer-dash:$exo_version"
|
||||||
|
|
|
@ -34,7 +34,6 @@ import io.ktor.http.*
|
||||||
import io.ktor.serialization.*
|
import io.ktor.serialization.*
|
||||||
import io.ktor.serialization.kotlinx.json.*
|
import io.ktor.serialization.kotlinx.json.*
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.serialization.decodeFromString
|
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.JsonObject
|
import kotlinx.serialization.json.JsonObject
|
||||||
import kotlinx.serialization.json.buildJsonObject
|
import kotlinx.serialization.json.buildJsonObject
|
||||||
|
@ -59,16 +58,12 @@ object Crunchyroll {
|
||||||
|
|
||||||
private lateinit var token: Token
|
private lateinit var token: Token
|
||||||
private var tokenValidUntil: Long = 0
|
private var tokenValidUntil: Long = 0
|
||||||
@OptIn(DelicateCoroutinesApi::class)
|
@OptIn(DelicateCoroutinesApi::class, ExperimentalCoroutinesApi::class)
|
||||||
private val tokenRefreshContext = newSingleThreadContext("TokenRefreshContext")
|
private val tokenRefreshContext = newSingleThreadContext("TokenRefreshContext")
|
||||||
|
|
||||||
private var accountID = ""
|
private var accountID = ""
|
||||||
private var externalID = ""
|
private var externalID = ""
|
||||||
|
|
||||||
private var policy = ""
|
|
||||||
private var signature = ""
|
|
||||||
private var keyPairID = ""
|
|
||||||
|
|
||||||
private val browsingCache = hashMapOf<String, BrowseResult>()
|
private val browsingCache = hashMapOf<String, BrowseResult>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -213,27 +208,10 @@ object Crunchyroll {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Basic functions: index, account
|
* Basic functions: account
|
||||||
* Needed for other functions to work properly!
|
* Needed for other functions to work properly!
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the identifiers necessary for streaming. If the identifiers are
|
|
||||||
* retrieved, set the corresponding global var. The identifiers are valid for 24h.
|
|
||||||
*/
|
|
||||||
suspend fun index() {
|
|
||||||
val indexEndpoint = "/index/v2"
|
|
||||||
|
|
||||||
val index: Index = requestGet(indexEndpoint)
|
|
||||||
policy = index.cms.policy
|
|
||||||
signature = index.cms.signature
|
|
||||||
keyPairID = index.cms.keyPairId
|
|
||||||
|
|
||||||
Log.i(TAG, "Policy : $policy")
|
|
||||||
Log.i(TAG, "Signature : $signature")
|
|
||||||
Log.i(TAG, "Key Pair ID : $keyPairID")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve the account id and set the corresponding global var.
|
* Retrieve the account id and set the corresponding global var.
|
||||||
* The account id is needed for other calls.
|
* The account id is needed for other calls.
|
||||||
|
@ -261,24 +239,30 @@ object Crunchyroll {
|
||||||
/**
|
/**
|
||||||
* Browse the media available on crunchyroll.
|
* Browse the media available on crunchyroll.
|
||||||
*
|
*
|
||||||
* @param sortBy
|
* @param start start of the item list, used for pagination, default = 0
|
||||||
* @param n Number of items to return, defaults to 10
|
* @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.
|
* @return A **[BrowseResult]** object is returned.
|
||||||
*/
|
*/
|
||||||
suspend fun browse(
|
suspend fun browse(
|
||||||
categories: List<Categories> = emptyList(),
|
|
||||||
sortBy: SortBy = SortBy.ALPHABETICAL,
|
|
||||||
seasonTag: String = "",
|
|
||||||
start: Int = 0,
|
start: Int = 0,
|
||||||
n: Int = 10
|
n: Int = 10,
|
||||||
|
sortBy: SortBy = SortBy.ALPHABETICAL,
|
||||||
|
ratings: Boolean = false,
|
||||||
|
seasonTag: String = "",
|
||||||
|
categories: List<Categories> = emptyList()
|
||||||
): BrowseResult {
|
): BrowseResult {
|
||||||
val browseEndpoint = "/content/v1/browse"
|
val browseEndpoint = "/content/v2/discover/browse"
|
||||||
val parameters = mutableListOf(
|
val parameters = mutableListOf(
|
||||||
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag(),
|
|
||||||
"sort_by" to sortBy.str,
|
|
||||||
"start" to start,
|
"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
|
// if a season tag is present add it to the parameters
|
||||||
|
@ -303,9 +287,10 @@ object Crunchyroll {
|
||||||
NoneBrowseResult
|
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
|
// Note: this value is totally guessed and should be replaced by a properly researched value
|
||||||
// TODO 100 is way to high as it's not the number of items but BrowseResults
|
|
||||||
if (browsingCache.size > 10) {
|
if (browsingCache.size > 10) {
|
||||||
browsingCache.clear()
|
browsingCache.clear()
|
||||||
}
|
}
|
||||||
|
@ -321,19 +306,20 @@ object Crunchyroll {
|
||||||
* Search fo a query term.
|
* Search fo a query term.
|
||||||
* Note: currently this function only supports series/tv shows.
|
* Note: currently this function only supports series/tv shows.
|
||||||
*
|
*
|
||||||
* TODO migrate to v2
|
|
||||||
*
|
|
||||||
* @param query The query term as String
|
* @param query The query term as String
|
||||||
* @param n The maximum number of results to return, default = 10
|
* @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
|
* @return A **[SearchResult]** object
|
||||||
*/
|
*/
|
||||||
suspend fun search(query: String, n: Int = 10): SearchResult {
|
suspend fun search(query: String, n: Int = 10, ratings: Boolean = false): SearchResult {
|
||||||
val searchEndpoint = "/content/v1/search"
|
val searchEndpoint = "/content/v2/discover/search"
|
||||||
val parameters = listOf(
|
val parameters = listOf(
|
||||||
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag(),
|
|
||||||
"q" to query,
|
"q" to query,
|
||||||
"n" to n,
|
"n" to n,
|
||||||
"type" to "series"
|
"type" to "series",
|
||||||
|
"ratings" to ratings,
|
||||||
|
"preferred_audio_language" to Preferences.preferredSubtitleLocale.toLanguageTag(),
|
||||||
|
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag()
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO episodes have thumbnails as image, and not poster_tall/poster_tall,
|
// TODO episodes have thumbnails as image, and not poster_tall/poster_tall,
|
||||||
|
@ -352,22 +338,22 @@ object Crunchyroll {
|
||||||
* Note: episode objects are currently not supported
|
* Note: episode objects are currently not supported
|
||||||
*
|
*
|
||||||
* @param objects The object IDs as list of Strings
|
* @param objects The object IDs as list of Strings
|
||||||
|
* @param ratings add user rating to the objects
|
||||||
* @return A **[Collection]** of Panels
|
* @return A **[Collection]** of Panels
|
||||||
*/
|
*/
|
||||||
suspend fun objects(objects: List<String>): Collection<Item> {
|
suspend fun objects(objects: List<String>, ratings: Boolean = false): CollectionV2<Item> {
|
||||||
val episodesEndpoint = "/cms/v2/DE/M3/crunchyroll/objects/${objects.joinToString(",")}"
|
val episodesEndpoint = "/content/v2/cms/objects/${objects.joinToString(",")}"
|
||||||
val parameters = listOf(
|
val parameters = listOf(
|
||||||
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag(),
|
"ratings" to ratings,
|
||||||
"Signature" to signature,
|
"preferred_audio_language" to Preferences.preferredAudioLocale.toLanguageTag(),
|
||||||
"Policy" to policy,
|
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag()
|
||||||
"Key-Pair-Id" to keyPairID
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
requestGet(episodesEndpoint, parameters)
|
requestGet(episodesEndpoint, parameters)
|
||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
Log.e(TAG, "Exception in objects().", ex)
|
Log.e(TAG, "Exception in objects().", ex)
|
||||||
NoneCollection
|
NoneCollectionV2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -508,7 +494,7 @@ object Crunchyroll {
|
||||||
)
|
)
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
(requestGet(watchlistSeriesEndpoint, parameters) as Collection2<IsWatchlistItem>)
|
(requestGet(watchlistSeriesEndpoint, parameters) as CollectionV2<IsWatchlistItem>)
|
||||||
.total == 1
|
.total == 1
|
||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
Log.e(TAG, "Exception in isWatchlist() with seriesId $seriesId", ex)
|
Log.e(TAG, "Exception in isWatchlist() with seriesId $seriesId", ex)
|
||||||
|
@ -630,14 +616,16 @@ object Crunchyroll {
|
||||||
*
|
*
|
||||||
* @param seriesId The crunchyroll series id of the media
|
* @param seriesId The crunchyroll series id of the media
|
||||||
* @param n The maximum number of results to return, default = 10
|
* @param n The maximum number of results to return, default = 10
|
||||||
|
* @param ratings add user rating to the objects
|
||||||
* @return A **[SimilarToResult]** object
|
* @return A **[SimilarToResult]** object
|
||||||
*/
|
*/
|
||||||
suspend fun similarTo(seriesId: String, n: Int = 10): SimilarToResult {
|
suspend fun similarTo(seriesId: String, n: Int = 10, ratings: Boolean = false): SimilarToResult {
|
||||||
val similarToEndpoint = "/content/v1/$accountID/similar_to"
|
val similarToEndpoint = "/content/v2/discover/$accountID/similar_to/$seriesId"
|
||||||
val parameters = listOf(
|
val parameters = listOf(
|
||||||
"guid" to seriesId,
|
"n" to n,
|
||||||
|
"ratings" to ratings,
|
||||||
|
"preferred_audio_language" to Preferences.preferredSubtitleLocale.toLanguageTag(),
|
||||||
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag(),
|
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag(),
|
||||||
"n" to n
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
|
@ -658,7 +646,7 @@ object Crunchyroll {
|
||||||
* @param n Number of items to return, defaults to 20.
|
* @param n Number of items to return, defaults to 20.
|
||||||
* @return A **[Collection]** containing up to n **[Item]**.
|
* @return A **[Collection]** containing up to n **[Item]**.
|
||||||
*/
|
*/
|
||||||
suspend fun watchlist(n: Int = 20): Collection<Item> {
|
suspend fun watchlist(n: Int = 20): CollectionV2<Item> {
|
||||||
val watchlistEndpoint = "/content/v2/discover/$accountID/watchlist"
|
val watchlistEndpoint = "/content/v2/discover/$accountID/watchlist"
|
||||||
val parameters = listOf(
|
val parameters = listOf(
|
||||||
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag(),
|
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag(),
|
||||||
|
@ -680,10 +668,10 @@ object Crunchyroll {
|
||||||
/**
|
/**
|
||||||
* List the next up episodes for the logged in account.
|
* List the next up episodes for the logged in account.
|
||||||
*
|
*
|
||||||
* @param n Number of items to return, defaults to 20.
|
* @param n Number of items to return, default = 20
|
||||||
* @return A **[HistoryList]** containing up to n **[UpNextAccountItem]**.
|
* @return A **[HistoryList]** containing up to n **[UpNextAccountItem]**.
|
||||||
*/
|
*/
|
||||||
suspend fun upNextAccount(n: Int = 20): HistoryList {
|
suspend fun upNextAccount(n: Int = 10): HistoryList {
|
||||||
val watchlistEndpoint = "/content/v2/discover/$accountID/history"
|
val watchlistEndpoint = "/content/v2/discover/$accountID/history"
|
||||||
val parameters = listOf(
|
val parameters = listOf(
|
||||||
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag(),
|
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag(),
|
||||||
|
@ -698,13 +686,21 @@ object Crunchyroll {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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(
|
val parameters = listOf(
|
||||||
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag(),
|
|
||||||
"n" to n,
|
|
||||||
"start" to start,
|
"start" to start,
|
||||||
"variant_id" to 0
|
"n" to n,
|
||||||
|
"ratings" to ratings,
|
||||||
|
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag(),
|
||||||
)
|
)
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
|
@ -740,7 +736,7 @@ object Crunchyroll {
|
||||||
*
|
*
|
||||||
* @param languageTag the preferred language as language tag
|
* @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 profileEndpoint = "/accounts/v1/me/profile"
|
||||||
val json = buildJsonObject {
|
val json = buildJsonObject {
|
||||||
put("preferred_content_subtitle_language", languageTag)
|
put("preferred_content_subtitle_language", languageTag)
|
||||||
|
@ -749,6 +745,20 @@ object Crunchyroll {
|
||||||
requestPatch(profileEndpoint, bodyObject = json)
|
requestPatch(profileEndpoint, bodyObject = json)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Patch the preferred content audio language.
|
||||||
|
*
|
||||||
|
* @param languageTag the preferred language as language tag
|
||||||
|
*/
|
||||||
|
suspend fun setPreferredAudioLanguage(languageTag: String) {
|
||||||
|
val profileEndpoint = "/accounts/v1/me/profile"
|
||||||
|
val json = buildJsonObject {
|
||||||
|
put("preferred_content_audio_language", languageTag)
|
||||||
|
}
|
||||||
|
|
||||||
|
requestPatch(profileEndpoint, bodyObject = json)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get additional profile (benefits) information for the currently logged in account.
|
* Get additional profile (benefits) information for the currently logged in account.
|
||||||
*
|
*
|
||||||
|
|
|
@ -24,19 +24,47 @@ package org.mosad.teapod.parser.crunchyroll
|
||||||
|
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import java.util.*
|
import java.util.Locale
|
||||||
|
|
||||||
val supportedLocals = listOf(
|
val supportedAudioLocals = listOf(
|
||||||
Locale.forLanguageTag("ar-SA"),
|
Locale.forLanguageTag("ar-SA"),
|
||||||
|
Locale.forLanguageTag("ca-ES"),
|
||||||
|
Locale.forLanguageTag("de-DE"),
|
||||||
|
Locale.forLanguageTag("en-US"),
|
||||||
|
Locale.forLanguageTag("en-IN"),
|
||||||
|
Locale.forLanguageTag("es-419"),
|
||||||
|
Locale.forLanguageTag("es-ES"),
|
||||||
|
Locale.forLanguageTag("fr-FR"),
|
||||||
|
Locale.forLanguageTag("hi-IN"),
|
||||||
|
Locale.forLanguageTag("it-IT"),
|
||||||
|
Locale.forLanguageTag("ko-KR"),
|
||||||
|
Locale.forLanguageTag("pl-PL"),
|
||||||
|
Locale.forLanguageTag("pt-BR"),
|
||||||
|
Locale.forLanguageTag("pt-PT"),
|
||||||
|
Locale.forLanguageTag("ru-RU"),
|
||||||
|
Locale.forLanguageTag("ta-IN"),
|
||||||
|
Locale.forLanguageTag("th-TH"),
|
||||||
|
Locale.forLanguageTag("zh-CN"),
|
||||||
|
Locale.forLanguageTag("zh-TW"),
|
||||||
|
Locale.ROOT
|
||||||
|
)
|
||||||
|
|
||||||
|
val supportedSubtitleLocals = listOf(
|
||||||
|
Locale.forLanguageTag("ar-SA"),
|
||||||
|
Locale.forLanguageTag("ca-ES"),
|
||||||
Locale.forLanguageTag("de-DE"),
|
Locale.forLanguageTag("de-DE"),
|
||||||
Locale.forLanguageTag("en-US"),
|
Locale.forLanguageTag("en-US"),
|
||||||
Locale.forLanguageTag("es-419"),
|
Locale.forLanguageTag("es-419"),
|
||||||
Locale.forLanguageTag("es-ES"),
|
Locale.forLanguageTag("es-ES"),
|
||||||
Locale.forLanguageTag("fr-FR"),
|
Locale.forLanguageTag("fr-FR"),
|
||||||
|
Locale.forLanguageTag("hi-IN"),
|
||||||
Locale.forLanguageTag("it-IT"),
|
Locale.forLanguageTag("it-IT"),
|
||||||
|
Locale.forLanguageTag("ms-MY"),
|
||||||
|
Locale.forLanguageTag("pl-PL"),
|
||||||
Locale.forLanguageTag("pt-BR"),
|
Locale.forLanguageTag("pt-BR"),
|
||||||
Locale.forLanguageTag("pt-PT"),
|
Locale.forLanguageTag("pt-PT"),
|
||||||
Locale.forLanguageTag("ru-RU"),
|
Locale.forLanguageTag("ru-RU"),
|
||||||
|
Locale.forLanguageTag("tr-TR"),
|
||||||
Locale.ROOT
|
Locale.ROOT
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -44,6 +72,10 @@ val supportedLocals = listOf(
|
||||||
* data classes for browse
|
* data classes for browse
|
||||||
* TODO make class names more clear/possibly overlapping for now
|
* TODO make class names more clear/possibly overlapping for now
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enum of all supported sorting orders.
|
||||||
|
*/
|
||||||
enum class SortBy(val str: String) {
|
enum class SortBy(val str: String) {
|
||||||
ALPHABETICAL("alphabetical"),
|
ALPHABETICAL("alphabetical"),
|
||||||
NEWLY_ADDED("newly_added"),
|
NEWLY_ADDED("newly_added"),
|
||||||
|
@ -112,23 +144,22 @@ val NoneAccount = Account("", "", false, "")
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Collection<T>(
|
data class CollectionV1<T>(
|
||||||
@SerialName("total") val total: Int,
|
@SerialName("total") val total: Int,
|
||||||
@SerialName("items") val items: List<T>
|
@SerialName("items") val items: List<T>
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Collection2<T>(
|
data class CollectionV2<T>(
|
||||||
@SerialName("total") val total: Int,
|
@SerialName("total") val total: Int,
|
||||||
@SerialName("data") val data: List<T>
|
@SerialName("data") val data: List<T>
|
||||||
)
|
)
|
||||||
|
|
||||||
typealias SearchResult = Collection<SearchCollection>
|
typealias SearchResult = CollectionV2<SearchTypedList<Item>>
|
||||||
typealias SearchCollection = Collection<Item>
|
typealias BrowseResult = CollectionV2<Item>
|
||||||
typealias BrowseResult = Collection<Item>
|
typealias SimilarToResult = CollectionV2<Item>
|
||||||
typealias SimilarToResult = Collection<Item>
|
typealias RecommendationsList = CollectionV2<Item>
|
||||||
typealias RecommendationsList = Collection<Item>
|
typealias Benefits = CollectionV1<Benefit>
|
||||||
typealias Benefits = Collection<Benefit>
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* panel data classes
|
* panel data classes
|
||||||
|
@ -159,9 +190,9 @@ data class Poster(val height: Int, val width: Int, val source: String, val type:
|
||||||
* up next & watchlist data classes
|
* up next & watchlist data classes
|
||||||
*/
|
*/
|
||||||
|
|
||||||
typealias Watchlist = Collection2<WatchlistItem>
|
typealias Watchlist = CollectionV2<WatchlistItem>
|
||||||
typealias HistoryList = Collection2<UpNextAccountItem>
|
typealias HistoryList = CollectionV2<UpNextAccountItem>
|
||||||
typealias UpNextSeriesList = Collection2<UpNextSeriesItem>
|
typealias UpNextSeriesList = CollectionV2<UpNextSeriesItem>
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class WatchlistItem(
|
data class WatchlistItem(
|
||||||
|
@ -221,7 +252,7 @@ data class EpisodeMetadata(
|
||||||
@SerialName("series_title") val seriesTitle: String,
|
@SerialName("series_title") val seriesTitle: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
val NoneCollection = Collection<Item>(0, emptyList())
|
val NoneCollectionV2 = CollectionV2<Item>(0, emptyList())
|
||||||
val NoneSearchResult = SearchResult(0, emptyList())
|
val NoneSearchResult = SearchResult(0, emptyList())
|
||||||
val NoneBrowseResult = BrowseResult(0, emptyList())
|
val NoneBrowseResult = BrowseResult(0, emptyList())
|
||||||
val NoneSimilarToResult = SimilarToResult(0, emptyList())
|
val NoneSimilarToResult = SimilarToResult(0, emptyList())
|
||||||
|
@ -235,7 +266,7 @@ val NoneBenefits = Benefits(0, emptyList())
|
||||||
* series data class
|
* series data class
|
||||||
*/
|
*/
|
||||||
|
|
||||||
typealias Series = Collection2<SeriesItem>
|
typealias Series = CollectionV2<SeriesItem>
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class SeriesItem(
|
data class SeriesItem(
|
||||||
|
@ -353,7 +384,7 @@ val NoneVersion = Version(
|
||||||
variant = ""
|
variant = ""
|
||||||
)
|
)
|
||||||
|
|
||||||
typealias Playheads = Collection2<PlayheadObject>
|
typealias Playheads = CollectionV2<PlayheadObject>
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class PlayheadObject(
|
data class PlayheadObject(
|
||||||
|
@ -449,7 +480,18 @@ data class Benefit(
|
||||||
@SerialName("benefit") val benefit: String,
|
@SerialName("benefit") val benefit: String,
|
||||||
@SerialName("source") val source: String,
|
@SerialName("source") val source: String,
|
||||||
)
|
)
|
||||||
|
@Suppress("unused")
|
||||||
val NoneBenefit = Benefit(
|
val NoneBenefit = Benefit(
|
||||||
benefit = "",
|
benefit = "",
|
||||||
source = ""
|
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>
|
||||||
|
)
|
||||||
|
|
|
@ -12,8 +12,6 @@ object Preferences {
|
||||||
internal set
|
internal set
|
||||||
var preferredSubtitleLocale: Locale = Locale.forLanguageTag("en-US")
|
var preferredSubtitleLocale: Locale = Locale.forLanguageTag("en-US")
|
||||||
internal set
|
internal set
|
||||||
var preferSubbed = false
|
|
||||||
internal set
|
|
||||||
var autoplay = true
|
var autoplay = true
|
||||||
internal set
|
internal set
|
||||||
var devSettings = false
|
var devSettings = false
|
||||||
|
@ -50,15 +48,6 @@ object Preferences {
|
||||||
this.preferredSubtitleLocale = preferredLocale
|
this.preferredSubtitleLocale = preferredLocale
|
||||||
}
|
}
|
||||||
|
|
||||||
fun savePreferSecondary(context: Context, preferSubbed: Boolean) {
|
|
||||||
with(getSharedPref(context).edit()) {
|
|
||||||
putBoolean(context.getString(R.string.save_key_prefer_secondary), preferSubbed)
|
|
||||||
apply()
|
|
||||||
}
|
|
||||||
|
|
||||||
this.preferSubbed = preferSubbed
|
|
||||||
}
|
|
||||||
|
|
||||||
fun saveAutoplay(context: Context, autoplay: Boolean) {
|
fun saveAutoplay(context: Context, autoplay: Boolean) {
|
||||||
with(getSharedPref(context).edit()) {
|
with(getSharedPref(context).edit()) {
|
||||||
putBoolean(context.getString(R.string.save_key_autoplay), autoplay)
|
putBoolean(context.getString(R.string.save_key_autoplay), autoplay)
|
||||||
|
@ -111,9 +100,6 @@ object Preferences {
|
||||||
context.getString(R.string.save_key_preferred_local), "en-US"
|
context.getString(R.string.save_key_preferred_local), "en-US"
|
||||||
) ?: "en-US"
|
) ?: "en-US"
|
||||||
)
|
)
|
||||||
preferSubbed = sharedPref.getBoolean(
|
|
||||||
context.getString(R.string.save_key_prefer_secondary), false
|
|
||||||
)
|
|
||||||
autoplay = sharedPref.getBoolean(
|
autoplay = sharedPref.getBoolean(
|
||||||
context.getString(R.string.save_key_autoplay), true
|
context.getString(R.string.save_key_autoplay), true
|
||||||
)
|
)
|
||||||
|
|
|
@ -173,7 +173,6 @@ class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListen
|
||||||
private fun initCrunchyroll(): List<Job> {
|
private fun initCrunchyroll(): List<Job> {
|
||||||
val scope = CoroutineScope(Dispatchers.IO + CoroutineName("InitialCrunchyLoading"))
|
val scope = CoroutineScope(Dispatchers.IO + CoroutineName("InitialCrunchyLoading"))
|
||||||
return listOf(
|
return listOf(
|
||||||
scope.launch { Crunchyroll.index() },
|
|
||||||
scope.launch { Crunchyroll.account() },
|
scope.launch { Crunchyroll.account() },
|
||||||
scope.launch {
|
scope.launch {
|
||||||
// update the local preferred content language, since it may have changed
|
// update the local preferred content language, since it may have changed
|
||||||
|
|
|
@ -15,7 +15,8 @@ import org.mosad.teapod.databinding.FragmentAccountBinding
|
||||||
import org.mosad.teapod.parser.crunchyroll.Benefits
|
import org.mosad.teapod.parser.crunchyroll.Benefits
|
||||||
import org.mosad.teapod.parser.crunchyroll.Crunchyroll
|
import org.mosad.teapod.parser.crunchyroll.Crunchyroll
|
||||||
import org.mosad.teapod.parser.crunchyroll.Profile
|
import org.mosad.teapod.parser.crunchyroll.Profile
|
||||||
import org.mosad.teapod.parser.crunchyroll.supportedLocals
|
import org.mosad.teapod.parser.crunchyroll.supportedAudioLocals
|
||||||
|
import org.mosad.teapod.parser.crunchyroll.supportedSubtitleLocals
|
||||||
import org.mosad.teapod.preferences.EncryptedPreferences
|
import org.mosad.teapod.preferences.EncryptedPreferences
|
||||||
import org.mosad.teapod.preferences.Preferences
|
import org.mosad.teapod.preferences.Preferences
|
||||||
import org.mosad.teapod.ui.activity.main.MainActivity
|
import org.mosad.teapod.ui.activity.main.MainActivity
|
||||||
|
@ -61,11 +62,13 @@ class AccountFragment : Fragment() {
|
||||||
|
|
||||||
// add preferred subtitles
|
// add preferred subtitles
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
binding.textSettingsContentLanguageDesc.text = Locale.forLanguageTag(
|
binding.textSettingsAudioLanguageDesc.text = Locale.forLanguageTag(
|
||||||
|
profile.await().preferredContentAudioLanguage
|
||||||
|
).displayLanguage
|
||||||
|
binding.textSettingsSubtitleLanguageDesc.text = Locale.forLanguageTag(
|
||||||
profile.await().preferredContentSubtitleLanguage
|
profile.await().preferredContentSubtitleLanguage
|
||||||
).displayLanguage
|
).displayLanguage
|
||||||
}
|
}
|
||||||
binding.switchSecondary.isChecked = Preferences.preferSubbed
|
|
||||||
binding.switchAutoplay.isChecked = Preferences.autoplay
|
binding.switchAutoplay.isChecked = Preferences.autoplay
|
||||||
binding.textThemeSelected.text = when (Preferences.theme) {
|
binding.textThemeSelected.text = when (Preferences.theme) {
|
||||||
Theme.SYSTEM -> getString(R.string.theme_system)
|
Theme.SYSTEM -> getString(R.string.theme_system)
|
||||||
|
@ -86,12 +89,12 @@ class AccountFragment : Fragment() {
|
||||||
showLoginDialog()
|
showLoginDialog()
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.linearSettingsContentLanguage.setOnClickListener {
|
binding.linearSettingsAudioLanguage.setOnClickListener {
|
||||||
showContentLanguageSelection()
|
showAudioLanguageSelection()
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.switchSecondary.setOnClickListener {
|
binding.linearSettingsSubtitleLanguage.setOnClickListener {
|
||||||
Preferences.savePreferSecondary(requireContext(), binding.switchSecondary.isChecked)
|
showSubtitleLanguageSelection()
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.switchAutoplay.setOnClickListener {
|
binding.switchAutoplay.setOnClickListener {
|
||||||
|
@ -136,43 +139,86 @@ class AccountFragment : Fragment() {
|
||||||
activity?.let { loginModal.show(it.supportFragmentManager, LoginModalBottomSheet.TAG) }
|
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
|
// 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))
|
it.toDisplayString(getString(R.string.settings_content_language_none))
|
||||||
}.toTypedArray()
|
}.toTypedArray()
|
||||||
|
|
||||||
var initialSelection: Int
|
var initialSelection: Int
|
||||||
// profile should be completed here, therefore blocking
|
// profile should be completed here, therefore blocking
|
||||||
runBlocking {
|
runBlocking {
|
||||||
initialSelection = supportedLocals.indexOf(Locale.forLanguageTag(
|
initialSelection = supportedAudioLocals.indexOf(Locale.forLanguageTag(
|
||||||
profile.await().preferredContentSubtitleLanguage))
|
profile.await().preferredContentAudioLanguage))
|
||||||
if (initialSelection < 0) initialSelection = supportedLocals.lastIndex
|
if (initialSelection < 0) initialSelection = supportedAudioLocals.lastIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
MaterialAlertDialogBuilder(requireContext())
|
MaterialAlertDialogBuilder(requireContext())
|
||||||
.setTitle(R.string.settings_content_language)
|
.setTitle(R.string.settings_audio_language)
|
||||||
.setSingleChoiceItems(items, initialSelection){ dialog, which ->
|
.setSingleChoiceItems(items, initialSelection){ dialog, which ->
|
||||||
updatePrefContentLanguage(supportedLocals[which])
|
updateAudioLanguage(supportedAudioLocals[which])
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showSubtitleLanguageSelection() {
|
||||||
|
// we should be able to use the index of supportedLocals for language selection, items is GUI only
|
||||||
|
val items = supportedSubtitleLocals.map {
|
||||||
|
it.toDisplayString(getString(R.string.settings_content_language_none))
|
||||||
|
}.toTypedArray()
|
||||||
|
|
||||||
|
var initialSelection: Int
|
||||||
|
// profile should be completed here, therefore blocking
|
||||||
|
runBlocking {
|
||||||
|
initialSelection = supportedSubtitleLocals.indexOf(Locale.forLanguageTag(
|
||||||
|
profile.await().preferredContentSubtitleLanguage))
|
||||||
|
if (initialSelection < 0) initialSelection = supportedSubtitleLocals.lastIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setTitle(R.string.settings_audio_language)
|
||||||
|
.setSingleChoiceItems(items, initialSelection){ dialog, which ->
|
||||||
|
updateSubtitleLanguage(supportedSubtitleLocals[which])
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
}
|
}
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
private fun updatePrefContentLanguage(preferredLocale: Locale) {
|
private fun updateAudioLanguage(preferredLocale: Locale) {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
Crunchyroll.postPrefSubLanguage(preferredLocale.toLanguageTag())
|
Crunchyroll.setPreferredAudioLanguage(preferredLocale.toLanguageTag())
|
||||||
|
|
||||||
}.invokeOnCompletion {
|
}.invokeOnCompletion {
|
||||||
// update the local preferred content language
|
// update the local preferred audio language
|
||||||
Preferences.savePreferredSubtitleLocal(requireContext(), preferredLocale)
|
Preferences.savePreferredAudioLocal(requireContext(), preferredLocale)
|
||||||
|
|
||||||
// update profile since the language selection might have changed
|
// update profile since the language selection might have changed
|
||||||
profile = lifecycleScope.async { Crunchyroll.profile() }
|
profile = lifecycleScope.async { Crunchyroll.profile() }
|
||||||
profile.invokeOnCompletion {
|
profile.invokeOnCompletion {
|
||||||
// update language once loading profile is completed
|
// update language once loading profile is completed
|
||||||
binding.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
|
profile.getCompleted().preferredContentSubtitleLanguage
|
||||||
).displayLanguage
|
).displayLanguage
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,21 @@
|
||||||
package org.mosad.teapod.ui.activity.main.fragments
|
package org.mosad.teapod.ui.activity.main.fragments
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.viewModels
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||||
import com.google.android.material.tabs.TabLayoutMediator
|
import com.google.android.material.tabs.TabLayoutMediator
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.mosad.teapod.R
|
import org.mosad.teapod.R
|
||||||
import org.mosad.teapod.databinding.FragmentMyListsBinding
|
import org.mosad.teapod.databinding.FragmentMyListsBinding
|
||||||
import org.mosad.teapod.parser.crunchyroll.Crunchyroll
|
import org.mosad.teapod.ui.activity.main.viewmodel.MyListsFragmentViewModel
|
||||||
import org.mosad.teapod.util.toItemMediaList
|
import org.mosad.teapod.util.toItemMediaList
|
||||||
|
|
||||||
class MyListsFragment : Fragment() {
|
class MyListsFragment : Fragment() {
|
||||||
|
@ -19,6 +23,8 @@ class MyListsFragment : Fragment() {
|
||||||
private lateinit var binding: FragmentMyListsBinding
|
private lateinit var binding: FragmentMyListsBinding
|
||||||
private lateinit var pagerAdapter: FragmentStateAdapter
|
private lateinit var pagerAdapter: FragmentStateAdapter
|
||||||
|
|
||||||
|
private val model: MyListsFragmentViewModel by viewModels()
|
||||||
|
|
||||||
private val fragments = arrayListOf<Fragment>()
|
private val fragments = arrayListOf<Fragment>()
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
|
@ -33,7 +39,6 @@ class MyListsFragment : Fragment() {
|
||||||
pagerAdapter = ScreenSlidePagerAdapter(this)
|
pagerAdapter = ScreenSlidePagerAdapter(this)
|
||||||
binding.pagerMyLists.adapter = pagerAdapter
|
binding.pagerMyLists.adapter = pagerAdapter
|
||||||
|
|
||||||
// TODO is position 0 always episodes? (and 1 always similar titles)
|
|
||||||
TabLayoutMediator(binding.tabMyLists, binding.pagerMyLists) { tab, position ->
|
TabLayoutMediator(binding.tabMyLists, binding.pagerMyLists) { tab, position ->
|
||||||
tab.text = when(position) {
|
tab.text = when(position) {
|
||||||
0 -> getString(R.string.my_list)
|
0 -> getString(R.string.my_list)
|
||||||
|
@ -43,15 +48,33 @@ class MyListsFragment : Fragment() {
|
||||||
}
|
}
|
||||||
}.attach()
|
}.attach()
|
||||||
|
|
||||||
lifecycleScope.launch {
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
val items = Crunchyroll.watchlist(50)
|
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
|
model.onUiState(viewLifecycleOwner.lifecycleScope) { uiState ->
|
||||||
MediaFragmentSimilar(items.toItemMediaList()).also {
|
when (uiState) {
|
||||||
fragments.add(it)
|
is MyListsFragmentViewModel.UiState.Normal -> bindUiStateNormal(uiState)
|
||||||
pagerAdapter.notifyItemInserted(fragments.indexOf(it))
|
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}")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -32,7 +32,7 @@ import kotlinx.coroutines.launch
|
||||||
import org.mosad.teapod.parser.crunchyroll.*
|
import org.mosad.teapod.parser.crunchyroll.*
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
class HomeViewModel : ViewModel() {
|
class HomeViewModel : ViewModel() {
|
||||||
|
|
||||||
private val WATCHLIST_LENGTH = 50
|
private val WATCHLIST_LENGTH = 50
|
||||||
|
|
||||||
|
@ -66,16 +66,16 @@ class HomeViewModel : ViewModel() {
|
||||||
uiState.emit(UiState.Loading)
|
uiState.emit(UiState.Loading)
|
||||||
try {
|
try {
|
||||||
// run the loading in parallel to speed up the process
|
// run the loading in parallel to speed up the process
|
||||||
val upNextJob = viewModelScope.async { Crunchyroll.upNextAccount().data }
|
val upNextJob = viewModelScope.async { Crunchyroll.upNextAccount(n = 20).data }
|
||||||
val watchlistJob = viewModelScope.async { Crunchyroll.watchlist(WATCHLIST_LENGTH).items }
|
val watchlistJob = viewModelScope.async { Crunchyroll.watchlist(WATCHLIST_LENGTH).data }
|
||||||
val recommendationsJob = viewModelScope.async {
|
val recommendationsJob = viewModelScope.async {
|
||||||
Crunchyroll.recommendations(20).items
|
Crunchyroll.recommendations(n = 20).data
|
||||||
}
|
}
|
||||||
val recentlyAddedJob = viewModelScope.async {
|
val recentlyAddedJob = viewModelScope.async {
|
||||||
Crunchyroll.browse(sortBy = SortBy.NEWLY_ADDED, n = 50).items
|
Crunchyroll.browse(sortBy = SortBy.NEWLY_ADDED, n = 50).data
|
||||||
}
|
}
|
||||||
val topTenJob = viewModelScope.async {
|
val topTenJob = viewModelScope.async {
|
||||||
Crunchyroll.browse(sortBy = SortBy.POPULARITY, n = 10).items
|
Crunchyroll.browse(sortBy = SortBy.POPULARITY, n = 10).data
|
||||||
}
|
}
|
||||||
|
|
||||||
val recentlyAddedItems = recentlyAddedJob.await()
|
val recentlyAddedItems = recentlyAddedJob.await()
|
||||||
|
@ -114,7 +114,7 @@ class HomeViewModel : ViewModel() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// update the watchlist after a item has been added/removed
|
// update the watchlist after a item has been added/removed
|
||||||
val watchlistItems = Crunchyroll.watchlist(WATCHLIST_LENGTH).items
|
val watchlistItems = Crunchyroll.watchlist(WATCHLIST_LENGTH).data
|
||||||
|
|
||||||
currentUiState.copy(
|
currentUiState.copy(
|
||||||
watchlistItems = watchlistItems,
|
watchlistItems = watchlistItems,
|
||||||
|
@ -133,7 +133,7 @@ class HomeViewModel : ViewModel() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
uiState.update { currentUiState ->
|
uiState.update { currentUiState ->
|
||||||
if (currentUiState is UiState.Normal) {
|
if (currentUiState is UiState.Normal) {
|
||||||
val upNextItems = Crunchyroll.upNextAccount().data
|
val upNextItems = Crunchyroll.upNextAccount(n = 20).data
|
||||||
currentUiState.copy(upNextItems = upNextItems)
|
currentUiState.copy(upNextItems = upNextItems)
|
||||||
} else {
|
} else {
|
||||||
currentUiState
|
currentUiState
|
||||||
|
|
|
@ -90,7 +90,7 @@ class LibraryFragmentViewModel : ViewModel() {
|
||||||
delay(250)
|
delay(250)
|
||||||
|
|
||||||
val results = Crunchyroll.search(query, 50)
|
val results = Crunchyroll.search(query, 50)
|
||||||
.items.firstOrNull()?.items?.toItemMediaList()
|
.data.firstOrNull()?.items?.toItemMediaList()
|
||||||
?: listOf()
|
?: listOf()
|
||||||
uiState.emit(UiState.Search(results))
|
uiState.emit(UiState.Search(results))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -222,12 +222,9 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
|
||||||
// needs to be blocking, currentPlayback must be present when calling playCurrentMedia()
|
// needs to be blocking, currentPlayback must be present when calling playCurrentMedia()
|
||||||
joinAll(
|
joinAll(
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
currentVersion = if (Preferences.preferSubbed) {
|
currentVersion = currentEpisode.versions?.firstOrNull {
|
||||||
currentEpisode.versions?.first { it.original } ?: NoneVersion
|
it.audioLocale == currentAudioLocale.toLanguageTag()
|
||||||
} else {
|
} ?: currentEpisode.versions?.first() ?: NoneVersion
|
||||||
currentEpisode.versions?.firstOrNull { it.audioLocale == currentAudioLocale.toLanguageTag() }
|
|
||||||
?: currentEpisode.versions?.first() ?: NoneVersion
|
|
||||||
}
|
|
||||||
|
|
||||||
// get the current streams object, if no version is set, use streamsLink
|
// get the current streams object, if no version is set, use streamsLink
|
||||||
currentStreams = if (currentVersion != NoneVersion) {
|
currentStreams = if (currentVersion != NoneVersion) {
|
||||||
|
|
|
@ -9,12 +9,11 @@ import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.core.view.WindowInsetsControllerCompat
|
import androidx.core.view.WindowInsetsControllerCompat
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import org.mosad.teapod.R
|
import org.mosad.teapod.R
|
||||||
import org.mosad.teapod.parser.crunchyroll.Collection
|
import org.mosad.teapod.parser.crunchyroll.CollectionV2
|
||||||
import org.mosad.teapod.parser.crunchyroll.Collection2
|
|
||||||
import org.mosad.teapod.parser.crunchyroll.Item
|
import org.mosad.teapod.parser.crunchyroll.Item
|
||||||
import org.mosad.teapod.parser.crunchyroll.PlayheadObject
|
import org.mosad.teapod.parser.crunchyroll.PlayheadObject
|
||||||
import org.mosad.teapod.ui.activity.player.PlayerActivity
|
import org.mosad.teapod.ui.activity.player.PlayerActivity
|
||||||
import java.util.*
|
import java.util.Locale
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a Intent for PlayerActivity with season and episode id.
|
* Create a Intent for PlayerActivity with season and episode id.
|
||||||
|
@ -36,8 +35,8 @@ fun <T> concatenate(vararg lists: List<T>): List<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO move to correct location
|
// TODO move to correct location
|
||||||
fun Collection<Item>.toItemMediaList(): List<ItemMedia> {
|
fun CollectionV2<Item>.toItemMediaList(): List<ItemMedia> {
|
||||||
return this.items.map {
|
return this.data.map {
|
||||||
ItemMedia(it.id, it.title, it.images.poster_wide[0][0].source)
|
ItemMedia(it.id, it.title, it.images.poster_wide[0][0].source)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -59,7 +58,7 @@ fun Locale.toDisplayString(fallback: String): String {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Collection2<PlayheadObject>.toPlayheadsMap(): Map<String, PlayheadObject> {
|
fun CollectionV2<PlayheadObject>.toPlayheadsMap(): Map<String, PlayheadObject> {
|
||||||
return this.data.associateBy { it.contentId }
|
return this.data.associateBy { it.contentId }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
|
@ -141,7 +141,7 @@
|
||||||
android:textStyle="bold" />
|
android:textStyle="bold" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/linear_settings_content_language"
|
android:id="@+id/linear_settings_audio_language"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
|
@ -151,12 +151,12 @@
|
||||||
android:id="@+id/imageView4"
|
android:id="@+id/imageView4"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:contentDescription="@string/settings_content_language"
|
android:contentDescription="@string/settings_audio_language"
|
||||||
android:minWidth="48dp"
|
android:minWidth="48dp"
|
||||||
android:minHeight="48dp"
|
android:minHeight="48dp"
|
||||||
android:padding="9dp"
|
android:padding="9dp"
|
||||||
android:scaleType="fitXY"
|
android:scaleType="fitXY"
|
||||||
android:src="@drawable/ic_baseline_language_24" />
|
android:src="@drawable/ic_baseline_audiotrack_24" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
@ -167,11 +167,11 @@
|
||||||
android:id="@+id/text_settings_content_language"
|
android:id="@+id/text_settings_content_language"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/settings_content_language"
|
android:text="@string/settings_audio_language"
|
||||||
android:textAppearance="@style/TextAppearance.Material3.BodyLarge" />
|
android:textAppearance="@style/TextAppearance.Material3.BodyLarge" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/text_settings_content_language_desc"
|
android:id="@+id/text_settings_audio_language_desc"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/settings_content_language_desc" />
|
android:text="@string/settings_content_language_desc" />
|
||||||
|
@ -179,67 +179,41 @@
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/linear_settings_secondary"
|
android:id="@+id/linear_settings_subtitle_language"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:gravity="center"
|
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:padding="7dp">
|
android:padding="7dp">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/imageView3"
|
android:id="@+id/imageView7"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:contentDescription="@string/settings_prefer_subbed"
|
android:contentDescription="@string/settings_subtitle_language"
|
||||||
android:minWidth="48dp"
|
android:minWidth="48dp"
|
||||||
android:minHeight="48dp"
|
android:minHeight="48dp"
|
||||||
android:padding="9dp"
|
android:padding="9dp"
|
||||||
android:scaleType="fitXY"
|
android:scaleType="fitXY"
|
||||||
android:src="@drawable/ic_baseline_subtitles_24" />
|
android:src="@drawable/ic_baseline_subtitles_24" />
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
<LinearLayout
|
<TextView
|
||||||
android:id="@+id/linearLayout"
|
android:id="@+id/text_settings_subtitle_language"
|
||||||
android:layout_width="0dp"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical"
|
android:text="@string/settings_subtitle_language"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
android:textAppearance="@style/TextAppearance.Material3.BodyLarge" />
|
||||||
app:layout_constraintEnd_toStartOf="@+id/switch_secondary"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent">
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/text_settings_secondary"
|
android:id="@+id/text_settings_subtitle_language_desc"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:text="@string/settings_prefer_subbed"
|
|
||||||
android:textAppearance="@style/TextAppearance.Material3.BodyLarge" />
|
|
||||||
|
|
||||||
<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" />
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
|
||||||
android:id="@+id/switch_secondary"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:checked="true"
|
android:text="@string/settings_content_language_desc" />
|
||||||
android:contentDescription="@string/settings_prefer_subbed"
|
</LinearLayout>
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
|
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
|
|
@ -45,7 +45,8 @@
|
||||||
<string name="info">Info</string>
|
<string name="info">Info</string>
|
||||||
<string name="info_about_desc">Version %1$s (%2$s)</string>
|
<string name="info_about_desc">Version %1$s (%2$s)</string>
|
||||||
<string name="settings">Einstellungen</string>
|
<string name="settings">Einstellungen</string>
|
||||||
<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_desc">Englisch</string>
|
||||||
<string name="settings_content_language_none">Keine</string>
|
<string name="settings_content_language_none">Keine</string>
|
||||||
<string name="settings_prefer_subbed">Bevorzuge OmU</string>
|
<string name="settings_prefer_subbed">Bevorzuge OmU</string>
|
||||||
|
|
|
@ -59,7 +59,8 @@
|
||||||
<string name="account_tier_mega_fan" translatable="false">Mega Fan</string>
|
<string name="account_tier_mega_fan" translatable="false">Mega Fan</string>
|
||||||
<string name="account_tier_ultimate_fan" translatable="false">Ultimate Fan</string>
|
<string name="account_tier_ultimate_fan" translatable="false">Ultimate Fan</string>
|
||||||
<string name="settings">Settings</string>
|
<string name="settings">Settings</string>
|
||||||
<string name="settings_content_language">Preferred content language</string>
|
<string name="settings_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_desc">English</string>
|
||||||
<string name="settings_content_language_none">None</string>
|
<string name="settings_content_language_none">None</string>
|
||||||
<string name="settings_prefer_subbed">Prefer subbed</string>
|
<string name="settings_prefer_subbed">Prefer subbed</string>
|
||||||
|
|
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.
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
buildscript {
|
buildscript {
|
||||||
ext.kotlin_version = "1.8.10"
|
ext.kotlin_version = "1.9.22"
|
||||||
ext.ktor_version = "2.2.4"
|
ext.ktor_version = "2.3.6"
|
||||||
ext.exo_version = "2.18.5"
|
ext.exo_version = "2.18.7"
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:8.0.0'
|
classpath 'com.android.tools.build:gradle:8.3.0'
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
|
|
||||||
// NOTE: Do not place your application dependencies here; they belong
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
|
@ -23,6 +23,6 @@ allprojects {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
task clean(type: Delete) {
|
tasks.register('clean', Delete) {
|
||||||
delete rootProject.buildDir
|
delete rootProject.layout.buildDirectory
|
||||||
}
|
}
|
|
@ -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
|
|
@ -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,9 +16,8 @@ org.gradle.jvmargs=-Xmx2048m
|
||||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
# Automatically convert third-party libraries to use AndroidX
|
# 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 for this project: "official" or "obsolete":
|
||||||
kotlin.code.style=official
|
kotlin.code.style=official
|
||||||
android.defaults.buildfeatures.buildconfig=true
|
|
||||||
android.nonTransitiveRClass=false
|
android.nonTransitiveRClass=false
|
||||||
android.nonFinalResIds=false
|
android.nonFinalResIds=false
|
Binary file not shown.
|
@ -1,6 +1,7 @@
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|
|
@ -83,10 +83,8 @@ done
|
||||||
# This is normally unused
|
# This is normally unused
|
||||||
# shellcheck disable=SC2034
|
# shellcheck disable=SC2034
|
||||||
APP_BASE_NAME=${0##*/}
|
APP_BASE_NAME=${0##*/}
|
||||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||||
|
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
|
||||||
# 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"'
|
|
||||||
|
|
||||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
MAX_FD=maximum
|
MAX_FD=maximum
|
||||||
|
@ -133,10 +131,13 @@ location of your Java installation."
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
JAVACMD=java
|
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
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
location of your Java installation."
|
location of your Java installation."
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Increase the maximum file descriptors if we can.
|
# Increase the maximum file descriptors if we can.
|
||||||
|
@ -144,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
case $MAX_FD in #(
|
case $MAX_FD in #(
|
||||||
max*)
|
max*)
|
||||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||||
# shellcheck disable=SC3045
|
# shellcheck disable=SC2039,SC3045
|
||||||
MAX_FD=$( ulimit -H -n ) ||
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
warn "Could not query maximum file descriptor limit"
|
warn "Could not query maximum file descriptor limit"
|
||||||
esac
|
esac
|
||||||
|
@ -152,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
'' | soft) :;; #(
|
'' | soft) :;; #(
|
||||||
*)
|
*)
|
||||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||||
# shellcheck disable=SC3045
|
# shellcheck disable=SC2039,SC3045
|
||||||
ulimit -n "$MAX_FD" ||
|
ulimit -n "$MAX_FD" ||
|
||||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||||
esac
|
esac
|
||||||
|
@ -197,11 +198,15 @@ if "$cygwin" || "$msys" ; then
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Collect all arguments for the java command;
|
|
||||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
# shell script including quotes and variable substitutions, so put them in
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
# double quotes to make sure that they get re-expanded; and
|
|
||||||
# * put everything else in single quotes, so that it's not re-expanded.
|
# 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 -- \
|
set -- \
|
||||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
|
|
Loading…
Reference in New Issue