Compare commits

..

No commits in common. "1.0.0-beta3" and "1.0.0-beta2" have entirely different histories.

30 changed files with 149 additions and 537 deletions

View File

@ -5,15 +5,15 @@ plugins {
}
android {
compileSdkVersion 33
compileSdkVersion 31
buildToolsVersion "30.0.3"
defaultConfig {
applicationId "org.mosad.teapod"
minSdkVersion 23
targetSdkVersion 32
versionCode 9020 //00.09.020
versionName "1.0.0-beta3"
targetSdkVersion 31
versionCode 9010 //00.09.010
versionName "1.0.0-beta2"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resValue "string", "build_time", buildTime()
@ -48,36 +48,33 @@ 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.4'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1'
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.3'
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.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.security:security-crypto:1.1.0-alpha03'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1'
implementation 'com.google.android.material:material:1.6.1'
implementation 'com.google.android.material:material:1.5.0'
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.facebook.shimmer:shimmer:0.5.0'
implementation 'com.github.bumptech.glide:glide:4.13.2'
implementation 'com.github.bumptech.glide:glide:4.13.1'
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-content-negotiation:$ktor_version"
implementation "io.ktor:ktor-serialization-kotlinx-json:$ktor_version"
implementation "io.ktor:ktor-client-serialization:$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.plugins.*
import io.ktor.client.plugins.contentnegotiation.*
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.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(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
})
install(JsonFeature) {
serializer = KotlinxSerializer(json)
}
}
private const val baseUrl = "https://beta-api.crunchyroll.com"
@ -61,7 +61,6 @@ object Crunchyroll {
private val tokenRefreshContext = newSingleThreadContext("TokenRefreshContext")
private var accountID = ""
private var externalID = ""
private var policy = ""
private var signature = ""
@ -77,7 +76,7 @@ object Crunchyroll {
*/
fun initBasicApiToken() = runBlocking {
withContext(Dispatchers.IO) {
basicApiToken = client.get { url(basicApiTokenUrl) }.bodyAsText()
basicApiToken = (client.get(basicApiTokenUrl) as HttpResponse).readText()
Log.i(TAG, "basic auth token: $basicApiToken")
}
}
@ -107,7 +106,7 @@ object Crunchyroll {
val response: HttpResponse = client.submitForm("$baseUrl$tokenEndpoint", formParameters = formData) {
header("Authorization", "Basic $basicApiToken")
}
token = response.body()
token = response.receive()
tokenValidUntil = System.currentTimeMillis() + (token.expiresIn * 1000)
response.status
} catch (ex: ClientRequestException) {
@ -155,10 +154,10 @@ object Crunchyroll {
// for json set body and content type
if (bodyObject is JsonObject) {
setBody(bodyObject)
body = bodyObject
contentType(ContentType.Application.Json)
}
}.body()
}
response
}
@ -246,7 +245,6 @@ object Crunchyroll {
}
accountID = account.accountId
externalID = account.externalId
}
/**
@ -333,7 +331,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
}
@ -357,7 +355,7 @@ object Crunchyroll {
return try {
requestGet(episodesEndpoint, parameters)
} catch (ex: SerializationException) {
}catch (ex: SerializationException) {
Log.e(TAG, "SerializationException in objects().", ex)
NoneCollection
}
@ -373,7 +371,7 @@ object Crunchyroll {
return try {
requestGet(seasonListEndpoint, parameters)
} catch (ex: SerializationException) {
}catch (ex: SerializationException) {
Log.e(TAG, "SerializationException in seasonList().", ex)
NoneDiscSeasonList
}
@ -397,7 +395,7 @@ object Crunchyroll {
return try {
requestGet(seriesEndpoint, parameters)
} catch (ex: SerializationException) {
}catch (ex: SerializationException) {
Log.e(TAG, "SerializationException in series().", ex)
NoneSeries
}
@ -418,7 +416,7 @@ object Crunchyroll {
return try {
requestGet(upNextSeriesEndpoint, parameters)
} catch (ex: SerializationException) {
}catch (ex: SerializationException) {
Log.e(TAG, "SerializationException in upNextSeries().", ex)
NoneUpNextSeriesItem
}
@ -442,7 +440,7 @@ object Crunchyroll {
return try {
requestGet(seasonsEndpoint, parameters)
} catch (ex: SerializationException) {
}catch (ex: SerializationException) {
Log.e(TAG, "SerializationException in seasons().", ex)
NoneSeasons
}
@ -466,7 +464,7 @@ object Crunchyroll {
return try {
requestGet(episodesEndpoint, parameters)
} catch (ex: SerializationException) {
}catch (ex: SerializationException) {
Log.e(TAG, "SerializationException in episodes().", ex)
NoneEpisodes
}
@ -481,7 +479,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
}
@ -504,7 +502,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
}
@ -600,7 +598,7 @@ object Crunchyroll {
return try {
requestGet(similarToEndpoint, parameters)
} catch (ex: SerializationException) {
}catch (ex: SerializationException) {
Log.e(TAG, "SerializationException in similarTo().", ex)
NoneSimilarToResult
}
@ -625,7 +623,7 @@ object Crunchyroll {
val list: ContinueWatchingList = try {
requestGet(watchlistEndpoint, parameters)
} catch (ex: SerializationException) {
}catch (ex: SerializationException) {
Log.e(TAG, "SerializationException in watchlist().", ex)
NoneContinueWatchingList
}
@ -649,7 +647,7 @@ object Crunchyroll {
return try {
requestGet(watchlistEndpoint, parameters)
} catch (ex: SerializationException) {
}catch (ex: SerializationException) {
Log.e(TAG, "SerializationException in upNextAccount().", ex)
NoneContinueWatchingList
}
@ -666,7 +664,7 @@ object Crunchyroll {
return try {
requestGet(recommendationsEndpoint, parameters)
} catch (ex: SerializationException) {
}catch (ex: SerializationException) {
Log.e(TAG, "SerializationException in recommendations().", ex)
NoneRecommendationsList
}
@ -686,7 +684,7 @@ object Crunchyroll {
return try {
requestGet(profileEndpoint)
} catch (ex: SerializationException) {
}catch (ex: SerializationException) {
Log.e(TAG, "SerializationException in profile().", ex)
NoneProfile
}
@ -706,20 +704,4 @@ 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,7 +125,6 @@ typealias DiscSeasonList = Collection<SeasonListItem>
typealias Watchlist = Collection<Item>
typealias ContinueWatchingList = Collection<ContinueWatchingItem>
typealias RecommendationsList = Collection<Item>
typealias Benefits = Collection<Benefit>
@Serializable
data class UpNextSeriesItem(
@ -227,7 +226,6 @@ 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,
@ -382,9 +380,9 @@ data class Streams(
@Serializable
data class Stream(
@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
@SerialName("hardsub_locale") val hardsubLocale: String,
@SerialName("url") val url: String,
@SerialName("vcodec") val vcodec: String,
)
val NonePlayback = Playback(
@ -414,16 +412,3 @@ 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,7 +26,6 @@ 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
@ -79,14 +78,16 @@ class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListen
supportFragmentManager.commit {
replace(R.id.nav_host_fragment, activeBaseFragment, activeBaseFragment.javaClass.simpleName)
}
}
onBackPressedDispatcher.addCallback {
if (supportFragmentManager.backStackEntryCount > 0) {
supportFragmentManager.popBackStack()
override fun onBackPressed() {
if (supportFragmentManager.backStackEntryCount > 0) {
supportFragmentManager.popBackStack()
} else {
if (activeBaseFragment !is HomeFragment) {
binding.navView.selectedItemId = R.id.navigation_home
} else {
if (activeBaseFragment !is HomeFragment) {
binding.navView.selectedItemId = R.id.navigation_home
}
super.onBackPressed()
}
}
}

View File

@ -15,7 +15,6 @@ 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
@ -34,9 +33,6 @@ 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)
@ -48,18 +44,14 @@ class AccountFragment : Fragment() {
binding.textAccountLogin.text = EncryptedPreferences.login
// load account status and tier (async) info before anything else
// 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))
lifecycleScope.launch {
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))
}
}
binding.textAccountSubscription.text = getString(
R.string.account_subscription,
"TODO"
)
}
// add preferred subtitles
@ -88,6 +80,12 @@ class AccountFragment : Fragment() {
showLoginDialog()
}
binding.linearAccountSubscription.setOnClickListener {
// TODO
//startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(AoDParser.getSubscriptionUrl())))
}
binding.linearSettingsContentLanguage.setOnClickListener {
showContentLanguageSelection()
}

View File

@ -27,15 +27,12 @@ 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
@ -164,44 +161,10 @@ 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() {
// hide highlights layout
binding.linearHighlight.isVisible = false
binding.root.children.filter { it is ShimmerFrameLayout }.forEach {
it as ShimmerFrameLayout
it.startShimmer()
}
// currently not used
}
private fun bindUiStateError(uiState: HomeViewModel.UiState.Error) {

View File

@ -3,14 +3,13 @@ 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.databinding.ActivityOnboardingBinding
import org.mosad.teapod.ui.activity.main.MainActivity
import org.mosad.teapod.databinding.ActivityOnboardingBinding
class OnboardingActivity : AppCompatActivity() {
@ -36,11 +35,13 @@ class OnboardingActivity : AppCompatActivity() {
if (fragments.size <= 1) {
binding.tabLayout.visibility = View.GONE
}
}
onBackPressedDispatcher.addCallback {
if (binding.viewPager.currentItem != 0) {
binding.viewPager.currentItem = binding.viewPager.currentItem - 1
}
override fun onBackPressed() {
if (binding.viewPager.currentItem == 0) {
super.onBackPressed()
} else {
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 controlsUpdates: TimerTask
private lateinit var timerUpdates: TimerTask
private var wasInPiP = false
private var remainingTime: Long = 0
@ -85,6 +85,8 @@ 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(
@ -192,11 +194,9 @@ class PlayerActivity : AppCompatActivity() {
override fun onPictureInPictureModeChanged(
isInPictureInPictureMode: Boolean,
newConfig: Configuration
newConfig: Configuration?
) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
}
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
// Hide the full-screen UI (controls, etc.) while in picture-in-picture mode.
playerBinding.videoView.useController = !isInPictureInPictureMode
@ -229,11 +229,7 @@ class PlayerActivity : AppCompatActivity() {
else -> View.GONE
}
// 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
}
controlsBinding.exoPlayPause.isVisible = !playerBinding.loading.isVisible
if (state == ExoPlayer.STATE_ENDED && hasNextEpisode() && Preferences.autoplay) {
playNextEpisode()
@ -288,11 +284,11 @@ class PlayerActivity : AppCompatActivity() {
}
private fun initTimeUpdates() {
if (this::controlsUpdates.isInitialized) {
controlsUpdates.cancel()
if (this::timerUpdates.isInitialized) {
timerUpdates.cancel()
}
controlsUpdates = Timer().scheduleAtFixedRate(0, 500) {
timerUpdates = Timer().scheduleAtFixedRate(0, 500) {
lifecycleScope.launch {
val currentPosition = model.player.currentPosition
val btnNextEpIsVisible = playerBinding.buttonNextEp.isVisible
@ -302,14 +298,12 @@ 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 > 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 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 (!btnNextEpIsVisible && hasNextEpisode() && Preferences.autoplay && !isInPiPMode()) {
showButtonNextEp()
}
@ -343,7 +337,7 @@ class PlayerActivity : AppCompatActivity() {
private fun onPauseOnStop() {
playerBinding.videoView.onPause()
model.player.pause()
controlsUpdates.cancel()
timerUpdates.cancel()
}
/**
@ -430,16 +424,8 @@ class PlayerActivity : AppCompatActivity() {
}
private fun 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
hideButtonNextEp()
}
private fun skipOpening() {
@ -471,7 +457,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
}
@ -491,7 +477,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
}
@ -523,7 +509,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()
}
@ -534,8 +520,8 @@ class PlayerActivity : AppCompatActivity() {
/**
* on double tap rewind or forward
*/
override fun onDoubleTap(e: MotionEvent): Boolean {
val eventPosX = e.x.toInt()
override fun onDoubleTap(e: MotionEvent?): Boolean {
val eventPosX = e?.x?.toInt() ?: 0
val viewCenterX = playerBinding.videoView.measuredWidth / 2
// if the event position is on the left side rewind, if it's on the right forward
@ -547,14 +533,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,7 +32,10 @@ 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.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.mosad.teapod.R
import org.mosad.teapod.parser.crunchyroll.*
import org.mosad.teapod.preferences.Preferences
@ -41,7 +44,6 @@ 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.
@ -53,7 +55,6 @@ 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
@ -95,14 +96,6 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
if (!isPlaying) updatePlayhead()
}
})
playheadAutoUpdate = Timer().scheduleAtFixedRate(0, 30000) {
viewModelScope.launch {
if (player.isPlaying){
updatePlayhead()
}
}
}
}
override fun onCleared() {
@ -135,6 +128,8 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
currentPlayheads = Crunchyroll.playheads(episodeIDs)
}
).joinAll()
Log.d(classTag, "meta: $mediaMeta")
setCurrentEpisode(episodeId)
@ -281,8 +276,7 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
val playhead = (player.currentPosition / 1000)
if (playhead > 0 && Preferences.updatePlayhead) {
// 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()) }
viewModelScope.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
)
// get the position/index of the currently playing episode
adapterRecEpisodes.currentSelected = model.episodes.items.indexOfFirst { it.id == model.currentEpisode.id }
// 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)

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,12 +24,11 @@ package org.mosad.teapod.util.metadb
import android.util.Log
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.plugins.*
import io.ktor.client.plugins.contentnegotiation.*
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 io.ktor.serialization.kotlinx.json.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.decodeFromString
@ -41,8 +40,8 @@ object MetaDBController {
private const val repoUrl = "https://gitlab.com/Seil0/teapodmetadb/-/raw/main/crunchy/"
private val client = HttpClient {
install(ContentNegotiation) {
json()
install(JsonFeature) {
serializer = KotlinxSerializer(Json)
}
}
@ -50,7 +49,7 @@ object MetaDBController {
private var metaCacheList = arrayListOf<Meta>()
suspend fun list() = withContext(Dispatchers.IO) {
val raw: String = client.get("$repoUrl/list.json").body()
val raw: String = client.get("$repoUrl/list.json")
mediaList = Json.decodeFromString(raw)
}
@ -71,7 +70,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").body()
val raw: String = client.get("$repoUrl/tv/$crSeriesId/media.json")
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.plugins.contentnegotiation.*
import io.ktor.client.features.json.*
import io.ktor.client.features.json.serializer.*
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,11 +46,10 @@ import org.mosad.teapod.util.concatenate
class TMDBApiController {
private val classTag = javaClass.name
private val json = Json { ignoreUnknownKeys = true }
private val client = HttpClient {
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
})
install(JsonFeature) {
serializer = KotlinxSerializer(json)
}
}
@ -79,7 +78,7 @@ class TMDBApiController {
}
}
response.body<T>()
response.receive<T>()
}
}

