Compare commits

...

26 Commits

Author SHA1 Message Date
Jannik e835715b9c
fix item_media width
don't hardcode layout_width to 195dp, set layout_constraintWidth_max and image_poster layout_constraintWidth this fixes issues if the screen is not wide enough to show multiple item_media elements
2022-09-18 13:53:19 +02:00
Jannik 001141337d
add shimmer for highlight in home screen, update agp to version 7.3.0 2022-09-18 13:33:22 +02:00
Jannik 5cd3d25ebe
fix shimmer for light theme 2022-09-15 18:02:48 +02:00
Jannik 215e01c53a
add changelog for beta3 release 2022-09-14 22:00:00 +02:00
Jannik 1751963574
update gradle wrapper to version 7.5.1 2022-09-14 21:42:23 +02:00
Jannik 9c3548a866
add shimmer effect while loading to the lists in home fragment 2022-09-14 21:31:27 +02:00
Jannik ebd96f9849
compileSdkVersion 33 and library updates
* core-ktx 1.8.0 -> 1.9.0
* appcompat 1.5.0 -> 1.5.1
* navigation-fragment-ktx 2.5.1 -> 2.5.2
* navigation-ui-ktx 2.5.1 -> 2.5.2
2022-09-14 20:33:08 +02:00
Jannik 85b17d7a76
improve buttonNextEp hiding behaviour
* the button will be diabled on PlayerActivity.playNextEpisode()
* the button will only be enabled if PlayerViewModel.playNextEpisode() returns
* remainingTime will be set to 0, if duration < 0, this fixes the button reapring after a few 100 ms when beeing pressed

fixes #53
2022-08-27 13:59:30 +02:00
Jannik f128efea0d
set compileSdkVersion and targetSdkVersion to 32 2022-08-27 13:56:15 +02:00
Jannik da94003368
update agp and libraries
* agp 7.2.1 -> 7.2.2
* kotlinx-coroutines-android 1.6.3 -> 1.6.4
* core-splashscreen 1.0.0-rc01 -> 1.0.0
* appcompat 1.4.2 -> 1.5.0
* navigation-fragment-ktx 2.5.0 -> 2.5.1
* navigation-ui-ktx 2.5.0 -> 2.5.1
* lifecycle-runtime-ktx 2.5.0 -> 2.5.1
* lifecycle-viewmodel-ktx 2.5.0 -> 2.5.1
2022-08-19 22:54:38 +02:00
Jannik 3fdc2aff1b Merge pull request 'update ktor to version 2.x' (#63) from feature/ktor_update into develop
Reviewed-on: #63
2022-08-19 22:40:55 +02:00
Jannik 326da147f1
update ktor to version 2.1.0 2022-08-19 18:18:09 +02:00
Jannik f398c82f62
update ktor to version 2.0.3 2022-08-19 18:15:37 +02:00
Jannik 821f8b5590
add subscription status and tier to the AccountFragment 2022-07-21 22:06:41 +02:00
Jannik 0028cb6dd7
fix EpisodesListDialogFragment current episode selection
fix EpisodesListDialogFragment not selecting the correct episode, if the episode number doens't start at 0, if episodes are count across seasons
2022-07-21 18:49:29 +02:00
Jannik 127bd030b9
add unit test for token type serialization 2022-07-16 15:08:13 +02:00
Jannik 3cadaa5c7a
update playhead every 30 seconds while playback is active 2022-07-16 14:35:22 +02:00
Jannik 97966f5ad3
fix a crash when url or vcodes are missing for a stream
always initialize them, also initialize hardsub_locale since it might be optional too
2022-07-16 14:13:08 +02:00
Jannik 4c55bb771f
partially revert c34b95795f 2022-07-16 13:48:28 +02:00
Jannik 8eb737a831
use a separate scope to update playheads
viewModelScope will be cleard when the activity is stopped, but the playhead update should be done anyway

fixes #62
2022-07-10 13:50:53 +02:00
Jannik 522b893dc8
update kotlin coroutines library
* kotlinx-coroutines-android 1.6.2 -> 1.6.3
2022-07-10 13:26:23 +02:00
Jannik 69e0b6bcca
update kotlin and libraries
* kotlin 1.6.21 -> 1.7.10
* navigation-fragment-ktx 2.4.2 -> 2.5.0
* navigation-ui-ktx 2.4.2 -> 2.5.0
* lifecycle-runtime-ktx 2.4.1 -> 2.5.0
* lifecycle-viewmodel-ktx 2.4.1 -> 2.5.0
2022-07-10 13:19:59 +02:00
Jannik c34b95795f
fix rwd/ffwd button pos when animation is running, clean up rwd/ffwd animation handling 2022-07-10 12:53:03 +02:00
Jannik 9059306e90
add icon to fastlane metadata 2022-06-07 22:04:45 +02:00
Jannik ed0c0a4c61
update libraries
* kotlinx-coroutines 1.6.1 -> 1.6.2
* core-ktx 1.7.0 -> 1.8.0
* appcompat 1.4.1 -> 1.4.2
* constraintlayout 2.1.3 -> 2.1.4
* material 1.5.0 -> 1.6.1
* glide 4.13.1 -> 4.13.2
2022-06-06 13:53:49 +02:00
Jannik 03a79346b7
update version code and name -> beta3
update after tagging of beta2
2022-06-06 13:45:13 +02:00
30 changed files with 537 additions and 149 deletions

View File

@ -5,15 +5,15 @@ plugins {
}
android {
compileSdkVersion 31
compileSdkVersion 33
buildToolsVersion "30.0.3"
defaultConfig {
applicationId "org.mosad.teapod"
minSdkVersion 23
targetSdkVersion 31
versionCode 9010 //00.09.010
versionName "1.0.0-beta2"
targetSdkVersion 32
versionCode 9020 //00.09.020
versionName "1.0.0-beta3"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resValue "string", "build_time", buildTime()
@ -48,33 +48,36 @@ android {
dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.3'
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.core:core-splashscreen:1.0.0-rc01'
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
implementation 'androidx.navigation:navigation-fragment-ktx:2.4.2'
implementation 'androidx.navigation:navigation-ui-ktx:2.4.2'
implementation 'androidx.core:core-ktx:1.9.0'
implementation 'androidx.core:core-splashscreen:1.0.0'
implementation 'androidx.appcompat:appcompat:1.5.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.navigation:navigation-fragment-ktx:2.5.2'
implementation 'androidx.navigation:navigation-ui-ktx:2.5.2'
implementation 'androidx.security:security-crypto:1.1.0-alpha03'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
implementation 'com.google.android.material:material:1.5.0'
implementation 'com.google.android.material:material:1.6.1'
implementation "com.google.android.exoplayer:exoplayer-core:$exo_version"
implementation "com.google.android.exoplayer:exoplayer-hls:$exo_version"
implementation "com.google.android.exoplayer:exoplayer-dash:$exo_version"
implementation "com.google.android.exoplayer:exoplayer-ui:$exo_version"
implementation "com.google.android.exoplayer:extension-mediasession:$exo_version"
implementation 'com.github.bumptech.glide:glide:4.13.1'
implementation 'com.facebook.shimmer:shimmer:0.5.0'
implementation 'com.github.bumptech.glide:glide:4.13.2'
implementation 'jp.wasabeef:glide-transformations:4.3.0'
implementation "io.ktor:ktor-client-core:$ktor_version"
implementation "io.ktor:ktor-client-android:$ktor_version"
implementation "io.ktor:ktor-client-serialization:$ktor_version"
implementation "io.ktor:ktor-client-content-negotiation:$ktor_version"
implementation "io.ktor:ktor-serialization-kotlinx-json:$ktor_version"
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'

View File

@ -25,13 +25,13 @@ package org.mosad.teapod.parser.crunchyroll
import android.util.Log
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.features.*
import io.ktor.client.features.json.*
import io.ktor.client.features.json.serializer.*
import io.ktor.client.plugins.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.client.request.forms.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.coroutines.*
import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.Json
@ -41,14 +41,14 @@ import kotlinx.serialization.json.put
import org.mosad.teapod.preferences.EncryptedPreferences
import org.mosad.teapod.preferences.Preferences
private val json = Json { ignoreUnknownKeys = true }
object Crunchyroll {
private val TAG = javaClass.name
private val client = HttpClient {
install(JsonFeature) {
serializer = KotlinxSerializer(json)
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
})
}
}
private const val baseUrl = "https://beta-api.crunchyroll.com"
@ -61,6 +61,7 @@ object Crunchyroll {
private val tokenRefreshContext = newSingleThreadContext("TokenRefreshContext")
private var accountID = ""
private var externalID = ""
private var policy = ""
private var signature = ""
@ -76,7 +77,7 @@ object Crunchyroll {
*/
fun initBasicApiToken() = runBlocking {
withContext(Dispatchers.IO) {
basicApiToken = (client.get(basicApiTokenUrl) as HttpResponse).readText()
basicApiToken = client.get { url(basicApiTokenUrl) }.bodyAsText()
Log.i(TAG, "basic auth token: $basicApiToken")
}
}
@ -106,7 +107,7 @@ object Crunchyroll {
val response: HttpResponse = client.submitForm("$baseUrl$tokenEndpoint", formParameters = formData) {
header("Authorization", "Basic $basicApiToken")
}
token = response.receive()
token = response.body()
tokenValidUntil = System.currentTimeMillis() + (token.expiresIn * 1000)
response.status
} catch (ex: ClientRequestException) {
@ -154,10 +155,10 @@ object Crunchyroll {
// for json set body and content type
if (bodyObject is JsonObject) {
body = bodyObject
setBody(bodyObject)
contentType(ContentType.Application.Json)
}
}
}.body()
response
}
@ -245,6 +246,7 @@ object Crunchyroll {
}
accountID = account.accountId
externalID = account.externalId
}
/**
@ -331,7 +333,7 @@ object Crunchyroll {
return try {
requestGet(searchEndpoint, parameters)
}catch (ex: SerializationException) {
} catch (ex: SerializationException) {
Log.e(TAG, "SerializationException in search(), with query = \"$query\".", ex)
NoneSearchResult
}
@ -355,7 +357,7 @@ object Crunchyroll {
return try {
requestGet(episodesEndpoint, parameters)
}catch (ex: SerializationException) {
} catch (ex: SerializationException) {
Log.e(TAG, "SerializationException in objects().", ex)
NoneCollection
}
@ -371,7 +373,7 @@ object Crunchyroll {
return try {
requestGet(seasonListEndpoint, parameters)
}catch (ex: SerializationException) {
} catch (ex: SerializationException) {
Log.e(TAG, "SerializationException in seasonList().", ex)
NoneDiscSeasonList
}
@ -395,7 +397,7 @@ object Crunchyroll {
return try {
requestGet(seriesEndpoint, parameters)
}catch (ex: SerializationException) {
} catch (ex: SerializationException) {
Log.e(TAG, "SerializationException in series().", ex)
NoneSeries
}
@ -416,7 +418,7 @@ object Crunchyroll {
return try {
requestGet(upNextSeriesEndpoint, parameters)
}catch (ex: SerializationException) {
} catch (ex: SerializationException) {
Log.e(TAG, "SerializationException in upNextSeries().", ex)
NoneUpNextSeriesItem
}
@ -440,7 +442,7 @@ object Crunchyroll {
return try {
requestGet(seasonsEndpoint, parameters)
}catch (ex: SerializationException) {
} catch (ex: SerializationException) {
Log.e(TAG, "SerializationException in seasons().", ex)
NoneSeasons
}
@ -464,7 +466,7 @@ object Crunchyroll {
return try {
requestGet(episodesEndpoint, parameters)
}catch (ex: SerializationException) {
} catch (ex: SerializationException) {
Log.e(TAG, "SerializationException in episodes().", ex)
NoneEpisodes
}
@ -479,7 +481,7 @@ object Crunchyroll {
suspend fun playback(url: String): Playback {
return try {
requestGet("", url = url)
}catch (ex: SerializationException) {
} catch (ex: SerializationException) {
Log.e(TAG, "SerializationException in playback(), with url = $url.", ex)
NonePlayback
}
@ -502,7 +504,7 @@ object Crunchyroll {
return try {
(requestGet(watchlistSeriesEndpoint, parameters) as JsonObject)
.containsKey(seriesId)
}catch (ex: SerializationException) {
} catch (ex: SerializationException) {
Log.e(TAG, "SerializationException in isWatchlist() with seriesId = $seriesId", ex)
false
}
@ -598,7 +600,7 @@ object Crunchyroll {
return try {
requestGet(similarToEndpoint, parameters)
}catch (ex: SerializationException) {
} catch (ex: SerializationException) {
Log.e(TAG, "SerializationException in similarTo().", ex)
NoneSimilarToResult
}
@ -623,7 +625,7 @@ object Crunchyroll {
val list: ContinueWatchingList = try {
requestGet(watchlistEndpoint, parameters)
}catch (ex: SerializationException) {
} catch (ex: SerializationException) {
Log.e(TAG, "SerializationException in watchlist().", ex)
NoneContinueWatchingList
}
@ -647,7 +649,7 @@ object Crunchyroll {
return try {
requestGet(watchlistEndpoint, parameters)
}catch (ex: SerializationException) {
} catch (ex: SerializationException) {
Log.e(TAG, "SerializationException in upNextAccount().", ex)
NoneContinueWatchingList
}
@ -664,7 +666,7 @@ object Crunchyroll {
return try {
requestGet(recommendationsEndpoint, parameters)
}catch (ex: SerializationException) {
} catch (ex: SerializationException) {
Log.e(TAG, "SerializationException in recommendations().", ex)
NoneRecommendationsList
}
@ -684,7 +686,7 @@ object Crunchyroll {
return try {
requestGet(profileEndpoint)
}catch (ex: SerializationException) {
} catch (ex: SerializationException) {
Log.e(TAG, "SerializationException in profile().", ex)
NoneProfile
}
@ -704,4 +706,20 @@ object Crunchyroll {
requestPatch(profileEndpoint, bodyObject = json)
}
/**
* Get additional profile (benefits) information for the currently logged in account.
*
* * @return A **[Profile]** object
*/
suspend fun benefits(): Benefits {
val profileEndpoint = "/subs/v1/subscriptions/$externalID/benefits"
return try {
requestGet(profileEndpoint)
} catch (ex: SerializationException) {
Log.e(TAG, "SerializationException in benefits().", ex)
NoneBenefits
}
}
}

View File

@ -125,6 +125,7 @@ typealias DiscSeasonList = Collection<SeasonListItem>
typealias Watchlist = Collection<Item>
typealias ContinueWatchingList = Collection<ContinueWatchingItem>
typealias RecommendationsList = Collection<Item>
typealias Benefits = Collection<Benefit>
@Serializable
data class UpNextSeriesItem(
@ -226,6 +227,7 @@ val NoneSimilarToResult = SimilarToResult(0, emptyList())
val NoneDiscSeasonList = DiscSeasonList(0, emptyList())
val NoneContinueWatchingList = ContinueWatchingList(0, emptyList())
val NoneRecommendationsList = RecommendationsList(0, emptyList())
val NoneBenefits = Benefits(0, emptyList())
val NoneUpNextSeriesItem = UpNextSeriesItem(
playhead = 0,
@ -380,9 +382,9 @@ data class Streams(
@Serializable
data class Stream(
@SerialName("hardsub_locale") val hardsubLocale: String,
@SerialName("url") val url: String,
@SerialName("vcodec") val vcodec: String,
@SerialName("hardsub_locale") val hardsubLocale: String = "", // default/nullable value since might be optional
@SerialName("url") val url: String = "", // default/nullable value since optional
@SerialName("vcodec") val vcodec: String = "", // default/nullable value since optional
)
val NonePlayback = Playback(
@ -412,3 +414,16 @@ val NoneProfile = Profile(
preferredContentSubtitleLanguage = "",
username = ""
)
/**
* benefit data class
*/
@Serializable
data class Benefit(
@SerialName("benefit") val benefit: String,
@SerialName("source") val source: String,
)
val NoneBenefit = Benefit(
benefit = "",
source = ""
)

View File

@ -26,6 +26,7 @@ import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.MenuItem
import androidx.activity.addCallback
import androidx.appcompat.app.AppCompatActivity
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.fragment.app.Fragment
@ -78,16 +79,14 @@ class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListen
supportFragmentManager.commit {
replace(R.id.nav_host_fragment, activeBaseFragment, activeBaseFragment.javaClass.simpleName)
}
}
override fun onBackPressed() {
if (supportFragmentManager.backStackEntryCount > 0) {
supportFragmentManager.popBackStack()
} else {
if (activeBaseFragment !is HomeFragment) {
binding.navView.selectedItemId = R.id.navigation_home
onBackPressedDispatcher.addCallback {
if (supportFragmentManager.backStackEntryCount > 0) {
supportFragmentManager.popBackStack()
} else {
super.onBackPressed()
if (activeBaseFragment !is HomeFragment) {
binding.navView.selectedItemId = R.id.navigation_home
}
}
}
}

View File

@ -15,6 +15,7 @@ import kotlinx.coroutines.runBlocking
import org.mosad.teapod.BuildConfig
import org.mosad.teapod.R
import org.mosad.teapod.databinding.FragmentAccountBinding
import org.mosad.teapod.parser.crunchyroll.Benefits
import org.mosad.teapod.parser.crunchyroll.Crunchyroll
import org.mosad.teapod.parser.crunchyroll.Profile
import org.mosad.teapod.parser.crunchyroll.supportedLocals
@ -33,6 +34,9 @@ class AccountFragment : Fragment() {
private var profile: Deferred<Profile> = lifecycleScope.async {
Crunchyroll.profile()
}
private var benefits: Deferred<Benefits> = lifecycleScope.async {
Crunchyroll.benefits()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = FragmentAccountBinding.inflate(inflater, container, false)
@ -44,14 +48,18 @@ class AccountFragment : Fragment() {
binding.textAccountLogin.text = EncryptedPreferences.login
// TODO reimplement for cr, if possible (maybe account status would be better? (premium))
// load subscription (async) info before anything else
binding.textAccountSubscription.text = getString(R.string.account_subscription, getString(R.string.loading))
// load account status and tier (async) info before anything else
lifecycleScope.launch {
binding.textAccountSubscription.text = getString(
R.string.account_subscription,
"TODO"
)
benefits.await().apply {
this.items.firstOrNull { it.benefit == "cr_premium" }?.let {
binding.textAccountSubscription.text = getString(R.string.account_premium)
}
this.items.firstOrNull { it.benefit == "cr_fan_pack" }?.let {
binding.textAccountSubscriptionDesc.text =
getString(R.string.account_tier, getString(R.string.account_tier_mega_fan))
}
}
}
// add preferred subtitles
@ -80,12 +88,6 @@ class AccountFragment : Fragment() {
showLoginDialog()
}
binding.linearAccountSubscription.setOnClickListener {
// TODO
//startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(AoDParser.getSubscriptionUrl())))
}
binding.linearSettingsContentLanguage.setOnClickListener {
showContentLanguageSelection()
}

View File

@ -27,12 +27,15 @@ import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.children
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.bumptech.glide.Glide
import com.facebook.shimmer.ShimmerFrameLayout
import kotlinx.coroutines.launch
import org.mosad.teapod.R
import org.mosad.teapod.databinding.FragmentHomeBinding
@ -161,10 +164,44 @@ class HomeFragment : Fragment() {
binding.textHighlightInfo.setOnClickListener {
activity?.showFragment(MediaFragment(uiState.highlightItem.id))
}
// disable the shimmer effect and hide the shimmer layouts
binding.shimmerLayoutHighlight.apply {
stopShimmer()
isVisible = false
}
binding.shimmerLayoutUpNext.apply {
stopShimmer()
isVisible = false
}
binding.shimmerLayoutWatchlist.apply {
stopShimmer()
isVisible = false
}
binding.shimmerLayoutRecommendations.apply {
stopShimmer()
isVisible = false
}
binding.shimmerLayoutNewTitles.apply {
stopShimmer()
isVisible = false
}
binding.shimmerLayoutTopTen.apply {
stopShimmer()
isVisible = false
}
// make highlights layout visible again
binding.linearHighlight.isVisible = true
}
private fun bindUiStateLoading() {
// currently not used
// hide highlights layout
binding.linearHighlight.isVisible = false
binding.root.children.filter { it is ShimmerFrameLayout }.forEach {
it as ShimmerFrameLayout
it.startShimmer()
}
}
private fun bindUiStateError(uiState: HomeViewModel.UiState.Error) {

View File

@ -3,13 +3,14 @@ package org.mosad.teapod.ui.activity.onboarding
import android.content.Intent
import android.os.Bundle
import android.view.View
import androidx.activity.addCallback
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.google.android.material.tabs.TabLayoutMediator
import org.mosad.teapod.ui.activity.main.MainActivity
import org.mosad.teapod.databinding.ActivityOnboardingBinding
import org.mosad.teapod.ui.activity.main.MainActivity
class OnboardingActivity : AppCompatActivity() {
@ -35,13 +36,11 @@ class OnboardingActivity : AppCompatActivity() {
if (fragments.size <= 1) {
binding.tabLayout.visibility = View.GONE
}
}
override fun onBackPressed() {
if (binding.viewPager.currentItem == 0) {
super.onBackPressed()
} else {
binding.viewPager.currentItem = binding.viewPager.currentItem - 1
onBackPressedDispatcher.addCallback {
if (binding.viewPager.currentItem != 0) {
binding.viewPager.currentItem = binding.viewPager.currentItem - 1
}
}
}

View File

@ -70,7 +70,7 @@ class PlayerActivity : AppCompatActivity() {
private lateinit var controller: StyledPlayerControlView
private lateinit var gestureDetector: GestureDetectorCompat
private lateinit var timerUpdates: TimerTask
private lateinit var controlsUpdates: TimerTask
private var wasInPiP = false
private var remainingTime: Long = 0
@ -85,8 +85,6 @@ class PlayerActivity : AppCompatActivity() {
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(
@ -194,9 +192,11 @@ class PlayerActivity : AppCompatActivity() {
override fun onPictureInPictureModeChanged(
isInPictureInPictureMode: Boolean,
newConfig: Configuration?
newConfig: Configuration
) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
}
// Hide the full-screen UI (controls, etc.) while in picture-in-picture mode.
playerBinding.videoView.useController = !isInPictureInPictureMode
@ -229,7 +229,11 @@ class PlayerActivity : AppCompatActivity() {
else -> View.GONE
}
controlsBinding.exoPlayPause.isVisible = !playerBinding.loading.isVisible
// don't use isVisible to hide exoPlayPause, as it will set the visibility to GONE
controlsBinding.exoPlayPause.visibility = when(playerBinding.loading.isVisible) {
true -> View.INVISIBLE
false -> View.VISIBLE
}
if (state == ExoPlayer.STATE_ENDED && hasNextEpisode() && Preferences.autoplay) {
playNextEpisode()
@ -284,11 +288,11 @@ class PlayerActivity : AppCompatActivity() {
}
private fun initTimeUpdates() {
if (this::timerUpdates.isInitialized) {
timerUpdates.cancel()
if (this::controlsUpdates.isInitialized) {
controlsUpdates.cancel()
}
timerUpdates = Timer().scheduleAtFixedRate(0, 500) {
controlsUpdates = Timer().scheduleAtFixedRate(0, 500) {
lifecycleScope.launch {
val currentPosition = model.player.currentPosition
val btnNextEpIsVisible = playerBinding.buttonNextEp.isVisible
@ -298,12 +302,14 @@ class PlayerActivity : AppCompatActivity() {
if (model.player.duration > 0) {
remainingTime = model.player.duration - currentPosition
remainingTime = if (remainingTime < 0) 0 else remainingTime
} else {
remainingTime = 0
}
// TODO add metaDB ending_start support
// if remaining time < 20 sec, a next ep is set, autoplay is enabled and not in pip:
// show next ep button
if (remainingTime in 1..20000) {
// if remaining time > 1 and < 20 sec, a next ep is set, autoplay is enabled
// and not in pip: show next ep button
if (remainingTime in 1000..20000) {
if (!btnNextEpIsVisible && hasNextEpisode() && Preferences.autoplay && !isInPiPMode()) {
showButtonNextEp()
}
@ -337,7 +343,7 @@ class PlayerActivity : AppCompatActivity() {
private fun onPauseOnStop() {
playerBinding.videoView.onPause()
model.player.pause()
timerUpdates.cancel()
controlsUpdates.cancel()
}
/**
@ -424,8 +430,16 @@ class PlayerActivity : AppCompatActivity() {
}
private fun playNextEpisode() {
model.playNextEpisode()
// disable the next episode buttons, so a user can't double click it
playerBinding.buttonNextEp.isClickable = false
controlsBinding.buttonNextEpC.isClickable = false
hideButtonNextEp()
model.playNextEpisode()
// enable the next episode buttons when playNextEpisode() has returned
playerBinding.buttonNextEp.isClickable = true
controlsBinding.buttonNextEpC.isClickable = true
}
private fun skipOpening() {
@ -457,7 +471,7 @@ class PlayerActivity : AppCompatActivity() {
playerBinding.buttonNextEp.animate()
.alpha(0.0f)
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator?) {
override fun onAnimationEnd(animation: Animator) {
super.onAnimationEnd(animation)
playerBinding.buttonNextEp.isVisible = false
}
@ -477,7 +491,7 @@ class PlayerActivity : AppCompatActivity() {
playerBinding.buttonSkipOp.animate()
.alpha(0.0f)
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator?) {
override fun onAnimationEnd(animation: Animator) {
super.onAnimationEnd(animation)
playerBinding.buttonSkipOp.isVisible = false
}
@ -509,7 +523,7 @@ class PlayerActivity : AppCompatActivity() {
/**
* on single tap hide or show the controls
*/
override fun onSingleTapConfirmed(e: MotionEvent?): Boolean {
override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
if (!isInPiPMode()) {
if (controller.isVisible) controller.hide() else controller.show()
}
@ -520,8 +534,8 @@ class PlayerActivity : AppCompatActivity() {
/**
* on double tap rewind or forward
*/
override fun onDoubleTap(e: MotionEvent?): Boolean {
val eventPosX = e?.x?.toInt() ?: 0
override fun onDoubleTap(e: MotionEvent): Boolean {
val eventPosX = e.x.toInt()
val viewCenterX = playerBinding.videoView.measuredWidth / 2
// if the event position is on the left side rewind, if it's on the right forward
@ -533,14 +547,14 @@ class PlayerActivity : AppCompatActivity() {
/**
* not used
*/
override fun onDoubleTapEvent(e: MotionEvent?): Boolean {
override fun onDoubleTapEvent(e: MotionEvent): Boolean {
return true
}
/**
* on long press toggle pause/play
*/
override fun onLongPress(e: MotionEvent?) {
override fun onLongPress(e: MotionEvent) {
model.togglePausePlay()
}

View File

@ -32,10 +32,7 @@ import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.MediaItem
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.*
import org.mosad.teapod.R
import org.mosad.teapod.parser.crunchyroll.*
import org.mosad.teapod.preferences.Preferences
@ -44,6 +41,7 @@ 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 kotlin.concurrent.scheduleAtFixedRate
/**
* PlayerViewModel handles all stuff related to media/episodes.
@ -55,6 +53,7 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
val player = ExoPlayer.Builder(application).build()
private val mediaSession = MediaSessionCompat(application, "TEAPOD_PLAYER_SESSION")
private val playheadAutoUpdate: TimerTask
val currentEpisodeChangedListener = ArrayList<() -> Unit>()
private var currentPlayhead: Long = 0
@ -96,6 +95,14 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
if (!isPlaying) updatePlayhead()
}
})
playheadAutoUpdate = Timer().scheduleAtFixedRate(0, 30000) {
viewModelScope.launch {
if (player.isPlaying){
updatePlayhead()
}
}
}
}
override fun onCleared() {
@ -128,8 +135,6 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
currentPlayheads = Crunchyroll.playheads(episodeIDs)
}
).joinAll()
Log.d(classTag, "meta: $mediaMeta")
setCurrentEpisode(episodeId)
@ -276,7 +281,8 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
val playhead = (player.currentPosition / 1000)
if (playhead > 0 && Preferences.updatePlayhead) {
viewModelScope.launch { Crunchyroll.postPlayheads(currentEpisode.id, playhead.toInt()) }
// don't use viewModelScope here. This task may needs to finish, when ViewModel will be cleared
CoroutineScope(Dispatchers.IO).launch { Crunchyroll.postPlayheads(currentEpisode.id, playhead.toInt()) }
Log.i(javaClass.name, "Set playhead for episode ${currentEpisode.id} to $playhead sec.")
}

View File

@ -51,8 +51,8 @@ class EpisodeListDialogFragment : DialogFragment() {
EpisodeItemAdapter.ViewType.PLAYER
)
// episodeNumber starts at 1, we need the episode index -> - 1
adapterRecEpisodes.currentSelected = model.currentEpisode.episodeNumber?.minus(1) ?: 0
// get the position/index of the currently playing episode
adapterRecEpisodes.currentSelected = model.episodes.items.indexOfFirst { it.id == model.currentEpisode.id }
binding.recyclerEpisodesPlayer.adapter = adapterRecEpisodes
binding.recyclerEpisodesPlayer.scrollToPosition(adapterRecEpisodes.currentSelected)

View File

@ -28,7 +28,7 @@ class FastForwardButton(context: Context, attrs: AttributeSet?): FrameLayout(con
repeatCount = 1
repeatMode = ObjectAnimator.REVERSE
addListener(object : AnimatorListenerAdapter() {
override fun onAnimationStart(animation: Animator?) {
override fun onAnimationStart(animation: Animator) {
binding.imageButton.isEnabled = false // disable button
binding.imageButton.setBackgroundResource(R.drawable.ic_baseline_forward_24)
}
@ -39,7 +39,7 @@ class FastForwardButton(context: Context, attrs: AttributeSet?): FrameLayout(con
duration = animationDuration
addListener(object : AnimatorListenerAdapter() {
// the label animation takes longer then the button animation, reset stuff in here
override fun onAnimationEnd(animation: Animator?) {
override fun onAnimationEnd(animation: Animator) {
binding.imageButton.isEnabled = true // enable button
binding.imageButton.setBackgroundResource(R.drawable.ic_baseline_forward_10_24)

View File

@ -28,7 +28,7 @@ class RewindButton(context: Context, attrs: AttributeSet): FrameLayout(context,
repeatCount = 1
repeatMode = ObjectAnimator.REVERSE
addListener(object : AnimatorListenerAdapter() {
override fun onAnimationStart(animation: Animator?) {
override fun onAnimationStart(animation: Animator) {
binding.imageButton.isEnabled = false // disable button
binding.imageButton.setBackgroundResource(R.drawable.ic_baseline_rewind_24)
}
@ -38,7 +38,7 @@ class RewindButton(context: Context, attrs: AttributeSet): FrameLayout(context,
labelAnimation = ObjectAnimator.ofFloat(binding.textView, View.TRANSLATION_X, -35f).apply {
duration = animationDuration
addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator?) {
override fun onAnimationEnd(animation: Animator) {
binding.imageButton.isEnabled = true // enable button
binding.imageButton.setBackgroundResource(R.drawable.ic_baseline_rewind_10_24)

View File

@ -24,11 +24,12 @@ 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.call.*
import io.ktor.client.plugins.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.decodeFromString
@ -40,8 +41,8 @@ object MetaDBController {
private const val repoUrl = "https://gitlab.com/Seil0/teapodmetadb/-/raw/main/crunchy/"
private val client = HttpClient {
install(JsonFeature) {
serializer = KotlinxSerializer(Json)
install(ContentNegotiation) {
json()
}
}
@ -49,7 +50,7 @@ object MetaDBController {
private var metaCacheList = arrayListOf<Meta>()
suspend fun list() = withContext(Dispatchers.IO) {
val raw: String = client.get("$repoUrl/list.json")
val raw: String = client.get("$repoUrl/list.json").body()
mediaList = Json.decodeFromString(raw)
}
@ -70,7 +71,7 @@ object MetaDBController {
private suspend fun getTVShowMetadataFromDB(crSeriesId: String): TVShowMeta? = withContext(Dispatchers.IO) {
return@withContext try {
val raw: String = client.get("$repoUrl/tv/$crSeriesId/media.json")
val raw: String = client.get("$repoUrl/tv/$crSeriesId/media.json").body()
val meta: TVShowMeta = Json.decodeFromString(raw)
metaCacheList.add(meta)

View File

@ -25,10 +25,10 @@ package org.mosad.teapod.util.tmdb
import android.util.Log
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.features.json.*
import io.ktor.client.features.json.serializer.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.invoke
@ -46,10 +46,11 @@ import org.mosad.teapod.util.concatenate
class TMDBApiController {
private val classTag = javaClass.name
private val json = Json { ignoreUnknownKeys = true }
private val client = HttpClient {
install(JsonFeature) {
serializer = KotlinxSerializer(json)
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
})
}
}
@ -78,7 +79,7 @@ class TMDBApiController {
}
}
response.receive<T>()
response.body<T>()
}
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="?shapeTextBackground"/>
<size
android:width="1920px"
android:height="1080px"/>
</shape>

View File

@ -112,7 +112,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/account_subscription"
android:text="@string/loading"
android:textSize="16sp" />
<TextView
@ -120,7 +120,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/account_subscription_desc"
android:text="@string/account_tier"
android:textColor="?textSecondary" />
</LinearLayout>
</LinearLayout>

View File

@ -17,6 +17,16 @@
android:layout_height="wrap_content"
android:orientation="vertical">
<com.facebook.shimmer.ShimmerFrameLayout
android:id="@+id/shimmer_layout_highlight"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:visibility="gone">
<include layout="@layout/item_highlight_shimmer" />
</com.facebook.shimmer.ShimmerFrameLayout>
<LinearLayout
android:id="@+id/linear_highlight"
android:layout_width="match_parent"
@ -126,6 +136,23 @@
android:textSize="16sp"
android:textStyle="bold" />
<com.facebook.shimmer.ShimmerFrameLayout
android:id="@+id/shimmer_layout_up_next"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:visibility="gone">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<include layout="@layout/item_media_shimmer" />
<include layout="@layout/item_media_shimmer" />
<include layout="@layout/item_media_shimmer" />
</LinearLayout>
</com.facebook.shimmer.ShimmerFrameLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_up_next"
android:layout_width="match_parent"
@ -154,6 +181,23 @@
android:textSize="16sp"
android:textStyle="bold" />
<com.facebook.shimmer.ShimmerFrameLayout
android:id="@+id/shimmer_layout_watchlist"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:visibility="gone">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<include layout="@layout/item_media_shimmer" />
<include layout="@layout/item_media_shimmer" />
<include layout="@layout/item_media_shimmer" />
</LinearLayout>
</com.facebook.shimmer.ShimmerFrameLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_watchlist"
android:layout_width="match_parent"
@ -182,6 +226,23 @@
android:textSize="16sp"
android:textStyle="bold" />
<com.facebook.shimmer.ShimmerFrameLayout
android:id="@+id/shimmer_layout_recommendations"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:visibility="gone">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<include layout="@layout/item_media_shimmer" />
<include layout="@layout/item_media_shimmer" />
<include layout="@layout/item_media_shimmer" />
</LinearLayout>
</com.facebook.shimmer.ShimmerFrameLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_recommendations"
android:layout_width="match_parent"
@ -210,6 +271,23 @@
android:textSize="16sp"
android:textStyle="bold" />
<com.facebook.shimmer.ShimmerFrameLayout
android:id="@+id/shimmer_layout_new_titles"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:visibility="gone">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<include layout="@layout/item_media_shimmer" />
<include layout="@layout/item_media_shimmer" />
<include layout="@layout/item_media_shimmer" />
</LinearLayout>
</com.facebook.shimmer.ShimmerFrameLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_new_titles"
android:layout_width="match_parent"
@ -238,6 +316,23 @@
android:textSize="16sp"
android:textStyle="bold" />
<com.facebook.shimmer.ShimmerFrameLayout
android:id="@+id/shimmer_layout_top_ten"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:visibility="gone">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<include layout="@layout/item_media_shimmer" />
<include layout="@layout/item_media_shimmer" />
<include layout="@layout/item_media_shimmer" />
</LinearLayout>
</com.facebook.shimmer.ShimmerFrameLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_top_ten"
android:layout_width="match_parent"

View File

@ -0,0 +1,96 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?themePrimary">
<ImageView
android:id="@+id/shimmer_image_highlight"
android:layout_width="0dp"
android:layout_height="0dp"
android:src="@drawable/placeholder_image"
app:layout_constraintDimensionRatio="H,16:9"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription" />
<LinearLayout
android:id="@+id/shimmer_linear_highlight"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="?themePrimary"
android:orientation="vertical"
android:paddingBottom="7dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/shimmer_image_highlight">
<TextView
android:id="@+id/shimmer_text_highlight_title"
android:layout_width="120dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="7dp"
android:background="?shapeTextBackground"
android:textSize="16sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="7dp"
android:gravity="center"
android:orientation="horizontal">
<Space
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="1" />
<TextView
android:id="@+id/shimmer_text_highlight_my_list"
android:layout_width="64dp"
android:layout_height="wrap_content"
android:gravity="center"
android:textSize="12sp"
app:drawableTint="?shapeTextBackground"
app:drawableTopCompat="@drawable/ic_baseline_add_24" />
<Space
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="1" />
<com.google.android.material.button.MaterialButton
android:id="@+id/shimmer_button_play_highlight"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:textSize="16sp"
app:backgroundTint="?shapeTextBackground" />
<Space
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="1" />
<TextView
android:id="@+id/shimmer_text_highlight_info"
android:layout_width="64dp"
android:layout_height="wrap_content"
android:gravity="center"
app:drawableTint="?shapeTextBackground"
app:drawableTopCompat="@drawable/ic_outline_info_24" />
<Space
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="1" />
</LinearLayout>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -2,16 +2,16 @@
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="195dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:backgroundTint="?themeSecondary"
android:visibility="visible"
app:cardCornerRadius="7dp"
app:cardElevation="4dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintWidth_max="195dp">
<FrameLayout
android:id="@+id/frame_image_progress"
@ -21,7 +21,8 @@
app:layout_constraintDimensionRatio="H,16:9"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth="195dp">
<ImageView
android:id="@+id/image_poster"
@ -53,7 +54,7 @@
<TextView
android:id="@+id/text_title"
android:layout_width="match_parent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:gravity="center"
android:lines="2"
@ -62,6 +63,8 @@
android:text="@string/text_title_ex"
android:textAlignment="center"
android:textSize="15sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/frame_image_progress" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginEnd="3dp"
android:backgroundTint="?themeSecondary"
app:cardCornerRadius="7dp"
app:cardElevation="4dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintWidth_max="195dp">
<FrameLayout
android:id="@+id/frame_image_progress"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/text_title"
app:layout_constraintDimensionRatio="H,16:9"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth="195dp">
<ImageView
android:id="@+id/image_poster"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?shapeTextBackground"
tools:ignore="ContentDescription" />
</FrameLayout>
<TextView
android:id="@+id/text_title"
android:layout_width="128dp"
android:layout_height="wrap_content"
android:layout_margin="11dp"
android:background="?shapeTextBackground"
android:textSize="15sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/frame_image_progress" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>

View File

@ -37,6 +37,8 @@
<string name="account_login_desc">Zum bearbeiten tippen</string>
<string name="account_subscription">Abo %1$s</string>
<string name="account_subscription_desc">Zum verlängern tippen</string>
<string name="account_premium">Premium Mitglied</string>
<string name="account_tier">Typ: %1$s</string>
<string name="info">Info</string>
<string name="info_about_desc">Version %1$s (%2$s)</string>
<string name="settings">Einstellungen</string>

View File

@ -49,6 +49,11 @@
<string name="account_login_desc">Tap to edit</string>
<string name="account_subscription">Subscription %1$s</string>
<string name="account_subscription_desc">Tap to extend</string>
<string name="account_premium">Premium member</string>
<string name="account_tier">Tier: %1$s</string>
<string name="account_tier_fan" translatable="false">Fan</string>
<string name="account_tier_mega_fan" translatable="false">Mega Fan</string>
<string name="account_tier_ultimate_fan" translatable="false">Ultimate Fan</string>
<string name="settings">Settings</string>
<string name="settings_content_language">Preferred content language</string>
<string name="settings_content_language_desc">English</string>

View File

@ -1,17 +0,0 @@
package org.mosad.teapod
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}

View File

@ -0,0 +1,24 @@
package org.mosad.teapod.parser.crunchyroll
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import org.junit.Assert
import org.junit.Test
class DataTypesTest {
@Test
fun testTokenType() {
val testToken = javaClass.getResource("/token.json")!!.readText()
val token: Token = Json.decodeFromString(testToken)
Assert.assertEquals("TestAccessToken-1_TestAccessToken", token.accessToken)
Assert.assertEquals("00000000-0000-0000-0000-000000000000", token.refreshToken)
Assert.assertEquals(300, token.expiresIn)
Assert.assertEquals("Bearer", token.tokenType)
Assert.assertEquals("account content offline_access reviews talkbox", token.scope)
Assert.assertEquals("DE", token.country)
Assert.assertEquals("00000000-0000-0000-0000-000000000000", token.accountId)
}
}

View File

@ -0,0 +1,9 @@
{
"access_token":"TestAccessToken-1_TestAccessToken",
"refresh_token":"00000000-0000-0000-0000-000000000000",
"expires_in":300,
"token_type":"Bearer",
"scope":"account content offline_access reviews talkbox",
"country":"DE",
"account_id":"00000000-0000-0000-0000-000000000000"
}

View File

@ -1,14 +1,14 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = "1.6.21"
ext.ktor_version = "1.6.8"
ext.kotlin_version = "1.7.10"
ext.ktor_version = "2.1.1"
ext.exo_version = "2.17.1"
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.2.1'
classpath 'com.android.tools.build:gradle:7.3.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong

View File

@ -0,0 +1,9 @@
Dies ist der dritte beta Release von Teapod 1.0.0 mit Unterstützung für Cunchyroll.
* Diverse UI/UX Verbesserungen
* Playhead Updates werden nun alle 30 Sekunden durchgeführt
* Fehlende Playhead Updates beim schließen des Players behoben (#62)
* Abo Status und Stufe zum Accountscreen hinzugefügt
* Das Verhalten des "Nächste Episode" Buttons wurde verbessert (#53)
Alle Änderungen https://git.mosad.xyz/Seil0/teapod/compare/1.0.0-beta2...1.0.0-beta3

View File

@ -0,0 +1,9 @@
This is the third beta release of Teapod 1.0.0 with support for crunchyroll.
* UI/UX improvements
* Playhead is now updated every 30 seconds
* Fixed missing playhead updates when closing the player (#62)
* Add subscription status and tier info to the account screen
* Improved the behaviour of the "next episde" button (#53)
Full changelog https://git.mosad.xyz/Seil0/teapod/compare/1.0.0-beta2...1.0.0-beta3

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists