Compare commits
41 Commits
1.0.0-beta
...
1.0.0-beta
Author | SHA1 | Date | |
---|---|---|---|
ad1e3068cd
|
|||
de1f19c2b7
|
|||
12bbc2ef5f
|
|||
0186cef79e
|
|||
bc5509cf93
|
|||
ef9a0f00d0
|
|||
b85d7ae025
|
|||
69c9666d2b
|
|||
7d6c300f7e
|
|||
1ebc1194e6
|
|||
c48328723b
|
|||
95c8a72c94
|
|||
fc04e8e222
|
|||
a898a70653
|
|||
58aab72097
|
|||
35157b78f5
|
|||
c6a00ea061
|
|||
80a7fc4398
|
|||
dd6ca8b90e
|
|||
e80e81af0f
|
|||
f852600dc7
|
|||
aa49169034
|
|||
7abb5cd3e8
|
|||
3a71bdd2c7
|
|||
629c144c5b
|
|||
b2196f11da
|
|||
5b5a74a1de
|
|||
7a860a7270
|
|||
e97ad9a245
|
|||
cf435fdb72
|
|||
42895a6fba
|
|||
eaf1cf78e9
|
|||
1af82f8370
|
|||
d31a19a4f1
|
|||
b27666ee69 | |||
e76cbda04d
|
|||
7fbf639a70
|
|||
ff63b3d7a4
|
|||
7d32cecd89
|
|||
72280f29d8
|
|||
cd4cfb7a0c
|
@ -1,20 +1,19 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id 'com.android.application'
|
id 'com.android.application'
|
||||||
id 'kotlin-android'
|
id 'kotlin-android'
|
||||||
id 'kotlin-android-extensions'
|
|
||||||
id 'org.jetbrains.kotlin.plugin.serialization' version "$kotlin_version"
|
id 'org.jetbrains.kotlin.plugin.serialization' version "$kotlin_version"
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 30
|
compileSdkVersion 31
|
||||||
buildToolsVersion "30.0.3"
|
buildToolsVersion "30.0.3"
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "org.mosad.teapod"
|
applicationId "org.mosad.teapod"
|
||||||
minSdkVersion 23
|
minSdkVersion 23
|
||||||
targetSdkVersion 30
|
targetSdkVersion 31
|
||||||
versionCode 9000 //00.09.000
|
versionCode 9010 //00.09.010
|
||||||
versionName "1.0.0-beta1"
|
versionName "1.0.0-beta2"
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
resValue "string", "build_time", buildTime()
|
resValue "string", "build_time", buildTime()
|
||||||
@ -39,37 +38,39 @@ android {
|
|||||||
}
|
}
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = '1.8'
|
jvmTarget = '1.8'
|
||||||
|
kotlin.sourceSets.all {
|
||||||
|
languageSettings.optIn("kotlin.RequiresOptIn")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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.5.2'
|
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1'
|
||||||
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.1'
|
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.3'
|
||||||
|
|
||||||
implementation 'androidx.core:core-ktx:1.6.0'
|
implementation 'androidx.core:core-ktx:1.7.0'
|
||||||
implementation 'androidx.appcompat:appcompat:1.3.1'
|
implementation 'androidx.core:core-splashscreen:1.0.0-rc01'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.0'
|
implementation 'androidx.appcompat:appcompat:1.4.1'
|
||||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5'
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
|
||||||
implementation 'androidx.navigation:navigation-ui-ktx:2.3.5'
|
implementation 'androidx.navigation:navigation-fragment-ktx:2.4.2'
|
||||||
|
implementation 'androidx.navigation:navigation-ui-ktx:2.4.2'
|
||||||
implementation 'androidx.security:security-crypto:1.1.0-alpha03'
|
implementation 'androidx.security:security-crypto:1.1.0-alpha03'
|
||||||
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.3.1'
|
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1'
|
||||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1'
|
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1'
|
||||||
|
|
||||||
implementation 'com.google.android.material:material:1.4.0'
|
implementation 'com.google.android.material:material:1.5.0'
|
||||||
implementation 'com.google.code.gson:gson:2.8.8' // TODO remove, still used by metadb
|
implementation "com.google.android.exoplayer:exoplayer-core:$exo_version"
|
||||||
implementation 'com.google.android.exoplayer:exoplayer-core:2.15.0'
|
implementation "com.google.android.exoplayer:exoplayer-hls:$exo_version"
|
||||||
implementation 'com.google.android.exoplayer:exoplayer-hls:2.15.0'
|
implementation "com.google.android.exoplayer:exoplayer-dash:$exo_version"
|
||||||
implementation 'com.google.android.exoplayer:exoplayer-dash:2.15.0'
|
implementation "com.google.android.exoplayer:exoplayer-ui:$exo_version"
|
||||||
implementation 'com.google.android.exoplayer:exoplayer-ui:2.15.0'
|
implementation "com.google.android.exoplayer:extension-mediasession:$exo_version"
|
||||||
implementation 'com.google.android.exoplayer:extension-mediasession:2.15.0'
|
|
||||||
|
|
||||||
implementation 'com.github.bumptech.glide:glide:4.12.0'
|
implementation 'com.github.bumptech.glide:glide:4.13.1'
|
||||||
implementation 'jp.wasabeef:glide-transformations:4.3.0'
|
implementation 'jp.wasabeef:glide-transformations:4.3.0'
|
||||||
implementation 'com.afollestad.material-dialogs:core:3.3.0' // TODO remove once unused
|
|
||||||
implementation 'com.afollestad.material-dialogs:bottomsheets:3.3.0' // TODO remove once unused
|
|
||||||
|
|
||||||
implementation "io.ktor:ktor-client-core:$ktor_version"
|
implementation "io.ktor:ktor-client-core:$ktor_version"
|
||||||
implementation "io.ktor:ktor-client-android:$ktor_version"
|
implementation "io.ktor:ktor-client-android:$ktor_version"
|
||||||
|
4
app/proguard-rules.pro
vendored
@ -24,10 +24,6 @@
|
|||||||
|
|
||||||
-keep class org.json.** { *; }
|
-keep class org.json.** { *; }
|
||||||
|
|
||||||
#Gson
|
|
||||||
-keepattributes Signature
|
|
||||||
-dontwarn sun.misc.**
|
|
||||||
|
|
||||||
# kotlinx.serialization
|
# kotlinx.serialization
|
||||||
# Keep `Companion` object fields of serializable classes.
|
# Keep `Companion` object fields of serializable classes.
|
||||||
# This avoids serializer lookup through `getDeclaredClasses` as done for named companion objects.
|
# This avoids serializer lookup through `getDeclaredClasses` as done for named companion objects.
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
package="org.mosad.teapod">
|
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
|
||||||
@ -13,32 +12,27 @@
|
|||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/AppTheme.Dark">
|
android:theme="@style/AppTheme.Dark">
|
||||||
<activity
|
<activity
|
||||||
android:name="org.mosad.teapod.ui.activity.SplashActivity"
|
android:exported="true"
|
||||||
android:label="@string/app_name"
|
android:name="org.mosad.teapod.ui.activity.main.MainActivity"
|
||||||
android:theme="@style/SplashTheme"
|
android:screenOrientation="portrait"
|
||||||
android:screenOrientation="portrait">
|
android:theme="@style/Theme.App.Starting">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
|
android:exported="false"
|
||||||
android:name="org.mosad.teapod.ui.activity.onboarding.OnboardingActivity"
|
android:name="org.mosad.teapod.ui.activity.onboarding.OnboardingActivity"
|
||||||
android:label="@string/app_name"
|
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="portrait"
|
||||||
android:launchMode="singleTop"
|
android:launchMode="singleTop"
|
||||||
android:windowSoftInputMode="adjustPan">
|
android:windowSoftInputMode="adjustPan">
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name="org.mosad.teapod.ui.activity.main.MainActivity"
|
android:exported="false"
|
||||||
android:label="@string/app_name"
|
|
||||||
android:screenOrientation="portrait">
|
|
||||||
</activity>
|
|
||||||
<activity
|
|
||||||
android:name="org.mosad.teapod.ui.activity.player.PlayerActivity"
|
android:name="org.mosad.teapod.ui.activity.player.PlayerActivity"
|
||||||
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|layoutDirection"
|
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|layoutDirection"
|
||||||
android:autoRemoveFromRecents="true"
|
android:autoRemoveFromRecents="true"
|
||||||
android:label="@string/app_name"
|
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:parentActivityName="org.mosad.teapod.ui.activity.main.MainActivity"
|
android:parentActivityName="org.mosad.teapod.ui.activity.main.MainActivity"
|
||||||
android:supportsPictureInPicture="true"
|
android:supportsPictureInPicture="true"
|
||||||
|
@ -25,6 +25,7 @@ package org.mosad.teapod.parser.crunchyroll
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
import io.ktor.client.call.*
|
import io.ktor.client.call.*
|
||||||
|
import io.ktor.client.features.*
|
||||||
import io.ktor.client.features.json.*
|
import io.ktor.client.features.json.*
|
||||||
import io.ktor.client.features.json.serializer.*
|
import io.ktor.client.features.json.serializer.*
|
||||||
import io.ktor.client.request.*
|
import io.ktor.client.request.*
|
||||||
@ -39,7 +40,6 @@ import kotlinx.serialization.json.buildJsonObject
|
|||||||
import kotlinx.serialization.json.put
|
import kotlinx.serialization.json.put
|
||||||
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.util.concatenate
|
|
||||||
|
|
||||||
private val json = Json { ignoreUnknownKeys = true }
|
private val json = Json { ignoreUnknownKeys = true }
|
||||||
|
|
||||||
@ -57,6 +57,8 @@ 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)
|
||||||
|
private val tokenRefreshContext = newSingleThreadContext("TokenRefreshContext")
|
||||||
|
|
||||||
private var accountID = ""
|
private var accountID = ""
|
||||||
|
|
||||||
@ -64,7 +66,7 @@ object Crunchyroll {
|
|||||||
private var signature = ""
|
private var signature = ""
|
||||||
private var keyPairID = ""
|
private var keyPairID = ""
|
||||||
|
|
||||||
private val browsingCache = arrayListOf<Item>()
|
private val browsingCache = hashMapOf<String, BrowseResult>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load the pai token, see:
|
* Load the pai token, see:
|
||||||
@ -98,15 +100,27 @@ object Crunchyroll {
|
|||||||
|
|
||||||
var success = false// is false
|
var success = false// is false
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
// TODO handle exceptions
|
Log.i(TAG, "getting token ...")
|
||||||
|
|
||||||
|
val status = try {
|
||||||
val response: HttpResponse = client.submitForm("$baseUrl$tokenEndpoint", formParameters = formData) {
|
val response: HttpResponse = client.submitForm("$baseUrl$tokenEndpoint", formParameters = formData) {
|
||||||
header("Authorization", "Basic $basicApiToken")
|
header("Authorization", "Basic $basicApiToken")
|
||||||
}
|
}
|
||||||
token = response.receive()
|
token = response.receive()
|
||||||
tokenValidUntil = System.currentTimeMillis() + (token.expiresIn * 1000)
|
tokenValidUntil = System.currentTimeMillis() + (token.expiresIn * 1000)
|
||||||
|
response.status
|
||||||
|
} catch (ex: ClientRequestException) {
|
||||||
|
val status = ex.response.status
|
||||||
|
if (status == HttpStatusCode.Unauthorized) {
|
||||||
|
Log.e(TAG, "Could not complete login: " +
|
||||||
|
"${status.value} ${status.description}. " +
|
||||||
|
"Probably wrong username or password")
|
||||||
|
}
|
||||||
|
|
||||||
Log.i(TAG, "login complete with code ${response.status}")
|
status
|
||||||
success = (response.status == HttpStatusCode.OK)
|
}
|
||||||
|
Log.i(TAG, "Login complete with code $status")
|
||||||
|
success = (status == HttpStatusCode.OK)
|
||||||
}
|
}
|
||||||
|
|
||||||
return@runBlocking success
|
return@runBlocking success
|
||||||
@ -126,7 +140,9 @@ object Crunchyroll {
|
|||||||
params: List<Pair<String, Any?>> = listOf(),
|
params: List<Pair<String, Any?>> = listOf(),
|
||||||
bodyObject: Any = Any()
|
bodyObject: Any = Any()
|
||||||
): T = coroutineScope {
|
): T = coroutineScope {
|
||||||
|
withContext(tokenRefreshContext) {
|
||||||
if (System.currentTimeMillis() > tokenValidUntil) refreshToken()
|
if (System.currentTimeMillis() > tokenValidUntil) refreshToken()
|
||||||
|
}
|
||||||
|
|
||||||
return@coroutineScope (Dispatchers.IO) {
|
return@coroutineScope (Dispatchers.IO) {
|
||||||
val response: T = client.request(url) {
|
val response: T = client.request(url) {
|
||||||
@ -235,7 +251,6 @@ object Crunchyroll {
|
|||||||
* General element/media functions: browse, search, objects, season_list
|
* General element/media functions: browse, search, objects, season_list
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// TODO categories
|
|
||||||
/**
|
/**
|
||||||
* Browse the media available on crunchyroll.
|
* Browse the media available on crunchyroll.
|
||||||
*
|
*
|
||||||
@ -245,13 +260,14 @@ object Crunchyroll {
|
|||||||
* @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,
|
sortBy: SortBy = SortBy.ALPHABETICAL,
|
||||||
seasonTag: String = "",
|
seasonTag: String = "",
|
||||||
start: Int = 0,
|
start: Int = 0,
|
||||||
n: Int = 10
|
n: Int = 10
|
||||||
): BrowseResult {
|
): BrowseResult {
|
||||||
val browseEndpoint = "/content/v1/browse"
|
val browseEndpoint = "/content/v1/browse"
|
||||||
val noneOptParams = listOf(
|
val parameters = mutableListOf(
|
||||||
"locale" to Preferences.preferredLocale.toLanguageTag(),
|
"locale" to Preferences.preferredLocale.toLanguageTag(),
|
||||||
"sort_by" to sortBy.str,
|
"sort_by" to sortBy.str,
|
||||||
"start" to start,
|
"start" to start,
|
||||||
@ -259,12 +275,20 @@ object Crunchyroll {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// if a season tag is present add it to the parameters
|
// if a season tag is present add it to the parameters
|
||||||
val parameters = if (seasonTag.isNotEmpty()) {
|
if (seasonTag.isNotEmpty()) {
|
||||||
concatenate(noneOptParams, listOf("season_tag" to seasonTag))
|
parameters.add("season_tag" to seasonTag)
|
||||||
} else {
|
|
||||||
noneOptParams
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if a season tag is present add it to the parameters
|
||||||
|
if (categories.isNotEmpty()) {
|
||||||
|
parameters.add("categories" to categories.joinToString(",") { it.str })
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetch result if not already cached
|
||||||
|
if (browsingCache.contains(parameters.toString())) {
|
||||||
|
Log.d(TAG, "browse result cached: $parameters")
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, "browse result not cached, fetching: $parameters")
|
||||||
val browseResult: BrowseResult = try {
|
val browseResult: BrowseResult = try {
|
||||||
requestGet(browseEndpoint, parameters)
|
requestGet(browseEndpoint, parameters)
|
||||||
}catch (ex: SerializationException) {
|
}catch (ex: SerializationException) {
|
||||||
@ -272,15 +296,26 @@ object Crunchyroll {
|
|||||||
NoneBrowseResult
|
NoneBrowseResult
|
||||||
}
|
}
|
||||||
|
|
||||||
// add results to cache TODO improve
|
// if the cache has more than 100 entries clear it, so it doesn't become a memory problem
|
||||||
|
// Note: this value is totally guessed and should be replaced by a properly researched value
|
||||||
|
if (browsingCache.size > 100) {
|
||||||
browsingCache.clear()
|
browsingCache.clear()
|
||||||
browsingCache.addAll(browseResult.items)
|
}
|
||||||
|
|
||||||
return browseResult
|
// add results to cache
|
||||||
|
browsingCache[parameters.toString()] = browseResult
|
||||||
|
}
|
||||||
|
|
||||||
|
return browsingCache[parameters.toString()] ?: NoneBrowseResult
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO
|
* Search fo a query term.
|
||||||
|
* Note: currently this function only supports series/tv shows.
|
||||||
|
*
|
||||||
|
* @param query The query term as String
|
||||||
|
* @param n The maximum number of results to return, default = 10
|
||||||
|
* @return A **[SearchResult]** object
|
||||||
*/
|
*/
|
||||||
suspend fun search(query: String, n: Int = 10): SearchResult {
|
suspend fun search(query: String, n: Int = 10): SearchResult {
|
||||||
val searchEndpoint = "/content/v1/search"
|
val searchEndpoint = "/content/v1/search"
|
||||||
@ -367,7 +402,10 @@ object Crunchyroll {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO
|
* Get the next episode for a series.
|
||||||
|
*
|
||||||
|
* @param seriesId The series id for which to call up next
|
||||||
|
* @return A **[UpNextSeriesItem]** with a Panel representing the up next episode
|
||||||
*/
|
*/
|
||||||
suspend fun upNextSeries(seriesId: String): UpNextSeriesItem {
|
suspend fun upNextSeries(seriesId: String): UpNextSeriesItem {
|
||||||
val upNextSeriesEndpoint = "/content/v1/up_next_series"
|
val upNextSeriesEndpoint = "/content/v1/up_next_series"
|
||||||
@ -384,6 +422,12 @@ object Crunchyroll {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all available seasons for a series.
|
||||||
|
*
|
||||||
|
* @param seriesId The series id for which to get the seasons
|
||||||
|
* @return A **[Seasons]** object with a list of **[Season]**
|
||||||
|
*/
|
||||||
suspend fun seasons(seriesId: String): Seasons {
|
suspend fun seasons(seriesId: String): Seasons {
|
||||||
val seasonsEndpoint = "/cms/v2/${token.country}/M3/crunchyroll/seasons"
|
val seasonsEndpoint = "/cms/v2/${token.country}/M3/crunchyroll/seasons"
|
||||||
val parameters = listOf(
|
val parameters = listOf(
|
||||||
@ -402,6 +446,12 @@ object Crunchyroll {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all available episodes for a season.
|
||||||
|
*
|
||||||
|
* @param seasonId The season id for which to get the episodes
|
||||||
|
* @return A **[Episodes]** object with a list of **[Episode]**
|
||||||
|
*/
|
||||||
suspend fun episodes(seasonId: String): Episodes {
|
suspend fun episodes(seasonId: String): Episodes {
|
||||||
val episodesEndpoint = "/cms/v2/${token.country}/M3/crunchyroll/episodes"
|
val episodesEndpoint = "/cms/v2/${token.country}/M3/crunchyroll/episodes"
|
||||||
val parameters = listOf(
|
val parameters = listOf(
|
||||||
@ -420,6 +470,12 @@ object Crunchyroll {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all available subtitles and streams of a episode.
|
||||||
|
*
|
||||||
|
* @param url The playback url of a episode
|
||||||
|
* @return A **[Playback]** object
|
||||||
|
*/
|
||||||
suspend fun playback(url: String): Playback {
|
suspend fun playback(url: String): Playback {
|
||||||
return try {
|
return try {
|
||||||
requestGet("", url = url)
|
requestGet("", url = url)
|
||||||
@ -430,7 +486,7 @@ object Crunchyroll {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Additional media functions: watchlist (series), playhead
|
* Additional media functions: watchlist (series), playhead, similar to
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -495,11 +551,20 @@ object Crunchyroll {
|
|||||||
return try {
|
return try {
|
||||||
requestGet(playheadsEndpoint, parameters)
|
requestGet(playheadsEndpoint, parameters)
|
||||||
} catch (ex: SerializationException) {
|
} catch (ex: SerializationException) {
|
||||||
Log.e(TAG, "SerializationException in upNextSeries().", ex)
|
Log.e(TAG, "SerializationException in playheads().", ex)
|
||||||
|
emptyMap()
|
||||||
|
} catch (ex: Throwable) {
|
||||||
|
Log.e(TAG, "Exception in playheads().", ex.cause)
|
||||||
emptyMap()
|
emptyMap()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Post the playhead to crunchy (playhead position,watched state)
|
||||||
|
*
|
||||||
|
* @param episodeId A episode ID as strings.
|
||||||
|
* @param playhead The episodes playhead in seconds.
|
||||||
|
*/
|
||||||
suspend fun postPlayheads(episodeId: String, playhead: Int) {
|
suspend fun postPlayheads(episodeId: String, playhead: Int) {
|
||||||
val playheadsEndpoint = "/content/v1/playheads/$accountID"
|
val playheadsEndpoint = "/content/v1/playheads/$accountID"
|
||||||
val parameters = listOf("locale" to Preferences.preferredLocale.toLanguageTag())
|
val parameters = listOf("locale" to Preferences.preferredLocale.toLanguageTag())
|
||||||
@ -509,7 +574,34 @@ object Crunchyroll {
|
|||||||
put("playhead", playhead)
|
put("playhead", playhead)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
requestPost(playheadsEndpoint, parameters, json)
|
requestPost(playheadsEndpoint, parameters, json)
|
||||||
|
} catch (ex: Throwable) {
|
||||||
|
Log.e(TAG, "Exception in postPlayheads()", ex.cause)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get similar media for a show/movie.
|
||||||
|
*
|
||||||
|
* @param seriesId The crunchyroll series id of the media
|
||||||
|
* @param n The maximum number of results to return, default = 10
|
||||||
|
* @return A **[SimilarToResult]** object
|
||||||
|
*/
|
||||||
|
suspend fun similarTo(seriesId: String, n: Int = 10): SimilarToResult {
|
||||||
|
val similarToEndpoint = "/content/v1/$accountID/similar_to"
|
||||||
|
val parameters = listOf(
|
||||||
|
"guid" to seriesId,
|
||||||
|
"locale" to Preferences.preferredLocale.toLanguageTag(),
|
||||||
|
"n" to n
|
||||||
|
)
|
||||||
|
|
||||||
|
return try {
|
||||||
|
requestGet(similarToEndpoint, parameters)
|
||||||
|
}catch (ex: SerializationException) {
|
||||||
|
Log.e(TAG, "SerializationException in similarTo().", ex)
|
||||||
|
NoneSimilarToResult
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -561,10 +653,32 @@ object Crunchyroll {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun recommendations(n: Int = 20, start: Int = 0): RecommendationsList {
|
||||||
|
val recommendationsEndpoint = "/content/v1/$accountID/recommendations"
|
||||||
|
val parameters = listOf(
|
||||||
|
"locale" to Preferences.preferredLocale.toLanguageTag(),
|
||||||
|
"n" to n,
|
||||||
|
"start" to start,
|
||||||
|
"variant_id" to 0
|
||||||
|
)
|
||||||
|
|
||||||
|
return try {
|
||||||
|
requestGet(recommendationsEndpoint, parameters)
|
||||||
|
}catch (ex: SerializationException) {
|
||||||
|
Log.e(TAG, "SerializationException in recommendations().", ex)
|
||||||
|
NoneRecommendationsList
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Account/Profile functions
|
* Account/Profile functions
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get profile information for the currently logged in account.
|
||||||
|
*
|
||||||
|
* @return A **[Profile]** object
|
||||||
|
*/
|
||||||
suspend fun profile(): Profile {
|
suspend fun profile(): Profile {
|
||||||
val profileEndpoint = "/accounts/v1/me/profile"
|
val profileEndpoint = "/accounts/v1/me/profile"
|
||||||
|
|
||||||
@ -576,6 +690,11 @@ object Crunchyroll {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Post the preferred content subtitle language.
|
||||||
|
*
|
||||||
|
* @param languageTag the preferred language as language tag
|
||||||
|
*/
|
||||||
suspend fun postPrefSubLanguage(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 {
|
||||||
|
@ -50,6 +50,25 @@ enum class SortBy(val str: String) {
|
|||||||
POPULARITY("popularity")
|
POPULARITY("popularity")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("unused")
|
||||||
|
enum class Categories(val str: String) {
|
||||||
|
ACTION("action"),
|
||||||
|
ADVENTURE("adventure"),
|
||||||
|
COMEDY("comedy"),
|
||||||
|
DRAMA("drama"),
|
||||||
|
FANTASY("fantasy"),
|
||||||
|
MUSIC("music"),
|
||||||
|
ROMANCE("romance"),
|
||||||
|
SCI_FI("sci-fi"),
|
||||||
|
SEINEN("seinen"),
|
||||||
|
SHOJO("shojo"),
|
||||||
|
SHONEN("shonen"),
|
||||||
|
SLICE_OF_LIFE("slice+of+life"),
|
||||||
|
SPORTS("sports"),
|
||||||
|
SUPERNATURAL("supernatural"),
|
||||||
|
THRILLER("thriller")
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* token, index, account. This must pe present for the app to work!
|
* token, index, account. This must pe present for the app to work!
|
||||||
*/
|
*/
|
||||||
@ -101,9 +120,11 @@ data class Collection<T>(
|
|||||||
typealias SearchResult = Collection<SearchCollection>
|
typealias SearchResult = Collection<SearchCollection>
|
||||||
typealias SearchCollection = Collection<Item>
|
typealias SearchCollection = Collection<Item>
|
||||||
typealias BrowseResult = Collection<Item>
|
typealias BrowseResult = Collection<Item>
|
||||||
|
typealias SimilarToResult = Collection<Item>
|
||||||
typealias DiscSeasonList = Collection<SeasonListItem>
|
typealias DiscSeasonList = Collection<SeasonListItem>
|
||||||
typealias Watchlist = Collection<Item>
|
typealias Watchlist = Collection<Item>
|
||||||
typealias ContinueWatchingList = Collection<ContinueWatchingItem>
|
typealias ContinueWatchingList = Collection<ContinueWatchingItem>
|
||||||
|
typealias RecommendationsList = Collection<Item>
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class UpNextSeriesItem(
|
data class UpNextSeriesItem(
|
||||||
@ -117,7 +138,7 @@ data class UpNextSeriesItem(
|
|||||||
* panel data classes
|
* panel data classes
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// the data class Item is used in browse and search
|
// the data class Item is used in browse, search, watchlist and similar to
|
||||||
// TODO rename to MediaPanel
|
// TODO rename to MediaPanel
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Item(
|
data class Item(
|
||||||
@ -128,6 +149,7 @@ data class Item(
|
|||||||
val description: String,
|
val description: String,
|
||||||
val images: Images
|
val images: Images
|
||||||
// TODO series_metadata etc.
|
// TODO series_metadata etc.
|
||||||
|
// TODO add slug_title if present in search, browse, similar to
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@ -169,7 +191,7 @@ data class ContinueWatchingItem(
|
|||||||
@SerialName("fully_watched") val fullyWatched: Boolean = false,
|
@SerialName("fully_watched") val fullyWatched: Boolean = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
// EpisodePanel is used in ContinueWatchingItem
|
// EpisodePanel is used in ContinueWatchingItem and UpNextSeriesItem
|
||||||
@Serializable
|
@Serializable
|
||||||
data class EpisodePanel(
|
data class EpisodePanel(
|
||||||
@SerialName("id") val id: String,
|
@SerialName("id") val id: String,
|
||||||
@ -185,25 +207,35 @@ data class EpisodePanel(
|
|||||||
@Serializable
|
@Serializable
|
||||||
data class EpisodeMetadata(
|
data class EpisodeMetadata(
|
||||||
@SerialName("duration_ms") val durationMs: Int,
|
@SerialName("duration_ms") val durationMs: Int,
|
||||||
|
@SerialName("episode_number") val episodeNumber: Int? = null, // default/nullable value since optional
|
||||||
@SerialName("season_id") val seasonId: String,
|
@SerialName("season_id") val seasonId: String,
|
||||||
|
@SerialName("season_number") val seasonNumber: Int,
|
||||||
|
@SerialName("season_title") val seasonTitle: String,
|
||||||
@SerialName("series_id") val seriesId: String,
|
@SerialName("series_id") val seriesId: String,
|
||||||
@SerialName("series_title") val seriesTitle: String,
|
@SerialName("series_title") val seriesTitle: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
val NoneItem = Item("", "", "", "", "", Images(emptyList(), emptyList()))
|
val NoneItem = Item("", "", "", "", "", Images(emptyList(), emptyList()))
|
||||||
val NoneEpisodeMetadata = EpisodeMetadata(0, "", "", "")
|
val NoneEpisodeMetadata = EpisodeMetadata(0, 0, "", 0, "", "", "")
|
||||||
val NoneEpisodePanel = EpisodePanel("", "", "", "", "", NoneEpisodeMetadata, Thumbnail(listOf()), "")
|
val NoneEpisodePanel = EpisodePanel("", "", "", "", "", NoneEpisodeMetadata, Thumbnail(listOf()), "")
|
||||||
|
|
||||||
val NoneCollection = Collection<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 NoneDiscSeasonList = DiscSeasonList(0, emptyList())
|
val NoneDiscSeasonList = DiscSeasonList(0, emptyList())
|
||||||
val NoneContinueWatchingList = ContinueWatchingList(0, emptyList())
|
val NoneContinueWatchingList = ContinueWatchingList(0, emptyList())
|
||||||
|
val NoneRecommendationsList = RecommendationsList(0, emptyList())
|
||||||
|
|
||||||
val NoneUpNextSeriesItem = UpNextSeriesItem(0, false, false, NoneEpisodePanel)
|
val NoneUpNextSeriesItem = UpNextSeriesItem(
|
||||||
|
playhead = 0,
|
||||||
|
fullyWatched = false,
|
||||||
|
neverWatched = false,
|
||||||
|
panel = NoneEpisodePanel
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Series data type
|
* series data class
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Series(
|
data class Series(
|
||||||
@ -216,7 +248,7 @@ data class Series(
|
|||||||
val NoneSeries = Series("", "", "", Images(emptyList(), emptyList()), emptyList())
|
val NoneSeries = Series("", "", "", Images(emptyList(), emptyList()), emptyList())
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Seasons data type
|
* Seasons data classes
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Seasons(
|
data class Seasons(
|
||||||
@ -250,7 +282,7 @@ val NoneSeason = Season("", "", "", "", 0, isSubbed = false, isDubbed = false)
|
|||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Episodes data type
|
* Episodes data classes
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Episodes(
|
data class Episodes(
|
||||||
@ -314,7 +346,7 @@ data class PlayheadObject(
|
|||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Playback/stream data type
|
* playback/stream data classes
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Playback(
|
data class Playback(
|
||||||
@ -362,6 +394,9 @@ val NonePlayback = Playback(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* profile data class
|
||||||
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Profile(
|
data class Profile(
|
||||||
@SerialName("avatar") val avatar: String,
|
@SerialName("avatar") val avatar: String,
|
||||||
|
@ -19,6 +19,10 @@ object Preferences {
|
|||||||
var theme = DataTypes.Theme.DARK
|
var theme = DataTypes.Theme.DARK
|
||||||
internal set
|
internal set
|
||||||
|
|
||||||
|
// dev settings
|
||||||
|
var updatePlayhead = true
|
||||||
|
internal set
|
||||||
|
|
||||||
private fun getSharedPref(context: Context): SharedPreferences {
|
private fun getSharedPref(context: Context): SharedPreferences {
|
||||||
return context.getSharedPreferences(
|
return context.getSharedPreferences(
|
||||||
context.getString(R.string.preference_file_key),
|
context.getString(R.string.preference_file_key),
|
||||||
@ -71,6 +75,15 @@ object Preferences {
|
|||||||
this.theme = theme
|
this.theme = theme
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun saveUpdatePlayhead(context: Context, updatePlayhead: Boolean) {
|
||||||
|
with(getSharedPref(context).edit()) {
|
||||||
|
putBoolean(context.getString(R.string.save_key_update_playhead), updatePlayhead)
|
||||||
|
apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updatePlayhead = updatePlayhead
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* initially load the stored values
|
* initially load the stored values
|
||||||
*/
|
*/
|
||||||
@ -96,6 +109,11 @@ object Preferences {
|
|||||||
context.getString(R.string.save_key_theme), DataTypes.Theme.DARK.toString()
|
context.getString(R.string.save_key_theme), DataTypes.Theme.DARK.toString()
|
||||||
) ?: DataTypes.Theme.DARK.toString()
|
) ?: DataTypes.Theme.DARK.toString()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// dev settings
|
||||||
|
updatePlayhead = sharedPref.getBoolean(
|
||||||
|
context.getString(R.string.save_key_update_playhead), true
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,18 +0,0 @@
|
|||||||
package org.mosad.teapod.ui.activity
|
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import org.mosad.teapod.ui.activity.main.MainActivity
|
|
||||||
|
|
||||||
|
|
||||||
class SplashActivity : AppCompatActivity() {
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
|
|
||||||
val intent = Intent(this, MainActivity::class.java)
|
|
||||||
startActivity(intent)
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
}
|
|
@ -27,6 +27,7 @@ import android.os.Bundle
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.commit
|
import androidx.fragment.app.commit
|
||||||
import com.google.android.material.navigation.NavigationBarView
|
import com.google.android.material.navigation.NavigationBarView
|
||||||
@ -42,8 +43,8 @@ import org.mosad.teapod.ui.activity.main.fragments.LibraryFragment
|
|||||||
import org.mosad.teapod.ui.activity.main.fragments.SearchFragment
|
import org.mosad.teapod.ui.activity.main.fragments.SearchFragment
|
||||||
import org.mosad.teapod.ui.activity.onboarding.OnboardingActivity
|
import org.mosad.teapod.ui.activity.onboarding.OnboardingActivity
|
||||||
import org.mosad.teapod.ui.activity.player.PlayerActivity
|
import org.mosad.teapod.ui.activity.player.PlayerActivity
|
||||||
import org.mosad.teapod.ui.components.LoginDialog
|
|
||||||
import org.mosad.teapod.util.DataTypes
|
import org.mosad.teapod.util.DataTypes
|
||||||
|
import org.mosad.teapod.util.metadb.MetaDBController
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.system.measureTimeMillis
|
import kotlin.system.measureTimeMillis
|
||||||
|
|
||||||
@ -62,6 +63,9 @@ class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListen
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
// Handle the splash screen transition.
|
||||||
|
installSplashScreen()
|
||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
load() // start the initial loading
|
load() // start the initial loading
|
||||||
@ -137,6 +141,9 @@ class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListen
|
|||||||
Preferences.load(this)
|
Preferences.load(this)
|
||||||
EncryptedPreferences.readCredentials(this)
|
EncryptedPreferences.readCredentials(this)
|
||||||
|
|
||||||
|
// load meta db at the start, it doesn't depend on any third party
|
||||||
|
val metaJob = initMetaDB()
|
||||||
|
|
||||||
// always initialize the api token
|
// always initialize the api token
|
||||||
Crunchyroll.initBasicApiToken()
|
Crunchyroll.initBasicApiToken()
|
||||||
|
|
||||||
@ -148,14 +155,17 @@ class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListen
|
|||||||
) {
|
) {
|
||||||
showOnboarding()
|
showOnboarding()
|
||||||
} else {
|
} else {
|
||||||
runBlocking { initCrunchyroll().joinAll() }
|
runBlocking {
|
||||||
|
initCrunchyroll().joinAll()
|
||||||
|
metaJob.join() // meta loading should be done here
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Log.i(classTag, "loading in $time ms")
|
Log.i(classTag, "loading in $time ms")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initCrunchyroll(): List<Job> {
|
private fun initCrunchyroll(): List<Job> {
|
||||||
val scope = CoroutineScope(Dispatchers.IO + CoroutineName("InitialLoadingScope"))
|
val scope = CoroutineScope(Dispatchers.IO + CoroutineName("InitialCrunchyLoading"))
|
||||||
return listOf(
|
return listOf(
|
||||||
scope.launch { Crunchyroll.index() },
|
scope.launch { Crunchyroll.index() },
|
||||||
scope.launch { Crunchyroll.account() },
|
scope.launch { Crunchyroll.account() },
|
||||||
@ -168,19 +178,9 @@ class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListen
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showLoginDialog() {
|
private fun initMetaDB(): Job {
|
||||||
LoginDialog(this, false).positiveButton {
|
val scope = CoroutineScope(Dispatchers.IO + CoroutineName("InitialMetaDBLoading"))
|
||||||
EncryptedPreferences.saveCredentials(login, password, context)
|
return scope.launch { MetaDBController.list() }
|
||||||
|
|
||||||
// TODO
|
|
||||||
// if (!AoDParser.login()) {
|
|
||||||
// showLoginDialog()
|
|
||||||
// Log.w(javaClass.name, "Login failed, please try again.")
|
|
||||||
// }
|
|
||||||
}.negativeButton {
|
|
||||||
Log.i(classTag, "Login canceled, exiting.")
|
|
||||||
finish()
|
|
||||||
}.show()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -107,16 +107,14 @@ class AboutFragment : Fragment() {
|
|||||||
"https://github.com/material-components/material-components-android", License.APACHE2),
|
"https://github.com/material-components/material-components-android", License.APACHE2),
|
||||||
ThirdPartyComponent("ExoPlayer", "2014 - 2020", "The Android Open Source Project",
|
ThirdPartyComponent("ExoPlayer", "2014 - 2020", "The Android Open Source Project",
|
||||||
"https://github.com/google/ExoPlayer", License.APACHE2),
|
"https://github.com/google/ExoPlayer", License.APACHE2),
|
||||||
ThirdPartyComponent("Gson", "2008", "Google Inc.",
|
|
||||||
"https://github.com/google/gson", License.APACHE2),
|
|
||||||
ThirdPartyComponent("Material design icons", "2020", "Google Inc.",
|
ThirdPartyComponent("Material design icons", "2020", "Google Inc.",
|
||||||
"https://github.com/google/material-design-icons", License.APACHE2),
|
"https://github.com/google/material-design-icons", License.APACHE2),
|
||||||
ThirdPartyComponent("Material Dialogs", "", "Aidan Follestad",
|
|
||||||
"https://github.com/afollestad/material-dialogs", License.APACHE2),
|
|
||||||
ThirdPartyComponent("Ktor", "2014-2021", "JetBrains s.r.o and contributors",
|
ThirdPartyComponent("Ktor", "2014-2021", "JetBrains s.r.o and contributors",
|
||||||
"https://ktor.io/", License.APACHE2),
|
"https://ktor.io/", License.APACHE2),
|
||||||
ThirdPartyComponent("kotlinx.coroutines", "2016-2021", "JetBrains s.r.o",
|
ThirdPartyComponent("kotlinx.coroutines", "2016-2021", "JetBrains s.r.o",
|
||||||
"https://github.com/Kotlin/kotlinx.coroutines", License.APACHE2),
|
"https://github.com/Kotlin/kotlinx.coroutines", License.APACHE2),
|
||||||
|
ThirdPartyComponent(" kotlinx.serialization", "2017-2021", "JetBrains s.r.o",
|
||||||
|
"https://github.com/Kotlin/kotlinx.serialization", License.APACHE2),
|
||||||
ThirdPartyComponent("Glide", "2014", "Google Inc.",
|
ThirdPartyComponent("Glide", "2014", "Google Inc.",
|
||||||
"https://github.com/bumptech/glide", License.BSD2),
|
"https://github.com/bumptech/glide", License.BSD2),
|
||||||
ThirdPartyComponent("Glide Transformations", "2020", "Wasabeef",
|
ThirdPartyComponent("Glide Transformations", "2020", "Wasabeef",
|
||||||
|
@ -1,12 +1,9 @@
|
|||||||
package org.mosad.teapod.ui.activity.main.fragments
|
package org.mosad.teapod.ui.activity.main.fragments
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
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.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
@ -24,7 +21,7 @@ import org.mosad.teapod.parser.crunchyroll.supportedLocals
|
|||||||
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
|
||||||
import org.mosad.teapod.ui.components.LoginDialog
|
import org.mosad.teapod.ui.components.LoginModalBottomSheet
|
||||||
import org.mosad.teapod.util.DataTypes.Theme
|
import org.mosad.teapod.util.DataTypes.Theme
|
||||||
import org.mosad.teapod.util.showFragment
|
import org.mosad.teapod.util.showFragment
|
||||||
import org.mosad.teapod.util.toDisplayString
|
import org.mosad.teapod.util.toDisplayString
|
||||||
@ -37,28 +34,6 @@ class AccountFragment : Fragment() {
|
|||||||
Crunchyroll.profile()
|
Crunchyroll.profile()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val getUriExport = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
|
||||||
if (result.resultCode == Activity.RESULT_OK) {
|
|
||||||
result.data?.data?.also { uri ->
|
|
||||||
//StorageController.exportMyList(requireContext(), uri)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val getUriImport = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
|
||||||
if (result.resultCode == Activity.RESULT_OK) {
|
|
||||||
result.data?.data?.also { uri ->
|
|
||||||
// val success = StorageController.importMyList(requireContext(), uri)
|
|
||||||
// if (success == 0) {
|
|
||||||
// Toast.makeText(
|
|
||||||
// context, getString(R.string.import_data_success),
|
|
||||||
// Toast.LENGTH_SHORT
|
|
||||||
// ).show()
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
binding = FragmentAccountBinding.inflate(inflater, container, false)
|
binding = FragmentAccountBinding.inflate(inflater, container, false)
|
||||||
return binding.root
|
return binding.root
|
||||||
@ -93,6 +68,7 @@ class AccountFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
binding.linearDevSettings.isVisible = Preferences.devSettings
|
binding.linearDevSettings.isVisible = Preferences.devSettings
|
||||||
|
binding.switchUpdatePlayhead.isChecked = Preferences.updatePlayhead
|
||||||
|
|
||||||
binding.textInfoAboutDesc.text = getString(R.string.info_about_desc, BuildConfig.VERSION_NAME, getString(R.string.build_time))
|
binding.textInfoAboutDesc.text = getString(R.string.info_about_desc, BuildConfig.VERSION_NAME, getString(R.string.build_time))
|
||||||
|
|
||||||
@ -101,7 +77,7 @@ class AccountFragment : Fragment() {
|
|||||||
|
|
||||||
private fun initActions() {
|
private fun initActions() {
|
||||||
binding.linearAccountLogin.setOnClickListener {
|
binding.linearAccountLogin.setOnClickListener {
|
||||||
showLoginDialog(true)
|
showLoginDialog()
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.linearAccountSubscription.setOnClickListener {
|
binding.linearAccountSubscription.setOnClickListener {
|
||||||
@ -130,37 +106,34 @@ class AccountFragment : Fragment() {
|
|||||||
activity?.showFragment(AboutFragment())
|
activity?.showFragment(AboutFragment())
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.linearExportData.setOnClickListener {
|
binding.switchUpdatePlayhead.setOnClickListener {
|
||||||
val i = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
Preferences.saveUpdatePlayhead(requireContext(), binding.switchUpdatePlayhead.isChecked)
|
||||||
addCategory(Intent.CATEGORY_OPENABLE)
|
|
||||||
type = "text/json"
|
|
||||||
putExtra(Intent.EXTRA_TITLE, "my-list.json")
|
|
||||||
}
|
}
|
||||||
getUriExport.launch(i)
|
|
||||||
|
binding.linearExportData.setOnClickListener {
|
||||||
|
// unused
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.linearImportData.setOnClickListener {
|
binding.linearImportData.setOnClickListener {
|
||||||
val i = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
// unused
|
||||||
addCategory(Intent.CATEGORY_OPENABLE)
|
|
||||||
type = "*/*"
|
|
||||||
}
|
|
||||||
getUriImport.launch(i)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showLoginDialog(firstTry: Boolean) {
|
private fun showLoginDialog() {
|
||||||
LoginDialog(requireContext(), firstTry).positiveButton {
|
val loginModal = LoginModalBottomSheet().apply {
|
||||||
EncryptedPreferences.saveCredentials(login, password, context)
|
|
||||||
|
|
||||||
// TODO
|
|
||||||
// if (!AoDParser.login()) {
|
|
||||||
// showLoginDialog(false)
|
|
||||||
// Log.w(javaClass.name, "Login failed, please try again.")
|
|
||||||
// }
|
|
||||||
}.show {
|
|
||||||
login = EncryptedPreferences.login
|
login = EncryptedPreferences.login
|
||||||
password = ""
|
password = ""
|
||||||
|
positiveAction = {
|
||||||
|
EncryptedPreferences.saveCredentials(login, password, requireContext())
|
||||||
|
|
||||||
|
// TODO only dismiss if login was successful
|
||||||
|
this.dismiss()
|
||||||
}
|
}
|
||||||
|
negativeAction = {
|
||||||
|
this.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
activity?.let { loginModal.show(it.supportFragmentManager, LoginModalBottomSheet.TAG) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showContentLanguageSelection() {
|
private fun showContentLanguageSelection() {
|
||||||
|
@ -1,34 +1,55 @@
|
|||||||
|
/**
|
||||||
|
* Teapod
|
||||||
|
*
|
||||||
|
* Copyright 2020-2022 <seil0@mosad.xyz>
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation; either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||||
|
* MA 02110-1301, USA.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
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 com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.joinAll
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import org.mosad.teapod.R
|
||||||
import org.mosad.teapod.databinding.FragmentHomeBinding
|
import org.mosad.teapod.databinding.FragmentHomeBinding
|
||||||
import org.mosad.teapod.parser.crunchyroll.Crunchyroll
|
import org.mosad.teapod.ui.activity.main.MainActivity
|
||||||
import org.mosad.teapod.parser.crunchyroll.Item
|
import org.mosad.teapod.ui.activity.main.viewmodel.HomeViewModel
|
||||||
import org.mosad.teapod.parser.crunchyroll.SortBy
|
import org.mosad.teapod.util.adapter.MediaEpisodeListAdapter
|
||||||
import org.mosad.teapod.util.adapter.MediaItemAdapter
|
import org.mosad.teapod.util.adapter.MediaItemListAdapter
|
||||||
import org.mosad.teapod.util.decoration.MediaItemDecoration
|
import org.mosad.teapod.util.decoration.MediaItemDecoration
|
||||||
|
import org.mosad.teapod.util.setDrawableTop
|
||||||
import org.mosad.teapod.util.showFragment
|
import org.mosad.teapod.util.showFragment
|
||||||
import org.mosad.teapod.util.toItemMediaList
|
import org.mosad.teapod.util.toItemMediaList
|
||||||
import kotlin.random.Random
|
|
||||||
|
|
||||||
class HomeFragment : Fragment() {
|
class HomeFragment : Fragment() {
|
||||||
|
|
||||||
|
private val classTag = javaClass.name
|
||||||
|
private val model: HomeViewModel by viewModels()
|
||||||
private lateinit var binding: FragmentHomeBinding
|
private lateinit var binding: FragmentHomeBinding
|
||||||
private lateinit var adapterUpNext: MediaItemAdapter
|
|
||||||
private lateinit var adapterWatchlist: MediaItemAdapter
|
|
||||||
private lateinit var adapterNewTitles: MediaItemAdapter
|
|
||||||
private lateinit var adapterTopTen: MediaItemAdapter
|
|
||||||
|
|
||||||
private lateinit var highlightMedia: Item
|
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
binding = FragmentHomeBinding.inflate(inflater, container, false)
|
binding = FragmentHomeBinding.inflate(inflater, container, false)
|
||||||
@ -38,84 +59,53 @@ class HomeFragment : Fragment() {
|
|||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
lifecycleScope.launch {
|
binding.recyclerUpNext.addItemDecoration(MediaItemDecoration(9))
|
||||||
context?.let {
|
|
||||||
initHighlight()
|
|
||||||
initRecyclerViews()
|
|
||||||
initActions()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initHighlight() {
|
|
||||||
lifecycleScope.launch {
|
|
||||||
val newTitles = Crunchyroll.browse(sortBy = SortBy.NEWLY_ADDED, n = 10)
|
|
||||||
// FIXME crashes on newTitles.items.size == 0
|
|
||||||
highlightMedia = newTitles.items[Random.nextInt(newTitles.items.size)]
|
|
||||||
|
|
||||||
// add media item to gui
|
|
||||||
binding.textHighlightTitle.text = highlightMedia.title
|
|
||||||
Glide.with(requireContext()).load(highlightMedia.images.poster_wide[0][3].source)
|
|
||||||
.into(binding.imageHighlight)
|
|
||||||
|
|
||||||
// TODO watchlist indicator
|
|
||||||
// if (StorageController.myList.contains(0)) {
|
|
||||||
// binding.textHighlightMyList.setDrawableTop(R.drawable.ic_baseline_check_24)
|
|
||||||
// } else {
|
|
||||||
// binding.textHighlightMyList.setDrawableTop(R.drawable.ic_baseline_add_24)
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Suspend, since adapters need to be initialized before we can initialize the actions.
|
|
||||||
*/
|
|
||||||
private suspend fun initRecyclerViews() {
|
|
||||||
binding.recyclerWatchlist.addItemDecoration(MediaItemDecoration(9))
|
binding.recyclerWatchlist.addItemDecoration(MediaItemDecoration(9))
|
||||||
binding.recyclerNewEpisodes.addItemDecoration(MediaItemDecoration(9))
|
binding.recyclerRecommendations.addItemDecoration(MediaItemDecoration(9))
|
||||||
binding.recyclerNewTitles.addItemDecoration(MediaItemDecoration(9))
|
binding.recyclerNewTitles.addItemDecoration(MediaItemDecoration(9))
|
||||||
binding.recyclerTopTen.addItemDecoration(MediaItemDecoration(9))
|
binding.recyclerTopTen.addItemDecoration(MediaItemDecoration(9))
|
||||||
|
|
||||||
val asyncJobList = arrayListOf<Job>()
|
binding.recyclerUpNext.adapter = MediaEpisodeListAdapter(
|
||||||
|
MediaEpisodeListAdapter.OnClickListener {
|
||||||
// continue watching
|
val activity = activity
|
||||||
val upNextJob = lifecycleScope.launch {
|
if (activity is MainActivity) {
|
||||||
// TODO create EpisodeItemAdapter, which will start the playback of the selected episode immediately
|
activity.startPlayer(it.panel.episodeMetadata.seasonId, it.panel.id)
|
||||||
adapterUpNext = MediaItemAdapter(Crunchyroll.upNextAccount().items
|
|
||||||
.filter { !it.fullyWatched }.toItemMediaList())
|
|
||||||
binding.recyclerNewEpisodes.adapter = adapterUpNext
|
|
||||||
}
|
}
|
||||||
asyncJobList.add(upNextJob)
|
|
||||||
|
|
||||||
// watchlist
|
|
||||||
val watchlistJob = lifecycleScope.launch {
|
|
||||||
adapterWatchlist = MediaItemAdapter(Crunchyroll.watchlist(50).toItemMediaList())
|
|
||||||
binding.recyclerWatchlist.adapter = adapterWatchlist
|
|
||||||
}
|
}
|
||||||
asyncJobList.add(watchlistJob)
|
)
|
||||||
|
|
||||||
// new simulcasts
|
binding.recyclerWatchlist.adapter = MediaItemListAdapter(
|
||||||
val simulcastsJob = lifecycleScope.launch {
|
MediaItemListAdapter.OnClickListener {
|
||||||
// val latestSeasonTag = Crunchyroll.seasonList().items.first().id
|
activity?.showFragment(MediaFragment(it.id))
|
||||||
// val newSimulcasts = Crunchyroll.browse(seasonTag = latestSeasonTag, n = 50)
|
|
||||||
val newSimulcasts = Crunchyroll.browse(sortBy = SortBy.NEWLY_ADDED, n = 50)
|
|
||||||
|
|
||||||
adapterNewTitles = MediaItemAdapter(newSimulcasts.toItemMediaList())
|
|
||||||
binding.recyclerNewTitles.adapter = adapterNewTitles
|
|
||||||
}
|
}
|
||||||
asyncJobList.add(simulcastsJob)
|
)
|
||||||
|
|
||||||
// newly added / top ten
|
binding.recyclerRecommendations.adapter = MediaItemListAdapter(
|
||||||
val newlyAddedJob = lifecycleScope.launch {
|
MediaItemListAdapter.OnClickListener {
|
||||||
adapterTopTen = MediaItemAdapter(Crunchyroll.browse(sortBy = SortBy.POPULARITY, n = 10).toItemMediaList())
|
activity?.showFragment(MediaFragment(it.id))
|
||||||
binding.recyclerTopTen.adapter = adapterTopTen
|
|
||||||
}
|
}
|
||||||
asyncJobList.add(newlyAddedJob)
|
)
|
||||||
|
|
||||||
asyncJobList.joinAll()
|
binding.recyclerNewTitles.adapter = MediaItemListAdapter(
|
||||||
|
MediaItemListAdapter.OnClickListener {
|
||||||
|
activity?.showFragment(MediaFragment(it.id))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
binding.recyclerTopTen.adapter = MediaItemListAdapter(
|
||||||
|
MediaItemListAdapter.OnClickListener {
|
||||||
|
activity?.showFragment(MediaFragment(it.id))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
binding.textHighlightMyList.setOnClickListener {
|
||||||
|
model.toggleHighlightWatchlist()
|
||||||
|
|
||||||
|
// disable the watchlist button until the result has been loaded
|
||||||
|
binding.textHighlightMyList.isClickable = false
|
||||||
|
// TODO since this might take a few seconds show a loading animation for the watchlist button
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initActions() {
|
|
||||||
binding.buttonPlayHighlight.setOnClickListener {
|
binding.buttonPlayHighlight.setOnClickListener {
|
||||||
// TODO implement
|
// TODO implement
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
@ -126,37 +116,60 @@ class HomeFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.textHighlightMyList.setOnClickListener {
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
// TODO implement
|
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
// if (StorageController.myList.contains(0)) {
|
model.onUiState(viewLifecycleOwner.lifecycleScope) { uiState ->
|
||||||
// StorageController.myList.remove(0)
|
when (uiState) {
|
||||||
// binding.textHighlightMyList.setDrawableTop(R.drawable.ic_baseline_add_24)
|
is HomeViewModel.UiState.Normal -> bindUiStateNormal(uiState)
|
||||||
// } else {
|
is HomeViewModel.UiState.Loading -> bindUiStateLoading()
|
||||||
// StorageController.myList.add(0)
|
is HomeViewModel.UiState.Error -> bindUiStateError(uiState)
|
||||||
// binding.textHighlightMyList.setDrawableTop(R.drawable.ic_baseline_check_24)
|
|
||||||
// }
|
|
||||||
// StorageController.saveMyList(requireContext())
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bindUiStateNormal(uiState: HomeViewModel.UiState.Normal) {
|
||||||
|
val adapterUpNext = binding.recyclerUpNext.adapter as MediaEpisodeListAdapter
|
||||||
|
adapterUpNext.submitList(uiState.upNextItems.filter { !it.fullyWatched })
|
||||||
|
|
||||||
|
val adapterWatchlist = binding.recyclerWatchlist.adapter as MediaItemListAdapter
|
||||||
|
adapterWatchlist.submitList(uiState.watchlistItems.toItemMediaList())
|
||||||
|
|
||||||
|
val adapterRecommendations = binding.recyclerRecommendations.adapter as MediaItemListAdapter
|
||||||
|
adapterRecommendations.submitList(uiState.recommendationsItems.toItemMediaList())
|
||||||
|
|
||||||
|
val adapterNewTitles = binding.recyclerNewTitles.adapter as MediaItemListAdapter
|
||||||
|
adapterNewTitles.submitList(uiState.recentlyAddedItems.toItemMediaList())
|
||||||
|
|
||||||
|
val adapterTopTen = binding.recyclerTopTen.adapter as MediaItemListAdapter
|
||||||
|
adapterTopTen.submitList(uiState.topTenItems.toItemMediaList())
|
||||||
|
|
||||||
|
// highlight item
|
||||||
|
binding.textHighlightTitle.text = uiState.highlightItem.title
|
||||||
|
Glide.with(requireContext()).load(uiState.highlightItem.images.poster_wide[0][3].source)
|
||||||
|
.into(binding.imageHighlight)
|
||||||
|
|
||||||
|
val iconHighlightWatchlist = if (uiState.highlightIsWatchlist) {
|
||||||
|
R.drawable.ic_baseline_check_24
|
||||||
|
} else {
|
||||||
|
R.drawable.ic_baseline_add_24
|
||||||
|
}
|
||||||
|
binding.textHighlightMyList.setDrawableTop(iconHighlightWatchlist)
|
||||||
|
binding.textHighlightMyList.isClickable = true
|
||||||
|
|
||||||
binding.textHighlightInfo.setOnClickListener {
|
binding.textHighlightInfo.setOnClickListener {
|
||||||
activity?.showFragment(MediaFragment(highlightMedia.id))
|
activity?.showFragment(MediaFragment(uiState.highlightItem.id))
|
||||||
}
|
|
||||||
|
|
||||||
adapterUpNext.onItemClick = { id, _ ->
|
|
||||||
activity?.showFragment(MediaFragment(id))
|
|
||||||
}
|
|
||||||
|
|
||||||
adapterWatchlist.onItemClick = { id, _ ->
|
|
||||||
activity?.showFragment(MediaFragment(id))
|
|
||||||
}
|
|
||||||
|
|
||||||
adapterNewTitles.onItemClick = { id, _ ->
|
|
||||||
activity?.showFragment(MediaFragment(id))
|
|
||||||
}
|
|
||||||
|
|
||||||
adapterTopTen.onItemClick = { id, _ ->
|
|
||||||
activity?.showFragment(MediaFragment(id)) //(mediaId))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun bindUiStateLoading() {
|
||||||
|
// currently not used
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bindUiStateError(uiState: HomeViewModel.UiState.Error) {
|
||||||
|
// currently not used
|
||||||
|
Log.e(classTag, "A error occurred while loading a UiState: ${uiState.message}")
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -8,8 +8,7 @@ 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.FragmentActivity
|
import androidx.fragment.app.viewModels
|
||||||
import androidx.fragment.app.activityViewModels
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
@ -37,7 +36,7 @@ class MediaFragment(private val mediaIdStr: String) : Fragment() {
|
|||||||
private lateinit var binding: FragmentMediaBinding
|
private lateinit var binding: FragmentMediaBinding
|
||||||
private lateinit var pagerAdapter: FragmentStateAdapter
|
private lateinit var pagerAdapter: FragmentStateAdapter
|
||||||
|
|
||||||
private val model: MediaFragmentViewModel by activityViewModels()
|
private val model: MediaFragmentViewModel by viewModels()
|
||||||
|
|
||||||
private val fragments = arrayListOf<Fragment>()
|
private val fragments = arrayListOf<Fragment>()
|
||||||
private var watchlistJobRunning = false
|
private var watchlistJobRunning = false
|
||||||
@ -54,7 +53,7 @@ class MediaFragment(private val mediaIdStr: String) : Fragment() {
|
|||||||
binding.frameLoading.visibility = View.VISIBLE
|
binding.frameLoading.visibility = View.VISIBLE
|
||||||
|
|
||||||
// tab layout and pager
|
// tab layout and pager
|
||||||
pagerAdapter = ScreenSlidePagerAdapter(requireActivity())
|
pagerAdapter = ScreenSlidePagerAdapter(this)
|
||||||
// fix material components issue #1878, if more tabs are added increase
|
// fix material components issue #1878, if more tabs are added increase
|
||||||
binding.pagerEpisodesSimilar.offscreenPageLimit = 2
|
binding.pagerEpisodesSimilar.offscreenPageLimit = 2
|
||||||
binding.pagerEpisodesSimilar.adapter = pagerAdapter
|
binding.pagerEpisodesSimilar.adapter = pagerAdapter
|
||||||
@ -79,6 +78,12 @@ class MediaFragment(private val mediaIdStr: String) : Fragment() {
|
|||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
if (runOnResume) {
|
if (runOnResume) {
|
||||||
|
/**
|
||||||
|
* FIXME
|
||||||
|
* this is currently also run on back press when multiple MediaFragments have
|
||||||
|
* been open and closed via similar tab
|
||||||
|
*/
|
||||||
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
model.updateOnResume()
|
model.updateOnResume()
|
||||||
|
|
||||||
@ -130,12 +135,15 @@ class MediaFragment(private val mediaIdStr: String) : Fragment() {
|
|||||||
val watchlistIcon = if (isWatchlist) R.drawable.ic_baseline_check_24 else R.drawable.ic_baseline_add_24
|
val watchlistIcon = if (isWatchlist) R.drawable.ic_baseline_check_24 else R.drawable.ic_baseline_add_24
|
||||||
Glide.with(requireContext()).load(watchlistIcon).into(binding.imageMyListAction)
|
Glide.with(requireContext()).load(watchlistIcon).into(binding.imageMyListAction)
|
||||||
|
|
||||||
// clear fragments, since it lives in onCreate scope (don't do this in onPause/onStop -> FragmentManager transaction)
|
/**
|
||||||
val fragmentsSize = if (fragments.lastIndex < 0) 0 else fragments.lastIndex
|
* clear fragments, since it lives in onCreate scope,
|
||||||
|
* don't do this in onPause/onStop -> FragmentManager transaction
|
||||||
|
* (will be called on similar -> new MediaFragment -> onBackPressed)
|
||||||
|
*/
|
||||||
|
val fragmentsSize = fragments.size
|
||||||
fragments.clear()
|
fragments.clear()
|
||||||
pagerAdapter.notifyItemRangeRemoved(0, fragmentsSize)
|
pagerAdapter.notifyItemRangeRemoved(0, fragmentsSize)
|
||||||
|
|
||||||
// add the episodes fragment (as tab). Note: Movies are tv shows!
|
|
||||||
MediaFragmentEpisodes().also {
|
MediaFragmentEpisodes().also {
|
||||||
fragments.add(it)
|
fragments.add(it)
|
||||||
pagerAdapter.notifyItemInserted(fragments.indexOf(it))
|
pagerAdapter.notifyItemInserted(fragments.indexOf(it))
|
||||||
@ -170,13 +178,12 @@ class MediaFragment(private val mediaIdStr: String) : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// if has similar titles
|
// if has similar titles
|
||||||
// TODO reimplement
|
if (model.similarTo.total > 0) {
|
||||||
// if (media.similar.isNotEmpty()) {
|
MediaFragmentSimilar().also {
|
||||||
// MediaFragmentSimilar().also {
|
fragments.add(it)
|
||||||
// fragments.add(it)
|
pagerAdapter.notifyItemInserted(fragments.indexOf(it))
|
||||||
// pagerAdapter.notifyItemInserted(fragments.indexOf(it))
|
}
|
||||||
// }
|
}
|
||||||
// }
|
|
||||||
|
|
||||||
// disable scrolling on appbar, if no tabs where added
|
// disable scrolling on appbar, if no tabs where added
|
||||||
if(fragments.isEmpty()) {
|
if(fragments.isEmpty()) {
|
||||||
@ -225,7 +232,7 @@ class MediaFragment(private val mediaIdStr: String) : Fragment() {
|
|||||||
/**
|
/**
|
||||||
* A simple pager adapter
|
* A simple pager adapter
|
||||||
*/
|
*/
|
||||||
private inner class ScreenSlidePagerAdapter(fa: FragmentActivity) : FragmentStateAdapter(fa) {
|
private inner class ScreenSlidePagerAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) {
|
||||||
override fun getItemCount(): Int = fragments.size
|
override fun getItemCount(): Int = fragments.size
|
||||||
|
|
||||||
override fun createFragment(position: Int): Fragment = fragments[position]
|
override fun createFragment(position: Int): Fragment = fragments[position]
|
||||||
|
@ -8,7 +8,7 @@ import android.view.View
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.appcompat.widget.PopupMenu
|
import androidx.appcompat.widget.PopupMenu
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.viewModels
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.mosad.teapod.R
|
import org.mosad.teapod.R
|
||||||
@ -22,7 +22,7 @@ class MediaFragmentEpisodes : Fragment() {
|
|||||||
private lateinit var binding: FragmentMediaEpisodesBinding
|
private lateinit var binding: FragmentMediaEpisodesBinding
|
||||||
private lateinit var adapterRecEpisodes: EpisodeItemAdapter
|
private lateinit var adapterRecEpisodes: EpisodeItemAdapter
|
||||||
|
|
||||||
private val model: MediaFragmentViewModel by activityViewModels()
|
private val model: MediaFragmentViewModel by viewModels({requireParentFragment()})
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
binding = FragmentMediaEpisodesBinding.inflate(inflater, container, false)
|
binding = FragmentMediaEpisodesBinding.inflate(inflater, container, false)
|
||||||
@ -35,15 +35,14 @@ class MediaFragmentEpisodes : Fragment() {
|
|||||||
adapterRecEpisodes = EpisodeItemAdapter(
|
adapterRecEpisodes = EpisodeItemAdapter(
|
||||||
model.currentEpisodesCrunchy,
|
model.currentEpisodesCrunchy,
|
||||||
model.tmdbTVSeason.episodes,
|
model.tmdbTVSeason.episodes,
|
||||||
model.currentPlayheads
|
model.currentPlayheads,
|
||||||
|
EpisodeItemAdapter.OnClickListener { episode ->
|
||||||
|
playEpisode(episode.seasonId, episode.id)
|
||||||
|
},
|
||||||
|
EpisodeItemAdapter.ViewType.MEDIA_FRAGMENT
|
||||||
)
|
)
|
||||||
binding.recyclerEpisodes.adapter = adapterRecEpisodes
|
binding.recyclerEpisodes.adapter = adapterRecEpisodes
|
||||||
|
|
||||||
// set onItemClick, adapter is initialized
|
|
||||||
adapterRecEpisodes.onImageClick = { seasonId, episodeId ->
|
|
||||||
playEpisode(seasonId, episodeId)
|
|
||||||
}
|
|
||||||
|
|
||||||
// don't show season selection if only one season is present
|
// don't show season selection if only one season is present
|
||||||
if (model.seasonsCrunchy.total < 2) {
|
if (model.seasonsCrunchy.total < 2) {
|
||||||
binding.buttonSeasonSelection.visibility = View.GONE
|
binding.buttonSeasonSelection.visibility = View.GONE
|
||||||
@ -62,8 +61,10 @@ class MediaFragmentEpisodes : Fragment() {
|
|||||||
@SuppressLint("NotifyDataSetChanged")
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
fun updateWatchedState() {
|
fun updateWatchedState() {
|
||||||
// model.currentPlayheads is a val mutable map -> notify dataset changed
|
// model.currentPlayheads is a val mutable map -> notify dataset changed
|
||||||
|
if (this::adapterRecEpisodes.isInitialized) {
|
||||||
adapterRecEpisodes.notifyDataSetChanged()
|
adapterRecEpisodes.notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun showSeasonSelection(v: View) {
|
private fun showSeasonSelection(v: View) {
|
||||||
// TODO replace with Exposed dropdown menu: https://material.io/components/menus/android#exposed-dropdown-menus
|
// TODO replace with Exposed dropdown menu: https://material.io/components/menus/android#exposed-dropdown-menus
|
||||||
|
@ -1,3 +1,25 @@
|
|||||||
|
/**
|
||||||
|
* Teapod
|
||||||
|
*
|
||||||
|
* Copyright 2020-2022 <seil0@mosad.xyz>
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation; either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||||
|
* MA 02110-1301, USA.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
package org.mosad.teapod.ui.activity.main.fragments
|
package org.mosad.teapod.ui.activity.main.fragments
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
@ -5,19 +27,18 @@ 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.activityViewModels
|
import androidx.fragment.app.viewModels
|
||||||
import org.mosad.teapod.databinding.FragmentMediaSimilarBinding
|
import org.mosad.teapod.databinding.FragmentMediaSimilarBinding
|
||||||
import org.mosad.teapod.ui.activity.main.viewmodel.MediaFragmentViewModel
|
import org.mosad.teapod.ui.activity.main.viewmodel.MediaFragmentViewModel
|
||||||
import org.mosad.teapod.util.adapter.MediaItemAdapter
|
import org.mosad.teapod.util.adapter.MediaItemListAdapter
|
||||||
import org.mosad.teapod.util.decoration.MediaItemDecoration
|
import org.mosad.teapod.util.decoration.MediaItemDecoration
|
||||||
import org.mosad.teapod.util.showFragment
|
import org.mosad.teapod.util.showFragment
|
||||||
|
import org.mosad.teapod.util.toItemMediaList
|
||||||
|
|
||||||
class MediaFragmentSimilar : Fragment() {
|
class MediaFragmentSimilar : Fragment() {
|
||||||
|
|
||||||
|
private val model: MediaFragmentViewModel by viewModels({requireParentFragment()})
|
||||||
private lateinit var binding: FragmentMediaSimilarBinding
|
private lateinit var binding: FragmentMediaSimilarBinding
|
||||||
private val model: MediaFragmentViewModel by activityViewModels()
|
|
||||||
|
|
||||||
private lateinit var adapterSimilar: MediaItemAdapter
|
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
binding = FragmentMediaSimilarBinding.inflate(inflater, container, false)
|
binding = FragmentMediaSimilarBinding.inflate(inflater, container, false)
|
||||||
@ -27,15 +48,14 @@ class MediaFragmentSimilar : Fragment() {
|
|||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
adapterSimilar = MediaItemAdapter(emptyList()) //(model.media.similar)
|
|
||||||
binding.recyclerMediaSimilar.adapter = adapterSimilar
|
|
||||||
binding.recyclerMediaSimilar.addItemDecoration(MediaItemDecoration(9))
|
binding.recyclerMediaSimilar.addItemDecoration(MediaItemDecoration(9))
|
||||||
|
binding.recyclerMediaSimilar.adapter = MediaItemListAdapter(
|
||||||
|
MediaItemListAdapter.OnClickListener {
|
||||||
|
activity?.showFragment(MediaFragment(it.id))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// set onItemClick only in adapter is initialized
|
val adapterSimilar = binding.recyclerMediaSimilar.adapter as MediaItemListAdapter
|
||||||
if (this::adapterSimilar.isInitialized) {
|
adapterSimilar.submitList(model.similarTo.toItemMediaList())
|
||||||
adapterSimilar.onItemClick = { mediaId, _ ->
|
|
||||||
activity?.showFragment(MediaFragment("")) //(mediaId))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -0,0 +1,123 @@
|
|||||||
|
/**
|
||||||
|
* Teapod
|
||||||
|
*
|
||||||
|
* Copyright 2020-2022 <seil0@mosad.xyz>
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation; either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||||
|
* MA 02110-1301, USA.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
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.*
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.mosad.teapod.parser.crunchyroll.*
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
class HomeViewModel : ViewModel() {
|
||||||
|
|
||||||
|
private val uiState = MutableStateFlow<UiState>(UiState.Loading)
|
||||||
|
|
||||||
|
sealed class UiState {
|
||||||
|
object Loading : UiState()
|
||||||
|
data class Normal(
|
||||||
|
val upNextItems: List<ContinueWatchingItem>,
|
||||||
|
val watchlistItems: List<Item>,
|
||||||
|
val recommendationsItems: List<Item>,
|
||||||
|
val recentlyAddedItems: List<Item>,
|
||||||
|
val topTenItems: List<Item>,
|
||||||
|
val highlightItem: Item,
|
||||||
|
val highlightIsWatchlist:Boolean
|
||||||
|
) : 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 upNextJob = viewModelScope.async { Crunchyroll.upNextAccount().items }
|
||||||
|
val watchlistJob = viewModelScope.async { Crunchyroll.watchlist(50).items }
|
||||||
|
val recommendationsJob = viewModelScope.async {
|
||||||
|
Crunchyroll.recommendations(20).items
|
||||||
|
}
|
||||||
|
val recentlyAddedJob = viewModelScope.async {
|
||||||
|
Crunchyroll.browse(sortBy = SortBy.NEWLY_ADDED, n = 50).items
|
||||||
|
}
|
||||||
|
val topTenJob = viewModelScope.async {
|
||||||
|
Crunchyroll.browse(sortBy = SortBy.POPULARITY, n = 10).items
|
||||||
|
}
|
||||||
|
|
||||||
|
val recentlyAddedItems = recentlyAddedJob.await()
|
||||||
|
// FIXME crashes on newTitles.items.size == 0
|
||||||
|
val highlightItem = recentlyAddedItems[Random.nextInt(recentlyAddedItems.size)]
|
||||||
|
val highlightItemIsWatchlist = Crunchyroll.isWatchlist(highlightItem.id)
|
||||||
|
|
||||||
|
uiState.emit(UiState.Normal(
|
||||||
|
upNextJob.await(), watchlistJob.await(), recommendationsJob.await(),
|
||||||
|
recentlyAddedJob.await(), topTenJob.await(), highlightItem,
|
||||||
|
highlightItemIsWatchlist
|
||||||
|
))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
uiState.emit(UiState.Error(e.message))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle the watchlist state of the highlight media.
|
||||||
|
*/
|
||||||
|
fun toggleHighlightWatchlist() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
uiState.update { currentUiState ->
|
||||||
|
if (currentUiState is UiState.Normal) {
|
||||||
|
if (currentUiState.highlightIsWatchlist) {
|
||||||
|
Crunchyroll.deleteWatchlist(currentUiState.highlightItem.id)
|
||||||
|
} else {
|
||||||
|
Crunchyroll.postWatchlist(currentUiState.highlightItem.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// update the watchlist after a item has been added/removed
|
||||||
|
val watchlistItems = Crunchyroll.watchlist(50).items
|
||||||
|
|
||||||
|
currentUiState.copy(
|
||||||
|
watchlistItems = watchlistItems,
|
||||||
|
highlightIsWatchlist = !currentUiState.highlightIsWatchlist)
|
||||||
|
} else {
|
||||||
|
currentUiState
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -8,7 +8,6 @@ import kotlinx.coroutines.launch
|
|||||||
import org.mosad.teapod.parser.crunchyroll.*
|
import org.mosad.teapod.parser.crunchyroll.*
|
||||||
import org.mosad.teapod.preferences.Preferences
|
import org.mosad.teapod.preferences.Preferences
|
||||||
import org.mosad.teapod.util.DataTypes.MediaType
|
import org.mosad.teapod.util.DataTypes.MediaType
|
||||||
import org.mosad.teapod.util.Meta
|
|
||||||
import org.mosad.teapod.util.tmdb.*
|
import org.mosad.teapod.util.tmdb.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -17,8 +16,6 @@ import org.mosad.teapod.util.tmdb.*
|
|||||||
*/
|
*/
|
||||||
class MediaFragmentViewModel(application: Application) : AndroidViewModel(application) {
|
class MediaFragmentViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
|
|
||||||
// var mediaCrunchy = NoneItem
|
|
||||||
// internal set
|
|
||||||
var seriesCrunchy = NoneSeries // movies are also series
|
var seriesCrunchy = NoneSeries // movies are also series
|
||||||
internal set
|
internal set
|
||||||
var seasonsCrunchy = NoneSeasons
|
var seasonsCrunchy = NoneSeasons
|
||||||
@ -34,6 +31,9 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic
|
|||||||
var isWatchlist = false
|
var isWatchlist = false
|
||||||
internal set
|
internal set
|
||||||
var upNextSeries = NoneUpNextSeriesItem
|
var upNextSeries = NoneUpNextSeriesItem
|
||||||
|
internal set
|
||||||
|
var similarTo = NoneSimilarToResult
|
||||||
|
internal set
|
||||||
|
|
||||||
// TMDB stuff
|
// TMDB stuff
|
||||||
var mediaType = MediaType.OTHER
|
var mediaType = MediaType.OTHER
|
||||||
@ -42,8 +42,6 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic
|
|||||||
internal set
|
internal set
|
||||||
var tmdbTVSeason: TMDBTVSeason = NoneTMDBTVSeason
|
var tmdbTVSeason: TMDBTVSeason = NoneTMDBTVSeason
|
||||||
internal set
|
internal set
|
||||||
var mediaMeta: Meta? = null
|
|
||||||
internal set
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param crunchyId the crunchyroll series id
|
* @param crunchyId the crunchyroll series id
|
||||||
@ -55,22 +53,17 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic
|
|||||||
viewModelScope.launch { seriesCrunchy = Crunchyroll.series(crunchyId) },
|
viewModelScope.launch { seriesCrunchy = Crunchyroll.series(crunchyId) },
|
||||||
viewModelScope.launch { seasonsCrunchy = Crunchyroll.seasons(crunchyId) },
|
viewModelScope.launch { seasonsCrunchy = Crunchyroll.seasons(crunchyId) },
|
||||||
viewModelScope.launch { isWatchlist = Crunchyroll.isWatchlist(crunchyId) },
|
viewModelScope.launch { isWatchlist = Crunchyroll.isWatchlist(crunchyId) },
|
||||||
viewModelScope.launch { upNextSeries = Crunchyroll.upNextSeries(crunchyId) }
|
viewModelScope.launch { upNextSeries = Crunchyroll.upNextSeries(crunchyId) },
|
||||||
|
viewModelScope.launch { similarTo = Crunchyroll.similarTo(crunchyId) }
|
||||||
).joinAll()
|
).joinAll()
|
||||||
// println("series: $seriesCrunchy")
|
|
||||||
// println("seasons: $seasonsCrunchy")
|
|
||||||
println(upNextSeries)
|
|
||||||
|
|
||||||
// load the preferred season (preferred language, language per season, not per stream)
|
// load the preferred season (preferred language, language per season, not per stream)
|
||||||
currentSeasonCrunchy = seasonsCrunchy.getPreferredSeason(Preferences.preferredLocale)
|
currentSeasonCrunchy = seasonsCrunchy.getPreferredSeason(Preferences.preferredLocale)
|
||||||
|
|
||||||
// load episodes and metaDB in parallel (tmdb needs mediaType, which is set via episodes)
|
// Note: if we need to query metaDB, do it now
|
||||||
listOf(
|
|
||||||
viewModelScope.launch { episodesCrunchy = Crunchyroll.episodes(currentSeasonCrunchy.id) },
|
|
||||||
viewModelScope.launch { mediaMeta = null }, // TODO metaDB
|
|
||||||
).joinAll()
|
|
||||||
// println("episodes: $episodesCrunchy")
|
|
||||||
|
|
||||||
|
// load episodes and metaDB in parallel (tmdb needs mediaType, which is set via episodes)
|
||||||
|
viewModelScope.launch { episodesCrunchy = Crunchyroll.episodes(currentSeasonCrunchy.id) }.join()
|
||||||
currentEpisodesCrunchy.clear()
|
currentEpisodesCrunchy.clear()
|
||||||
currentEpisodesCrunchy.addAll(episodesCrunchy.items)
|
currentEpisodesCrunchy.addAll(episodesCrunchy.items)
|
||||||
|
|
||||||
@ -103,7 +96,7 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic
|
|||||||
MediaType.TVSHOW -> tmdbApiController.searchTVShow(seriesCrunchy.title)
|
MediaType.TVSHOW -> tmdbApiController.searchTVShow(seriesCrunchy.title)
|
||||||
else -> NoneTMDBSearch
|
else -> NoneTMDBSearch
|
||||||
}
|
}
|
||||||
println(tmdbSearchResult)
|
// println(tmdbSearchResult)
|
||||||
|
|
||||||
tmdbResult = if (tmdbSearchResult.results.isNotEmpty()) {
|
tmdbResult = if (tmdbSearchResult.results.isNotEmpty()) {
|
||||||
when (val result = tmdbSearchResult.results.first()) {
|
when (val result = tmdbSearchResult.results.first()) {
|
||||||
@ -112,8 +105,7 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic
|
|||||||
else -> NoneTMDB
|
else -> NoneTMDB
|
||||||
}
|
}
|
||||||
} else NoneTMDB
|
} else NoneTMDB
|
||||||
|
// println(tmdbResult)
|
||||||
println(tmdbResult)
|
|
||||||
|
|
||||||
// currently not used
|
// currently not used
|
||||||
// tmdbTVSeason = if (tmdbResult is TMDBTVShow) {
|
// tmdbTVSeason = if (tmdbResult is TMDBTVShow) {
|
||||||
@ -139,6 +131,11 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic
|
|||||||
episodesCrunchy = Crunchyroll.episodes(currentSeasonCrunchy.id)
|
episodesCrunchy = Crunchyroll.episodes(currentSeasonCrunchy.id)
|
||||||
currentEpisodesCrunchy.clear()
|
currentEpisodesCrunchy.clear()
|
||||||
currentEpisodesCrunchy.addAll(episodesCrunchy.items)
|
currentEpisodesCrunchy.addAll(episodesCrunchy.items)
|
||||||
|
|
||||||
|
// update playheads playheads (including fully watched state)
|
||||||
|
val episodeIDs = episodesCrunchy.items.map { it.id }
|
||||||
|
currentPlayheads.clear()
|
||||||
|
currentPlayheads.putAll(Crunchyroll.playheads(episodeIDs))
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun setWatchlist() {
|
suspend fun setWatchlist() {
|
||||||
@ -162,16 +159,4 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* get the next episode based on episodeId
|
|
||||||
* if no matching is found, use first episode
|
|
||||||
*/
|
|
||||||
fun updateNextEpisode(episodeId: Int) {
|
|
||||||
// TODO reimplement if needed
|
|
||||||
// if (media.type == MediaType.MOVIE) return // return if movie
|
|
||||||
//
|
|
||||||
// nextEpisodeId = media.playlist.firstOrNull { it.index > media.getEpisodeById(episodeId).index }?.mediaId
|
|
||||||
// ?: media.playlist.first().mediaId
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -47,15 +47,17 @@ import com.google.android.exoplayer2.ExoPlayer
|
|||||||
import com.google.android.exoplayer2.Player
|
import com.google.android.exoplayer2.Player
|
||||||
import com.google.android.exoplayer2.ui.StyledPlayerControlView
|
import com.google.android.exoplayer2.ui.StyledPlayerControlView
|
||||||
import com.google.android.exoplayer2.util.Util
|
import com.google.android.exoplayer2.util.Util
|
||||||
import kotlinx.android.synthetic.main.activity_player.*
|
|
||||||
import kotlinx.android.synthetic.main.player_controls.*
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.mosad.teapod.R
|
import org.mosad.teapod.R
|
||||||
|
import org.mosad.teapod.databinding.ActivityPlayerBinding
|
||||||
|
import org.mosad.teapod.databinding.PlayerControlsBinding
|
||||||
import org.mosad.teapod.parser.crunchyroll.NoneEpisode
|
import org.mosad.teapod.parser.crunchyroll.NoneEpisode
|
||||||
import org.mosad.teapod.preferences.Preferences
|
import org.mosad.teapod.preferences.Preferences
|
||||||
import org.mosad.teapod.ui.components.EpisodesListPlayer
|
import org.mosad.teapod.ui.activity.player.fragment.EpisodeListDialogFragment
|
||||||
import org.mosad.teapod.ui.components.LanguageSettingsPlayer
|
import org.mosad.teapod.ui.activity.player.fragment.LanguageSettingsDialogFragment
|
||||||
import org.mosad.teapod.util.*
|
import org.mosad.teapod.util.hideBars
|
||||||
|
import org.mosad.teapod.util.isInPiPMode
|
||||||
|
import org.mosad.teapod.util.navToLauncherTask
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import kotlin.concurrent.scheduleAtFixedRate
|
import kotlin.concurrent.scheduleAtFixedRate
|
||||||
@ -63,6 +65,8 @@ import kotlin.concurrent.scheduleAtFixedRate
|
|||||||
class PlayerActivity : AppCompatActivity() {
|
class PlayerActivity : AppCompatActivity() {
|
||||||
|
|
||||||
private val model: PlayerViewModel by viewModels()
|
private val model: PlayerViewModel by viewModels()
|
||||||
|
private lateinit var playerBinding: ActivityPlayerBinding
|
||||||
|
private lateinit var controlsBinding: PlayerControlsBinding
|
||||||
|
|
||||||
private lateinit var controller: StyledPlayerControlView
|
private lateinit var controller: StyledPlayerControlView
|
||||||
private lateinit var gestureDetector: GestureDetectorCompat
|
private lateinit var gestureDetector: GestureDetectorCompat
|
||||||
@ -80,6 +84,11 @@ class PlayerActivity : AppCompatActivity() {
|
|||||||
setContentView(R.layout.activity_player)
|
setContentView(R.layout.activity_player)
|
||||||
hideBars() // Initial hide the bars
|
hideBars() // Initial hide the bars
|
||||||
|
|
||||||
|
playerBinding = ActivityPlayerBinding.bind(findViewById(R.id.player_root))
|
||||||
|
|
||||||
|
println(findViewById(R.id.player_controls_root))
|
||||||
|
controlsBinding = PlayerControlsBinding.bind(findViewById(R.id.player_controls_root))
|
||||||
|
|
||||||
model.loadMediaAsync(
|
model.loadMediaAsync(
|
||||||
intent.getStringExtra(getString(R.string.intent_season_id)) ?: "",
|
intent.getStringExtra(getString(R.string.intent_season_id)) ?: "",
|
||||||
intent.getStringExtra(getString(R.string.intent_episode_id)) ?: ""
|
intent.getStringExtra(getString(R.string.intent_episode_id)) ?: ""
|
||||||
@ -87,7 +96,7 @@ class PlayerActivity : AppCompatActivity() {
|
|||||||
model.currentEpisodeChangedListener.add { onMediaChanged() }
|
model.currentEpisodeChangedListener.add { onMediaChanged() }
|
||||||
gestureDetector = GestureDetectorCompat(this, PlayerGestureListener())
|
gestureDetector = GestureDetectorCompat(this, PlayerGestureListener())
|
||||||
|
|
||||||
controller = video_view.findViewById(R.id.exo_controller)
|
controller = playerBinding.videoView.findViewById(R.id.exo_controller)
|
||||||
controller.isAnimationEnabled = false // disable controls (time-bar) animation
|
controller.isAnimationEnabled = false // disable controls (time-bar) animation
|
||||||
|
|
||||||
initExoPlayer() // call in onCreate, exoplayer lives in view model
|
initExoPlayer() // call in onCreate, exoplayer lives in view model
|
||||||
@ -104,7 +113,7 @@ class PlayerActivity : AppCompatActivity() {
|
|||||||
super.onStart()
|
super.onStart()
|
||||||
if (Util.SDK_INT > 23) {
|
if (Util.SDK_INT > 23) {
|
||||||
initPlayer()
|
initPlayer()
|
||||||
video_view?.onResume()
|
playerBinding.videoView.onResume()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,7 +123,7 @@ class PlayerActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
if (Util.SDK_INT <= 23) {
|
if (Util.SDK_INT <= 23) {
|
||||||
initPlayer()
|
initPlayer()
|
||||||
video_view?.onResume()
|
playerBinding.videoView.onResume()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -166,7 +175,7 @@ class PlayerActivity : AppCompatActivity() {
|
|||||||
} else {
|
} else {
|
||||||
val width = model.player.videoFormat?.width ?: 0
|
val width = model.player.videoFormat?.width ?: 0
|
||||||
val height = model.player.videoFormat?.height ?: 0
|
val height = model.player.videoFormat?.height ?: 0
|
||||||
val contentFrame: View = video_view.findViewById(R.id.exo_content_frame)
|
val contentFrame: View = playerBinding.videoView.findViewById(R.id.exo_content_frame)
|
||||||
val contentRect = with(contentFrame) {
|
val contentRect = with(contentFrame) {
|
||||||
val (x, y) = intArrayOf(0, 0).also(::getLocationInWindow)
|
val (x, y) = intArrayOf(0, 0).also(::getLocationInWindow)
|
||||||
Rect(x, y, x + width, y + height)
|
Rect(x, y, x + width, y + height)
|
||||||
@ -190,7 +199,9 @@ class PlayerActivity : AppCompatActivity() {
|
|||||||
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
|
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
|
||||||
|
|
||||||
// Hide the full-screen UI (controls, etc.) while in picture-in-picture mode.
|
// Hide the full-screen UI (controls, etc.) while in picture-in-picture mode.
|
||||||
video_view.useController = !isInPictureInPictureMode
|
playerBinding.videoView.useController = !isInPictureInPictureMode
|
||||||
|
|
||||||
|
// TODO also hide language settings/episodes list
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initPlayer() {
|
private fun initPlayer() {
|
||||||
@ -212,17 +223,13 @@ class PlayerActivity : AppCompatActivity() {
|
|||||||
override fun onPlaybackStateChanged(state: Int) {
|
override fun onPlaybackStateChanged(state: Int) {
|
||||||
super.onPlaybackStateChanged(state)
|
super.onPlaybackStateChanged(state)
|
||||||
|
|
||||||
loading.visibility = when (state) {
|
playerBinding.loading.visibility = when (state) {
|
||||||
ExoPlayer.STATE_READY -> View.GONE
|
ExoPlayer.STATE_READY -> View.GONE
|
||||||
ExoPlayer.STATE_BUFFERING -> View.VISIBLE
|
ExoPlayer.STATE_BUFFERING -> View.VISIBLE
|
||||||
else -> View.GONE
|
else -> View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
exo_play_pause.visibility = when (loading.visibility) {
|
controlsBinding.exoPlayPause.isVisible = !playerBinding.loading.isVisible
|
||||||
View.GONE -> View.VISIBLE
|
|
||||||
View.VISIBLE -> View.INVISIBLE
|
|
||||||
else -> View.VISIBLE
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state == ExoPlayer.STATE_ENDED && hasNextEpisode() && Preferences.autoplay) {
|
if (state == ExoPlayer.STATE_ENDED && hasNextEpisode() && Preferences.autoplay) {
|
||||||
playNextEpisode()
|
playNextEpisode()
|
||||||
@ -237,10 +244,10 @@ class PlayerActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
private fun initVideoView() {
|
private fun initVideoView() {
|
||||||
video_view.player = model.player
|
playerBinding.videoView.player = model.player
|
||||||
|
|
||||||
// when the player controls get hidden, hide the bars too
|
// when the player controls get hidden, hide the bars too
|
||||||
video_view.setControllerVisibilityListener {
|
playerBinding.videoView.setControllerVisibilityListener {
|
||||||
when (it) {
|
when (it) {
|
||||||
View.GONE -> {
|
View.GONE -> {
|
||||||
hideBars()
|
hideBars()
|
||||||
@ -250,23 +257,23 @@ class PlayerActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
video_view.setOnTouchListener { _, event ->
|
playerBinding.videoView.setOnTouchListener { _, event ->
|
||||||
gestureDetector.onTouchEvent(event)
|
gestureDetector.onTouchEvent(event)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initActions() {
|
private fun initActions() {
|
||||||
exo_close_player.setOnClickListener {
|
controlsBinding.exoClosePlayer.setOnClickListener {
|
||||||
this.finish()
|
this.finish()
|
||||||
}
|
}
|
||||||
rwd_10.setOnButtonClickListener { rewind() }
|
controlsBinding.rwd10.setOnButtonClickListener { rewind() }
|
||||||
ffwd_10.setOnButtonClickListener { fastForward() }
|
controlsBinding.ffwd10.setOnButtonClickListener { fastForward() }
|
||||||
button_next_ep.setOnClickListener { playNextEpisode() }
|
playerBinding.buttonNextEp.setOnClickListener { playNextEpisode() }
|
||||||
button_skip_op.setOnClickListener { skipOpening() }
|
playerBinding.buttonSkipOp.setOnClickListener { skipOpening() }
|
||||||
button_language.setOnClickListener { showLanguageSettings() }
|
controlsBinding.buttonLanguage.setOnClickListener { showLanguageSettings() }
|
||||||
button_episodes.setOnClickListener { showEpisodesList() }
|
controlsBinding.buttonEpisodes.setOnClickListener { showEpisodesList() }
|
||||||
button_next_ep_c.setOnClickListener { playNextEpisode() }
|
controlsBinding.buttonNextEpC.setOnClickListener { playNextEpisode() }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initGUI() {
|
private fun initGUI() {
|
||||||
@ -284,7 +291,7 @@ class PlayerActivity : AppCompatActivity() {
|
|||||||
timerUpdates = Timer().scheduleAtFixedRate(0, 500) {
|
timerUpdates = Timer().scheduleAtFixedRate(0, 500) {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
val currentPosition = model.player.currentPosition
|
val currentPosition = model.player.currentPosition
|
||||||
val btnNextEpIsVisible = button_next_ep.isVisible
|
val btnNextEpIsVisible = playerBinding.buttonNextEp.isVisible
|
||||||
val controlsVisible = controller.isVisible
|
val controlsVisible = controller.isVisible
|
||||||
|
|
||||||
// make sure remaining time is > 0
|
// make sure remaining time is > 0
|
||||||
@ -308,10 +315,12 @@ class PlayerActivity : AppCompatActivity() {
|
|||||||
model.currentEpisodeMeta?.let {
|
model.currentEpisodeMeta?.let {
|
||||||
if (it.openingDuration > 0 &&
|
if (it.openingDuration > 0 &&
|
||||||
currentPosition in it.openingStart..(it.openingStart + 10000) &&
|
currentPosition in it.openingStart..(it.openingStart + 10000) &&
|
||||||
!button_skip_op.isVisible
|
!playerBinding.buttonSkipOp.isVisible
|
||||||
) {
|
) {
|
||||||
showButtonSkipOp()
|
showButtonSkipOp()
|
||||||
} else if (button_skip_op.isVisible && currentPosition !in it.openingStart..(it.openingStart + 10000)) {
|
} else if (playerBinding.buttonSkipOp.isVisible &&
|
||||||
|
currentPosition !in it.openingStart..(it.openingStart + 10000)
|
||||||
|
) {
|
||||||
// the button should only be visible, if currentEpisodeMeta != null
|
// the button should only be visible, if currentEpisodeMeta != null
|
||||||
hideButtonSkipOp()
|
hideButtonSkipOp()
|
||||||
}
|
}
|
||||||
@ -326,7 +335,7 @@ class PlayerActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun onPauseOnStop() {
|
private fun onPauseOnStop() {
|
||||||
video_view?.onPause()
|
playerBinding.videoView.onPause()
|
||||||
model.player.pause()
|
model.player.pause()
|
||||||
timerUpdates.cancel()
|
timerUpdates.cancel()
|
||||||
}
|
}
|
||||||
@ -341,7 +350,7 @@ class PlayerActivity : AppCompatActivity() {
|
|||||||
val seconds = TimeUnit.MILLISECONDS.toSeconds(remainingTime) % 60
|
val seconds = TimeUnit.MILLISECONDS.toSeconds(remainingTime) % 60
|
||||||
|
|
||||||
// if remaining time is below 60 minutes, don't show hours
|
// if remaining time is below 60 minutes, don't show hours
|
||||||
exo_remaining.text = if (TimeUnit.MILLISECONDS.toMinutes(remainingTime) < 60) {
|
controlsBinding.exoRemaining.text = if (TimeUnit.MILLISECONDS.toMinutes(remainingTime) < 60) {
|
||||||
getString(R.string.time_min_sec, minutes, seconds)
|
getString(R.string.time_min_sec, minutes, seconds)
|
||||||
} else {
|
} else {
|
||||||
getString(R.string.time_hour_min_sec, hours, minutes, seconds)
|
getString(R.string.time_hour_min_sec, hours, minutes, seconds)
|
||||||
@ -359,10 +368,10 @@ class PlayerActivity : AppCompatActivity() {
|
|||||||
this.finish()
|
this.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
exo_text_title.text = model.getMediaTitle()
|
controlsBinding.exoTextTitle.text = model.getMediaTitle()
|
||||||
|
|
||||||
// hide the next episode button, if there is none
|
// hide the next episode button, if there is none
|
||||||
button_next_ep_c.visibility = if (hasNextEpisode()) View.VISIBLE else View.GONE
|
controlsBinding.buttonNextEpC.isVisible = hasNextEpisode()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -382,36 +391,36 @@ class PlayerActivity : AppCompatActivity() {
|
|||||||
model.seekToOffset(rwdTime)
|
model.seekToOffset(rwdTime)
|
||||||
|
|
||||||
// hide/show needed components
|
// hide/show needed components
|
||||||
exo_double_tap_indicator.visibility = View.VISIBLE
|
playerBinding.exoDoubleTapIndicator.visibility = View.VISIBLE
|
||||||
ffwd_10_indicator.visibility = View.INVISIBLE
|
playerBinding.ffwd10Indicator.visibility = View.INVISIBLE
|
||||||
rwd_10.visibility = View.INVISIBLE
|
controlsBinding.rwd10.visibility = View.INVISIBLE
|
||||||
|
|
||||||
rwd_10_indicator.onAnimationEndCallback = {
|
playerBinding.rwd10Indicator.onAnimationEndCallback = {
|
||||||
exo_double_tap_indicator.visibility = View.GONE
|
playerBinding.exoDoubleTapIndicator.visibility = View.GONE
|
||||||
ffwd_10_indicator.visibility = View.VISIBLE
|
playerBinding.ffwd10Indicator.visibility = View.VISIBLE
|
||||||
rwd_10.visibility = View.VISIBLE
|
controlsBinding.rwd10.visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
|
|
||||||
// run animation
|
// run animation
|
||||||
rwd_10_indicator.runOnClickAnimation()
|
playerBinding.rwd10Indicator.runOnClickAnimation()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fastForward() {
|
private fun fastForward() {
|
||||||
model.seekToOffset(fwdTime)
|
model.seekToOffset(fwdTime)
|
||||||
|
|
||||||
// hide/show needed components
|
// hide/show needed components
|
||||||
exo_double_tap_indicator.visibility = View.VISIBLE
|
playerBinding.exoDoubleTapIndicator.visibility = View.VISIBLE
|
||||||
rwd_10_indicator.visibility = View.INVISIBLE
|
playerBinding.rwd10Indicator.visibility = View.INVISIBLE
|
||||||
ffwd_10.visibility = View.INVISIBLE
|
controlsBinding.ffwd10.visibility = View.INVISIBLE
|
||||||
|
|
||||||
ffwd_10_indicator.onAnimationEndCallback = {
|
playerBinding.ffwd10Indicator.onAnimationEndCallback = {
|
||||||
exo_double_tap_indicator.visibility = View.GONE
|
playerBinding.exoDoubleTapIndicator.visibility = View.GONE
|
||||||
rwd_10_indicator.visibility = View.VISIBLE
|
playerBinding.rwd10Indicator.visibility = View.VISIBLE
|
||||||
ffwd_10.visibility = View.VISIBLE
|
controlsBinding.ffwd10.visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
|
|
||||||
// run animation
|
// run animation
|
||||||
ffwd_10_indicator.runOnClickAnimation()
|
playerBinding.ffwd10Indicator.runOnClickAnimation()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun playNextEpisode() {
|
private fun playNextEpisode() {
|
||||||
@ -425,7 +434,6 @@ class PlayerActivity : AppCompatActivity() {
|
|||||||
val seekTime = (it.openingStart + it.openingDuration) - model.player.currentPosition
|
val seekTime = (it.openingStart + it.openingDuration) - model.player.currentPosition
|
||||||
model.seekToOffset(seekTime)
|
model.seekToOffset(seekTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -433,10 +441,10 @@ class PlayerActivity : AppCompatActivity() {
|
|||||||
* TODO improve the show animation
|
* TODO improve the show animation
|
||||||
*/
|
*/
|
||||||
private fun showButtonNextEp() {
|
private fun showButtonNextEp() {
|
||||||
button_next_ep.isVisible = true
|
playerBinding.buttonNextEp.isVisible = true
|
||||||
button_next_ep.alpha = 0.0f
|
playerBinding.buttonNextEp.alpha = 0.0f
|
||||||
|
|
||||||
button_next_ep.animate()
|
playerBinding.buttonNextEp.animate()
|
||||||
.alpha(1.0f)
|
.alpha(1.0f)
|
||||||
.setListener(null)
|
.setListener(null)
|
||||||
}
|
}
|
||||||
@ -446,52 +454,45 @@ class PlayerActivity : AppCompatActivity() {
|
|||||||
* TODO improve the hide animation
|
* TODO improve the hide animation
|
||||||
*/
|
*/
|
||||||
private fun hideButtonNextEp() {
|
private fun hideButtonNextEp() {
|
||||||
button_next_ep.animate()
|
playerBinding.buttonNextEp.animate()
|
||||||
.alpha(0.0f)
|
.alpha(0.0f)
|
||||||
.setListener(object : AnimatorListenerAdapter() {
|
.setListener(object : AnimatorListenerAdapter() {
|
||||||
override fun onAnimationEnd(animation: Animator?) {
|
override fun onAnimationEnd(animation: Animator?) {
|
||||||
super.onAnimationEnd(animation)
|
super.onAnimationEnd(animation)
|
||||||
button_next_ep.isVisible = false
|
playerBinding.buttonNextEp.isVisible = false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showButtonSkipOp() {
|
private fun showButtonSkipOp() {
|
||||||
button_skip_op.isVisible = true
|
playerBinding.buttonSkipOp.isVisible = true
|
||||||
button_skip_op.alpha = 0.0f
|
playerBinding.buttonSkipOp.alpha = 0.0f
|
||||||
|
|
||||||
button_skip_op.animate()
|
playerBinding.buttonSkipOp.animate()
|
||||||
.alpha(1.0f)
|
.alpha(1.0f)
|
||||||
.setListener(null)
|
.setListener(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun hideButtonSkipOp() {
|
private fun hideButtonSkipOp() {
|
||||||
button_skip_op.animate()
|
playerBinding.buttonSkipOp.animate()
|
||||||
.alpha(0.0f)
|
.alpha(0.0f)
|
||||||
.setListener(object : AnimatorListenerAdapter() {
|
.setListener(object : AnimatorListenerAdapter() {
|
||||||
override fun onAnimationEnd(animation: Animator?) {
|
override fun onAnimationEnd(animation: Animator?) {
|
||||||
super.onAnimationEnd(animation)
|
super.onAnimationEnd(animation)
|
||||||
button_skip_op.isVisible = false
|
playerBinding.buttonSkipOp.isVisible = false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showEpisodesList() {
|
private fun showEpisodesList() {
|
||||||
val episodesList = EpisodesListPlayer(this, model = model).apply {
|
|
||||||
onViewRemovedAction = { model.player.play() }
|
|
||||||
}
|
|
||||||
player_layout.addView(episodesList)
|
|
||||||
pauseAndHideControls()
|
pauseAndHideControls()
|
||||||
|
EpisodeListDialogFragment().show(supportFragmentManager, EpisodeListDialogFragment.TAG)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showLanguageSettings() {
|
private fun showLanguageSettings() {
|
||||||
val languageSettings = LanguageSettingsPlayer(this, model = model).apply {
|
|
||||||
onViewRemovedAction = { model.player.play() }
|
|
||||||
}
|
|
||||||
player_layout.addView(languageSettings)
|
|
||||||
pauseAndHideControls()
|
pauseAndHideControls()
|
||||||
|
LanguageSettingsDialogFragment().show(supportFragmentManager, LanguageSettingsDialogFragment.TAG)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -521,7 +522,7 @@ class PlayerActivity : AppCompatActivity() {
|
|||||||
*/
|
*/
|
||||||
override fun onDoubleTap(e: MotionEvent?): Boolean {
|
override fun onDoubleTap(e: MotionEvent?): Boolean {
|
||||||
val eventPosX = e?.x?.toInt() ?: 0
|
val eventPosX = e?.x?.toInt() ?: 0
|
||||||
val viewCenterX = video_view.measuredWidth / 2
|
val viewCenterX = playerBinding.videoView.measuredWidth / 2
|
||||||
|
|
||||||
// if the event position is on the left side rewind, if it's on the right forward
|
// if the event position is on the left side rewind, if it's on the right forward
|
||||||
if (eventPosX < viewCenterX) rewind() else fastForward()
|
if (eventPosX < viewCenterX) rewind() else fastForward()
|
||||||
|
@ -31,23 +31,18 @@ import androidx.lifecycle.viewModelScope
|
|||||||
import com.google.android.exoplayer2.ExoPlayer
|
import com.google.android.exoplayer2.ExoPlayer
|
||||||
import com.google.android.exoplayer2.MediaItem
|
import com.google.android.exoplayer2.MediaItem
|
||||||
import com.google.android.exoplayer2.Player
|
import com.google.android.exoplayer2.Player
|
||||||
import com.google.android.exoplayer2.SimpleExoPlayer
|
|
||||||
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
|
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
|
||||||
import com.google.android.exoplayer2.source.hls.HlsMediaSource
|
|
||||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory
|
|
||||||
import com.google.android.exoplayer2.util.Util
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.joinAll
|
import kotlinx.coroutines.joinAll
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.mosad.teapod.R
|
import org.mosad.teapod.R
|
||||||
import org.mosad.teapod.parser.crunchyroll.Crunchyroll
|
import org.mosad.teapod.parser.crunchyroll.*
|
||||||
import org.mosad.teapod.parser.crunchyroll.NoneEpisode
|
|
||||||
import org.mosad.teapod.parser.crunchyroll.NoneEpisodes
|
|
||||||
import org.mosad.teapod.parser.crunchyroll.NonePlayback
|
|
||||||
import org.mosad.teapod.preferences.Preferences
|
import org.mosad.teapod.preferences.Preferences
|
||||||
import org.mosad.teapod.util.EpisodeMeta
|
import org.mosad.teapod.util.metadb.EpisodeMeta
|
||||||
import org.mosad.teapod.util.tmdb.TMDBTVSeason
|
import org.mosad.teapod.util.metadb.Meta
|
||||||
|
import org.mosad.teapod.util.metadb.MetaDBController
|
||||||
|
import org.mosad.teapod.util.metadb.TVShowMeta
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -56,22 +51,23 @@ import java.util.*
|
|||||||
* the next episode will be update and the callback is handled.
|
* the next episode will be update and the callback is handled.
|
||||||
*/
|
*/
|
||||||
class PlayerViewModel(application: Application) : AndroidViewModel(application) {
|
class PlayerViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
|
private val classTag = javaClass.name
|
||||||
|
|
||||||
val player = SimpleExoPlayer.Builder(application).build()
|
val player = ExoPlayer.Builder(application).build()
|
||||||
private val dataSourceFactory = DefaultDataSourceFactory(application, Util.getUserAgent(application, "Teapod"))
|
|
||||||
private val mediaSession = MediaSessionCompat(application, "TEAPOD_PLAYER_SESSION")
|
private val mediaSession = MediaSessionCompat(application, "TEAPOD_PLAYER_SESSION")
|
||||||
|
|
||||||
val currentEpisodeChangedListener = ArrayList<() -> Unit>()
|
val currentEpisodeChangedListener = ArrayList<() -> Unit>()
|
||||||
private var currentPlayhead: Long = 0
|
private var currentPlayhead: Long = 0
|
||||||
|
|
||||||
// tmdb/meta data
|
// tmdb/meta data
|
||||||
// TODO meta data currently not implemented for cr
|
var mediaMeta: Meta? = null
|
||||||
// var mediaMeta: Meta? = null
|
|
||||||
// internal set
|
|
||||||
var tmdbTVSeason: TMDBTVSeason? =null
|
|
||||||
internal set
|
internal set
|
||||||
var currentEpisodeMeta: EpisodeMeta? = null
|
var currentEpisodeMeta: EpisodeMeta? = null
|
||||||
internal set
|
internal set
|
||||||
|
var currentPlayheads: PlayheadsMap = mutableMapOf()
|
||||||
|
internal set
|
||||||
|
// var tmdbTVSeason: TMDBTVSeason? =null
|
||||||
|
// internal set
|
||||||
|
|
||||||
// crunchyroll episodes/playback
|
// crunchyroll episodes/playback
|
||||||
var episodes = NoneEpisodes
|
var episodes = NoneEpisodes
|
||||||
@ -108,7 +104,7 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
|
|||||||
mediaSession.release()
|
mediaSession.release()
|
||||||
player.release()
|
player.release()
|
||||||
|
|
||||||
Log.d(javaClass.name, "Released player")
|
Log.d(classTag, "Released player")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -125,21 +121,19 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
|
|||||||
fun loadMediaAsync(seasonId: String, episodeId: String) = viewModelScope.launch {
|
fun loadMediaAsync(seasonId: String, episodeId: String) = viewModelScope.launch {
|
||||||
episodes = Crunchyroll.episodes(seasonId)
|
episodes = Crunchyroll.episodes(seasonId)
|
||||||
|
|
||||||
|
listOf(
|
||||||
|
viewModelScope.launch { mediaMeta = loadMediaMeta(episodes.items.first().seriesId) },
|
||||||
|
viewModelScope.launch {
|
||||||
|
val episodeIDs = episodes.items.map { it.id }
|
||||||
|
currentPlayheads = Crunchyroll.playheads(episodeIDs)
|
||||||
|
}
|
||||||
|
).joinAll()
|
||||||
|
|
||||||
|
|
||||||
|
Log.d(classTag, "meta: $mediaMeta")
|
||||||
|
|
||||||
setCurrentEpisode(episodeId)
|
setCurrentEpisode(episodeId)
|
||||||
playCurrentMedia(currentPlayhead)
|
playCurrentMedia(currentPlayhead)
|
||||||
|
|
||||||
// TODO reimplement for cr
|
|
||||||
// run async as it should be loaded by the time the episodes a
|
|
||||||
// viewModelScope.launch {
|
|
||||||
// // get tmdb season info, if metaDB knows the tv show
|
|
||||||
// if (media.type == DataTypes.MediaType.TVSHOW && mediaMeta != null) {
|
|
||||||
// val tvShowMeta = mediaMeta as TVShowMeta
|
|
||||||
// tmdbTVSeason = TMDBApiController().getTVSeasonDetails(tvShowMeta.tmdbId, tvShowMeta.tmdbSeasonNumber)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// currentEpisodeMeta = getEpisodeMetaByAoDMediaId(currentEpisodeAoD.mediaId)
|
|
||||||
// currentLanguage = currentEpisodeAoD.getPreferredStream(preferredLanguage).language
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setLanguage(language: Locale) {
|
fun setLanguage(language: Locale) {
|
||||||
@ -174,6 +168,16 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
|
|||||||
episode.id == episodeId
|
episode.id == episodeId
|
||||||
} ?: NoneEpisode
|
} ?: NoneEpisode
|
||||||
|
|
||||||
|
// TODO improve handling of none present seasons/episodes
|
||||||
|
// update current episode meta
|
||||||
|
currentEpisodeMeta = if (mediaMeta is TVShowMeta && currentEpisode.episodeNumber != null) {
|
||||||
|
(mediaMeta as TVShowMeta)
|
||||||
|
.seasons.getOrNull(currentEpisode.seasonNumber - 1)
|
||||||
|
?.episodes?.getOrNull(currentEpisode.episodeNumber!! - 1)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
// update player gui (title, next ep button) after currentEpisode has changed
|
// update player gui (title, next ep button) after currentEpisode has changed
|
||||||
currentEpisodeChangedListener.forEach { it() }
|
currentEpisodeChangedListener.forEach { it() }
|
||||||
|
|
||||||
@ -195,9 +199,7 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
println("loaded playback ${currentEpisode.playback}")
|
Log.d(classTag, "playback: ${currentEpisode.playback}")
|
||||||
|
|
||||||
// TODO update metadata and language (it should not be needed to update the language here!)
|
|
||||||
|
|
||||||
if (startPlayback) {
|
if (startPlayback) {
|
||||||
playCurrentMedia()
|
playCurrentMedia()
|
||||||
@ -227,16 +229,13 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
|
|||||||
currentPlayback.streams.adaptive_hls.entries.first().value.url
|
currentPlayback.streams.adaptive_hls.entries.first().value.url
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
println("stream url: $url")
|
Log.i(classTag, "stream url: $url")
|
||||||
|
|
||||||
// create the media source object
|
// create the media item
|
||||||
val mediaSource = HlsMediaSource.Factory(dataSourceFactory).createMediaSource(
|
val mediaItem = MediaItem.fromUri(Uri.parse(url))
|
||||||
MediaItem.fromUri(Uri.parse(url))
|
player.setMediaItem(mediaItem)
|
||||||
)
|
|
||||||
|
|
||||||
// the actual player playback code
|
|
||||||
player.setMediaSource(mediaSource)
|
|
||||||
player.prepare()
|
player.prepare()
|
||||||
|
|
||||||
if (seekPosition > 0) player.seekTo(seekPosition)
|
if (seekPosition > 0) player.seekTo(seekPosition)
|
||||||
player.playWhenReady = true
|
player.playWhenReady = true
|
||||||
}
|
}
|
||||||
@ -266,25 +265,9 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
|
|||||||
return episodes.items.lastOrNull()?.id == currentEpisode.id
|
return episodes.items.lastOrNull()?.id == currentEpisode.id
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO reimplement for cr
|
private suspend fun loadMediaMeta(crSeriesId: String): Meta? {
|
||||||
// fun getEpisodeMetaByAoDMediaId(aodMediaId: Int): EpisodeMeta? {
|
return MetaDBController.getTVShowMetadata(crSeriesId)
|
||||||
// val meta = mediaMeta
|
}
|
||||||
// return if (meta is TVShowMeta) {
|
|
||||||
// meta.episodes.firstOrNull { it.aodMediaId == aodMediaId }
|
|
||||||
// } else {
|
|
||||||
// null
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// private suspend fun loadMediaMeta(aodId: Int): Meta? {
|
|
||||||
// return if (media.type == DataTypes.MediaType.TVSHOW) {
|
|
||||||
// MetaDBController().getTVShowMetadata(aodId)
|
|
||||||
// } else {
|
|
||||||
// null
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// return null
|
|
||||||
// }
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the playhead of the current episode, if currentPosition > 1000ms.
|
* Update the playhead of the current episode, if currentPosition > 1000ms.
|
||||||
@ -292,10 +275,15 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
|
|||||||
private fun updatePlayhead() {
|
private fun updatePlayhead() {
|
||||||
val playhead = (player.currentPosition / 1000)
|
val playhead = (player.currentPosition / 1000)
|
||||||
|
|
||||||
if (playhead > 0) {
|
if (playhead > 0 && Preferences.updatePlayhead) {
|
||||||
viewModelScope.launch { Crunchyroll.postPlayheads(currentEpisode.id, playhead.toInt()) }
|
viewModelScope.launch { Crunchyroll.postPlayheads(currentEpisode.id, playhead.toInt()) }
|
||||||
Log.i(javaClass.name, "Set playhead for episode ${currentEpisode.id} to $playhead sec.")
|
Log.i(javaClass.name, "Set playhead for episode ${currentEpisode.id} to $playhead sec.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
val episodeIDs = episodes.items.map { it.id }
|
||||||
|
currentPlayheads = Crunchyroll.playheads(episodeIDs)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,68 @@
|
|||||||
|
package org.mosad.teapod.ui.activity.player.fragment
|
||||||
|
|
||||||
|
import android.content.DialogInterface
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import org.mosad.teapod.R
|
||||||
|
import org.mosad.teapod.databinding.PlayerEpisodesListBinding
|
||||||
|
import org.mosad.teapod.ui.activity.player.PlayerViewModel
|
||||||
|
import org.mosad.teapod.util.adapter.EpisodeItemAdapter
|
||||||
|
import org.mosad.teapod.util.hideBars
|
||||||
|
|
||||||
|
class EpisodeListDialogFragment : DialogFragment() {
|
||||||
|
|
||||||
|
private lateinit var model: PlayerViewModel
|
||||||
|
private lateinit var binding: PlayerEpisodesListBinding
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TAG = "LanguageSettingsDialogFragment"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setStyle(STYLE_NO_TITLE, R.style.FullScreenDialogStyle)
|
||||||
|
model = ViewModelProvider(requireActivity())[PlayerViewModel::class.java]
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
|
binding = PlayerEpisodesListBinding.inflate(inflater, container, false)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
binding.buttonCloseEpisodesList.setOnClickListener {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
val adapterRecEpisodes = EpisodeItemAdapter(
|
||||||
|
model.episodes.items,
|
||||||
|
null,
|
||||||
|
model.currentPlayheads.toMap(),
|
||||||
|
EpisodeItemAdapter.OnClickListener { episode ->
|
||||||
|
dismiss()
|
||||||
|
model.setCurrentEpisode(episode.id, startPlayback = true)
|
||||||
|
},
|
||||||
|
EpisodeItemAdapter.ViewType.PLAYER
|
||||||
|
)
|
||||||
|
|
||||||
|
// episodeNumber starts at 1, we need the episode index -> - 1
|
||||||
|
adapterRecEpisodes.currentSelected = model.currentEpisode.episodeNumber?.minus(1) ?: 0
|
||||||
|
|
||||||
|
binding.recyclerEpisodesPlayer.adapter = adapterRecEpisodes
|
||||||
|
binding.recyclerEpisodesPlayer.scrollToPosition(adapterRecEpisodes.currentSelected)
|
||||||
|
|
||||||
|
// initially hide the status and navigation bar
|
||||||
|
hideBars(requireDialog().window, binding.root)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDismiss(dialog: DialogInterface) {
|
||||||
|
super.onDismiss(dialog)
|
||||||
|
model.player.play()
|
||||||
|
}
|
||||||
|
}
|
@ -1,54 +1,75 @@
|
|||||||
package org.mosad.teapod.ui.components
|
package org.mosad.teapod.ui.activity.player.fragment
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.DialogInterface
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.graphics.Typeface
|
import android.graphics.Typeface
|
||||||
import android.util.AttributeSet
|
import android.os.Bundle
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.Gravity
|
import android.view.Gravity
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.LinearLayout
|
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.core.view.children
|
import androidx.core.view.children
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
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
|
||||||
|
import org.mosad.teapod.util.hideBars
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
// TODO port to DialogFragment
|
class LanguageSettingsDialogFragment : DialogFragment() {
|
||||||
class LanguageSettingsPlayer @JvmOverloads constructor(
|
|
||||||
context: Context,
|
|
||||||
attrs: AttributeSet? = null,
|
|
||||||
defStyleAttr: Int = 0,
|
|
||||||
model: PlayerViewModel? = null
|
|
||||||
) : LinearLayout(context, attrs, defStyleAttr) {
|
|
||||||
|
|
||||||
private val binding = PlayerLanguageSettingsBinding.inflate(LayoutInflater.from(context), this, true)
|
private lateinit var model: PlayerViewModel
|
||||||
var onViewRemovedAction: (() -> Unit)? = null
|
private lateinit var binding: PlayerLanguageSettingsBinding
|
||||||
|
|
||||||
private var selectedLocale = model?.currentLanguage ?: Locale.ROOT
|
private var selectedLocale = Locale.ROOT
|
||||||
|
|
||||||
init {
|
companion object {
|
||||||
model?.let { m ->
|
const val TAG = "LanguageSettingsDialogFragment"
|
||||||
m.currentPlayback.streams.adaptive_hls.keys.forEach { languageTag ->
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setStyle(STYLE_NO_TITLE, R.style.FullScreenDialogStyle)
|
||||||
|
model = ViewModelProvider(requireActivity())[PlayerViewModel::class.java]
|
||||||
|
selectedLocale = model.currentLanguage
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
|
binding = PlayerLanguageSettingsBinding.inflate(inflater, container, false)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
model.currentPlayback.streams.adaptive_hls.keys.forEach { languageTag ->
|
||||||
val locale = Locale.forLanguageTag(languageTag)
|
val locale = Locale.forLanguageTag(languageTag)
|
||||||
addLanguage(locale, locale == m.currentLanguage) { v ->
|
addLanguage(locale, locale == model.currentLanguage) { v ->
|
||||||
selectedLocale = locale
|
selectedLocale = locale
|
||||||
updateSelectedLanguage(v as TextView)
|
updateSelectedLanguage(v as TextView)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
binding.buttonCloseLanguageSettings.setOnClickListener { close() }
|
binding.buttonCloseLanguageSettings.setOnClickListener { dismiss() }
|
||||||
binding.buttonCancel.setOnClickListener { close() }
|
binding.buttonCancel.setOnClickListener { dismiss() }
|
||||||
binding.buttonSelect.setOnClickListener {
|
binding.buttonSelect.setOnClickListener {
|
||||||
model?.setLanguage(selectedLocale)
|
model.setLanguage(selectedLocale)
|
||||||
close()
|
dismiss()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addLanguage(locale: Locale, isSelected: Boolean, onClick: OnClickListener) {
|
// initially hide the status and navigation bar
|
||||||
|
hideBars(requireDialog().window, binding.root)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDismiss(dialog: DialogInterface) {
|
||||||
|
super.onDismiss(dialog)
|
||||||
|
model.player.play()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addLanguage(locale: Locale, isSelected: Boolean, onClick: View.OnClickListener) {
|
||||||
val text = TextView(context).apply {
|
val text = TextView(context).apply {
|
||||||
height = 96
|
height = 96
|
||||||
gravity = Gravity.CENTER_VERTICAL
|
gravity = Gravity.CENTER_VERTICAL
|
||||||
@ -56,13 +77,13 @@ class LanguageSettingsPlayer @JvmOverloads constructor(
|
|||||||
setTextSize(TypedValue.COMPLEX_UNIT_SP, 16f)
|
setTextSize(TypedValue.COMPLEX_UNIT_SP, 16f)
|
||||||
|
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
setTextColor(context.resources.getColor(R.color.exo_white, context.theme))
|
setTextColor(context.resources.getColor(R.color.textPrimaryDark, context.theme))
|
||||||
setTypeface(null, Typeface.BOLD)
|
setTypeface(null, Typeface.BOLD)
|
||||||
setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_baseline_check_24, 0, 0, 0)
|
setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_baseline_check_24, 0, 0, 0)
|
||||||
compoundDrawablesRelative.getOrNull(0)?.setTint(Color.WHITE)
|
compoundDrawablesRelative.getOrNull(0)?.setTint(Color.WHITE)
|
||||||
compoundDrawablePadding = 12
|
compoundDrawablePadding = 12
|
||||||
} else {
|
} else {
|
||||||
setTextColor(context.resources.getColor(R.color.textPrimaryDark, context.theme))
|
setTextColor(context.resources.getColor(R.color.textSecondaryDark, context.theme))
|
||||||
setPadding(75, 0, 0, 0)
|
setPadding(75, 0, 0, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,12 +104,11 @@ class LanguageSettingsPlayer @JvmOverloads constructor(
|
|||||||
setPadding(75, 0, 0, 0)
|
setPadding(75, 0, 0, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// set selected to selected style
|
// set selected to selected style
|
||||||
selected.apply {
|
selected.apply {
|
||||||
setTextColor(context.resources.getColor(R.color.exo_white, context.theme))
|
setTextColor(context.resources.getColor(R.color.player_white, context.theme))
|
||||||
setTypeface(null, Typeface.BOLD)
|
setTypeface(null, Typeface.BOLD)
|
||||||
setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_baseline_check_24, 0, 0, 0)
|
setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_baseline_check_24, 0, 0, 0)
|
||||||
setPadding(0, 0, 0, 0)
|
setPadding(0, 0, 0, 0)
|
||||||
@ -96,10 +116,4 @@ class LanguageSettingsPlayer @JvmOverloads constructor(
|
|||||||
compoundDrawablePadding = 12
|
compoundDrawablePadding = 12
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun close() {
|
|
||||||
(this.parent as ViewGroup).removeView(this)
|
|
||||||
onViewRemovedAction?.invoke()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
@ -1,44 +0,0 @@
|
|||||||
package org.mosad.teapod.ui.components
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.AttributeSet
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.LinearLayout
|
|
||||||
import org.mosad.teapod.databinding.PlayerEpisodesListBinding
|
|
||||||
import org.mosad.teapod.ui.activity.player.PlayerViewModel
|
|
||||||
import org.mosad.teapod.util.adapter.PlayerEpisodeItemAdapter
|
|
||||||
|
|
||||||
class EpisodesListPlayer @JvmOverloads constructor(
|
|
||||||
context: Context,
|
|
||||||
attrs: AttributeSet? = null,
|
|
||||||
defStyleAttr: Int = 0,
|
|
||||||
model: PlayerViewModel? = null
|
|
||||||
) : LinearLayout(context, attrs, defStyleAttr) {
|
|
||||||
|
|
||||||
private val binding = PlayerEpisodesListBinding.inflate(LayoutInflater.from(context), this, true)
|
|
||||||
private lateinit var adapterRecEpisodes: PlayerEpisodeItemAdapter
|
|
||||||
|
|
||||||
var onViewRemovedAction: (() -> Unit)? = null // TODO find a better solution for this
|
|
||||||
|
|
||||||
init {
|
|
||||||
binding.buttonCloseEpisodesList.setOnClickListener {
|
|
||||||
(this.parent as ViewGroup).removeView(this)
|
|
||||||
onViewRemovedAction?.invoke()
|
|
||||||
}
|
|
||||||
|
|
||||||
model?.let {
|
|
||||||
adapterRecEpisodes = PlayerEpisodeItemAdapter(model.episodes, model.tmdbTVSeason?.episodes)
|
|
||||||
adapterRecEpisodes.onImageClick = {_, episodeId ->
|
|
||||||
(this.parent as ViewGroup).removeView(this)
|
|
||||||
model.setCurrentEpisode(episodeId, startPlayback = true)
|
|
||||||
}
|
|
||||||
// episodeNumber starts at 1, we need the episode index -> - 1
|
|
||||||
adapterRecEpisodes.currentSelected = model.currentEpisode.episodeNumber?.minus(1) ?: 0
|
|
||||||
|
|
||||||
binding.recyclerEpisodesPlayer.adapter = adapterRecEpisodes
|
|
||||||
binding.recyclerEpisodesPlayer.scrollToPosition(adapterRecEpisodes.currentSelected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,94 +0,0 @@
|
|||||||
/**
|
|
||||||
* ProjectLaogai
|
|
||||||
*
|
|
||||||
* Copyright 2019-2020 <seil0@mosad.xyz>
|
|
||||||
*
|
|
||||||
* This program is free software; you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation; either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program; if not, write to the Free Software
|
|
||||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
|
||||||
* MA 02110-1301, USA.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.mosad.teapod.ui.components
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.widget.EditText
|
|
||||||
import com.afollestad.materialdialogs.MaterialDialog
|
|
||||||
import com.afollestad.materialdialogs.bottomsheets.BottomSheet
|
|
||||||
import com.afollestad.materialdialogs.bottomsheets.setPeekHeight
|
|
||||||
import com.afollestad.materialdialogs.customview.customView
|
|
||||||
import com.afollestad.materialdialogs.customview.getCustomView
|
|
||||||
import org.mosad.teapod.R
|
|
||||||
|
|
||||||
// TODO rework and port away from MaterialDialog
|
|
||||||
class LoginDialog(val context: Context, firstTry: Boolean) {
|
|
||||||
|
|
||||||
private val dialog = MaterialDialog(context, BottomSheet())
|
|
||||||
|
|
||||||
private val editTextLogin: EditText
|
|
||||||
private val editTextPassword: EditText
|
|
||||||
|
|
||||||
var login = ""
|
|
||||||
var password = ""
|
|
||||||
|
|
||||||
init {
|
|
||||||
dialog.title(R.string.login)
|
|
||||||
.message(if (firstTry) R.string.login_desc else R.string.login_failed_desc)
|
|
||||||
.customView(R.layout.dialog_login)
|
|
||||||
.positiveButton(R.string.save)
|
|
||||||
.negativeButton(R.string.cancel)
|
|
||||||
.setPeekHeight(900)
|
|
||||||
|
|
||||||
editTextLogin = dialog.getCustomView().findViewById(R.id.edit_text_login)
|
|
||||||
editTextPassword = dialog.getCustomView().findViewById(R.id.edit_text_password)
|
|
||||||
|
|
||||||
// fix not working accent color
|
|
||||||
//dialog.getActionButton(WhichButton.POSITIVE).updateTextColor(Preferences.colorAccent)
|
|
||||||
//dialog.getActionButton(WhichButton.NEGATIVE).updateTextColor(Preferences.colorAccent)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun positiveButton(func: LoginDialog.() -> Unit): LoginDialog = apply {
|
|
||||||
dialog.positiveButton {
|
|
||||||
login = editTextLogin.text.toString()
|
|
||||||
password = editTextPassword.text.toString()
|
|
||||||
|
|
||||||
func()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun negativeButton(func: LoginDialog.() -> Unit): LoginDialog = apply {
|
|
||||||
dialog.negativeButton {
|
|
||||||
func()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun show() {
|
|
||||||
dialog.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun show(func: LoginDialog.() -> Unit): LoginDialog = apply {
|
|
||||||
func()
|
|
||||||
|
|
||||||
editTextLogin.setText(login)
|
|
||||||
editTextPassword.setText(password)
|
|
||||||
|
|
||||||
show()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("unused")
|
|
||||||
fun dismiss() {
|
|
||||||
dialog.dismiss()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,54 @@
|
|||||||
|
package org.mosad.teapod.ui.components
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||||
|
import org.mosad.teapod.databinding.ModalBottomSheetLoginBinding
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A bottom sheet with login credential input fields.
|
||||||
|
*
|
||||||
|
* To initialize login or password values, use apply.
|
||||||
|
*/
|
||||||
|
class LoginModalBottomSheet : BottomSheetDialogFragment() {
|
||||||
|
|
||||||
|
private lateinit var binding: ModalBottomSheetLoginBinding
|
||||||
|
|
||||||
|
var login = ""
|
||||||
|
var password = ""
|
||||||
|
|
||||||
|
lateinit var positiveAction: LoginModalBottomSheet.() -> Unit
|
||||||
|
lateinit var negativeAction: LoginModalBottomSheet.() -> Unit
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TAG = "LoginModalBottomSheet"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
binding = ModalBottomSheetLoginBinding.inflate(inflater, container, false)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
binding.editTextLogin.setText(login)
|
||||||
|
binding.editTextPassword.setText(password)
|
||||||
|
|
||||||
|
binding.positiveButton.setOnClickListener {
|
||||||
|
login = binding.editTextLogin.text.toString()
|
||||||
|
password = binding.editTextPassword.text.toString()
|
||||||
|
|
||||||
|
positiveAction.invoke(this)
|
||||||
|
}
|
||||||
|
binding.negativeButton.setOnClickListener {
|
||||||
|
negativeAction.invoke(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -5,9 +5,6 @@ import android.app.ActivityManager
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.view.View
|
|
||||||
import android.view.WindowInsets
|
|
||||||
import android.view.WindowInsetsController
|
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import androidx.fragment.app.commit
|
import androidx.fragment.app.commit
|
||||||
@ -31,23 +28,7 @@ fun FragmentActivity.showFragment(fragment: Fragment) {
|
|||||||
* hide the status and navigation bar
|
* hide the status and navigation bar
|
||||||
*/
|
*/
|
||||||
fun Activity.hideBars() {
|
fun Activity.hideBars() {
|
||||||
window.apply {
|
hideBars(window, window.decorView.rootView)
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
||||||
setDecorFitsSystemWindows(false)
|
|
||||||
insetsController?.apply {
|
|
||||||
hide(WindowInsets.Type.statusBars() or WindowInsets.Type.navigationBars())
|
|
||||||
systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_BARS_BY_SWIPE
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
@Suppress("deprecation")
|
|
||||||
decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_IMMERSIVE
|
|
||||||
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
|
||||||
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
|
||||||
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
|
||||||
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
|
||||||
or View.SYSTEM_UI_FLAG_FULLSCREEN)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Activity.isInPiPMode(): Boolean {
|
fun Activity.isInPiPMode(): Boolean {
|
||||||
|
@ -1,159 +0,0 @@
|
|||||||
/**
|
|
||||||
* Teapod
|
|
||||||
*
|
|
||||||
* Copyright 2020-2022 <seil0@mosad.xyz>
|
|
||||||
*
|
|
||||||
* This program is free software; you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation; either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program; if not, write to the Free Software
|
|
||||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
|
||||||
* MA 02110-1301, USA.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.mosad.teapod.util
|
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import com.google.gson.Gson
|
|
||||||
import com.google.gson.annotations.SerializedName
|
|
||||||
import kotlinx.coroutines.*
|
|
||||||
import java.io.FileNotFoundException
|
|
||||||
import java.net.URL
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO remove gson usage
|
|
||||||
*/
|
|
||||||
class MetaDBController {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val repoUrl = "https://gitlab.com/Seil0/teapodmetadb/-/raw/main/aod/"
|
|
||||||
|
|
||||||
var mediaList = MediaList(listOf())
|
|
||||||
private var metaCacheList = arrayListOf<Meta>()
|
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
|
||||||
suspend fun list() = withContext(Dispatchers.IO) {
|
|
||||||
val url = URL("$repoUrl/list.json")
|
|
||||||
val json = url.readText()
|
|
||||||
|
|
||||||
mediaList = Gson().fromJson(json, MediaList::class.java)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the meta data for a movie from MetaDB
|
|
||||||
* @param aodId The AoD id of the media
|
|
||||||
* @return A meta movie object, or null if not found
|
|
||||||
*/
|
|
||||||
suspend fun getMovieMetadata(aodId: Int): MovieMeta? {
|
|
||||||
return metaCacheList.firstOrNull {
|
|
||||||
it.aodId == aodId
|
|
||||||
} as MovieMeta? ?: getMovieMetadataFromDB(aodId)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the meta data for a tv show from MetaDB
|
|
||||||
* @param aodId The AoD id of the media
|
|
||||||
* @return A meta tv show object, or null if not found
|
|
||||||
*/
|
|
||||||
suspend fun getTVShowMetadata(aodId: Int): TVShowMeta? {
|
|
||||||
return metaCacheList.firstOrNull {
|
|
||||||
it.aodId == aodId
|
|
||||||
} as TVShowMeta? ?: getTVShowMetadataFromDB(aodId)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
|
||||||
private suspend fun getMovieMetadataFromDB(aodId: Int): MovieMeta? = withContext(Dispatchers.IO) {
|
|
||||||
val url = URL("$repoUrl/movie/$aodId/media.json")
|
|
||||||
return@withContext try {
|
|
||||||
val json = url.readText()
|
|
||||||
val meta = Gson().fromJson(json, MovieMeta::class.java)
|
|
||||||
metaCacheList.add(meta)
|
|
||||||
|
|
||||||
meta
|
|
||||||
} catch (ex: FileNotFoundException) {
|
|
||||||
Log.w(javaClass.name, "Waring: The requested file was not found. Requested ID: $aodId", ex)
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
|
||||||
private suspend fun getTVShowMetadataFromDB(aodId: Int): TVShowMeta? = withContext(Dispatchers.IO) {
|
|
||||||
val url = URL("$repoUrl/tv/$aodId/media.json")
|
|
||||||
return@withContext try {
|
|
||||||
val json = url.readText()
|
|
||||||
val meta = Gson().fromJson(json, TVShowMeta::class.java)
|
|
||||||
metaCacheList.add(meta)
|
|
||||||
|
|
||||||
meta
|
|
||||||
} catch (ex: FileNotFoundException) {
|
|
||||||
Log.w(javaClass.name, "Waring: The requested file was not found. Requested ID: $aodId", ex)
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// class representing the media list json object
|
|
||||||
data class MediaList(
|
|
||||||
val media: List<Int>
|
|
||||||
)
|
|
||||||
|
|
||||||
// abstract class used for meta data objects (tv, movie)
|
|
||||||
abstract class Meta {
|
|
||||||
abstract val id: Int
|
|
||||||
abstract val aodId: Int
|
|
||||||
abstract val tmdbId: Int
|
|
||||||
}
|
|
||||||
|
|
||||||
// class representing the movie json object
|
|
||||||
data class MovieMeta(
|
|
||||||
override val id: Int,
|
|
||||||
@SerializedName("aod_id")
|
|
||||||
override val aodId: Int,
|
|
||||||
@SerializedName("tmdb_id")
|
|
||||||
override val tmdbId: Int
|
|
||||||
): Meta()
|
|
||||||
|
|
||||||
// class representing the tv show json object
|
|
||||||
data class TVShowMeta(
|
|
||||||
override val id: Int,
|
|
||||||
@SerializedName("aod_id")
|
|
||||||
override val aodId: Int,
|
|
||||||
@SerializedName("tmdb_id")
|
|
||||||
override val tmdbId: Int,
|
|
||||||
@SerializedName("tmdb_season_id")
|
|
||||||
val tmdbSeasonId: Int,
|
|
||||||
@SerializedName("tmdb_season_number")
|
|
||||||
val tmdbSeasonNumber: Int,
|
|
||||||
@SerializedName("episodes")
|
|
||||||
val episodes: List<EpisodeMeta>
|
|
||||||
): Meta()
|
|
||||||
|
|
||||||
// class used in TVShowMeta, part of the tv show json object
|
|
||||||
data class EpisodeMeta(
|
|
||||||
val id: Int,
|
|
||||||
@SerializedName("aod_media_id")
|
|
||||||
val aodMediaId: Int,
|
|
||||||
@SerializedName("tmdb_id")
|
|
||||||
val tmdbId: Int,
|
|
||||||
@SerializedName("tmdb_number")
|
|
||||||
val tmdbNumber: Int,
|
|
||||||
@SerializedName("opening_start")
|
|
||||||
val openingStart: Long,
|
|
||||||
@SerializedName("opening_duration")
|
|
||||||
val openingDuration: Long,
|
|
||||||
@SerializedName("ending_start")
|
|
||||||
val endingStart: Long,
|
|
||||||
@SerializedName("ending_duration")
|
|
||||||
val endingDuration: Long
|
|
||||||
)
|
|
@ -1,6 +1,11 @@
|
|||||||
package org.mosad.teapod.util
|
package org.mosad.teapod.util
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import android.view.Window
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.core.view.WindowCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import androidx.core.view.WindowInsetsControllerCompat
|
||||||
import org.mosad.teapod.parser.crunchyroll.Collection
|
import org.mosad.teapod.parser.crunchyroll.Collection
|
||||||
import org.mosad.teapod.parser.crunchyroll.ContinueWatchingItem
|
import org.mosad.teapod.parser.crunchyroll.ContinueWatchingItem
|
||||||
import org.mosad.teapod.parser.crunchyroll.Item
|
import org.mosad.teapod.parser.crunchyroll.Item
|
||||||
@ -21,6 +26,13 @@ fun Collection<Item>.toItemMediaList(): List<ItemMedia> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmName("toItemMediaListItem")
|
||||||
|
fun List<Item>.toItemMediaList(): List<ItemMedia> {
|
||||||
|
return this.map {
|
||||||
|
ItemMedia(it.id, it.title, it.images.poster_wide[0][0].source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@JvmName("toItemMediaListContinueWatchingItem")
|
@JvmName("toItemMediaListContinueWatchingItem")
|
||||||
fun Collection<ContinueWatchingItem>.toItemMediaList(): List<ItemMedia> {
|
fun Collection<ContinueWatchingItem>.toItemMediaList(): List<ItemMedia> {
|
||||||
return items.map {
|
return items.map {
|
||||||
@ -43,3 +55,13 @@ fun Locale.toDisplayString(fallback: String): String {
|
|||||||
fallback
|
fallback
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun hideBars(window: Window?, root: View) {
|
||||||
|
if (window != null) {
|
||||||
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
|
WindowInsetsControllerCompat(window, root).let { controller ->
|
||||||
|
controller.hide(WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.navigationBars())
|
||||||
|
controller.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -4,6 +4,7 @@ import android.graphics.Color
|
|||||||
import android.graphics.drawable.ColorDrawable
|
import android.graphics.drawable.ColorDrawable
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
@ -12,84 +13,167 @@ import com.bumptech.glide.request.RequestOptions
|
|||||||
import jp.wasabeef.glide.transformations.RoundedCornersTransformation
|
import jp.wasabeef.glide.transformations.RoundedCornersTransformation
|
||||||
import org.mosad.teapod.R
|
import org.mosad.teapod.R
|
||||||
import org.mosad.teapod.databinding.ItemEpisodeBinding
|
import org.mosad.teapod.databinding.ItemEpisodeBinding
|
||||||
|
import org.mosad.teapod.databinding.ItemEpisodePlayerBinding
|
||||||
import org.mosad.teapod.parser.crunchyroll.Episode
|
import org.mosad.teapod.parser.crunchyroll.Episode
|
||||||
|
import org.mosad.teapod.parser.crunchyroll.PlayheadObject
|
||||||
import org.mosad.teapod.parser.crunchyroll.PlayheadsMap
|
import org.mosad.teapod.parser.crunchyroll.PlayheadsMap
|
||||||
import org.mosad.teapod.util.tmdb.TMDBTVEpisode
|
import org.mosad.teapod.util.tmdb.TMDBTVEpisode
|
||||||
|
|
||||||
class EpisodeItemAdapter(
|
class EpisodeItemAdapter(
|
||||||
private val episodes: List<Episode>,
|
private val episodes: List<Episode>,
|
||||||
private val tmdbEpisodes: List<TMDBTVEpisode>?,
|
private val tmdbEpisodes: List<TMDBTVEpisode>?,
|
||||||
private val playheads: PlayheadsMap
|
private val playheads: PlayheadsMap,
|
||||||
) : RecyclerView.Adapter<EpisodeItemAdapter.EpisodeViewHolder>() {
|
private val onClickListener: OnClickListener,
|
||||||
|
private val viewType: ViewType
|
||||||
|
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||||
|
|
||||||
var onImageClick: ((seasonId: String, episodeId: String) -> Unit)? = null
|
var currentSelected: Int = -1 // -1, since position should never be < 0
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EpisodeViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||||
return EpisodeViewHolder(ItemEpisodeBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
return when (viewType) {
|
||||||
|
ViewType.PLAYER.ordinal -> {
|
||||||
|
PlayerEpisodeViewHolder((ItemEpisodePlayerBinding.inflate(LayoutInflater.from(parent.context), parent, false)))
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
// media fragment episode list is default
|
||||||
|
EpisodeViewHolder(ItemEpisodeBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: EpisodeViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||||
val context = holder.binding.root.context
|
val episode = episodes[position]
|
||||||
val ep = episodes[position]
|
val playhead = playheads[episode.id]
|
||||||
|
val tmdbEpisode = tmdbEpisodes?.getOrNull(position)
|
||||||
|
|
||||||
val titleText = if (ep.episodeNumber != null) {
|
when (holder.itemViewType) {
|
||||||
// for tv shows add ep prefix and episode number
|
ViewType.MEDIA_FRAGMENT.ordinal -> {
|
||||||
if (ep.isDubbed) {
|
(holder as EpisodeViewHolder).bind(episode, playhead, tmdbEpisode)
|
||||||
context.getString(R.string.component_episode_title, ep.episode, ep.title)
|
}
|
||||||
} else {
|
ViewType.PLAYER.ordinal -> {
|
||||||
context.getString(R.string.component_episode_title_sub, ep.episode, ep.title)
|
(holder as PlayerEpisodeViewHolder).bind(episode, playhead, currentSelected)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
ep.title
|
|
||||||
}
|
}
|
||||||
|
|
||||||
holder.binding.textEpisodeTitle.text = titleText
|
override fun getItemViewType(position: Int): Int {
|
||||||
holder.binding.textEpisodeDesc.text = if (ep.description.isNotEmpty()) {
|
return when (viewType) {
|
||||||
ep.description
|
ViewType.MEDIA_FRAGMENT -> ViewType.MEDIA_FRAGMENT.ordinal
|
||||||
} else if (tmdbEpisodes != null && position < tmdbEpisodes.size){
|
ViewType.PLAYER -> ViewType.PLAYER.ordinal
|
||||||
tmdbEpisodes[position].overview
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO is isNotEmpty() needed? also in PlayerEpisodeItemAdapter
|
|
||||||
if (ep.images.thumbnail[0][0].source.isNotEmpty()) {
|
|
||||||
Glide.with(context).load(ep.images.thumbnail[0][0].source)
|
|
||||||
.apply(RequestOptions.placeholderOf(ColorDrawable(Color.DKGRAY)))
|
|
||||||
.apply(RequestOptions.bitmapTransform(RoundedCornersTransformation(10, 0)))
|
|
||||||
.into(holder.binding.imageEpisode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// add watched icon to episode, if the episode id is present in playheads and fullyWatched
|
|
||||||
val watchedImage: Drawable? = if (playheads[ep.id]?.fullyWatched == true) {
|
|
||||||
ContextCompat.getDrawable(context, R.drawable.ic_baseline_check_circle_24)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
holder.binding.imageWatched.setImageDrawable(watchedImage)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemCount(): Int {
|
override fun getItemCount(): Int {
|
||||||
return episodes.size
|
return episodes.size
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateWatchedState(watched: Boolean, position: Int) {
|
|
||||||
// use getOrNull as there could be a index out of bound when running this in onResume()
|
|
||||||
|
|
||||||
// TODO
|
|
||||||
//episodes.getOrNull(position)?.watched = watched
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class EpisodeViewHolder(val binding: ItemEpisodeBinding) :
|
inner class EpisodeViewHolder(val binding: ItemEpisodeBinding) :
|
||||||
RecyclerView.ViewHolder(binding.root) {
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
init {
|
|
||||||
// on image click return the episode id and index (within the adapter)
|
fun bind(episode: Episode, playhead: PlayheadObject?, tmdbEpisode: TMDBTVEpisode?) {
|
||||||
|
val context = binding.root.context
|
||||||
|
|
||||||
|
val titleText = if (episode.episodeNumber != null) {
|
||||||
|
// for tv shows add ep prefix and episode number
|
||||||
|
if (episode.isDubbed) {
|
||||||
|
context.getString(R.string.component_episode_title, episode.episode, episode.title)
|
||||||
|
} else {
|
||||||
|
context.getString(R.string.component_episode_title_sub, episode.episode, episode.title)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
episode.title
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.textEpisodeTitle.text = titleText
|
||||||
|
binding.textEpisodeDesc.text = episode.description.ifEmpty {
|
||||||
|
tmdbEpisode?.overview ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if (episode.images.thumbnail[0][0].source.isNotEmpty()) {
|
||||||
|
Glide.with(context).load(episode.images.thumbnail[0][0].source)
|
||||||
|
.apply(RequestOptions.placeholderOf(ColorDrawable(Color.DKGRAY)))
|
||||||
|
.apply(RequestOptions.bitmapTransform(RoundedCornersTransformation(10, 0)))
|
||||||
|
.into(binding.imageEpisode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// add watched progress
|
||||||
|
val playheadProgress = playhead?.playhead?.let {
|
||||||
|
((it.toFloat() / (episode.durationMs / 1000)) * 100).toInt()
|
||||||
|
} ?: 0
|
||||||
|
binding.progressPlayhead.setProgressCompat(playheadProgress, false)
|
||||||
|
binding.progressPlayhead.visibility = if (playheadProgress <= 0)
|
||||||
|
View.GONE else View.VISIBLE
|
||||||
|
|
||||||
|
// add watched icon to episode, if the episode id is present in playheads and fullyWatched
|
||||||
|
val watchedImage: Drawable? = if (playhead?.fullyWatched == true) {
|
||||||
|
ContextCompat.getDrawable(context, R.drawable.ic_baseline_check_circle_24)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
binding.imageWatched.setImageDrawable(watchedImage)
|
||||||
|
|
||||||
binding.imageEpisode.setOnClickListener {
|
binding.imageEpisode.setOnClickListener {
|
||||||
onImageClick?.invoke(
|
onClickListener.onClick(episode)
|
||||||
episodes[bindingAdapterPosition].seasonId,
|
}
|
||||||
episodes[bindingAdapterPosition].id
|
}
|
||||||
)
|
}
|
||||||
|
|
||||||
|
inner class PlayerEpisodeViewHolder(val binding: ItemEpisodePlayerBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
|
|
||||||
|
// -1, since position should never be < 0
|
||||||
|
fun bind(episode: Episode, playhead: PlayheadObject?, currentSelected: Int) {
|
||||||
|
val context = binding.root.context
|
||||||
|
|
||||||
|
val titleText = if (episode.episodeNumber != null) {
|
||||||
|
// for tv shows add ep prefix and episode number
|
||||||
|
if (episode.isDubbed) {
|
||||||
|
context.getString(R.string.component_episode_title, episode.episode, episode.title)
|
||||||
|
} else {
|
||||||
|
context.getString(R.string.component_episode_title_sub, episode.episode, episode.title)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
episode.title
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.textEpisodeTitle2.text = titleText
|
||||||
|
binding.textEpisodeDesc2.text = episode.description.ifEmpty { "" }
|
||||||
|
|
||||||
|
if (episode.images.thumbnail[0][0].source.isNotEmpty()) {
|
||||||
|
Glide.with(context).load(episode.images.thumbnail[0][0].source)
|
||||||
|
.apply(RequestOptions.bitmapTransform(RoundedCornersTransformation(10, 0)))
|
||||||
|
.into(binding.imageEpisode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// add watched progress
|
||||||
|
val playheadProgress = playhead?.playhead?.let {
|
||||||
|
((it.toFloat() / (episode.durationMs / 1000)) * 100).toInt()
|
||||||
|
} ?: 0
|
||||||
|
binding.progressPlayhead.setProgressCompat(playheadProgress, false)
|
||||||
|
binding.progressPlayhead.visibility = if (playheadProgress <= 0)
|
||||||
|
View.GONE else View.VISIBLE
|
||||||
|
|
||||||
|
// hide the play icon, if it's the current episode
|
||||||
|
binding.imageEpisodePlay.visibility = if (currentSelected == bindingAdapterPosition) {
|
||||||
|
View.GONE
|
||||||
|
} else {
|
||||||
|
View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentSelected != bindingAdapterPosition) {
|
||||||
|
binding.imageEpisode.setOnClickListener {
|
||||||
|
onClickListener.onClick(episode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class OnClickListener(val clickListener: (episode: Episode) -> Unit) {
|
||||||
|
fun onClick(episode: Episode) = clickListener(episode)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class ViewType {
|
||||||
|
MEDIA_FRAGMENT,
|
||||||
|
PLAYER
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,70 @@
|
|||||||
|
package org.mosad.teapod.util.adapter
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import org.mosad.teapod.R
|
||||||
|
import org.mosad.teapod.databinding.ItemMediaBinding
|
||||||
|
import org.mosad.teapod.parser.crunchyroll.ContinueWatchingItem
|
||||||
|
|
||||||
|
class MediaEpisodeListAdapter(private val onClickListener: OnClickListener) : ListAdapter<ContinueWatchingItem, MediaEpisodeListAdapter.MediaViewHolder>(DiffCallback) {
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MediaViewHolder {
|
||||||
|
return MediaViewHolder(
|
||||||
|
ItemMediaBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context),
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: MediaViewHolder, position: Int) {
|
||||||
|
val item = getItem(position)
|
||||||
|
holder.binding.root.setOnClickListener {
|
||||||
|
onClickListener.onClick(item)
|
||||||
|
}
|
||||||
|
holder.bind(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class MediaViewHolder(val binding: ItemMediaBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
|
|
||||||
|
fun bind(item: ContinueWatchingItem) {
|
||||||
|
val metadata = item.panel.episodeMetadata
|
||||||
|
|
||||||
|
binding.textTitle.text = binding.root.context.getString(R.string.season_episode_title,
|
||||||
|
metadata.seasonNumber, metadata.episodeNumber, metadata.seriesTitle
|
||||||
|
)
|
||||||
|
|
||||||
|
Glide.with(binding.imagePoster)
|
||||||
|
.load(item.panel.images.thumbnail[0][0].source)
|
||||||
|
.into(binding.imagePoster)
|
||||||
|
|
||||||
|
// add watched progress
|
||||||
|
val playheadProgress = ((item.playhead.toFloat() / (metadata.durationMs / 1000)) * 100)
|
||||||
|
.toInt()
|
||||||
|
binding.progressPlayhead.setProgressCompat(playheadProgress, false)
|
||||||
|
binding.progressPlayhead.visibility = if (playheadProgress <= 0)
|
||||||
|
View.GONE else View.VISIBLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object DiffCallback : DiffUtil.ItemCallback<ContinueWatchingItem>() {
|
||||||
|
override fun areItemsTheSame(oldItem: ContinueWatchingItem, newItem: ContinueWatchingItem): Boolean {
|
||||||
|
return oldItem.panel.id == newItem.panel.id
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItem: ContinueWatchingItem, newItem: ContinueWatchingItem): Boolean {
|
||||||
|
return oldItem == newItem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class OnClickListener(val clickListener: (item: ContinueWatchingItem) -> Unit) {
|
||||||
|
fun onClick(item: ContinueWatchingItem) = clickListener(item)
|
||||||
|
}
|
||||||
|
}
|
@ -2,11 +2,13 @@ package org.mosad.teapod.util.adapter
|
|||||||
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import org.mosad.teapod.databinding.ItemMediaBinding
|
import org.mosad.teapod.databinding.ItemMediaBinding
|
||||||
import org.mosad.teapod.util.ItemMedia
|
import org.mosad.teapod.util.ItemMedia
|
||||||
|
|
||||||
|
@Deprecated("Use MediaItemListAdapter instead")
|
||||||
class MediaItemAdapter(private val items: List<ItemMedia>) : RecyclerView.Adapter<MediaItemAdapter.MediaViewHolder>() {
|
class MediaItemAdapter(private val items: List<ItemMedia>) : RecyclerView.Adapter<MediaItemAdapter.MediaViewHolder>() {
|
||||||
|
|
||||||
var onItemClick: ((id: String, position: Int) -> Unit)? = null
|
var onItemClick: ((id: String, position: Int) -> Unit)? = null
|
||||||
@ -29,6 +31,7 @@ class MediaItemAdapter(private val items: List<ItemMedia>) : RecyclerView.Adapte
|
|||||||
inner class MediaViewHolder(val binding: ItemMediaBinding) :
|
inner class MediaViewHolder(val binding: ItemMediaBinding) :
|
||||||
RecyclerView.ViewHolder(binding.root) {
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
init {
|
init {
|
||||||
|
binding.imageEpisodePlay.isVisible = false // hide the play button for media items
|
||||||
binding.root.setOnClickListener {
|
binding.root.setOnClickListener {
|
||||||
onItemClick?.invoke(
|
onItemClick?.invoke(
|
||||||
items[bindingAdapterPosition].id,
|
items[bindingAdapterPosition].id,
|
||||||
|
@ -0,0 +1,61 @@
|
|||||||
|
package org.mosad.teapod.util.adapter
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import org.mosad.teapod.databinding.ItemMediaBinding
|
||||||
|
import org.mosad.teapod.util.ItemMedia
|
||||||
|
|
||||||
|
class MediaItemListAdapter(private val onClickListener: OnClickListener) : ListAdapter<ItemMedia, MediaItemListAdapter.MediaViewHolder>(DiffCallback) {
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MediaViewHolder {
|
||||||
|
return MediaViewHolder(
|
||||||
|
ItemMediaBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context),
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: MediaViewHolder, position: Int) {
|
||||||
|
val item = getItem(position)
|
||||||
|
holder.binding.root.setOnClickListener {
|
||||||
|
onClickListener.onClick(item)
|
||||||
|
}
|
||||||
|
holder.bind(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class MediaViewHolder(val binding: ItemMediaBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
|
|
||||||
|
fun bind(item: ItemMedia) {
|
||||||
|
binding.textTitle.text = item.title
|
||||||
|
|
||||||
|
Glide.with(binding.imagePoster)
|
||||||
|
.load(item.posterUrl)
|
||||||
|
.into(binding.imagePoster)
|
||||||
|
|
||||||
|
binding.imageEpisodePlay.isVisible = false
|
||||||
|
binding.progressPlayhead.isVisible = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object DiffCallback : DiffUtil.ItemCallback<ItemMedia>() {
|
||||||
|
override fun areItemsTheSame(oldItem: ItemMedia, newItem: ItemMedia): Boolean {
|
||||||
|
return oldItem.id == newItem.id
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItem: ItemMedia, newItem: ItemMedia): Boolean {
|
||||||
|
return oldItem == newItem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class OnClickListener(val clickListener: (item: ItemMedia) -> Unit) {
|
||||||
|
fun onClick(item: ItemMedia) = clickListener(item)
|
||||||
|
}
|
||||||
|
}
|
@ -1,79 +0,0 @@
|
|||||||
package org.mosad.teapod.util.adapter
|
|
||||||
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import com.bumptech.glide.Glide
|
|
||||||
import com.bumptech.glide.request.RequestOptions
|
|
||||||
import jp.wasabeef.glide.transformations.RoundedCornersTransformation
|
|
||||||
import org.mosad.teapod.R
|
|
||||||
import org.mosad.teapod.databinding.ItemEpisodePlayerBinding
|
|
||||||
import org.mosad.teapod.parser.crunchyroll.Episodes
|
|
||||||
import org.mosad.teapod.util.tmdb.TMDBTVEpisode
|
|
||||||
|
|
||||||
class PlayerEpisodeItemAdapter(private val episodes: Episodes, private val tmdbEpisodes: List<TMDBTVEpisode>?) : RecyclerView.Adapter<PlayerEpisodeItemAdapter.EpisodeViewHolder>() {
|
|
||||||
|
|
||||||
var onImageClick: ((seasonId: String, episodeId: String) -> Unit)? = null
|
|
||||||
var currentSelected: Int = -1 // -1, since position should never be < 0
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EpisodeViewHolder {
|
|
||||||
return EpisodeViewHolder(ItemEpisodePlayerBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: EpisodeViewHolder, position: Int) {
|
|
||||||
val context = holder.binding.root.context
|
|
||||||
val ep = episodes.items[position]
|
|
||||||
|
|
||||||
val titleText = if (ep.episodeNumber != null) {
|
|
||||||
// for tv shows add ep prefix and episode number
|
|
||||||
if (ep.isDubbed) {
|
|
||||||
context.getString(R.string.component_episode_title, ep.episode, ep.title)
|
|
||||||
} else {
|
|
||||||
context.getString(R.string.component_episode_title_sub, ep.episode, ep.title)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ep.title
|
|
||||||
}
|
|
||||||
|
|
||||||
holder.binding.textEpisodeTitle2.text = titleText
|
|
||||||
holder.binding.textEpisodeDesc2.text = if (ep.description.isNotEmpty()) {
|
|
||||||
ep.description
|
|
||||||
} else if (tmdbEpisodes != null && position < tmdbEpisodes.size){
|
|
||||||
tmdbEpisodes[position].overview
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ep.images.thumbnail[0][0].source.isNotEmpty()) {
|
|
||||||
Glide.with(context).load(ep.images.thumbnail[0][0].source)
|
|
||||||
.apply(RequestOptions.bitmapTransform(RoundedCornersTransformation(10, 0)))
|
|
||||||
.into(holder.binding.imageEpisode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// hide the play icon, if it's the current episode
|
|
||||||
holder.binding.imageEpisodePlay.visibility = if (currentSelected == position) {
|
|
||||||
View.GONE
|
|
||||||
} else {
|
|
||||||
View.VISIBLE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemCount(): Int {
|
|
||||||
return episodes.items.size
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class EpisodeViewHolder(val binding: ItemEpisodePlayerBinding) : RecyclerView.ViewHolder(binding.root) {
|
|
||||||
init {
|
|
||||||
binding.imageEpisode.setOnClickListener {
|
|
||||||
// don't execute, if it's the current episode
|
|
||||||
if (currentSelected != bindingAdapterPosition) {
|
|
||||||
onImageClick?.invoke(
|
|
||||||
episodes.items[bindingAdapterPosition].seasonId,
|
|
||||||
episodes.items[bindingAdapterPosition].id
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
57
app/src/main/java/org/mosad/teapod/util/metadb/DatTypes.kt
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
package org.mosad.teapod.util.metadb
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
// class representing the media list json object
|
||||||
|
@Serializable
|
||||||
|
data class MediaList(
|
||||||
|
@SerialName("media") val media: List<String>
|
||||||
|
)
|
||||||
|
|
||||||
|
// abstract class used for meta data objects (tv, movie)
|
||||||
|
abstract class Meta {
|
||||||
|
abstract val id: Int
|
||||||
|
abstract val tmdbId: Int
|
||||||
|
abstract val crSeriesId: String
|
||||||
|
}
|
||||||
|
|
||||||
|
// class representing the movie json object
|
||||||
|
@Serializable
|
||||||
|
data class MovieMeta(
|
||||||
|
@SerialName("id") override val id: Int,
|
||||||
|
@SerialName("tmdb_id") override val tmdbId: Int,
|
||||||
|
@SerialName("cr_series_id") override val crSeriesId: String,
|
||||||
|
): Meta()
|
||||||
|
|
||||||
|
// class representing the tv show json object
|
||||||
|
@Serializable
|
||||||
|
data class TVShowMeta(
|
||||||
|
@SerialName("id") override val id: Int,
|
||||||
|
@SerialName("tmdb_id") override val tmdbId: Int,
|
||||||
|
@SerialName("cr_series_id") override val crSeriesId: String,
|
||||||
|
@SerialName("seasons") val seasons: List<SeasonMeta>,
|
||||||
|
): Meta()
|
||||||
|
|
||||||
|
// class used in TVShowMeta, part of the tv show json object
|
||||||
|
@Serializable
|
||||||
|
data class SeasonMeta(
|
||||||
|
@SerialName("id") val id: Int,
|
||||||
|
@SerialName("tmdb_season_id") val tmdbSeasonId: Int,
|
||||||
|
@SerialName("tmdb_season_number") val tmdbSeasonNumber: Int,
|
||||||
|
@SerialName("cr_season_ids") val crSeasonIds: List<String>,
|
||||||
|
@SerialName("episodes") val episodes: List<EpisodeMeta>,
|
||||||
|
)
|
||||||
|
|
||||||
|
// class used in TVShowMeta, part of the tv show json object
|
||||||
|
@Serializable
|
||||||
|
data class EpisodeMeta(
|
||||||
|
@SerialName("id") val id: Int,
|
||||||
|
@SerialName("tmdb_episode_id") val tmdbEpisodeId: Int,
|
||||||
|
@SerialName("tmdb_episode_number") val tmdbEpisodeNumber: Int,
|
||||||
|
@SerialName("cr_episode_ids") val crEpisodeIds: List<String>,
|
||||||
|
@SerialName("opening_start") val openingStart: Long,
|
||||||
|
@SerialName("opening_duration") val openingDuration: Long,
|
||||||
|
@SerialName("ending_start") val endingStart: Long,
|
||||||
|
@SerialName("ending_duration") val endingDuration: Long
|
||||||
|
)
|
@ -0,0 +1,88 @@
|
|||||||
|
/**
|
||||||
|
* Teapod
|
||||||
|
*
|
||||||
|
* Copyright 2020-2022 <seil0@mosad.xyz>
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation; either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||||
|
* MA 02110-1301, USA.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.mosad.teapod.util.metadb
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import io.ktor.client.*
|
||||||
|
import io.ktor.client.features.*
|
||||||
|
import io.ktor.client.features.json.*
|
||||||
|
import io.ktor.client.features.json.serializer.*
|
||||||
|
import io.ktor.client.request.*
|
||||||
|
import io.ktor.http.*
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
|
object MetaDBController {
|
||||||
|
private val TAG = javaClass.name
|
||||||
|
|
||||||
|
private const val repoUrl = "https://gitlab.com/Seil0/teapodmetadb/-/raw/main/crunchy/"
|
||||||
|
|
||||||
|
private val client = HttpClient {
|
||||||
|
install(JsonFeature) {
|
||||||
|
serializer = KotlinxSerializer(Json)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var mediaList = MediaList(listOf())
|
||||||
|
private var metaCacheList = arrayListOf<Meta>()
|
||||||
|
|
||||||
|
suspend fun list() = withContext(Dispatchers.IO) {
|
||||||
|
val raw: String = client.get("$repoUrl/list.json")
|
||||||
|
mediaList = Json.decodeFromString(raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the meta data for a movie from MetaDB
|
||||||
|
* @param crSeriesId The crunchyroll media id
|
||||||
|
* @return A meta object, or null if not found
|
||||||
|
*/
|
||||||
|
suspend fun getTVShowMetadata(crSeriesId: String): TVShowMeta? {
|
||||||
|
return if (mediaList.media.contains(crSeriesId)) {
|
||||||
|
metaCacheList.firstOrNull {
|
||||||
|
it.crSeriesId == crSeriesId
|
||||||
|
} as TVShowMeta? ?: getTVShowMetadataFromDB(crSeriesId)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getTVShowMetadataFromDB(crSeriesId: String): TVShowMeta? = withContext(Dispatchers.IO) {
|
||||||
|
return@withContext try {
|
||||||
|
val raw: String = client.get("$repoUrl/tv/$crSeriesId/media.json")
|
||||||
|
val meta: TVShowMeta = Json.decodeFromString(raw)
|
||||||
|
metaCacheList.add(meta)
|
||||||
|
|
||||||
|
meta
|
||||||
|
} catch (ex: ClientRequestException) {
|
||||||
|
when (ex.response.status) {
|
||||||
|
HttpStatusCode.NotFound -> Log.w(TAG, "The requested file was not found. Series ID: $crSeriesId", ex)
|
||||||
|
else -> Log.e(TAG, "Error while requesting meta data. Series ID: $crSeriesId", ex)
|
||||||
|
}
|
||||||
|
|
||||||
|
null // todo return none object
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,12 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
|
|
||||||
<item android:drawable="@android:color/black"/>
|
|
||||||
|
|
||||||
<item android:gravity="center" android:width="144dp" android:height="144dp">
|
|
||||||
<bitmap
|
|
||||||
android:gravity="fill_horizontal|fill_vertical"
|
|
||||||
android:src="@drawable/ic_splash_logo"/>
|
|
||||||
</item>
|
|
||||||
|
|
||||||
</layer-list>
|
|
19
app/src/main/res/drawable/ic_splash_foreground.xml
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="108"
|
||||||
|
android:viewportHeight="108">
|
||||||
|
<group android:scaleX="0.03158203"
|
||||||
|
android:scaleY="0.03158203"
|
||||||
|
android:translateX="37.83"
|
||||||
|
android:translateY="44.778053">
|
||||||
|
<path
|
||||||
|
android:pathData="m850.19,372.71c87.88,-11.01 119.04,-84.97 123.1,-99.87 4.06,-14.89 24.91,-80.57 11.92,-129.36 -12.99,-48.79 -34.36,-72.36 -58.62,-77.25 -24.25,-4.9 -50.59,10.51 -65,32.81 -14.41,22.3 -14.68,45.14 -14.78,55.29 -0.11,10.15 0.76,23.2 -3.37,33.29 -4.13,10.09 3.23,25.71 6.04,35.23 2.81,9.52 9.67,82.62 5.78,115.57 -3.89,32.95 -5.07,34.29 -5.07,34.29zM0.4,23.58C55.81,77.29 56.45,120.86 56.08,132.92c-0.36,12.06 4.77,130.59 11.47,150.76 4.42,13.3 12.11,50.16 41.78,74.48 25.51,20.91 58.65,31.38 58.65,31.38 0,0 36.42,78.46 78.83,108.64 31.56,22.46 39.61,23.74 46.5,35.55 6.18,10.6 93.56,62.62 275.1,47.23 127.29,-10.79 138.56,-44.3 138.56,-44.3 0,0 49.41,-21.9 101.15,-80.43 12.87,-14.56 4.41,-13.21 28.57,-17.79 24.16,-4.58 138.01,-45.58 170.66,-154.36C1039.99,175.32 1017.81,96.01 994.52,69.12 971.23,42.22 931.6,24.18 912.25,24.93c-18.47,0.71 -44.78,4.24 -80.21,46.87 -35.43,42.62 -28.94,37.4 -39.36,41.73 -6.82,2.83 -5.68,3.91 -26.75,-11.65 -20.23,-14.93 -28.9,-21.24 -43.38,-27.24 -7.96,-3.3 2.05,-5.55 2.59,-19.48 0.54,-13.93 2.4,-23.51 -17.32,-23.77 -19.72,-0.26 -408.02,0.21 -408.02,0.21 0,0 -18.8,-1.29 -7.79,24.82 4.2,9.94 -1.45,6.43 -33.27,25.85 -31.82,19.42 -55.58,34.4 -72.28,66.09 -8.43,16 -22.91,23.02 -27.97,8.05C153.44,141.43 125.2,48.96 105.17,23.22 85.56,-1.97 77.8,0.26 77.8,0.26Z"
|
||||||
|
android:strokeLineJoin="miter"
|
||||||
|
android:strokeWidth="0.41878"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:strokeColor="#000000"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:strokeLineCap="butt"/>
|
||||||
|
</group>
|
||||||
|
</vector>
|
Before Width: | Height: | Size: 10 KiB |
@ -2,7 +2,7 @@
|
|||||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:id="@+id/player_layout"
|
android:id="@+id/player_root"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:background="#000000"
|
android:background="#000000"
|
||||||
@ -24,7 +24,7 @@
|
|||||||
android:layout_height="70dp"
|
android:layout_height="70dp"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
android:indeterminate="true"
|
android:indeterminate="true"
|
||||||
app:indicatorColor="@color/exo_white"
|
app:indicatorColor="@color/player_white"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
@ -77,14 +77,14 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="bottom|end"
|
android:layout_gravity="bottom|end"
|
||||||
android:layout_marginEnd="12dp"
|
android:layout_marginEnd="12dp"
|
||||||
android:layout_marginBottom="70dp"
|
android:layout_marginBottom="72dp"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:text="@string/next_episode"
|
android:text="@string/next_episode"
|
||||||
android:textAllCaps="false"
|
android:textAllCaps="false"
|
||||||
android:textColor="@android:color/primary_text_light"
|
android:textColor="@android:color/primary_text_light"
|
||||||
android:textSize="16sp"
|
android:textSize="16sp"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:backgroundTint="@color/exo_white"
|
app:backgroundTint="@color/player_white"
|
||||||
app:iconGravity="textStart" />
|
app:iconGravity="textStart" />
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
@ -93,14 +93,14 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="bottom|end"
|
android:layout_gravity="bottom|end"
|
||||||
android:layout_marginEnd="12dp"
|
android:layout_marginEnd="12dp"
|
||||||
android:layout_marginBottom="70dp"
|
android:layout_marginBottom="72dp"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:text="@string/skip_opening"
|
android:text="@string/skip_opening"
|
||||||
android:textAllCaps="false"
|
android:textAllCaps="false"
|
||||||
android:textColor="@android:color/primary_text_light"
|
android:textColor="@android:color/primary_text_light"
|
||||||
android:textSize="16sp"
|
android:textSize="16sp"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:backgroundTint="@color/exo_white"
|
app:backgroundTint="@color/player_white"
|
||||||
app:iconGravity="textStart" />
|
app:iconGravity="textStart" />
|
||||||
|
|
||||||
</FrameLayout>
|
</FrameLayout>
|
@ -1,30 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:id="@+id/linLayout_login"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:paddingStart="24dp"
|
|
||||||
android:paddingEnd="24dp">
|
|
||||||
|
|
||||||
<EditText
|
|
||||||
android:id="@+id/edit_text_login"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_margin="7dp"
|
|
||||||
android:ems="10"
|
|
||||||
android:hint="@string/login"
|
|
||||||
android:importantForAutofill="no"
|
|
||||||
android:inputType="textEmailAddress" />
|
|
||||||
|
|
||||||
<EditText
|
|
||||||
android:id="@+id/edit_text_password"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_margin="7dp"
|
|
||||||
android:ems="10"
|
|
||||||
android:hint="@string/password"
|
|
||||||
android:importantForAutofill="no"
|
|
||||||
android:inputType="textPassword" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
@ -243,6 +243,7 @@
|
|||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:checked="true"
|
android:checked="true"
|
||||||
|
android:contentDescription="@string/settings_prefer_subbed"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
@ -304,6 +305,7 @@
|
|||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:checked="true"
|
android:checked="true"
|
||||||
|
android:contentDescription="@string/settings_autoplay"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
@ -378,6 +380,69 @@
|
|||||||
android:textSize="16sp"
|
android:textSize="16sp"
|
||||||
android:textStyle="bold" />
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/linear_update_playhead"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:padding="7dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/imageView5"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:contentDescription="@string/update_playhead"
|
||||||
|
android:minWidth="48dp"
|
||||||
|
android:minHeight="48dp"
|
||||||
|
android:padding="9dp"
|
||||||
|
android:scaleType="fitXY"
|
||||||
|
android:src="@drawable/ic_baseline_access_time_24"
|
||||||
|
app:tint="?iconColor" />
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/linearLayout4"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/switch_update_playhead"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_update_playhead"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/update_playhead"
|
||||||
|
android:textSize="16sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_update_playhead_desc"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/update_playhead_desc"
|
||||||
|
android:textColor="?textSecondary" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||||
|
android:id="@+id/switch_update_playhead"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:checked="true"
|
||||||
|
android:contentDescription="@string/update_playhead"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/linear_export_data"
|
android:id="@+id/linear_export_data"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@ -385,7 +450,8 @@
|
|||||||
android:foreground="?android:selectableItemBackground"
|
android:foreground="?android:selectableItemBackground"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:padding="7dp">
|
android:padding="7dp"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/image_export_data"
|
android:id="@+id/image_export_data"
|
||||||
@ -430,7 +496,8 @@
|
|||||||
android:foreground="?android:selectableItemBackground"
|
android:foreground="?android:selectableItemBackground"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:padding="7dp">
|
android:padding="7dp"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/image_import_data"
|
android:id="@+id/image_import_data"
|
||||||
|
@ -115,7 +115,7 @@
|
|||||||
android:paddingBottom="7dp">
|
android:paddingBottom="7dp">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/text_new_episodes"
|
android:id="@+id/text_up_next"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:paddingStart="10dp"
|
android:paddingStart="10dp"
|
||||||
@ -127,7 +127,7 @@
|
|||||||
android:textStyle="bold" />
|
android:textStyle="bold" />
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/recycler_new_episodes"
|
android:id="@+id/recycler_up_next"
|
||||||
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"
|
||||||
@ -163,6 +163,34 @@
|
|||||||
tools:listitem="@layout/item_media" />
|
tools:listitem="@layout/item_media" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/linear_recommendations"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingBottom="7dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_recommendations"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingStart="10dp"
|
||||||
|
android:paddingTop="15dp"
|
||||||
|
android:paddingEnd="5dp"
|
||||||
|
android:paddingBottom="5dp"
|
||||||
|
android:text="@string/recommendations"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/recycler_recommendations"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||||
|
tools:listitem="@layout/item_media" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/linear_new_titles"
|
android:id="@+id/linear_new_titles"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<LinearLayout
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/linear_episodes"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
@ -10,21 +10,22 @@
|
|||||||
android:paddingBottom="7dp">
|
android:paddingBottom="7dp">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
android:id="@+id/linear_episode"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="horizontal">
|
android:orientation="horizontal">
|
||||||
|
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="128dp"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="72dp">
|
||||||
|
|
||||||
<com.google.android.material.imageview.ShapeableImageView
|
<com.google.android.material.imageview.ShapeableImageView
|
||||||
android:id="@+id/image_episode"
|
android:id="@+id/image_episode"
|
||||||
android:layout_width="128dp"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="72dp"
|
android:layout_height="match_parent"
|
||||||
android:contentDescription="@string/component_poster_desc"
|
android:contentDescription="@string/component_poster_desc"
|
||||||
app:shapeAppearance="@style/ShapeAppearance.Teapod.RoundedPoster"
|
app:shapeAppearance="@style/ShapeAppearance.Teapod.RoundedPoster"
|
||||||
app:srcCompat="@color/md_disabled_text_dark_theme" />
|
app:srcCompat="@color/imagePlaceholder" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/image_episode_play"
|
android:id="@+id/image_episode_play"
|
||||||
@ -35,6 +36,15 @@
|
|||||||
android:contentDescription="@string/button_play"
|
android:contentDescription="@string/button_play"
|
||||||
app:srcCompat="@drawable/ic_baseline_play_arrow_24"
|
app:srcCompat="@drawable/ic_baseline_play_arrow_24"
|
||||||
app:tint="#FFFFFF" />
|
app:tint="#FFFFFF" />
|
||||||
|
|
||||||
|
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||||
|
android:id="@+id/progress_playhead"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="bottom"
|
||||||
|
android:max="100"
|
||||||
|
app:trackColor="#00FFFFFF"
|
||||||
|
app:trackThickness="2dp" />
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
@ -43,6 +53,8 @@
|
|||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_marginStart="7dp"
|
android:layout_marginStart="7dp"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="3"
|
||||||
android:text="@string/component_episode_title"
|
android:text="@string/component_episode_title"
|
||||||
android:textColor="?textPrimary"
|
android:textColor="?textPrimary"
|
||||||
android:textSize="16sp" />
|
android:textSize="16sp" />
|
||||||
|
@ -7,16 +7,16 @@
|
|||||||
android:padding="7dp">
|
android:padding="7dp">
|
||||||
|
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="192dp"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="108dp">
|
||||||
|
|
||||||
<com.google.android.material.imageview.ShapeableImageView
|
<com.google.android.material.imageview.ShapeableImageView
|
||||||
android:id="@+id/image_episode"
|
android:id="@+id/image_episode"
|
||||||
android:layout_width="192dp"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="108dp"
|
android:layout_height="match_parent"
|
||||||
android:contentDescription="@string/component_poster_desc"
|
android:contentDescription="@string/component_poster_desc"
|
||||||
app:shapeAppearance="@style/ShapeAppearance.Teapod.RoundedPoster"
|
app:shapeAppearance="@style/ShapeAppearance.Teapod.RoundedPoster"
|
||||||
app:srcCompat="@color/md_disabled_text_dark_theme" />
|
app:srcCompat="@color/imagePlaceholder" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/image_episode_play"
|
android:id="@+id/image_episode_play"
|
||||||
@ -26,7 +26,16 @@
|
|||||||
android:background="@drawable/bg_circle__black_transparent_24dp"
|
android:background="@drawable/bg_circle__black_transparent_24dp"
|
||||||
android:contentDescription="@string/button_play"
|
android:contentDescription="@string/button_play"
|
||||||
app:srcCompat="@drawable/ic_baseline_play_arrow_24"
|
app:srcCompat="@drawable/ic_baseline_play_arrow_24"
|
||||||
app:tint="#FFFFFF" />
|
app:tint="@color/player_white" />
|
||||||
|
|
||||||
|
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||||
|
android:id="@+id/progress_playhead"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="bottom"
|
||||||
|
android:max="100"
|
||||||
|
app:trackColor="#00FFFFFF"
|
||||||
|
app:trackThickness="2dp" />
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
@ -13,18 +13,43 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
<ImageView
|
<FrameLayout
|
||||||
android:id="@+id/image_poster"
|
android:id="@+id/frame_image_progress"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:contentDescription="@string/media_poster_desc"
|
|
||||||
android:scaleType="centerCrop"
|
|
||||||
app:layout_constraintBottom_toTopOf="@+id/text_title"
|
app:layout_constraintBottom_toTopOf="@+id/text_title"
|
||||||
app:layout_constraintDimensionRatio="H,16:9"
|
app:layout_constraintDimensionRatio="H,16:9"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
tools:srcCompat="@color/md_disabled_text_dark_theme" />
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/image_poster"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:contentDescription="@string/media_poster_desc"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
tools:srcCompat="@color/imagePlaceholder" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/image_episode_play"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:background="@drawable/bg_circle__black_transparent_24dp"
|
||||||
|
android:contentDescription="@string/button_play"
|
||||||
|
app:srcCompat="@drawable/ic_baseline_play_arrow_24"
|
||||||
|
app:tint="#FFFFFF" />
|
||||||
|
|
||||||
|
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||||
|
android:id="@+id/progress_playhead"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="bottom"
|
||||||
|
android:max="100"
|
||||||
|
app:trackColor="#00FFFFFF"
|
||||||
|
app:trackThickness="2dp" />
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/text_title"
|
android:id="@+id/text_title"
|
||||||
@ -37,7 +62,7 @@
|
|||||||
android:text="@string/text_title_ex"
|
android:text="@string/text_title_ex"
|
||||||
android:textAlignment="center"
|
android:textAlignment="center"
|
||||||
android:textSize="15sp"
|
android:textSize="15sp"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/image_poster" />
|
app:layout_constraintTop_toBottomOf="@+id/frame_image_progress" />
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
</com.google.android.material.card.MaterialCardView>
|
</com.google.android.material.card.MaterialCardView>
|
77
app/src/main/res/layout/modal_bottom_sheet_login.xml
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:id="@+id/standard_bottom_sheet"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="?themeSecondary"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingTop="24dp"
|
||||||
|
android:paddingStart="24dp"
|
||||||
|
android:paddingEnd="24dp"
|
||||||
|
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_title"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingBottom="7dp"
|
||||||
|
android:text="@string/edit_login_credentials"
|
||||||
|
android:textSize="20sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_supporting_desc"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingBottom="5dp"
|
||||||
|
android:text="@string/edit_login_credentials_desc" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/edit_text_login"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="7dp"
|
||||||
|
android:ems="10"
|
||||||
|
android:hint="@string/login"
|
||||||
|
android:importantForAutofill="no"
|
||||||
|
android:inputType="textEmailAddress"
|
||||||
|
android:minHeight="48dp" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/edit_text_password"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="7dp"
|
||||||
|
android:ems="10"
|
||||||
|
android:hint="@string/password"
|
||||||
|
android:importantForAutofill="no"
|
||||||
|
android:inputType="textPassword"
|
||||||
|
android:minHeight="48dp" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="end"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/negative_button"
|
||||||
|
style="@android:style/Widget.Material.Button.Borderless.Small"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="24dp"
|
||||||
|
android:text="@string/cancel"
|
||||||
|
android:textColor="?colorPrimary" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/positive_button"
|
||||||
|
style="@android:style/Widget.Material.Button.Borderless.Small"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="24dp"
|
||||||
|
android:text="@string/save"
|
||||||
|
android:textColor="?colorPrimary" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
@ -1,6 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/player_controls_root"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:background="#73000000">
|
android:background="#73000000">
|
||||||
@ -17,12 +19,12 @@
|
|||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/exo_close_player"
|
android:id="@+id/exo_close_player"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
android:background="@android:color/transparent"
|
android:background="@android:color/transparent"
|
||||||
android:scaleType="fitXY"
|
|
||||||
android:layout_width="44dp"
|
|
||||||
android:layout_height="44dp"
|
|
||||||
android:contentDescription="@string/close_player"
|
android:contentDescription="@string/close_player"
|
||||||
android:padding="10dp"
|
android:padding="10dp"
|
||||||
|
android:scaleType="fitXY"
|
||||||
app:srcCompat="@drawable/ic_baseline_arrow_back_24" />
|
app:srcCompat="@drawable/ic_baseline_arrow_back_24" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
@ -32,8 +34,9 @@
|
|||||||
android:layout_marginEnd="44dp"
|
android:layout_marginEnd="44dp"
|
||||||
android:text="@string/text_title_ex"
|
android:text="@string/text_title_ex"
|
||||||
android:textAlignment="center"
|
android:textAlignment="center"
|
||||||
android:textColor="@color/exo_white"
|
android:textColor="@color/player_white"
|
||||||
android:textSize="16sp" />
|
android:textSize="16sp"
|
||||||
|
tools:ignore="TextContrastCheck" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
@ -90,13 +93,15 @@
|
|||||||
android:layout_gravity="bottom"
|
android:layout_gravity="bottom"
|
||||||
android:layout_marginStart="12dp"
|
android:layout_marginStart="12dp"
|
||||||
android:layout_marginEnd="12dp"
|
android:layout_marginEnd="12dp"
|
||||||
android:layout_marginBottom="@dimen/exo_styled_progress_margin_bottom">
|
android:layout_marginBottom="@dimen/player_styled_progress_margin_bottom">
|
||||||
|
|
||||||
<View
|
<com.google.android.exoplayer2.ui.DefaultTimeBar
|
||||||
android:id="@+id/exo_progress_placeholder"
|
android:id="@id/exo_progress"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="@dimen/exo_styled_progress_layout_height"
|
android:layout_height="@dimen/player_styled_progress_layout_height"
|
||||||
android:layout_marginBottom="2dp"
|
android:contentDescription="@string/desc_time_bar"
|
||||||
|
app:bar_height="3dp"
|
||||||
|
app:touch_target_height="@dimen/player_styled_progress_layout_height"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toStartOf="@+id/exo_remaining"
|
app:layout_constraintEnd_toStartOf="@+id/exo_remaining"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
@ -105,9 +110,10 @@
|
|||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/exo_remaining"
|
android:id="@+id/exo_remaining"
|
||||||
style="@style/ExoStyledControls.TimeText.Position"
|
style="@style/ExoStyledControls.TimeText.Position"
|
||||||
android:layout_height="0dp"
|
android:layout_height="wrap_content"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent" />
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
@ -22,12 +22,12 @@
|
|||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/button_close_episodes_list"
|
android:id="@+id/button_close_episodes_list"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
android:background="@android:color/transparent"
|
android:background="@android:color/transparent"
|
||||||
android:scaleType="fitXY"
|
|
||||||
android:layout_width="44dp"
|
|
||||||
android:layout_height="44dp"
|
|
||||||
android:contentDescription="@string/close_player"
|
android:contentDescription="@string/close_player"
|
||||||
android:padding="10dp"
|
android:padding="10dp"
|
||||||
|
android:scaleType="fitXY"
|
||||||
app:srcCompat="@drawable/ic_baseline_arrow_back_24" />
|
app:srcCompat="@drawable/ic_baseline_arrow_back_24" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:background="#73000000"
|
android:background="#73000000"
|
||||||
@ -22,12 +23,12 @@
|
|||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/button_close_language_settings"
|
android:id="@+id/button_close_language_settings"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
android:background="@android:color/transparent"
|
android:background="@android:color/transparent"
|
||||||
android:scaleType="fitXY"
|
|
||||||
android:layout_width="44dp"
|
|
||||||
android:layout_height="44dp"
|
|
||||||
android:contentDescription="@string/close_player"
|
android:contentDescription="@string/close_player"
|
||||||
android:padding="10dp"
|
android:padding="10dp"
|
||||||
|
android:scaleType="fitXY"
|
||||||
app:srcCompat="@drawable/ic_baseline_arrow_back_24" />
|
app:srcCompat="@drawable/ic_baseline_arrow_back_24" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
@ -37,8 +38,8 @@
|
|||||||
android:layout_marginEnd="44dp"
|
android:layout_marginEnd="44dp"
|
||||||
android:text="@string/subtitles"
|
android:text="@string/subtitles"
|
||||||
android:textAlignment="center"
|
android:textAlignment="center"
|
||||||
android:textColor="@color/exo_white"
|
android:textColor="@color/player_white"
|
||||||
android:textSize="16sp"
|
android:textSize="18sp"
|
||||||
android:textStyle="bold" />
|
android:textStyle="bold" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
@ -75,7 +76,7 @@
|
|||||||
android:layout_marginEnd="7dp"
|
android:layout_marginEnd="7dp"
|
||||||
android:text="@string/cancel"
|
android:text="@string/cancel"
|
||||||
android:textAllCaps="false"
|
android:textAllCaps="false"
|
||||||
android:textColor="@color/exo_white"
|
android:textColor="@color/player_white"
|
||||||
android:textSize="16sp"
|
android:textSize="16sp"
|
||||||
app:backgroundTint="@color/buttonBackgroundLight"
|
app:backgroundTint="@color/buttonBackgroundLight"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
@ -93,7 +94,8 @@
|
|||||||
app:backgroundTint="@color/buttonBackgroundDark"
|
app:backgroundTint="@color/buttonBackgroundDark"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:ignore="TextContrastCheck" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
5
app/src/main/res/mipmap-anydpi-v26/ic_splash_round.xml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/ic_splash_background"/>
|
||||||
|
<foreground android:drawable="@drawable/ic_splash_foreground"/>
|
||||||
|
</adaptive-icon>
|
BIN
app/src/main/res/mipmap-hdpi/ic_splash_round.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_splash_round.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_splash_round.png
Normal file
After Width: | Height: | Size: 4.4 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_splash_round.png
Normal file
After Width: | Height: | Size: 7.1 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_splash_round.png
Normal file
After Width: | Height: | Size: 10 KiB |
@ -9,6 +9,7 @@
|
|||||||
<string name="highlight_media">Highlight</string>
|
<string name="highlight_media">Highlight</string>
|
||||||
<string name="up_next">Weiterschauen</string>
|
<string name="up_next">Weiterschauen</string>
|
||||||
<string name="my_list">Meine Liste</string>
|
<string name="my_list">Meine Liste</string>
|
||||||
|
<string name="recommendations">Empfehlungen</string>
|
||||||
<string name="new_episodes">Neue Episoden</string>
|
<string name="new_episodes">Neue Episoden</string>
|
||||||
<string name="new_simulcasts">Neue Simulcasts</string>
|
<string name="new_simulcasts">Neue Simulcasts</string>
|
||||||
<string name="new_titles">Neue Titel</string>
|
<string name="new_titles">Neue Titel</string>
|
||||||
@ -50,11 +51,16 @@
|
|||||||
<string name="theme_light">Hell</string>
|
<string name="theme_light">Hell</string>
|
||||||
<string name="theme_dark">Dunkel</string>
|
<string name="theme_dark">Dunkel</string>
|
||||||
<string name="dev_settings">Entwickler Einstellungen</string>
|
<string name="dev_settings">Entwickler Einstellungen</string>
|
||||||
|
<string name="update_playhead">Playhead Updates</string>
|
||||||
|
<string name="update_playhead_desc">Fortschritt bei Episoden auf cr updaten</string>
|
||||||
<string name="export_data">Daten exportieren</string>
|
<string name="export_data">Daten exportieren</string>
|
||||||
<string name="export_data_desc">Speichere "Meine Liste" in eine Datei</string>
|
<string name="export_data_desc">Speichere "Meine Liste" in eine Datei</string>
|
||||||
<string name="import_data">Daten importieren</string>
|
<string name="import_data">Daten importieren</string>
|
||||||
<string name="import_data_desc">Lade "Meine Liste" aus einer Datei</string>
|
<string name="import_data_desc">Lade "Meine Liste" aus einer Datei</string>
|
||||||
<string name="import_data_success">"Meine Liste" erfolgreich importiert</string>
|
<string name="import_data_success">"Meine Liste" erfolgreich importiert</string>
|
||||||
|
<string name="edit_login_credentials">Anmeldedaten bearbeiten</string>
|
||||||
|
<string name="edit_login_credentials_desc">Bearbeite deine Crunchyroll Anmeldedaten. Die Anmeldedaten weden verschüsselt aud deinem Gerät gespeichert.</string>
|
||||||
|
<string name="edit_login_credentials_fail">Benutzername oder Passwort ungültig. Bitte versuche es erneut.</string>
|
||||||
|
|
||||||
<!-- about fragment -->
|
<!-- about fragment -->
|
||||||
<string name="version">Version</string>
|
<string name="version">Version</string>
|
||||||
@ -79,6 +85,7 @@
|
|||||||
<string name="episodes">Folgen</string>
|
<string name="episodes">Folgen</string>
|
||||||
<string name="episode">Folge</string>
|
<string name="episode">Folge</string>
|
||||||
<string name="no_subtitles">Aus</string>
|
<string name="no_subtitles">Aus</string>
|
||||||
|
<string name="desc_time_bar">Zeitleiste</string>
|
||||||
|
|
||||||
<!-- Onboarding -->
|
<!-- Onboarding -->
|
||||||
<string name="skip">Überspringen</string>
|
<string name="skip">Überspringen</string>
|
||||||
@ -101,7 +108,7 @@
|
|||||||
|
|
||||||
<!-- etc -->
|
<!-- etc -->
|
||||||
<string name="login">Login</string>
|
<string name="login">Login</string>
|
||||||
<string name="login_desc">Um Teapod zu benutzen musst du eingeloggt sein. Die Login-Daten weden verschüsselt aud deinem Gerät gespeichert.</string>
|
<string name="login_desc">Um Teapod zu benutzen musst du eingeloggt sein. Die Anmeldedaten weden verschüsselt aud deinem Gerät gespeichert.</string>
|
||||||
<string name="login_failed_desc">Login nicht erfolgreich. Bitte versuche es erneut.</string>
|
<string name="login_failed_desc">Login nicht erfolgreich. Bitte versuche es erneut.</string>
|
||||||
<string name="password">Passwort</string>
|
<string name="password">Passwort</string>
|
||||||
</resources>
|
</resources>
|
@ -5,6 +5,7 @@
|
|||||||
<color name="colorPrimaryLight">#99dc45</color>
|
<color name="colorPrimaryLight">#99dc45</color>
|
||||||
<color name="colorPrimaryDark">#317a00</color>
|
<color name="colorPrimaryDark">#317a00</color>
|
||||||
<color name="colorAccent">#607d8b</color>
|
<color name="colorAccent">#607d8b</color>
|
||||||
|
<color name="imagePlaceholder">#c2c2c2</color>
|
||||||
|
|
||||||
<!-- light theme colors -->
|
<!-- light theme colors -->
|
||||||
<color name="themePrimaryLight">#ffffff</color>
|
<color name="themePrimaryLight">#ffffff</color>
|
||||||
@ -25,5 +26,9 @@
|
|||||||
<color name="buttonBackgroundDark">#ffffff</color>
|
<color name="buttonBackgroundDark">#ffffff</color>
|
||||||
<color name="controlHighlightDark">#11ffffff</color>
|
<color name="controlHighlightDark">#11ffffff</color>
|
||||||
|
|
||||||
|
<!-- player colors -->
|
||||||
|
<color name="player_white">#ffffff</color>
|
||||||
|
|
||||||
<color name="ic_launcher_background">#ffffff</color>
|
<color name="ic_launcher_background">#ffffff</color>
|
||||||
|
<color name="ic_splash_background">#ffffff</color>
|
||||||
</resources>
|
</resources>
|
5
app/src/main/res/values/dimens.xml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<dimen name="player_styled_progress_layout_height">28dp</dimen>
|
||||||
|
<dimen name="player_styled_progress_margin_bottom">52dp</dimen>
|
||||||
|
</resources>
|
@ -9,10 +9,12 @@
|
|||||||
<string name="highlight_media">Highlight</string>
|
<string name="highlight_media">Highlight</string>
|
||||||
<string name="up_next">Up next</string>
|
<string name="up_next">Up next</string>
|
||||||
<string name="my_list">My list</string>
|
<string name="my_list">My list</string>
|
||||||
|
<string name="recommendations">Recommendations</string>
|
||||||
<string name="new_episodes">New episodes</string>
|
<string name="new_episodes">New episodes</string>
|
||||||
<string name="new_simulcasts">New simulcasts</string>
|
<string name="new_simulcasts">New simulcasts</string>
|
||||||
<string name="new_titles">New titles</string>
|
<string name="new_titles">New titles</string>
|
||||||
<string name="top_ten">Top 10</string>
|
<string name="top_ten">Top 10</string>
|
||||||
|
<string name="season_episode_title" translatable="false">S%1$d E%2$d - %3$s</string>
|
||||||
|
|
||||||
<!-- search fragment -->
|
<!-- search fragment -->
|
||||||
<string name="search_hint">Search for movies and series</string>
|
<string name="search_hint">Search for movies and series</string>
|
||||||
@ -59,6 +61,8 @@
|
|||||||
<string name="theme_light">Light</string>
|
<string name="theme_light">Light</string>
|
||||||
<string name="theme_dark">Dark</string>
|
<string name="theme_dark">Dark</string>
|
||||||
<string name="dev_settings">Developer Settings</string>
|
<string name="dev_settings">Developer Settings</string>
|
||||||
|
<string name="update_playhead">Playhead updates</string>
|
||||||
|
<string name="update_playhead_desc">Update episode playhead on cr</string>
|
||||||
<string name="export_data">export data</string>
|
<string name="export_data">export data</string>
|
||||||
<string name="export_data_desc">export "My list" to a file</string>
|
<string name="export_data_desc">export "My list" to a file</string>
|
||||||
<string name="import_data">import data</string>
|
<string name="import_data">import data</string>
|
||||||
@ -67,6 +71,9 @@
|
|||||||
<string name="info">Info</string>
|
<string name="info">Info</string>
|
||||||
<string name="info_about" translatable="false">Teapod by @Seil0</string>
|
<string name="info_about" translatable="false">Teapod by @Seil0</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="edit_login_credentials">Edit credentials</string>
|
||||||
|
<string name="edit_login_credentials_desc">Edit your crunchyroll login credentials. The credentials will be stored encrypted on your device.</string>
|
||||||
|
<string name="edit_login_credentials_fail">Invalid login or password. Please try again.</string>
|
||||||
|
|
||||||
<!-- about fragment -->
|
<!-- about fragment -->
|
||||||
<string name="version">Version</string>
|
<string name="version">Version</string>
|
||||||
@ -100,6 +107,7 @@
|
|||||||
<string name="episodes">Episodes</string>
|
<string name="episodes">Episodes</string>
|
||||||
<string name="episode">Episode</string>
|
<string name="episode">Episode</string>
|
||||||
<string name="no_subtitles">None</string>
|
<string name="no_subtitles">None</string>
|
||||||
|
<string name="desc_time_bar">time bar</string>
|
||||||
|
|
||||||
<!-- Onboarding -->
|
<!-- Onboarding -->
|
||||||
<string name="skip">Skip</string>
|
<string name="skip">Skip</string>
|
||||||
@ -137,6 +145,8 @@
|
|||||||
<string name="save_key_autoplay" translatable="false">org.mosad.teapod.autoplay</string>
|
<string name="save_key_autoplay" translatable="false">org.mosad.teapod.autoplay</string>
|
||||||
<string name="save_key_dev_settings" translatable="false">org.mosad.teapod.dev.settings</string>
|
<string name="save_key_dev_settings" translatable="false">org.mosad.teapod.dev.settings</string>
|
||||||
<string name="save_key_theme" translatable="false">org.mosad.teapod.theme</string>
|
<string name="save_key_theme" translatable="false">org.mosad.teapod.theme</string>
|
||||||
|
<!-- dev settings -->
|
||||||
|
<string name="save_key_update_playhead" translatable="false">org.mosad.teapod.update_playhead</string>
|
||||||
|
|
||||||
<!-- intents & states -->
|
<!-- intents & states -->
|
||||||
<string name="intent_media_id" translatable="false">intent_media_id</string>
|
<string name="intent_media_id" translatable="false">intent_media_id</string>
|
||||||
|
@ -18,11 +18,6 @@
|
|||||||
<item name="shapeTextBackground">@color/textBackgroundLight</item>
|
<item name="shapeTextBackground">@color/textBackgroundLight</item>
|
||||||
<item name="iconColor">@color/iconColorLight</item>
|
<item name="iconColor">@color/iconColorLight</item>
|
||||||
<item name="buttonBackground">@color/buttonBackgroundLight</item>
|
<item name="buttonBackground">@color/buttonBackgroundLight</item>
|
||||||
<item name="md_background_color">@color/themeSecondaryLight</item>
|
|
||||||
<item name="md_color_content">@color/textSecondaryLight</item>
|
|
||||||
|
|
||||||
<!-- without this, the unchecked single choice buttons while be white -->
|
|
||||||
<item name="md_color_widget_unchecked">@color/textSecondaryLight</item>
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="AppTheme.Dark" parent="AppTheme">
|
<style name="AppTheme.Dark" parent="AppTheme">
|
||||||
@ -37,11 +32,6 @@
|
|||||||
<item name="iconColor">@color/iconColorDark</item>
|
<item name="iconColor">@color/iconColorDark</item>
|
||||||
<item name="buttonBackground">@color/buttonBackgroundDark</item>
|
<item name="buttonBackground">@color/buttonBackgroundDark</item>
|
||||||
|
|
||||||
<item name="md_background_color">@color/themeSecondaryDark</item>
|
|
||||||
<item name="md_color_content">@color/textSecondaryDark</item>
|
|
||||||
<!-- without this, the unchecked single choice buttons while be black -->
|
|
||||||
<item name="md_color_widget_unchecked">@color/textSecondaryDark</item>
|
|
||||||
|
|
||||||
<item name="materialAlertDialogTheme">@style/ThemeOverlay.App.MaterialAlertDialog.Dark</item>
|
<item name="materialAlertDialogTheme">@style/ThemeOverlay.App.MaterialAlertDialog.Dark</item>
|
||||||
<!-- change on click indicator color for manually set components -->
|
<!-- change on click indicator color for manually set components -->
|
||||||
<item name="colorControlHighlight">@color/controlHighlightDark</item>
|
<item name="colorControlHighlight">@color/controlHighlightDark</item>
|
||||||
@ -61,7 +51,7 @@
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<!-- player theme -->
|
<!-- player theme -->
|
||||||
<style name="PlayerTheme" parent="Theme.MaterialComponents.Light.NoActionBar">
|
<style name="PlayerTheme" parent="AppTheme">
|
||||||
<item name="android:windowNoTitle">true</item>
|
<item name="android:windowNoTitle">true</item>
|
||||||
<item name="android:windowActionBar">false</item>
|
<item name="android:windowActionBar">false</item>
|
||||||
<item name="android:windowFullscreen">true</item>
|
<item name="android:windowFullscreen">true</item>
|
||||||
@ -71,10 +61,20 @@
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<!-- splash theme -->
|
<!-- splash theme -->
|
||||||
<style name="SplashTheme" parent="Theme.AppCompat.NoActionBar">
|
<style name="Theme.App.Starting" parent="Theme.SplashScreen">
|
||||||
<item name="android:windowBackground">@drawable/bg_splash</item>
|
<!-- Set the splash screen background, animated icon, and animation duration. -->
|
||||||
|
<item name="windowSplashScreenBackground">@android:color/black</item>
|
||||||
|
|
||||||
|
<!-- Use windowSplashScreenAnimatedIcon to add either a drawable or an -->
|
||||||
|
<!-- animated drawable. One of these is required. -->
|
||||||
|
<item name="windowSplashScreenAnimatedIcon">@mipmap/ic_splash_round</item>
|
||||||
|
<item name="windowSplashScreenAnimationDuration">200</item>
|
||||||
|
|
||||||
|
<!-- Set the theme of the Activity that directly follows your splash screen. -->
|
||||||
|
<item name="postSplashScreenTheme">@style/AppTheme.Dark</item> # Required.
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
<!-- shapes -->
|
<!-- shapes -->
|
||||||
<style name="ShapeAppearance.Teapod.RoundedPoster" parent="ShapeAppearance.MaterialComponents.LargeComponent">
|
<style name="ShapeAppearance.Teapod.RoundedPoster" parent="ShapeAppearance.MaterialComponents.LargeComponent">
|
||||||
<item name="cornerFamily">rounded</item>
|
<item name="cornerFamily">rounded</item>
|
||||||
@ -86,4 +86,14 @@
|
|||||||
<item name="android:popupBackground">?themeSecondary</item>
|
<item name="android:popupBackground">?themeSecondary</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<!-- fullscreen dialog fragments -->
|
||||||
|
<style name="FullScreenDialogStyle" parent="AppTheme">
|
||||||
|
<item name="android:windowFullscreen">true</item>
|
||||||
|
<item name="android:windowIsFloating">false</item>
|
||||||
|
<item name="android:windowBackground">@android:color/transparent</item>
|
||||||
|
<item name="android:windowTranslucentStatus">true</item>
|
||||||
|
<item name="android:windowTranslucentNavigation">true</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
</resources>
|
</resources>
|
@ -1,13 +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.6.10"
|
ext.kotlin_version = "1.6.21"
|
||||||
ext.ktor_version = "1.6.7"
|
ext.ktor_version = "1.6.8"
|
||||||
|
ext.exo_version = "2.17.1"
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:7.1.2'
|
classpath 'com.android.tools.build:gradle:7.2.1'
|
||||||
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
|
||||||
|
10
fastlane/metadata/android/de/changelogs/9010.txt
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
Dies ist der zweite beta Release von Teapod 1.0.0 mit Unterstützung für Cunchyroll.
|
||||||
|
|
||||||
|
* Unterstützung für Crunchyroll hinzugefügt (Ein premium Account wird benötigt)
|
||||||
|
* Crunchyroll metadb Unterstützung hinzugefügt (#54)
|
||||||
|
* Playhead Updates lassen sich nun ausschalten
|
||||||
|
* Ähnliche Titel zum Mediafragment hinzugefügt
|
||||||
|
* Empfehlungen für dich zum Homefragment hinzugefügt
|
||||||
|
* Einen Crash beim login wurde behoben
|
||||||
|
|
||||||
|
Alle Änderungen https://git.mosad.xyz/Seil0/teapod/compare/1.0.0-beta1...1.0.0-beta2
|
@ -3,8 +3,13 @@ Teapod ist eine inoffizielle App für Crunchyroll.
|
|||||||
* Schau dir alle Titel von Crunchyroll auf deinem Android Gerät an
|
* Schau dir alle Titel von Crunchyroll auf deinem Android Gerät an
|
||||||
* Nativer Player auf Basis des ExoPayers
|
* Nativer Player auf Basis des ExoPayers
|
||||||
* Bevorzuge die OmU Version über die App-Einstellungen
|
* Bevorzuge die OmU Version über die App-Einstellungen
|
||||||
|
* Picture in Picture Modus
|
||||||
|
* Überspringe das Intro/Ending dank der TeapodMetaDB Integration
|
||||||
|
|
||||||
Um Teapod zu verwenden musst du dich mit deinem Crunchyroll Account anmelden.
|
Um Teapod zu verwenden musst du dich mit deinem Crunchyroll Account anmelden.
|
||||||
Dieses Projekt ist in keiner Weise mit Crunchyroll verbunden.
|
Dieses Projekt ist in keiner Weise mit Crunchyroll verbunden.
|
||||||
|
|
||||||
|
TeapodMetaDB unterstützt ausschliesslich Serien, für die Metadaten vorliegen.
|
||||||
|
Hilf mit, die Datenbank auszubauen: https://gitlab.com/Seil0/teapodmetadb
|
||||||
|
|
||||||
Bitte melde Fehler und Probleme an support@mosad.xyz
|
Bitte melde Fehler und Probleme an support@mosad.xyz
|
||||||
|
10
fastlane/metadata/android/en-US/changelogs/9010.txt
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
This is the second beta release of Teapod 1.0.0 with support for crunchyroll.
|
||||||
|
|
||||||
|
* Support for crunchyroll (a premium account is needed)
|
||||||
|
* Crunchyroll metadb support (#54)
|
||||||
|
* Added a option to disable playhead updates/reporting
|
||||||
|
* Show similar titles in the media fragment
|
||||||
|
* Added recommendations to the home fragment
|
||||||
|
* Fixed a crash on login, which made the app unusable
|
||||||
|
|
||||||
|
Full changelog https://git.mosad.xyz/Seil0/teapod/compare/1.0.0-beta1...1.0.0-beta2
|
@ -3,8 +3,13 @@ Teapod is a unofficial App for Crunchyroll.
|
|||||||
* Watch all animes from Crunchyroll on your Android device
|
* Watch all animes from Crunchyroll on your Android device
|
||||||
* Native Player based on ExoPayer
|
* Native Player based on ExoPayer
|
||||||
* Prefer the OmU version via the app settings
|
* Prefer the OmU version via the app settings
|
||||||
|
* Picture in Picture Mode
|
||||||
|
* Skip the OP/ED thanks to the TeapodMetaDB integration
|
||||||
|
|
||||||
To use Teapod you have to login with your Crunchyroll account.
|
To use Teapod you have to login with your Crunchyroll account.
|
||||||
This Project is not associated with Crunchyroll in any way.
|
This Project is not associated with Crunchyroll in any way.
|
||||||
|
|
||||||
|
TeapodMetaDB supports only shows where metradata is present.
|
||||||
|
Help us to expand the database: https://gitlab.com/Seil0/teapodmetadb
|
||||||
|
|
||||||
Please report bugs and issues to support@mosad.xyz
|
Please report bugs and issues to support@mosad.xyz
|
||||||
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,5 +1,5 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|