View File

@ -1,7 +0,0 @@
<?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/loading"
android:text="@string/account_subscription"
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_tier"
android:text="@string/account_subscription_desc"
android:textColor="?textSecondary" />
</LinearLayout>
</LinearLayout>

View File

@ -17,16 +17,6 @@
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"
@ -136,23 +126,6 @@
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"
@ -181,23 +154,6 @@
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"
@ -226,23 +182,6 @@
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"
@ -271,23 +210,6 @@
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"
@ -316,23 +238,6 @@
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

@ -1,96 +0,0 @@
<?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="wrap_content"
android:layout_width="195dp"
android:layout_height="wrap_content"
android:backgroundTint="?themeSecondary"
android:visibility="visible"
app:cardCornerRadius="7dp"
app:cardElevation="4dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintWidth_max="195dp">
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="@+id/frame_image_progress"
@ -21,8 +21,7 @@
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">
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:id="@+id/image_poster"
@ -54,7 +53,7 @@
<TextView
android:id="@+id/text_title"
android:layout_width="0dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:lines="2"
@ -63,8 +62,6 @@
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

@ -1,51 +0,0 @@
<?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,8 +37,6 @@
<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,11 +49,6 @@
<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

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

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

@ -1,9 +0,0 @@
{
"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.7.10"
ext.ktor_version = "2.1.1"
ext.kotlin_version = "1.6.21"
ext.ktor_version = "1.6.8"
ext.exo_version = "2.17.1"
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.3.0'
classpath 'com.android.tools.build:gradle:7.2.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong

View File

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

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

Before

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.5.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists