Compare commits

...

12 Commits

Author SHA1 Message Date
Jannik b07a6fd407
update gradle wrapper, agp, kotlin and libraries 2024-03-03 21:21:12 +01:00
Jannik 7d661712f7
update to kotlin 1.9.0 2023-11-15 15:50:13 +01:00
Jannik 8fcf047e99
update agp and libraries
* agp 8.1.2 -> 8.1.3
* ktor 2.3.4 -> 2.3.6
* androidx.core 1.10.1 -> 1.12.0
* androidx.navigation 2.6.0 -> 2.7.5
* androidx.lifecycle 2.6.1 -> 2.6.1
* com.google.android.material 1.9.0 -> 1.10.0
* gradle wrapper 8.2.1 -> 8.4
2023-11-15 15:37:44 +01:00
Jannik 17dbe945e5
mograte MyListFragment to use a simple ViewModel
fixes crashes in MyListFragment if the User closes the fragment with a loading job still running, part of #56
2023-10-15 18:44:22 +02:00
Jannik 5f609d4c33
update agp and libraries
* agp 8.1.0 -> 8.1.2
* ktor 2.3.2 -> 2.3.4
* kotlinx-serialization-json 1.5.1 -> 1.6.0
2023-09-29 23:47:47 +02:00
Jannik 6515f657d0
partially revert c448b44fc4 2023-08-11 15:07:33 +02:00
Jannik c448b44fc4
updatelibraries and agp
* agp 8.0.2 -> 8.1.0
* kotlinx-coroutines-android 1.7.2 -> 1.7.3
* navigation-fragment-ktx 2.6.0 -> 2.7.0
* navigation-ui-ktx 2.6.0 -> 2.7.0
2023-08-11 14:58:17 +02:00
Jannik 88ebc378d3
add changelog for beta3; update gradle wrapper to 8.2.1 2023-08-11 14:41:55 +02:00
Jannik 1a012cba7d
add support for dedicated subtitle and audio language settings 2023-07-21 21:42:55 +02:00
Jannik 59a457430e
migrate more Crunchyroll API endpoints to v2 2023-07-21 17:22:45 +02:00
Jannik 0662d656ac
update libraries, agp and kotlin
* kotlin 1.8.10 -> 1.8.22
* kotlinx-coroutines-android 1.6.4 -> 1.7.2
* kotlinx-serialization-json 1.5.0 -> 1.5.1
* core-ktx 1.10.0 -> 1.10.1
* core-splashscreen 1.0.0 -> 1.0.1
* navigation-fragment-ktx 2.5.3 -> 2.6.0
* navigation-ui-ktx 2.5.3 -> 2.6.0
* security-crypto 1.1.0-alpha05 -> 1.1.0-alpha06
* material 1.8.0 -> 1.9.0
* ktor 2.2.4 -> 2.3.2
* exo-player 2.18.5 -> 2.18.7
* agp 8.0.0 -> 8.0.2
2023-07-21 11:43:38 +02:00
Jannik 3549a3d2a7
migrate Crunchyroll.objects() to new v2 endpoint
fixes #71
2023-07-21 11:39:48 +02:00
23 changed files with 402 additions and 240 deletions

View File

@ -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"

View File

@ -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.
* *

View File

@ -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>
)

View File

@ -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
) )

View File

@ -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

View File

@ -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
} }

View File

@ -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}")
} }
/** /**

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().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

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)
.items.firstOrNull()?.items?.toItemMediaList() .data.firstOrNull()?.items?.toItemMediaList()
?: listOf() ?: listOf()
uiState.emit(UiState.Search(results)) uiState.emit(UiState.Search(results))
} }

View File

@ -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))
}
}
}
}

View File

@ -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) {

View File

@ -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 }
} }

View File

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

View File

@ -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

View File

@ -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>

View File

@ -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>

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 = "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
} }

View File

@ -0,0 +1,10 @@
Dies ist der dritte beta Release von Teapod 1.1.
* Neues App Design (Material Design 3)
* Unterstützung für Crunchyroll v2 API
* Intro überspringen hinzugefügt
* Seperaten Screen für "Meine Liste"
* Dynamische Spaltenanzahl für alle Screens um große Bildschirme besser zu unterstützen
* Kleine UI/UX Verbesserungen
Alle Änderungen: https://git.mosad.xyz/Seil0/teapod/compare/1.0.0...1.1.0-beta3

View File

@ -0,0 +1,10 @@
This is the third beta release of Teapod 1.1.
* Migrate to material design 3
* Migrate crunchyroll parser to v2 (fixes crunchyroll)
* Add skip intro function
* Add a separate Watchlist fragment
* Dynamically set column count based on the display size
* Minor UI/UX improvements
Full changelog: https://git.mosad.xyz/Seil0/teapod/compare/1.0.0...1.1.0-beta3

View File

@ -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.

View File

@ -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

29
gradlew vendored
View File

@ -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" \