Compare commits

..

No commits in common. "develop" and "1.1.0-beta2" have entirely different histories.

25 changed files with 255 additions and 426 deletions

View File

@ -4,23 +4,16 @@ plugins {
id 'org.jetbrains.kotlin.plugin.serialization' version "$kotlin_version" id 'org.jetbrains.kotlin.plugin.serialization' version "$kotlin_version"
} }
kotlin {
jvmToolchain 17
sourceSets.configureEach {
languageSettings.optIn("kotlin.RequiresOptIn")
}
}
android { android {
compileSdk 34 compileSdkVersion 33
buildToolsVersion = '34.0.0' buildToolsVersion "30.0.3"
defaultConfig { defaultConfig {
applicationId "org.mosad.teapod" applicationId "org.mosad.teapod"
minSdk 23 minSdkVersion 23
targetSdk 33 targetSdkVersion 33
versionCode 100992 //01.00.000 versionCode 100991 //01.00.000
versionName "1.1.0-beta3" versionName "1.1.0-beta2"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resValue "string", "build_time", buildTime() resValue "string", "build_time", buildTime()
@ -29,7 +22,6 @@ android {
buildFeatures { buildFeatures {
viewBinding true viewBinding true
buildConfig true
} }
buildTypes { buildTypes {
@ -40,28 +32,37 @@ 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.9.0' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3' implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0'
implementation 'androidx.core:core-ktx:1.13.1' implementation 'androidx.core:core-ktx:1.10.0'
implementation 'androidx.core:core-splashscreen:1.0.1' implementation 'androidx.core:core-splashscreen:1.0.0'
implementation 'androidx.appcompat:appcompat:1.7.0' 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.8.3' implementation 'androidx.navigation:navigation-fragment-ktx:2.5.3'
implementation 'androidx.navigation:navigation-ui-ktx:2.8.3' implementation 'androidx.navigation:navigation-ui-ktx:2.5.3'
implementation 'androidx.security:security-crypto:1.1.0-alpha06' implementation 'androidx.security:security-crypto:1.1.0-alpha05'
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.8.6' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.6' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1'
implementation "androidx.paging:paging-runtime-ktx:3.3.2"
implementation 'com.google.android.material:material:1.12.0' implementation 'com.google.android.material:material:1.8.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"
@ -70,7 +71,7 @@ dependencies {
implementation 'com.facebook.shimmer:shimmer:0.5.0' implementation 'com.facebook.shimmer:shimmer:0.5.0'
implementation 'com.github.bumptech.glide:glide:4.16.0' implementation 'com.github.bumptech.glide:glide:4.15.1'
implementation 'jp.wasabeef:glide-transformations:4.3.0' implementation 'jp.wasabeef:glide-transformations:4.3.0'
implementation "io.ktor:ktor-client-core:$ktor_version" implementation "io.ktor:ktor-client-core:$ktor_version"
@ -79,8 +80,8 @@ dependencies {
implementation "io.ktor:ktor-serialization-kotlinx-json:$ktor_version" implementation "io.ktor:ktor-serialization-kotlinx-json:$ktor_version"
testImplementation 'junit:junit:4.13.2' testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.2.1' androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
} }

View File

@ -34,6 +34,7 @@ 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
@ -58,12 +59,16 @@ 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, ExperimentalCoroutinesApi::class) @OptIn(DelicateCoroutinesApi::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>()
/** /**
@ -208,10 +213,27 @@ object Crunchyroll {
} }
/** /**
* Basic functions: account * Basic functions: index, account
* Needed for other functions to work properly! * Needed for other functions to work properly!
*/ */
/**
* Retrieve the identifiers necessary for streaming. If the identifiers are
* retrieved, set the corresponding global var. The identifiers are valid for 24h.
*/
suspend fun index() {
val indexEndpoint = "/index/v2"
val index: Index = requestGet(indexEndpoint)
policy = index.cms.policy
signature = index.cms.signature
keyPairID = index.cms.keyPairId
Log.i(TAG, "Policy : $policy")
Log.i(TAG, "Signature : $signature")
Log.i(TAG, "Key Pair ID : $keyPairID")
}
/** /**
* Retrieve the account id and set the corresponding global var. * Retrieve the account id and set the corresponding global var.
* The account id is needed for other calls. * The account id is needed for other calls.
@ -239,30 +261,24 @@ object Crunchyroll {
/** /**
* Browse the media available on crunchyroll. * Browse the media available on crunchyroll.
* *
* @param start start of the item list, used for pagination, default = 0 * @param sortBy
* @param n number of items to return, default = 10 * @param n Number of items to return, defaults to 10
* @param sortBy the sort order, see **[SortBy]** *
* @param ratings add user rating to the objects, default = false
* @param seasonTag filter by season tag, if present
* @param categories filter by category, if present
* @return A **[BrowseResult]** object is returned. * @return A **[BrowseResult]** object is returned.
*/ */
suspend fun browse( suspend fun browse(
start: Int = 0, categories: List<Categories> = emptyList(),
n: Int = 10,
sortBy: SortBy = SortBy.ALPHABETICAL, sortBy: SortBy = SortBy.ALPHABETICAL,
ratings: Boolean = false,
seasonTag: String = "", seasonTag: String = "",
categories: List<Categories> = emptyList() start: Int = 0,
n: Int = 10
): BrowseResult { ): BrowseResult {
val browseEndpoint = "/content/v2/discover/browse" val browseEndpoint = "/content/v1/browse"
val parameters = mutableListOf( val parameters = mutableListOf(
"start" to start,
"n" to n,
"sort_by" to sortBy.str,
"ratings" to ratings,
"preferred_audio_language" to Preferences.preferredSubtitleLocale.toLanguageTag(),
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag(), "locale" to Preferences.preferredSubtitleLocale.toLanguageTag(),
"sort_by" to sortBy.str,
"start" to start,
"n" to n
) )
// if a season tag is present add it to the parameters // if a season tag is present add it to the parameters
@ -287,10 +303,9 @@ 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()
} }
@ -306,20 +321,19 @@ 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, ratings: Boolean = false): SearchResult { suspend fun search(query: String, n: Int = 10): SearchResult {
val searchEndpoint = "/content/v2/discover/search" val searchEndpoint = "/content/v1/search"
val parameters = listOf( val parameters = listOf(
"locale" to Preferences.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,
@ -338,22 +352,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>, ratings: Boolean = false): CollectionV2<Item> { suspend fun objects(objects: List<String>): Collection<Item> {
val episodesEndpoint = "/content/v2/cms/objects/${objects.joinToString(",")}" val episodesEndpoint = "/cms/v2/DE/M3/crunchyroll/objects/${objects.joinToString(",")}"
val parameters = listOf( val parameters = listOf(
"ratings" to ratings, "locale" to Preferences.preferredSubtitleLocale.toLanguageTag(),
"preferred_audio_language" to Preferences.preferredAudioLocale.toLanguageTag(), "Signature" to signature,
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag() "Policy" to policy,
"Key-Pair-Id" to keyPairID
) )
return try { return try {
requestGet(episodesEndpoint, parameters) requestGet(episodesEndpoint, parameters)
} catch (ex: Exception) { } catch (ex: Exception) {
Log.e(TAG, "Exception in objects().", ex) Log.e(TAG, "Exception in objects().", ex)
NoneCollectionV2 NoneCollection
} }
} }
@ -494,7 +508,7 @@ object Crunchyroll {
) )
return try { return try {
(requestGet(watchlistSeriesEndpoint, parameters) as CollectionV2<IsWatchlistItem>) (requestGet(watchlistSeriesEndpoint, parameters) as Collection2<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)
@ -616,16 +630,14 @@ 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, ratings: Boolean = false): SimilarToResult { suspend fun similarTo(seriesId: String, n: Int = 10): SimilarToResult {
val similarToEndpoint = "/content/v2/discover/$accountID/similar_to/$seriesId" val similarToEndpoint = "/content/v1/$accountID/similar_to"
val parameters = listOf( val parameters = listOf(
"n" to n, "guid" to seriesId,
"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 {
@ -646,7 +658,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): CollectionV2<Item> { suspend fun watchlist(n: Int = 20): Collection<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(),
@ -668,10 +680,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, default = 20 * @param n Number of items to return, defaults to 20.
* @return A **[HistoryList]** containing up to n **[UpNextAccountItem]**. * @return A **[HistoryList]** containing up to n **[UpNextAccountItem]**.
*/ */
suspend fun upNextAccount(n: Int = 10): HistoryList { suspend fun upNextAccount(n: Int = 20): 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(),
@ -686,21 +698,13 @@ object Crunchyroll {
} }
} }
/** suspend fun recommendations(n: Int = 20, start: Int = 0): RecommendationsList {
* Returns a collection of recommendations for the currently logged in account. val recommendationsEndpoint = "/content/v1/$accountID/recommendations"
*
* @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(
"start" to start,
"n" to n,
"ratings" to ratings,
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag(), "locale" to Preferences.preferredSubtitleLocale.toLanguageTag(),
"n" to n,
"start" to start,
"variant_id" to 0
) )
return try { return try {
@ -736,7 +740,7 @@ object Crunchyroll {
* *
* @param languageTag the preferred language as language tag * @param languageTag the preferred language as language tag
*/ */
suspend fun setPreferredSubtitleLanguage(languageTag: String) { suspend fun postPrefSubLanguage(languageTag: String) {
val profileEndpoint = "/accounts/v1/me/profile" val profileEndpoint = "/accounts/v1/me/profile"
val json = buildJsonObject { val json = buildJsonObject {
put("preferred_content_subtitle_language", languageTag) put("preferred_content_subtitle_language", languageTag)
@ -745,20 +749,6 @@ 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.
* *

View File

@ -24,47 +24,19 @@ package org.mosad.teapod.parser.crunchyroll
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import java.util.Locale import java.util.*
val supportedAudioLocals = listOf( val supportedLocals = listOf(
Locale.forLanguageTag("ar-SA"), Locale.forLanguageTag("ar-SA"),
Locale.forLanguageTag("ca-ES"),
Locale.forLanguageTag("de-DE"),
Locale.forLanguageTag("en-US"),
Locale.forLanguageTag("en-IN"),
Locale.forLanguageTag("es-419"),
Locale.forLanguageTag("es-ES"),
Locale.forLanguageTag("fr-FR"),
Locale.forLanguageTag("hi-IN"),
Locale.forLanguageTag("it-IT"),
Locale.forLanguageTag("ko-KR"),
Locale.forLanguageTag("pl-PL"),
Locale.forLanguageTag("pt-BR"),
Locale.forLanguageTag("pt-PT"),
Locale.forLanguageTag("ru-RU"),
Locale.forLanguageTag("ta-IN"),
Locale.forLanguageTag("th-TH"),
Locale.forLanguageTag("zh-CN"),
Locale.forLanguageTag("zh-TW"),
Locale.ROOT
)
val supportedSubtitleLocals = listOf(
Locale.forLanguageTag("ar-SA"),
Locale.forLanguageTag("ca-ES"),
Locale.forLanguageTag("de-DE"), Locale.forLanguageTag("de-DE"),
Locale.forLanguageTag("en-US"), Locale.forLanguageTag("en-US"),
Locale.forLanguageTag("es-419"), Locale.forLanguageTag("es-419"),
Locale.forLanguageTag("es-ES"), Locale.forLanguageTag("es-ES"),
Locale.forLanguageTag("fr-FR"), Locale.forLanguageTag("fr-FR"),
Locale.forLanguageTag("hi-IN"),
Locale.forLanguageTag("it-IT"), Locale.forLanguageTag("it-IT"),
Locale.forLanguageTag("ms-MY"),
Locale.forLanguageTag("pl-PL"),
Locale.forLanguageTag("pt-BR"), Locale.forLanguageTag("pt-BR"),
Locale.forLanguageTag("pt-PT"), Locale.forLanguageTag("pt-PT"),
Locale.forLanguageTag("ru-RU"), Locale.forLanguageTag("ru-RU"),
Locale.forLanguageTag("tr-TR"),
Locale.ROOT Locale.ROOT
) )
@ -72,10 +44,6 @@ val supportedSubtitleLocals = listOf(
* data classes for browse * data classes for browse
* TODO make class names more clear/possibly overlapping for now * TODO make class names more clear/possibly overlapping for now
*/ */
/**
* Enum of all supported sorting orders.
*/
enum class SortBy(val str: String) { enum class SortBy(val str: String) {
ALPHABETICAL("alphabetical"), ALPHABETICAL("alphabetical"),
NEWLY_ADDED("newly_added"), NEWLY_ADDED("newly_added"),
@ -144,22 +112,23 @@ val NoneAccount = Account("", "", false, "")
*/ */
@Serializable @Serializable
data class CollectionV1<T>( data class Collection<T>(
@SerialName("total") val total: Int, @SerialName("total") val total: Int,
@SerialName("items") val items: List<T> @SerialName("items") val items: List<T>
) )
@Serializable @Serializable
data class CollectionV2<T>( data class Collection2<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 = CollectionV2<SearchTypedList<Item>> typealias SearchResult = Collection<SearchCollection>
typealias BrowseResult = CollectionV2<Item> typealias SearchCollection = Collection<Item>
typealias SimilarToResult = CollectionV2<Item> typealias BrowseResult = Collection<Item>
typealias RecommendationsList = CollectionV2<Item> typealias SimilarToResult = Collection<Item>
typealias Benefits = CollectionV1<Benefit> typealias RecommendationsList = Collection<Item>
typealias Benefits = Collection<Benefit>
/** /**
* panel data classes * panel data classes
@ -190,9 +159,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 = CollectionV2<WatchlistItem> typealias Watchlist = Collection2<WatchlistItem>
typealias HistoryList = CollectionV2<UpNextAccountItem> typealias HistoryList = Collection2<UpNextAccountItem>
typealias UpNextSeriesList = CollectionV2<UpNextSeriesItem> typealias UpNextSeriesList = Collection2<UpNextSeriesItem>
@Serializable @Serializable
data class WatchlistItem( data class WatchlistItem(
@ -252,7 +221,7 @@ data class EpisodeMetadata(
@SerialName("series_title") val seriesTitle: String, @SerialName("series_title") val seriesTitle: String,
) )
val NoneCollectionV2 = CollectionV2<Item>(0, emptyList()) val NoneCollection = Collection<Item>(0, emptyList())
val NoneSearchResult = SearchResult(0, emptyList()) val NoneSearchResult = SearchResult(0, emptyList())
val NoneBrowseResult = BrowseResult(0, emptyList()) val NoneBrowseResult = BrowseResult(0, emptyList())
val NoneSimilarToResult = SimilarToResult(0, emptyList()) val NoneSimilarToResult = SimilarToResult(0, emptyList())
@ -266,7 +235,7 @@ val NoneBenefits = Benefits(0, emptyList())
* series data class * series data class
*/ */
typealias Series = CollectionV2<SeriesItem> typealias Series = Collection2<SeriesItem>
@Serializable @Serializable
data class SeriesItem( data class SeriesItem(
@ -384,7 +353,7 @@ val NoneVersion = Version(
variant = "" variant = ""
) )
typealias Playheads = CollectionV2<PlayheadObject> typealias Playheads = Collection2<PlayheadObject>
@Serializable @Serializable
data class PlayheadObject( data class PlayheadObject(
@ -480,18 +449,7 @@ 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>
)

View File

@ -12,6 +12,8 @@ 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
@ -48,6 +50,15 @@ 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)
@ -100,6 +111,9 @@ 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
) )

View File

@ -173,6 +173,7 @@ 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

View File

@ -15,8 +15,7 @@ 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.supportedAudioLocals import org.mosad.teapod.parser.crunchyroll.supportedLocals
import org.mosad.teapod.parser.crunchyroll.supportedSubtitleLocals
import org.mosad.teapod.preferences.EncryptedPreferences import org.mosad.teapod.preferences.EncryptedPreferences
import org.mosad.teapod.preferences.Preferences import org.mosad.teapod.preferences.Preferences
import org.mosad.teapod.ui.activity.main.MainActivity import org.mosad.teapod.ui.activity.main.MainActivity
@ -62,13 +61,11 @@ class AccountFragment : Fragment() {
// add preferred subtitles // add preferred subtitles
lifecycleScope.launch { lifecycleScope.launch {
binding.textSettingsAudioLanguageDesc.text = Locale.forLanguageTag( binding.textSettingsContentLanguageDesc.text = Locale.forLanguageTag(
profile.await().preferredContentAudioLanguage
).displayLanguage
binding.textSettingsSubtitleLanguageDesc.text = Locale.forLanguageTag(
profile.await().preferredContentSubtitleLanguage profile.await().preferredContentSubtitleLanguage
).displayLanguage ).displayLanguage
} }
binding.switchSecondary.isChecked = Preferences.preferSubbed
binding.switchAutoplay.isChecked = Preferences.autoplay binding.switchAutoplay.isChecked = Preferences.autoplay
binding.textThemeSelected.text = when (Preferences.theme) { binding.textThemeSelected.text = when (Preferences.theme) {
Theme.SYSTEM -> getString(R.string.theme_system) Theme.SYSTEM -> getString(R.string.theme_system)
@ -89,12 +86,12 @@ class AccountFragment : Fragment() {
showLoginDialog() showLoginDialog()
} }
binding.linearSettingsAudioLanguage.setOnClickListener { binding.linearSettingsContentLanguage.setOnClickListener {
showAudioLanguageSelection() showContentLanguageSelection()
} }
binding.linearSettingsSubtitleLanguage.setOnClickListener { binding.switchSecondary.setOnClickListener {
showSubtitleLanguageSelection() Preferences.savePreferSecondary(requireContext(), binding.switchSecondary.isChecked)
} }
binding.switchAutoplay.setOnClickListener { binding.switchAutoplay.setOnClickListener {
@ -139,86 +136,43 @@ class AccountFragment : Fragment() {
activity?.let { loginModal.show(it.supportFragmentManager, LoginModalBottomSheet.TAG) } activity?.let { loginModal.show(it.supportFragmentManager, LoginModalBottomSheet.TAG) }
} }
private fun showAudioLanguageSelection() { private fun showContentLanguageSelection() {
// we should be able to use the index of supportedLocals for language selection, items is GUI only // we should be able to use the index of supportedLocals for language selection, items is GUI only
val items = supportedAudioLocals.map { val items = supportedLocals.map {
it.toDisplayString(getString(R.string.settings_content_language_none)) it.toDisplayString(getString(R.string.settings_content_language_none))
}.toTypedArray() }.toTypedArray()
var initialSelection: Int var initialSelection: Int
// profile should be completed here, therefore blocking // profile should be completed here, therefore blocking
runBlocking { runBlocking {
initialSelection = supportedAudioLocals.indexOf(Locale.forLanguageTag( initialSelection = supportedLocals.indexOf(Locale.forLanguageTag(
profile.await().preferredContentAudioLanguage))
if (initialSelection < 0) initialSelection = supportedAudioLocals.lastIndex
}
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.settings_audio_language)
.setSingleChoiceItems(items, initialSelection){ dialog, which ->
updateAudioLanguage(supportedAudioLocals[which])
dialog.dismiss()
}
.show()
}
private fun showSubtitleLanguageSelection() {
// we should be able to use the index of supportedLocals for language selection, items is GUI only
val items = supportedSubtitleLocals.map {
it.toDisplayString(getString(R.string.settings_content_language_none))
}.toTypedArray()
var initialSelection: Int
// profile should be completed here, therefore blocking
runBlocking {
initialSelection = supportedSubtitleLocals.indexOf(Locale.forLanguageTag(
profile.await().preferredContentSubtitleLanguage)) profile.await().preferredContentSubtitleLanguage))
if (initialSelection < 0) initialSelection = supportedSubtitleLocals.lastIndex if (initialSelection < 0) initialSelection = supportedLocals.lastIndex
} }
MaterialAlertDialogBuilder(requireContext()) MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.settings_audio_language) .setTitle(R.string.settings_content_language)
.setSingleChoiceItems(items, initialSelection){ dialog, which -> .setSingleChoiceItems(items, initialSelection){ dialog, which ->
updateSubtitleLanguage(supportedSubtitleLocals[which]) updatePrefContentLanguage(supportedLocals[which])
dialog.dismiss() dialog.dismiss()
} }
.show() .show()
} }
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
private fun updateAudioLanguage(preferredLocale: Locale) { private fun updatePrefContentLanguage(preferredLocale: Locale) {
lifecycleScope.launch { lifecycleScope.launch {
Crunchyroll.setPreferredAudioLanguage(preferredLocale.toLanguageTag()) Crunchyroll.postPrefSubLanguage(preferredLocale.toLanguageTag())
}.invokeOnCompletion { }.invokeOnCompletion {
// update the local preferred audio language // update the local preferred content language
Preferences.savePreferredAudioLocal(requireContext(), preferredLocale) Preferences.savePreferredSubtitleLocal(requireContext(), preferredLocale)
// update profile since the language selection might have changed // update profile since the language selection might have changed
profile = lifecycleScope.async { Crunchyroll.profile() } profile = lifecycleScope.async { Crunchyroll.profile() }
profile.invokeOnCompletion { profile.invokeOnCompletion {
// update language once loading profile is completed // update language once loading profile is completed
binding.textSettingsAudioLanguageDesc.text = Locale.forLanguageTag( binding.textSettingsContentLanguageDesc.text = Locale.forLanguageTag(
profile.getCompleted().preferredContentAudioLanguage
).displayLanguage
}
}
}
@OptIn(ExperimentalCoroutinesApi::class)
private fun updateSubtitleLanguage(preferredLocal: Locale) {
lifecycleScope.launch {
Crunchyroll.setPreferredSubtitleLanguage(preferredLocal.toLanguageTag())
}.invokeOnCompletion {
// update the local preferred subtitle language
Preferences.savePreferredSubtitleLocal(requireContext(), preferredLocal)
// update profile since the language selection might have changed
profile = lifecycleScope.async { Crunchyroll.profile() }
profile.invokeOnCompletion {
// update language once loading profile is completed
binding.textSettingsAudioLanguageDesc.text = Locale.forLanguageTag(
profile.getCompleted().preferredContentSubtitleLanguage profile.getCompleted().preferredContentSubtitleLanguage
).displayLanguage ).displayLanguage
} }

View File

@ -1,21 +1,17 @@
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.ui.activity.main.viewmodel.MyListsFragmentViewModel import org.mosad.teapod.parser.crunchyroll.Crunchyroll
import org.mosad.teapod.util.toItemMediaList import org.mosad.teapod.util.toItemMediaList
class MyListsFragment : Fragment() { class MyListsFragment : Fragment() {
@ -23,8 +19,6 @@ 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 {
@ -39,6 +33,7 @@ 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)
@ -48,33 +43,15 @@ class MyListsFragment : Fragment() {
} }
}.attach() }.attach()
viewLifecycleOwner.lifecycleScope.launch { lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { val items = Crunchyroll.watchlist(50)
model.onUiState(viewLifecycleOwner.lifecycleScope) { uiState ->
when (uiState) { MediaFragmentSimilar(items.toItemMediaList()).also {
is MyListsFragmentViewModel.UiState.Normal -> bindUiStateNormal(uiState) fragments.add(it)
is MyListsFragmentViewModel.UiState.Loading -> bindUiStateLoading() pagerAdapter.notifyItemInserted(fragments.indexOf(it))
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}")
} }
/** /**

View File

@ -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(n = 20).data } val upNextJob = viewModelScope.async { Crunchyroll.upNextAccount().data }
val watchlistJob = viewModelScope.async { Crunchyroll.watchlist(WATCHLIST_LENGTH).data } val watchlistJob = viewModelScope.async { Crunchyroll.watchlist(WATCHLIST_LENGTH).items }
val recommendationsJob = viewModelScope.async { val recommendationsJob = viewModelScope.async {
Crunchyroll.recommendations(n = 20).data Crunchyroll.recommendations(20).items
} }
val recentlyAddedJob = viewModelScope.async { val recentlyAddedJob = viewModelScope.async {
Crunchyroll.browse(sortBy = SortBy.NEWLY_ADDED, n = 50).data Crunchyroll.browse(sortBy = SortBy.NEWLY_ADDED, n = 50).items
} }
val topTenJob = viewModelScope.async { val topTenJob = viewModelScope.async {
Crunchyroll.browse(sortBy = SortBy.POPULARITY, n = 10).data Crunchyroll.browse(sortBy = SortBy.POPULARITY, n = 10).items
} }
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).data val watchlistItems = Crunchyroll.watchlist(WATCHLIST_LENGTH).items
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(n = 20).data val upNextItems = Crunchyroll.upNextAccount().data
currentUiState.copy(upNextItems = upNextItems) currentUiState.copy(upNextItems = upNextItems)
} else { } else {
currentUiState currentUiState

View File

@ -90,7 +90,7 @@ class LibraryFragmentViewModel : ViewModel() {
delay(250) delay(250)
val results = Crunchyroll.search(query, 50) val results = Crunchyroll.search(query, 50)
.data.firstOrNull()?.items?.toItemMediaList() .items.firstOrNull()?.items?.toItemMediaList()
?: listOf() ?: listOf()
uiState.emit(UiState.Search(results)) uiState.emit(UiState.Search(results))
} }

View File

@ -1,50 +0,0 @@
package org.mosad.teapod.ui.activity.main.viewmodel
import androidx.lifecycle.LifecycleCoroutineScope
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import org.mosad.teapod.parser.crunchyroll.Crunchyroll
import org.mosad.teapod.parser.crunchyroll.Item
class MyListsFragmentViewModel : ViewModel() {
private val WATCHLIST_LENGTH = 50
private val uiState = MutableStateFlow<UiState>(UiState.Loading)
sealed class UiState {
object Loading : UiState()
data class Normal(
val watchlistItems: List<Item>
) : UiState()
data class Error(val message: String?) : UiState()
}
init {
load()
}
fun onUiState(scope: LifecycleCoroutineScope, collector: (UiState) -> Unit) {
scope.launch { uiState.collect { collector(it) } }
}
fun load() {
viewModelScope.launch {
uiState.emit(UiState.Loading)
try {
// run the loading in parallel to speed up the process
val watchlistJob = viewModelScope.async { Crunchyroll.watchlist(WATCHLIST_LENGTH).data }
uiState.emit(
UiState.Normal(watchlistJob.await())
)
} catch (e: Exception) {
uiState.emit(UiState.Error(e.message))
}
}
}
}

View File

@ -222,9 +222,12 @@ 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 = currentEpisode.versions?.firstOrNull { currentVersion = if (Preferences.preferSubbed) {
it.audioLocale == currentAudioLocale.toLanguageTag() currentEpisode.versions?.first { it.original } ?: NoneVersion
} ?: currentEpisode.versions?.first() ?: NoneVersion } else {
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) {

View File

@ -14,8 +14,6 @@ import android.widget.TextView
import androidx.core.view.children import androidx.core.view.children
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
import org.mosad.teapod.R import org.mosad.teapod.R
import org.mosad.teapod.databinding.PlayerLanguageSettingsBinding import org.mosad.teapod.databinding.PlayerLanguageSettingsBinding
import org.mosad.teapod.ui.activity.player.PlayerViewModel import org.mosad.teapod.ui.activity.player.PlayerViewModel
@ -83,10 +81,8 @@ class LanguageSettingsDialogFragment : DialogFragment() {
binding.buttonCloseLanguageSettings.setOnClickListener { dismiss() } binding.buttonCloseLanguageSettings.setOnClickListener { dismiss() }
binding.buttonCancel.setOnClickListener { dismiss() } binding.buttonCancel.setOnClickListener { dismiss() }
binding.buttonSelect.setOnClickListener { binding.buttonSelect.setOnClickListener {
lifecycleScope.launch { model.setLanguage(selectedAudioLocale, selectedSubtitleLocale)
model.setLanguage(selectedAudioLocale, selectedSubtitleLocale) dismiss()
dismiss()
}
} }
// initially hide the status and navigation bar // initially hide the status and navigation bar

View File

@ -9,11 +9,12 @@ 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.CollectionV2 import org.mosad.teapod.parser.crunchyroll.Collection
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.Locale import java.util.*
/** /**
* Create a Intent for PlayerActivity with season and episode id. * Create a Intent for PlayerActivity with season and episode id.
@ -35,8 +36,8 @@ fun <T> concatenate(vararg lists: List<T>): List<T> {
} }
// TODO move to correct location // TODO move to correct location
fun CollectionV2<Item>.toItemMediaList(): List<ItemMedia> { fun Collection<Item>.toItemMediaList(): List<ItemMedia> {
return this.data.map { return this.items.map {
ItemMedia(it.id, it.title, it.images.poster_wide[0][0].source) ItemMedia(it.id, it.title, it.images.poster_wide[0][0].source)
} }
} }
@ -58,7 +59,7 @@ fun Locale.toDisplayString(fallback: String): String {
} }
} }
fun CollectionV2<PlayheadObject>.toPlayheadsMap(): Map<String, PlayheadObject> { fun Collection2<PlayheadObject>.toPlayheadsMap(): Map<String, PlayheadObject> {
return this.data.associateBy { it.contentId } return this.data.associateBy { it.contentId }
} }

View File

@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M12,3v9.28c-0.47,-0.17 -0.97,-0.28 -1.5,-0.28C8.01,12 6,14.01 6,16.5S8.01,21 10.5,21c2.31,0 4.2,-1.75 4.45,-4H15V6h4V3h-7z"/>
</vector>

View File

@ -141,7 +141,7 @@
android:textStyle="bold" /> android:textStyle="bold" />
<LinearLayout <LinearLayout
android:id="@+id/linear_settings_audio_language" android:id="@+id/linear_settings_content_language"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="horizontal" android:orientation="horizontal"
@ -151,12 +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_audio_language" android:contentDescription="@string/settings_content_language"
android:minWidth="48dp" android:minWidth="48dp"
android:minHeight="48dp" android:minHeight="48dp"
android:padding="9dp" android:padding="9dp"
android:scaleType="fitXY" android:scaleType="fitXY"
android:src="@drawable/ic_baseline_audiotrack_24" /> android:src="@drawable/ic_baseline_language_24" />
<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_audio_language" android:text="@string/settings_content_language"
android:textAppearance="@style/TextAppearance.Material3.BodyLarge" /> android:textAppearance="@style/TextAppearance.Material3.BodyLarge" />
<TextView <TextView
android:id="@+id/text_settings_audio_language_desc" android:id="@+id/text_settings_content_language_desc"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/settings_content_language_desc" /> android:text="@string/settings_content_language_desc" />
@ -179,41 +179,67 @@
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout
android:id="@+id/linear_settings_subtitle_language" android:id="@+id/linear_settings_secondary"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:gravity="center"
android:orientation="horizontal" android:orientation="horizontal"
android:padding="7dp"> android:padding="7dp">
<ImageView <ImageView
android:id="@+id/imageView7" android:id="@+id/imageView3"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:contentDescription="@string/settings_subtitle_language" android:contentDescription="@string/settings_prefer_subbed"
android:minWidth="48dp" android:minWidth="48dp"
android:minHeight="48dp" android:minHeight="48dp"
android:padding="9dp" android:padding="9dp"
android:scaleType="fitXY" android:scaleType="fitXY"
android:src="@drawable/ic_baseline_subtitles_24" /> android:src="@drawable/ic_baseline_subtitles_24" />
<LinearLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="wrap_content">
android:orientation="vertical">
<TextView <LinearLayout
android:id="@+id/text_settings_subtitle_language" android:id="@+id/linearLayout"
android:layout_width="match_parent" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/settings_subtitle_language" android:orientation="vertical"
android:textAppearance="@style/TextAppearance.Material3.BodyLarge" /> app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/switch_secondary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView <TextView
android:id="@+id/text_settings_subtitle_language_desc" android:id="@+id/text_settings_secondary"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/settings_prefer_subbed"
android: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:text="@string/settings_content_language_desc" /> android:checked="true"
</LinearLayout> 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>
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout

View File

@ -45,8 +45,7 @@
<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_audio_language">Audio Sprache</string> <string name="settings_content_language">Bevorzuge Inhaltssprache</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>

View File

@ -59,8 +59,7 @@
<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_audio_language">Audio language</string> <string name="settings_content_language">Preferred content 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>

View File

@ -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 = "2.0.20" ext.kotlin_version = "1.8.10"
ext.ktor_version = "3.0.0" ext.ktor_version = "2.2.4"
ext.exo_version = "2.18.7" ext.exo_version = "2.18.5"
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:8.7.1' classpath 'com.android.tools.build:gradle:8.0.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 {
} }
} }
tasks.register('clean', Delete) { task clean(type: Delete) {
delete rootProject.layout.buildDirectory delete rootProject.buildDir
} }

View File

@ -1,10 +0,0 @@
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

View File

@ -1,10 +0,0 @@
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

View File

@ -16,8 +16,9 @@ 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=false android.enableJetifier=true
# 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.

View File

@ -1,7 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

34
gradlew vendored
View File

@ -15,8 +15,6 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
# #
# SPDX-License-Identifier: Apache-2.0
#
############################################################################## ##############################################################################
# #
@ -57,7 +55,7 @@
# Darwin, MinGW, and NonStop. # Darwin, MinGW, and NonStop.
# #
# (3) This script is generated from the Groovy template # (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project. # within the Gradle project.
# #
# You can find Gradle at https://github.com/gradle/gradle/. # You can find Gradle at https://github.com/gradle/gradle/.
@ -85,9 +83,10 @@ done
# This is normally unused # This is normally unused
# shellcheck disable=SC2034 # shellcheck disable=SC2034
APP_BASE_NAME=${0##*/} APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
' "$PWD" ) || 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
@ -134,13 +133,10 @@ location of your Java installation."
fi fi
else else
JAVACMD=java JAVACMD=java
if ! command -v java >/dev/null 2>&1 which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
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.
@ -148,7 +144,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=SC2039,SC3045 # shellcheck disable=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
@ -156,7 +152,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=SC2039,SC3045 # shellcheck disable=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
@ -201,15 +197,11 @@ if "$cygwin" || "$msys" ; then
done done
fi fi
# Collect all arguments for the java command;
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# Collect all arguments for the java command: # * put everything else in single quotes, so that it's not re-expanded.
# * 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" \

22
gradlew.bat vendored
View File

@ -13,8 +13,6 @@
@rem See the License for the specific language governing permissions and @rem See the License for the specific language governing permissions and
@rem limitations under the License. @rem limitations under the License.
@rem @rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off @if "%DEBUG%"=="" @echo off
@rem ########################################################################## @rem ##########################################################################
@ -45,11 +43,11 @@ set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1 %JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2 echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo. 1>&2 echo.
echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation. 1>&2 echo location of your Java installation.
goto fail goto fail
@ -59,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute if exist "%JAVA_EXE%" goto execute
echo. 1>&2 echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo. 1>&2 echo.
echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation. 1>&2 echo location of your Java installation.
goto fail goto fail