version 0.4.2 #44

Merged
Seil0 merged 24 commits from develop into master 2021-07-09 18:56:35 +02:00
33 changed files with 822 additions and 460 deletions

View File

@ -10,8 +10,8 @@ android {
applicationId "org.mosad.teapod" applicationId "org.mosad.teapod"
minSdkVersion 23 minSdkVersion 23
targetSdkVersion 30 targetSdkVersion 30
versionCode 4100 //00.04.100 versionCode 4200 //00.04.200
versionName "0.4.1" versionName "0.4.2"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resValue "string", "build_time", buildTime() resValue "string", "build_time", buildTime()
@ -41,22 +41,25 @@ android {
dependencies { dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"]) implementation fileTree(dir: "libs", include: ["*.jar"])
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.3' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0'
implementation 'androidx.core:core-ktx:1.3.2' implementation 'androidx.core:core-ktx:1.6.0'
implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'androidx.appcompat:appcompat:1.3.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.4' implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5'
implementation 'androidx.navigation:navigation-ui-ktx:2.3.4' implementation 'androidx.navigation:navigation-ui-ktx:2.3.5'
implementation 'androidx.security:security-crypto:1.1.0-alpha03' implementation 'androidx.security:security-crypto:1.1.0-alpha03'
implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1'
implementation 'com.google.android.material:material:1.3.0' implementation 'com.google.android.material:material:1.4.0'
implementation 'com.google.code.gson:gson:2.8.6' implementation 'com.google.code.gson:gson:2.8.7'
implementation 'com.google.android.exoplayer:exoplayer-core:2.13.2' implementation 'com.google.android.exoplayer:exoplayer-core:2.14.1'
implementation 'com.google.android.exoplayer:exoplayer-hls:2.13.2' implementation 'com.google.android.exoplayer:exoplayer-hls:2.14.1'
implementation 'com.google.android.exoplayer:exoplayer-dash:2.13.2' implementation 'com.google.android.exoplayer:exoplayer-dash:2.14.1'
implementation 'com.google.android.exoplayer:exoplayer-ui:2.13.2' implementation 'com.google.android.exoplayer:exoplayer-ui:2.14.1'
implementation 'com.google.android.exoplayer:extension-mediasession:2.14.1'
implementation 'org.jsoup:jsoup:1.13.1' implementation 'org.jsoup:jsoup:1.13.1'
implementation 'com.github.bumptech.glide:glide:4.12.0' implementation 'com.github.bumptech.glide:glide:4.12.0'
@ -64,12 +67,12 @@ dependencies {
implementation 'com.afollestad.material-dialogs:core:3.3.0' implementation 'com.afollestad.material-dialogs:core:3.3.0'
implementation 'com.afollestad.material-dialogs:bottomsheets:3.3.0' implementation 'com.afollestad.material-dialogs:bottomsheets:3.3.0'
testImplementation 'junit:junit:4.13.1' testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
} }
static def buildTime() { static def buildTime() {
return new Date().format("yyyy-MM-dd", TimeZone.getTimeZone("UTC")) return new Date().format("yyyy-MM-dd", TimeZone.getTimeZone("UTC"))
} }

View File

@ -40,6 +40,7 @@ object AoDParser {
private const val baseUrl = "https://www.anime-on-demand.de" private const val baseUrl = "https://www.anime-on-demand.de"
private const val loginPath = "/users/sign_in" private const val loginPath = "/users/sign_in"
private const val libraryPath = "/animes" private const val libraryPath = "/animes"
private const val subscriptionPath = "/mypools"
private const val userAgent = "Mozilla/5.0 (X11; Linux x86_64; rv:84.0) Gecko/20100101 Firefox/84.0" private const val userAgent = "Mozilla/5.0 (X11; Linux x86_64; rv:84.0) Gecko/20100101 Firefox/84.0"
@ -98,10 +99,12 @@ object AoDParser {
/** /**
* initially load all media and home screen data * initially load all media and home screen data
*/ */
fun initialLoading() = listOf( suspend fun initialLoading() {
loadHome(), coroutineScope {
listAnimes() launch { loadHome() }
) launch { listAnimes() }
}
}
/** /**
* get a media by it's ID (int) * get a media by it's ID (int)
@ -117,7 +120,27 @@ object AoDParser {
return media return media
} }
fun markAsWatched(mediaId: Int, episodeId: Int) = GlobalScope.launch { /**
* get subscription info from aod website, remove "Anime-Abo" Prefix and trim
*/
suspend fun getSubscriptionInfoAsync(): Deferred<String> {
return coroutineScope {
async(Dispatchers.IO) {
val res = Jsoup.connect(baseUrl + subscriptionPath)
.cookies(sessionCookies)
.get()
return@async res.select("a:contains(Anime-Abo)").text()
.removePrefix("Anime-Abo").trim()
}
}
}
fun getSubscriptionUrl(): String {
return baseUrl + subscriptionPath
}
suspend fun markAsWatched(mediaId: Int, episodeId: Int) {
val episode = getMediaById(mediaId).getEpisodeById(episodeId) val episode = getMediaById(mediaId).getEpisodeById(episodeId)
episode.watched = true episode.watched = true
sendCallback(episode.watchedCallback) sendCallback(episode.watchedCallback)
@ -126,250 +149,262 @@ object AoDParser {
} }
// TODO don't use jsoup here // TODO don't use jsoup here
private fun sendCallback(callbackPath: String) = GlobalScope.launch(Dispatchers.IO) { private suspend fun sendCallback(callbackPath: String) = coroutineScope {
val headers = mutableMapOf( launch(Dispatchers.IO) {
Pair("Accept", "application/json, text/javascript, */*; q=0.01"), val headers = mutableMapOf(
Pair("Accept-Language", "de,en-US;q=0.7,en;q=0.3"), Pair("Accept", "application/json, text/javascript, */*; q=0.01"),
Pair("Accept-Encoding", "gzip, deflate, br"), Pair("Accept-Language", "de,en-US;q=0.7,en;q=0.3"),
Pair("X-CSRF-Token", csrfToken), Pair("Accept-Encoding", "gzip, deflate, br"),
Pair("X-Requested-With", "XMLHttpRequest"), Pair("X-CSRF-Token", csrfToken),
) Pair("X-Requested-With", "XMLHttpRequest"),
)
try { try {
Jsoup.connect(baseUrl + callbackPath) Jsoup.connect(baseUrl + callbackPath)
.ignoreContentType(true) .ignoreContentType(true)
.cookies(sessionCookies) .cookies(sessionCookies)
.headers(headers) .headers(headers)
.execute() .execute()
} catch (ex: IOException) { } catch (ex: IOException) {
Log.e(javaClass.name, "Callback for $callbackPath failed.", ex) Log.e(javaClass.name, "Callback for $callbackPath failed.", ex)
}
} }
} }
/** /**
* load all media from aod into itemMediaList and mediaList * load all media from aod into itemMediaList and mediaList
* TODO private suspend fun listAnimes() = withContext(Dispatchers.IO) should also work, maybe a bug in android studio?
*/ */
private fun listAnimes() = GlobalScope.launch(Dispatchers.IO) { private suspend fun listAnimes() = withContext(Dispatchers.IO) {
val resAnimes = Jsoup.connect(baseUrl + libraryPath).get() launch(Dispatchers.IO) {
//println(resAnimes) val resAnimes = Jsoup.connect(baseUrl + libraryPath).get()
//println(resAnimes)
itemMediaList.clear() itemMediaList.clear()
mediaList.clear() mediaList.clear()
resAnimes.select("div.animebox").forEach { resAnimes.select("div.animebox").forEach {
val type = if (it.select("p.animebox-link").select("a").text().toLowerCase(Locale.ROOT) == "zur serie") { val type = if (it.select("p.animebox-link").select("a").text().lowercase(Locale.ROOT) == "zur serie") {
MediaType.TVSHOW MediaType.TVSHOW
} else { } else {
MediaType.MOVIE MediaType.MOVIE
}
val mediaTitle = it.select("h3.animebox-title").text()
val mediaLink = it.select("p.animebox-link").select("a").attr("href")
val mediaImage = it.select("p.animebox-image").select("img").attr("src")
val mediaShortText = it.select("p.animebox-shorttext").text()
val mediaId = mediaLink.substringAfterLast("/").toInt()
itemMediaList.add(ItemMedia(mediaId, mediaTitle, mediaImage))
mediaList.add(Media(mediaId, mediaLink, type).apply {
info.title = mediaTitle
info.posterUrl = mediaImage
info.shortDesc = mediaShortText
})
} }
val mediaTitle = it.select("h3.animebox-title").text()
val mediaLink = it.select("p.animebox-link").select("a").attr("href")
val mediaImage = it.select("p.animebox-image").select("img").attr("src")
val mediaShortText = it.select("p.animebox-shorttext").text()
val mediaId = mediaLink.substringAfterLast("/").toInt()
itemMediaList.add(ItemMedia(mediaId, mediaTitle, mediaImage)) Log.i(javaClass.name, "Total library size is: ${mediaList.size}")
mediaList.add(Media(mediaId, mediaLink, type).apply {
info.title = mediaTitle
info.posterUrl = mediaImage
info.shortDesc = mediaShortText
})
} }
Log.i(javaClass.name, "Total library size is: ${mediaList.size}")
} }
/** /**
* load new episodes, titles and highlights * load new episodes, titles and highlights
*/ */
private fun loadHome() = GlobalScope.launch(Dispatchers.IO) { private suspend fun loadHome() = withContext(Dispatchers.IO) {
val resHome = Jsoup.connect(baseUrl).get() launch(Dispatchers.IO) {
val resHome = Jsoup.connect(baseUrl).get()
// get highlights from AoD // get highlights from AoD
highlightsList.clear() highlightsList.clear()
resHome.select("#aod-highlights").select("div.news-item").forEach { resHome.select("#aod-highlights").select("div.news-item").forEach {
val mediaId = it.select("div.news-item-text").select("a.serienlink") val mediaId = it.select("div.news-item-text").select("a.serienlink")
.attr("href").substringAfterLast("/").toIntOrNull() .attr("href").substringAfterLast("/").toIntOrNull()
val mediaTitle = it.select("div.news-title").select("h2").text() val mediaTitle = it.select("div.news-title").select("h2").text()
val mediaImage = it.select("img").attr("src") val mediaImage = it.select("img").attr("src")
if (mediaId != null) { if (mediaId != null) {
highlightsList.add(ItemMedia(mediaId, mediaTitle, mediaImage)) highlightsList.add(ItemMedia(mediaId, mediaTitle, mediaImage))
}
} }
}
// get all new episodes from AoD // get all new episodes from AoD
newEpisodesList.clear() newEpisodesList.clear()
resHome.select("h2:contains(Neue Episoden)").next().select("li").forEach { resHome.select("h2:contains(Neue Episoden)").next().select("li").forEach {
val mediaId = it.select("a.thumbs").attr("href") val mediaId = it.select("a.thumbs").attr("href")
.substringAfterLast("/").toIntOrNull() .substringAfterLast("/").toIntOrNull()
val mediaImage = it.select("a.thumbs > img").attr("src") val mediaImage = it.select("a.thumbs > img").attr("src")
val mediaTitle = "${it.select("a").text()} - ${it.select("span.neweps").text()}" val mediaTitle = "${it.select("a").text()} - ${it.select("span.neweps").text()}"
if (mediaId != null) { if (mediaId != null) {
newEpisodesList.add(ItemMedia(mediaId, mediaTitle, mediaImage)) newEpisodesList.add(ItemMedia(mediaId, mediaTitle, mediaImage))
}
} }
}
// get new simulcasts from AoD // get new simulcasts from AoD
newSimulcastsList.clear() newSimulcastsList.clear()
resHome.select("h2:contains(Neue Simulcasts)").next().select("li").forEach { resHome.select("h2:contains(Neue Simulcasts)").next().select("li").forEach {
val mediaId = it.select("a.thumbs").attr("href") val mediaId = it.select("a.thumbs").attr("href")
.substringAfterLast("/").toIntOrNull() .substringAfterLast("/").toIntOrNull()
val mediaImage = it.select("a.thumbs > img").attr("src") val mediaImage = it.select("a.thumbs > img").attr("src")
val mediaTitle = it.select("a").text() val mediaTitle = it.select("a").text()
if (mediaId != null) { if (mediaId != null) {
newSimulcastsList.add(ItemMedia(mediaId, mediaTitle, mediaImage)) newSimulcastsList.add(ItemMedia(mediaId, mediaTitle, mediaImage))
}
} }
}
// get new titles from AoD // get new titles from AoD
newTitlesList.clear() newTitlesList.clear()
resHome.select("h2:contains(Neue Anime-Titel)").next().select("li").forEach { resHome.select("h2:contains(Neue Anime-Titel)").next().select("li").forEach {
val mediaId = it.select("a.thumbs").attr("href") val mediaId = it.select("a.thumbs").attr("href")
.substringAfterLast("/").toIntOrNull() .substringAfterLast("/").toIntOrNull()
val mediaImage = it.select("a.thumbs > img").attr("src") val mediaImage = it.select("a.thumbs > img").attr("src")
val mediaTitle = it.select("a").text() val mediaTitle = it.select("a").text()
if (mediaId != null) { if (mediaId != null) {
newTitlesList.add(ItemMedia(mediaId, mediaTitle, mediaImage)) newTitlesList.add(ItemMedia(mediaId, mediaTitle, mediaImage))
}
} }
}
// get top ten from AoD // get top ten from AoD
topTenList.clear() topTenList.clear()
resHome.select("h2:contains(Anime Top 10)").next().select("li").forEach { resHome.select("h2:contains(Anime Top 10)").next().select("li").forEach {
val mediaId = it.select("a.thumbs").attr("href") val mediaId = it.select("a.thumbs").attr("href")
.substringAfterLast("/").toIntOrNull() .substringAfterLast("/").toIntOrNull()
val mediaImage = it.select("a.thumbs > img").attr("src") val mediaImage = it.select("a.thumbs > img").attr("src")
val mediaTitle = it.select("a").text() val mediaTitle = it.select("a").text()
if (mediaId != null) { if (mediaId != null) {
topTenList.add(ItemMedia(mediaId, mediaTitle, mediaImage)) topTenList.add(ItemMedia(mediaId, mediaTitle, mediaImage))
}
} }
}
// if highlights is empty, add a random new title // if highlights is empty, add a random new title
if (highlightsList.isEmpty()) { if (highlightsList.isEmpty()) {
if (newTitlesList.isNotEmpty()) { if (newTitlesList.isNotEmpty()) {
highlightsList.add(newTitlesList[Random.nextInt(0, newTitlesList.size)]) highlightsList.add(newTitlesList[Random.nextInt(0, newTitlesList.size)])
} else { } else {
highlightsList.add(ItemMedia(0,"", "")) highlightsList.add(ItemMedia(0,"", ""))
}
} }
Log.i(javaClass.name, "loaded home")
} }
} }
/** /**
* TODO rework the media loading process, don't modify media object
* TODO catch SocketTimeoutException from loading to show a waring dialog
* load streams for the media path, movies have one episode * load streams for the media path, movies have one episode
* @param media is used as call ba reference * @param media is used as call ba reference
*/ */
private fun loadStreams(media: Media) = GlobalScope.launch(Dispatchers.IO) { private suspend fun loadStreams(media: Media) = coroutineScope {
if (sessionCookies.isEmpty()) login() launch(Dispatchers.IO) {
if (sessionCookies.isEmpty()) login()
if (!loginSuccess) { if (!loginSuccess) {
Log.w(javaClass.name, "Login, was not successful.") Log.w(javaClass.name, "Login, was not successful.")
return@launch return@launch
}
// get the media page
val res = Jsoup.connect(baseUrl + media.link)
.cookies(sessionCookies)
.get()
//println(res)
if (csrfToken.isEmpty()) {
csrfToken = res.select("meta[name=csrf-token]").attr("content")
//Log.i(javaClass.name, "New csrf token is $csrfToken")
}
val besides = res.select("div.besides").first()
val playlists = besides.select("input.streamstarter_html5").map { streamstarter ->
parsePlaylistAsync(
streamstarter.attr("data-playlist"),
streamstarter.attr("data-lang")
)
}.awaitAll()
playlists.forEach { aod ->
// TODO improve language handling
val locale = when (aod.extLanguage) {
"ger" -> Locale.GERMAN
"jap" -> Locale.JAPANESE
else -> Locale.ROOT
} }
aod.playlist.forEach { ep -> // get the media page
try { val res = Jsoup.connect(baseUrl + media.link)
if (media.hasEpisode(ep.mediaid)) { .cookies(sessionCookies)
media.getEpisodeById(ep.mediaid).streams.add( .get()
Stream(ep.sources.first().file, locale)
) //println(res)
} else {
media.episodes.add(Episode( if (csrfToken.isEmpty()) {
id = ep.mediaid, csrfToken = res.select("meta[name=csrf-token]").attr("content")
streams = mutableListOf(Stream(ep.sources.first().file, locale)), //Log.i(javaClass.name, "New csrf token is $csrfToken")
posterUrl = ep.image, }
title = ep.title,
description = ep.description, val besides = res.select("div.besides").first()
number = getNumberFromTitle(ep.title, media.type) val playlists = besides.select("input.streamstarter_html5").map { streamstarter ->
)) parsePlaylistAsync(
} streamstarter.attr("data-playlist"),
} catch (ex: Exception) { streamstarter.attr("data-lang")
Log.w(javaClass.name, "Could not parse episode information.", ex) )
}.awaitAll()
playlists.forEach { aod ->
// TODO improve language handling
val locale = when (aod.extLanguage) {
"ger" -> Locale.GERMAN
"jap" -> Locale.JAPANESE
else -> Locale.ROOT
} }
}
}
Log.i(javaClass.name, "Loaded playlists successfully")
// additional info from the media page aod.playlist.forEach { ep ->
res.select("table.vertical-table").select("tr").forEach { row -> try {
when (row.select("th").text().toLowerCase(Locale.ROOT)) { if (media.hasEpisode(ep.mediaid)) {
"produktionsjahr" -> media.info.year = row.select("td").text().toInt() media.getEpisodeById(ep.mediaid).streams.add(
"fsk" -> media.info.age = row.select("td").text().toInt() Stream(ep.sources.first().file, locale)
"episodenanzahl" -> { )
media.info.episodesCount = row.select("td").text() } else {
.substringBefore("/") media.episodes.add(Episode(
.filter { it.isDigit() } id = ep.mediaid,
.toInt() streams = mutableListOf(Stream(ep.sources.first().file, locale)),
} posterUrl = ep.image,
} title = ep.title,
} description = ep.description,
number = getNumberFromTitle(ep.title, media.type)
// similar titles from media page ))
media.info.similar = res.select("h2:contains(Ähnliche Animes)").next().select("li").mapNotNull { }
val mediaId = it.select("a.thumbs").attr("href") } catch (ex: Exception) {
.substringAfterLast("/").toIntOrNull() Log.w(javaClass.name, "Could not parse episode information.", ex)
val mediaImage = it.select("a.thumbs > img").attr("src")
val mediaTitle = it.select("a").text()
if (mediaId != null) {
ItemMedia(mediaId, mediaTitle, mediaImage)
} else {
null
}
}
// additional information for tv shows the episode title (description) is loaded from the "api"
if (media.type == MediaType.TVSHOW) {
res.select("div.three-box-container > div.episodebox").forEach { episodebox ->
// make sure the episode has a streaming link
if (episodebox.select("input.streamstarter_html5").isNotEmpty()) {
val episodeId = episodebox.select("div.flip-front").attr("id").substringAfter("-").toInt()
val episodeShortDesc = episodebox.select("p.episodebox-shorttext").text()
val episodeWatched = episodebox.select("div.episodebox-icons > div").hasClass("status-icon-orange")
val episodeWatchedCallback = episodebox.select("input.streamstarter_html5").eachAttr("data-playlist").first()
media.episodes.firstOrNull { it.id == episodeId }?.apply {
shortDesc = episodeShortDesc
watched = episodeWatched
watchedCallback = episodeWatchedCallback
} }
} }
} }
Log.i(javaClass.name, "Loaded playlists successfully")
// additional info from the media page
res.select("table.vertical-table").select("tr").forEach { row ->
when (row.select("th").text().lowercase(Locale.ROOT)) {
"produktionsjahr" -> media.info.year = row.select("td").text().toInt()
"fsk" -> media.info.age = row.select("td").text().toInt()
"episodenanzahl" -> {
media.info.episodesCount = row.select("td").text()
.substringBefore("/")
.filter { it.isDigit() }
.toInt()
}
}
}
// similar titles from media page
media.info.similar = res.select("h2:contains(Ähnliche Animes)").next().select("li").mapNotNull {
val mediaId = it.select("a.thumbs").attr("href")
.substringAfterLast("/").toIntOrNull()
val mediaImage = it.select("a.thumbs > img").attr("src")
val mediaTitle = it.select("a").text()
if (mediaId != null) {
ItemMedia(mediaId, mediaTitle, mediaImage)
} else {
null
}
}
// additional information for tv shows the episode title (description) is loaded from the "api"
if (media.type == MediaType.TVSHOW) {
res.select("div.three-box-container > div.episodebox").forEach { episodebox ->
// make sure the episode has a streaming link
if (episodebox.select("input.streamstarter_html5").isNotEmpty()) {
val episodeId = episodebox.select("div.flip-front").attr("id").substringAfter("-").toInt()
val episodeShortDesc = episodebox.select("p.episodebox-shorttext").text()
val episodeWatched = episodebox.select("div.episodebox-icons > div").hasClass("status-icon-orange")
val episodeWatchedCallback = episodebox.select("input.streamstarter_html5").eachAttr("data-playlist").first()
media.episodes.firstOrNull { it.id == episodeId }?.apply {
shortDesc = episodeShortDesc
watched = episodeWatched
watchedCallback = episodeWatchedCallback
}
}
}
}
Log.i(javaClass.name, "media loaded successfully")
} }
Log.i(javaClass.name, "media loaded successfully")
} }
/** /**
@ -380,7 +415,7 @@ object AoDParser {
return CompletableDeferred(AoDObject(listOf(), language)) return CompletableDeferred(AoDObject(listOf(), language))
} }
return GlobalScope.async(Dispatchers.IO) { return CoroutineScope(Dispatchers.IO).async(Dispatchers.IO) {
val headers = mutableMapOf( val headers = mutableMapOf(
Pair("Accept", "application/json, text/javascript, */*; q=0.01"), Pair("Accept", "application/json, text/javascript, */*; q=0.01"),
Pair("Accept-Language", "de,en-US;q=0.7,en;q=0.3"), Pair("Accept-Language", "de,en-US;q=0.7,en;q=0.3"),

View File

@ -11,6 +11,8 @@ object Preferences {
internal set internal set
var autoplay = true var autoplay = true
internal set internal set
var devSettings = false
internal set
var theme = DataTypes.Theme.DARK var theme = DataTypes.Theme.DARK
internal set internal set
@ -39,6 +41,15 @@ object Preferences {
this.autoplay = autoplay this.autoplay = autoplay
} }
fun saveDevSettings(context: Context, devSettings: Boolean) {
with(getSharedPref(context).edit()) {
putBoolean(context.getString(R.string.save_key_dev_settings), devSettings)
apply()
}
this.devSettings = devSettings
}
fun saveTheme(context: Context, theme: DataTypes.Theme) { fun saveTheme(context: Context, theme: DataTypes.Theme) {
with(getSharedPref(context).edit()) { with(getSharedPref(context).edit()) {
putString(context.getString(R.string.save_key_theme), theme.toString()) putString(context.getString(R.string.save_key_theme), theme.toString())
@ -60,6 +71,9 @@ object Preferences {
autoplay = sharedPref.getBoolean( autoplay = sharedPref.getBoolean(
context.getString(R.string.save_key_autoplay), true context.getString(R.string.save_key_autoplay), true
) )
devSettings = sharedPref.getBoolean(
context.getString(R.string.save_key_dev_settings), false
)
theme = DataTypes.Theme.valueOf( theme = DataTypes.Theme.valueOf(
sharedPref.getString( sharedPref.getString(
context.getString(R.string.save_key_theme), DataTypes.Theme.DARK.toString() context.getString(R.string.save_key_theme), DataTypes.Theme.DARK.toString()

View File

@ -31,28 +31,27 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.commit import androidx.fragment.app.commit
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.callbacks.onDismiss import com.afollestad.materialdialogs.callbacks.onDismiss
import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.navigation.NavigationBarView
import kotlinx.coroutines.joinAll import kotlinx.coroutines.*
import kotlinx.coroutines.runBlocking
import org.mosad.teapod.R import org.mosad.teapod.R
import org.mosad.teapod.databinding.ActivityMainBinding import org.mosad.teapod.databinding.ActivityMainBinding
import org.mosad.teapod.parser.AoDParser import org.mosad.teapod.parser.AoDParser
import org.mosad.teapod.ui.activity.player.PlayerActivity
import org.mosad.teapod.preferences.EncryptedPreferences import org.mosad.teapod.preferences.EncryptedPreferences
import org.mosad.teapod.preferences.Preferences import org.mosad.teapod.preferences.Preferences
import org.mosad.teapod.ui.components.LoginDialog
import org.mosad.teapod.ui.activity.main.fragments.AccountFragment import org.mosad.teapod.ui.activity.main.fragments.AccountFragment
import org.mosad.teapod.ui.activity.main.fragments.HomeFragment import org.mosad.teapod.ui.activity.main.fragments.HomeFragment
import org.mosad.teapod.ui.activity.main.fragments.LibraryFragment import org.mosad.teapod.ui.activity.main.fragments.LibraryFragment
import org.mosad.teapod.ui.activity.main.fragments.SearchFragment import org.mosad.teapod.ui.activity.main.fragments.SearchFragment
import org.mosad.teapod.ui.activity.onboarding.OnboardingActivity import org.mosad.teapod.ui.activity.onboarding.OnboardingActivity
import org.mosad.teapod.ui.activity.player.PlayerActivity
import org.mosad.teapod.ui.components.LoginDialog
import org.mosad.teapod.util.DataTypes import org.mosad.teapod.util.DataTypes
import org.mosad.teapod.util.StorageController import org.mosad.teapod.util.StorageController
import org.mosad.teapod.util.exitAndRemoveTask import org.mosad.teapod.util.exitAndRemoveTask
import java.net.SocketTimeoutException import java.net.SocketTimeoutException
import kotlin.system.measureTimeMillis import kotlin.system.measureTimeMillis
class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemSelectedListener { class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListener {
private lateinit var binding: ActivityMainBinding private lateinit var binding: ActivityMainBinding
private var activeBaseFragment: Fragment = HomeFragment() // the currently active fragment, home at the start private var activeBaseFragment: Fragment = HomeFragment() // the currently active fragment, home at the start
@ -73,7 +72,7 @@ class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemS
theme.applyStyle(getThemeResource(), true) theme.applyStyle(getThemeResource(), true)
binding = ActivityMainBinding.inflate(layoutInflater) binding = ActivityMainBinding.inflate(layoutInflater)
binding.navView.setOnNavigationItemSelectedListener(this) binding.navView.setOnItemSelectedListener(this)
setContentView(binding.root) setContentView(binding.root)
supportFragmentManager.commit { supportFragmentManager.commit {
@ -138,14 +137,15 @@ class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemS
*/ */
private fun load() { private fun load() {
val time = measureTimeMillis { val time = measureTimeMillis {
val loadingJob = AoDParser.initialLoading() // start the initial loading val loadingJob = CoroutineScope(Dispatchers.IO + CoroutineName("InitialLoadingScope"))
.async { AoDParser.initialLoading() } // start the initial loading
// load all saved stuff here // load all saved stuff here
Preferences.load(this) Preferences.load(this)
EncryptedPreferences.readCredentials(this) EncryptedPreferences.readCredentials(this)
StorageController.load(this) StorageController.load(this)
// show onbaording // show onboarding
if (EncryptedPreferences.password.isEmpty()) { if (EncryptedPreferences.password.isEmpty()) {
showOnboarding() showOnboarding()
} else { } else {
@ -165,7 +165,7 @@ class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemS
} }
} }
runBlocking { loadingJob.joinAll() } // wait for initial loading to finish runBlocking { loadingJob.await() } // wait for initial loading to finish
} }
Log.i(javaClass.name, "loading and login in $time ms") Log.i(javaClass.name, "loading and login in $time ms")

View File

@ -6,6 +6,7 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast
import androidx.annotation.RawRes import androidx.annotation.RawRes
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
@ -13,15 +14,21 @@ import org.mosad.teapod.BuildConfig
import org.mosad.teapod.R import org.mosad.teapod.R
import org.mosad.teapod.databinding.FragmentAboutBinding import org.mosad.teapod.databinding.FragmentAboutBinding
import org.mosad.teapod.databinding.ItemComponentBinding import org.mosad.teapod.databinding.ItemComponentBinding
import org.mosad.teapod.preferences.Preferences
import org.mosad.teapod.util.DataTypes.License import org.mosad.teapod.util.DataTypes.License
import org.mosad.teapod.util.ThirdPartyComponent import org.mosad.teapod.util.ThirdPartyComponent
import java.lang.StringBuilder import java.lang.StringBuilder
import java.util.Timer
import kotlin.concurrent.schedule
class AboutFragment : Fragment() { class AboutFragment : Fragment() {
private val teapodRepoUrl = "https://git.mosad.xyz/Seil0/teapod"
private lateinit var binding: FragmentAboutBinding private lateinit var binding: FragmentAboutBinding
private val teapodRepoUrl = "https://git.mosad.xyz/Seil0/teapod"
private val devClickMax = 5
private var devClickCount = 0
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = FragmentAboutBinding.inflate(inflater, container, false) binding = FragmentAboutBinding.inflate(inflater, container, false)
return binding.root return binding.root
@ -52,6 +59,10 @@ class AboutFragment : Fragment() {
} }
private fun initActions() { private fun initActions() {
binding.imageAppIcon.setOnClickListener {
checkDevSettings()
}
binding.linearSource.setOnClickListener { binding.linearSource.setOnClickListener {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(teapodRepoUrl))) startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(teapodRepoUrl)))
} }
@ -64,6 +75,30 @@ class AboutFragment : Fragment() {
} }
} }
/**
* check if dev settings shall be enabled
*/
private fun checkDevSettings() {
// if the dev settings are already enabled show a toast
if (Preferences.devSettings) {
Toast.makeText(context, getString(R.string.dev_settings_already), Toast.LENGTH_SHORT).show()
return
}
// reset dev settings count after 5 seconds
if (devClickCount == 0) {
Timer("", false).schedule(5000) {
devClickCount = 0
}
}
devClickCount++
if (devClickCount == devClickMax) {
Preferences.saveDevSettings(requireContext(), true)
Toast.makeText(context, getString(R.string.dev_settings_enabled), Toast.LENGTH_SHORT).show()
}
}
private fun getThirdPartyComponents(): List<ThirdPartyComponent> { private fun getThirdPartyComponents(): List<ThirdPartyComponent> {
return listOf( return listOf(
ThirdPartyComponent("AndroidX", "", "The Android Open Source Project", ThirdPartyComponent("AndroidX", "", "The Android Open Source Project",

View File

@ -1,28 +1,59 @@
package org.mosad.teapod.ui.activity.main.fragments package org.mosad.teapod.ui.activity.main.fragments
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.list.listItemsSingleChoice import com.afollestad.materialdialogs.list.listItemsSingleChoice
import kotlinx.coroutines.launch
import org.mosad.teapod.BuildConfig import org.mosad.teapod.BuildConfig
import org.mosad.teapod.ui.activity.main.MainActivity
import org.mosad.teapod.R import org.mosad.teapod.R
import org.mosad.teapod.databinding.FragmentAccountBinding import org.mosad.teapod.databinding.FragmentAccountBinding
import org.mosad.teapod.parser.AoDParser import org.mosad.teapod.parser.AoDParser
import org.mosad.teapod.preferences.EncryptedPreferences import org.mosad.teapod.preferences.EncryptedPreferences
import org.mosad.teapod.preferences.Preferences import org.mosad.teapod.preferences.Preferences
import org.mosad.teapod.ui.activity.main.MainActivity
import org.mosad.teapod.ui.components.LoginDialog import org.mosad.teapod.ui.components.LoginDialog
import org.mosad.teapod.util.DataTypes.Theme import org.mosad.teapod.util.DataTypes.Theme
import org.mosad.teapod.util.StorageController
import org.mosad.teapod.util.showFragment import org.mosad.teapod.util.showFragment
class AccountFragment : Fragment() { class AccountFragment : Fragment() {
private lateinit var binding: FragmentAccountBinding private lateinit var binding: FragmentAccountBinding
private val getUriExport = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
result.data?.data?.also { uri ->
StorageController.exportMyList(requireContext(), uri)
}
}
}
private val getUriImport = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
result.data?.data?.also { uri ->
val success = StorageController.importMyList(requireContext(), uri)
if (success == 0) {
Toast.makeText(
context, getString(R.string.import_data_success),
Toast.LENGTH_SHORT
).show()
}
}
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = FragmentAccountBinding.inflate(inflater, container, false) binding = FragmentAccountBinding.inflate(inflater, container, false)
return binding.root return binding.root
@ -31,6 +62,15 @@ class AccountFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
// load subscription (async) info before anything else
binding.textAccountSubscription.text = getString(R.string.account_subscription, getString(R.string.loading))
lifecycleScope.launch {
binding.textAccountSubscription.text = getString(
R.string.account_subscription,
AoDParser.getSubscriptionInfoAsync().await()
)
}
binding.textAccountLogin.text = EncryptedPreferences.login binding.textAccountLogin.text = EncryptedPreferences.login
binding.textInfoAboutDesc.text = getString(R.string.info_about_desc, BuildConfig.VERSION_NAME, getString(R.string.build_time)) binding.textInfoAboutDesc.text = getString(R.string.info_about_desc, BuildConfig.VERSION_NAME, getString(R.string.build_time))
binding.textThemeSelected.text = when (Preferences.theme) { binding.textThemeSelected.text = when (Preferences.theme) {
@ -41,6 +81,8 @@ class AccountFragment : Fragment() {
binding.switchSecondary.isChecked = Preferences.preferSecondary binding.switchSecondary.isChecked = Preferences.preferSecondary
binding.switchAutoplay.isChecked = Preferences.autoplay binding.switchAutoplay.isChecked = Preferences.autoplay
binding.linearDevSettings.isVisible = Preferences.devSettings
initActions() initActions()
} }
@ -49,6 +91,10 @@ class AccountFragment : Fragment() {
showLoginDialog(true) showLoginDialog(true)
} }
binding.linearAccountSubscription.setOnClickListener {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(AoDParser.getSubscriptionUrl())))
}
binding.linearTheme.setOnClickListener { binding.linearTheme.setOnClickListener {
showThemeDialog() showThemeDialog()
} }
@ -64,6 +110,23 @@ class AccountFragment : Fragment() {
binding.switchAutoplay.setOnClickListener { binding.switchAutoplay.setOnClickListener {
Preferences.saveAutoplay(requireContext(), binding.switchAutoplay.isChecked) Preferences.saveAutoplay(requireContext(), binding.switchAutoplay.isChecked)
} }
binding.linearExportData.setOnClickListener {
val i = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "text/json"
putExtra(Intent.EXTRA_TITLE, "my-list.json")
}
getUriExport.launch(i)
}
binding.linearImportData.setOnClickListener {
val i = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "*/*"
}
getUriImport.launch(i)
}
} }
private fun showLoginDialog(firstTry: Boolean) { private fun showLoginDialog(firstTry: Boolean) {
@ -92,11 +155,12 @@ class AccountFragment : Fragment() {
when(index) { when(index) {
0 -> Preferences.saveTheme(context, Theme.LIGHT) 0 -> Preferences.saveTheme(context, Theme.LIGHT)
1 -> Preferences.saveTheme(context, Theme.DARK) 1 -> Preferences.saveTheme(context, Theme.DARK)
else -> Preferences.saveTheme(context, Theme.LIGHT) else -> Preferences.saveTheme(context, Theme.DARK)
} }
(activity as MainActivity).restart() (activity as MainActivity).restart()
} }
} }
} }
} }

View File

@ -6,14 +6,13 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.mosad.teapod.R import org.mosad.teapod.R
import org.mosad.teapod.ui.activity.main.MainActivity
import org.mosad.teapod.databinding.FragmentHomeBinding import org.mosad.teapod.databinding.FragmentHomeBinding
import org.mosad.teapod.parser.AoDParser import org.mosad.teapod.parser.AoDParser
import org.mosad.teapod.ui.activity.main.MainActivity
import org.mosad.teapod.util.ItemMedia import org.mosad.teapod.util.ItemMedia
import org.mosad.teapod.util.StorageController import org.mosad.teapod.util.StorageController
import org.mosad.teapod.util.adapter.MediaItemAdapter import org.mosad.teapod.util.adapter.MediaItemAdapter
@ -40,7 +39,7 @@ class HomeFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
GlobalScope.launch(Dispatchers.Main) { lifecycleScope.launch {
context?.let { context?.let {
initHighlight() initHighlight()
initRecyclerViews() initRecyclerViews()
@ -73,12 +72,7 @@ class HomeFragment : Fragment() {
binding.recyclerTopTen.addItemDecoration(MediaItemDecoration(9)) binding.recyclerTopTen.addItemDecoration(MediaItemDecoration(9))
// my list // my list
val myListMedia = StorageController.myList.map { elementId -> adapterMyList = MediaItemAdapter(mapMyListToItemMedia())
AoDParser.itemMediaList.first {
elementId == it.id
}
}
adapterMyList = MediaItemAdapter(myListMedia)
binding.recyclerMyList.adapter = adapterMyList binding.recyclerMyList.adapter = adapterMyList
// new episodes // new episodes
@ -101,7 +95,7 @@ class HomeFragment : Fragment() {
private fun initActions() { private fun initActions() {
binding.buttonPlayHighlight.setOnClickListener { binding.buttonPlayHighlight.setOnClickListener {
// TODO get next episode // TODO get next episode
GlobalScope.launch { lifecycleScope.launch {
val media = AoDParser.getMediaById(highlightMedia.id) val media = AoDParser.getMediaById(highlightMedia.id)
Log.d(javaClass.name, "Starting Player with mediaId: ${media.id}") Log.d(javaClass.name, "Starting Player with mediaId: ${media.id}")
@ -154,14 +148,19 @@ class HomeFragment : Fragment() {
* * only update actual change and not all data (performance) * * only update actual change and not all data (performance)
*/ */
fun updateMyListMedia() { fun updateMyListMedia() {
val myListMedia = StorageController.myList.map { elementId -> adapterMyList.updateMediaList(mapMyListToItemMedia())
AoDParser.itemMediaList.first {
elementId == it.id
}
}
adapterMyList.updateMediaList(myListMedia)
adapterMyList.notifyDataSetChanged() adapterMyList.notifyDataSetChanged()
} }
private fun mapMyListToItemMedia(): List<ItemMedia> {
return StorageController.myList.mapNotNull { elementId ->
AoDParser.itemMediaList.firstOrNull { it.id == elementId }.also {
// it the my list entry wasn't found in itemMediaList Log it
if (it == null) {
Log.w(javaClass.name, "The element with the id $elementId was not found.")
}
}
}
}
} }

View File

@ -5,10 +5,8 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import kotlinx.coroutines.Dispatchers import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.mosad.teapod.databinding.FragmentLibraryBinding import org.mosad.teapod.databinding.FragmentLibraryBinding
import org.mosad.teapod.parser.AoDParser import org.mosad.teapod.parser.AoDParser
import org.mosad.teapod.util.adapter.MediaItemAdapter import org.mosad.teapod.util.adapter.MediaItemAdapter
@ -29,18 +27,16 @@ class LibraryFragment : Fragment() {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
// init async // init async
GlobalScope.launch { lifecycleScope.launch {
// create and set the adapter, needs context // create and set the adapter, needs context
withContext(Dispatchers.Main) { context?.let {
context?.let { adapter = MediaItemAdapter(AoDParser.itemMediaList)
adapter = MediaItemAdapter(AoDParser.itemMediaList) adapter.onItemClick = { mediaId, _ ->
adapter.onItemClick = { mediaId, _ -> activity?.showFragment(MediaFragment(mediaId))
activity?.showFragment(MediaFragment(mediaId))
}
binding.recyclerMediaLibrary.adapter = adapter
binding.recyclerMediaLibrary.addItemDecoration(MediaItemDecoration(9))
} }
binding.recyclerMediaLibrary.adapter = adapter
binding.recyclerMediaLibrary.addItemDecoration(MediaItemDecoration(9))
} }
} }

View File

@ -10,20 +10,21 @@ import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.adapter.FragmentStateAdapter
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.RequestOptions
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
import jp.wasabeef.glide.transformations.BlurTransformation import jp.wasabeef.glide.transformations.BlurTransformation
import kotlinx.coroutines.* import kotlinx.coroutines.launch
import org.mosad.teapod.R import org.mosad.teapod.R
import org.mosad.teapod.databinding.FragmentMediaBinding import org.mosad.teapod.databinding.FragmentMediaBinding
import org.mosad.teapod.ui.activity.main.MainActivity import org.mosad.teapod.ui.activity.main.MainActivity
import org.mosad.teapod.ui.activity.main.viewmodel.MediaFragmentViewModel import org.mosad.teapod.ui.activity.main.viewmodel.MediaFragmentViewModel
import org.mosad.teapod.util.*
import org.mosad.teapod.util.DataTypes.MediaType import org.mosad.teapod.util.DataTypes.MediaType
import org.mosad.teapod.util.Episode
import org.mosad.teapod.util.StorageController
/** /**
* The media detail fragment. * The media detail fragment.
@ -61,13 +62,12 @@ class MediaFragment(private val mediaId: Int) : Fragment() {
} }
}.attach() }.attach()
GlobalScope.launch(Dispatchers.Main) {
lifecycleScope.launch {
model.load(mediaId) // load the streams and tmdb for the selected media model.load(mediaId) // load the streams and tmdb for the selected media
if (this@MediaFragment.isAdded) { updateGUI()
updateGUI() initActions()
initActions()
}
} }
} }

View File

@ -6,7 +6,8 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.SearchView import android.widget.SearchView
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import kotlinx.coroutines.* import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
import org.mosad.teapod.databinding.FragmentSearchBinding import org.mosad.teapod.databinding.FragmentSearchBinding
import org.mosad.teapod.parser.AoDParser import org.mosad.teapod.parser.AoDParser
import org.mosad.teapod.util.decoration.MediaItemDecoration import org.mosad.teapod.util.decoration.MediaItemDecoration
@ -26,9 +27,8 @@ class SearchFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
GlobalScope.launch { lifecycleScope.launch {
// create and set the adapter, needs context // create and set the adapter, needs context
withContext(Dispatchers.Main) {
context?.let { context?.let {
adapter = MediaItemAdapter(AoDParser.itemMediaList) adapter = MediaItemAdapter(AoDParser.itemMediaList)
adapter!!.onItemClick = { mediaId, _ -> adapter!!.onItemClick = { mediaId, _ ->
@ -39,7 +39,6 @@ class SearchFragment : Fragment() {
binding.recyclerMediaSearch.adapter = adapter binding.recyclerMediaSearch.adapter = adapter
binding.recyclerMediaSearch.addItemDecoration(MediaItemDecoration(9)) binding.recyclerMediaSearch.addItemDecoration(MediaItemDecoration(9))
} }
}
} }
initActions() initActions()

View File

@ -5,6 +5,7 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.* import kotlinx.coroutines.*
import org.mosad.teapod.R import org.mosad.teapod.R
import org.mosad.teapod.databinding.FragmentOnLoginBinding import org.mosad.teapod.databinding.FragmentOnLoginBinding
@ -35,7 +36,7 @@ class OnLoginFragment: Fragment() {
EncryptedPreferences.saveCredentials(email, password, requireContext()) // save the credentials EncryptedPreferences.saveCredentials(email, password, requireContext()) // save the credentials
binding.buttonLogin.isClickable = false binding.buttonLogin.isClickable = false
loginJob = GlobalScope.launch { loginJob = lifecycleScope.launch {
if (AoDParser.login()) { if (AoDParser.login()) {
// if login was successful, switch to main // if login was successful, switch to main
if (activity is OnboardingActivity) { if (activity is OnboardingActivity) {

View File

@ -7,6 +7,7 @@ import android.app.PictureInPictureParams
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.res.Configuration import android.content.res.Configuration
import android.graphics.Rect
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
@ -19,16 +20,14 @@ import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.GestureDetectorCompat import androidx.core.view.GestureDetectorCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import com.google.android.exoplayer2.ExoPlayer import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.Player import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.ui.StyledPlayerControlView import com.google.android.exoplayer2.ui.StyledPlayerControlView
import com.google.android.exoplayer2.util.Util import com.google.android.exoplayer2.util.Util
import kotlinx.android.synthetic.main.activity_player.* import kotlinx.android.synthetic.main.activity_player.*
import kotlinx.android.synthetic.main.player_controls.* import kotlinx.android.synthetic.main.player_controls.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.mosad.teapod.R import org.mosad.teapod.R
import org.mosad.teapod.preferences.Preferences import org.mosad.teapod.preferences.Preferences
import org.mosad.teapod.ui.components.EpisodesListPlayer import org.mosad.teapod.ui.components.EpisodesListPlayer
@ -147,8 +146,15 @@ class PlayerActivity : AppCompatActivity() {
} else { } else {
val width = model.player.videoFormat?.width ?: 0 val width = model.player.videoFormat?.width ?: 0
val height = model.player.videoFormat?.height ?: 0 val height = model.player.videoFormat?.height ?: 0
val contentFrame: View = video_view.findViewById(R.id.exo_content_frame)
val contentRect = with(contentFrame) {
val (x, y) = intArrayOf(0, 0).also(::getLocationInWindow)
Rect(x, y, x + width, y + height)
}
val params = PictureInPictureParams.Builder() val params = PictureInPictureParams.Builder()
.setAspectRatio(Rational(width, height)) .setAspectRatio(Rational(width, height))
.setSourceRectHint(contentRect)
.build() .build()
enterPictureInPictureMode(params) enterPictureInPictureMode(params)
} }
@ -187,7 +193,7 @@ class PlayerActivity : AppCompatActivity() {
* set play when ready and listeners * set play when ready and listeners
*/ */
private fun initExoPlayer() { private fun initExoPlayer() {
model.player.addListener(object : Player.EventListener { model.player.addListener(object : Player.Listener {
override fun onPlaybackStateChanged(state: Int) { override fun onPlaybackStateChanged(state: Int) {
super.onPlaybackStateChanged(state) super.onPlaybackStateChanged(state)
@ -208,7 +214,7 @@ class PlayerActivity : AppCompatActivity() {
} }
} }
}) })
// start playing the current episode, after all needed player components have been initialized // start playing the current episode, after all needed player components have been initialized
model.playEpisode(model.currentEpisode, true) model.playEpisode(model.currentEpisode, true)
} }
@ -255,31 +261,27 @@ class PlayerActivity : AppCompatActivity() {
} }
timerUpdates = Timer().scheduleAtFixedRate(0, 500) { timerUpdates = Timer().scheduleAtFixedRate(0, 500) {
GlobalScope.launch { lifecycleScope.launch {
var btnNextEpIsVisible: Boolean val btnNextEpIsVisible = button_next_ep.isVisible
var controlsVisible: Boolean val controlsVisible = controller.isVisible
withContext(Dispatchers.Main) { if (model.player.duration > 0) {
if (model.player.duration > 0) { remainingTime = model.player.duration - model.player.currentPosition
remainingTime = model.player.duration - model.player.currentPosition remainingTime = if (remainingTime < 0) 0 else remainingTime
remainingTime = if (remainingTime < 0) 0 else remainingTime
}
btnNextEpIsVisible = button_next_ep.isVisible
controlsVisible = controller.isVisible
} }
if (remainingTime in 1..20000) { if (remainingTime in 1..20000) {
// if the next ep button is not visible, make it visible. Don't show in pip mode // if the next ep button is not visible, make it visible. Don't show in pip mode
if (!btnNextEpIsVisible && model.nextEpisode != null && Preferences.autoplay && !isInPiPMode()) { if (!btnNextEpIsVisible && model.nextEpisode != null && Preferences.autoplay && !isInPiPMode()) {
withContext(Dispatchers.Main) { showButtonNextEp() } showButtonNextEp()
} }
} else if (btnNextEpIsVisible) { } else if (btnNextEpIsVisible) {
withContext(Dispatchers.Main) { hideButtonNextEp() } hideButtonNextEp()
} }
// if controls are visible, update them // if controls are visible, update them
if (controlsVisible) { if (controlsVisible) {
withContext(Dispatchers.Main) { updateControls() } updateControls()
} }
} }
} }

View File

@ -2,15 +2,19 @@ package org.mosad.teapod.ui.activity.player
import android.app.Application import android.app.Application
import android.net.Uri import android.net.Uri
import android.support.v4.media.session.MediaSessionCompat
import android.util.Log import android.util.Log
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.google.android.exoplayer2.C import com.google.android.exoplayer2.C
import com.google.android.exoplayer2.MediaItem import com.google.android.exoplayer2.MediaItem
import com.google.android.exoplayer2.SimpleExoPlayer import com.google.android.exoplayer2.SimpleExoPlayer
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
import com.google.android.exoplayer2.source.MediaSource import com.google.android.exoplayer2.source.MediaSource
import com.google.android.exoplayer2.source.hls.HlsMediaSource import com.google.android.exoplayer2.source.hls.HlsMediaSource
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory
import com.google.android.exoplayer2.util.Util import com.google.android.exoplayer2.util.Util
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.mosad.teapod.R import org.mosad.teapod.R
import org.mosad.teapod.parser.AoDParser import org.mosad.teapod.parser.AoDParser
@ -29,10 +33,11 @@ import kotlin.collections.ArrayList
class PlayerViewModel(application: Application) : AndroidViewModel(application) { class PlayerViewModel(application: Application) : AndroidViewModel(application) {
val player = SimpleExoPlayer.Builder(application).build() val player = SimpleExoPlayer.Builder(application).build()
val dataSourceFactory = DefaultDataSourceFactory(application, Util.getUserAgent(application, "Teapod")) private val dataSourceFactory = DefaultDataSourceFactory(application, Util.getUserAgent(application, "Teapod"))
private val mediaSession = MediaSessionCompat(application, "TEAPOD_PLAYER_SESSION")
val currentEpisodeChangedListener = ArrayList<() -> Unit>() val currentEpisodeChangedListener = ArrayList<() -> Unit>()
val preferredLanguage = if (Preferences.preferSecondary) Locale.JAPANESE else Locale.GERMAN private val preferredLanguage = if (Preferences.preferSecondary) Locale.JAPANESE else Locale.GERMAN
var media: Media = Media(-1, "", DataTypes.MediaType.OTHER) var media: Media = Media(-1, "", DataTypes.MediaType.OTHER)
internal set internal set
@ -43,13 +48,30 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
var currentLanguage: Locale = Locale.ROOT var currentLanguage: Locale = Locale.ROOT
internal set internal set
init {
initMediaSession()
}
override fun onCleared() { override fun onCleared() {
super.onCleared() super.onCleared()
mediaSession.release()
player.release() player.release()
Log.d(javaClass.name, "Released player") Log.d(javaClass.name, "Released player")
} }
/**
* set the media session to active
* create a media session connector to set title and description
*/
private fun initMediaSession() {
val mediaSessionConnector = MediaSessionConnector(mediaSession)
mediaSessionConnector.setPlayer(player)
mediaSession.isActive = true
}
fun loadMedia(mediaId: Int, episodeId: Int) { fun loadMedia(mediaId: Int, episodeId: Int) {
runBlocking { runBlocking {
media = AoDParser.getMediaById(mediaId) media = AoDParser.getMediaById(mediaId)
@ -107,10 +129,15 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
// if episodes has not been watched, mark as watched // if episodes has not been watched, mark as watched
if (!episode.watched) { if (!episode.watched) {
AoDParser.markAsWatched(media.id, episode.id) viewModelScope.launch {
AoDParser.markAsWatched(media.id, episode.id)
}
} }
} }
/**
* change the players media source and start playback
*/
fun playMedia(source: MediaSource, replace: Boolean = false, seekPosition: Long = 0) { fun playMedia(source: MediaSource, replace: Boolean = false, seekPosition: Long = 0) {
if (replace || player.contentDuration == C.TIME_UNSET) { if (replace || player.contentDuration == C.TIME_UNSET) {
player.setMediaSource(source) player.setMediaSource(source)

View File

@ -1,12 +1,14 @@
package org.mosad.teapod.util package org.mosad.teapod.util
import android.content.Context import android.content.Context
import android.net.Uri
import android.util.Log import android.util.Log
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.JsonParser import com.google.gson.JsonParser
import kotlinx.coroutines.* import kotlinx.coroutines.*
import java.io.File import java.io.File
import java.lang.Exception import java.io.FileReader
import java.io.FileWriter
/** /**
* This controller contains the logic for permanently saved data. * This controller contains the logic for permanently saved data.
@ -19,6 +21,10 @@ object StorageController {
val myList = ArrayList<Int>() // a list of saved mediaIds val myList = ArrayList<Int>() // a list of saved mediaIds
fun load(context: Context) { fun load(context: Context) {
loadMyList(context)
}
fun loadMyList(context: Context) {
val file = File(context.filesDir, fileNameMyList) val file = File(context.filesDir, fileNameMyList)
if (!file.exists()) runBlocking { saveMyList(context).join() } if (!file.exists()) runBlocking { saveMyList(context).join() }
@ -30,15 +36,54 @@ object StorageController {
myList.clear() myList.clear()
Log.e(javaClass.name, "Parsing of My-List failed.") Log.e(javaClass.name, "Parsing of My-List failed.")
} }
} }
fun saveMyList(context: Context): Job { fun saveMyList(context: Context): Job {
val file = File(context.filesDir, fileNameMyList) val file = File(context.filesDir, fileNameMyList)
return GlobalScope.launch(Dispatchers.IO) { return CoroutineScope(Dispatchers.IO).launch {
file.writeText(Gson().toJson(myList.distinct())) file.writeText(Gson().toJson(myList.distinct()))
} }
} }
fun exportMyList(context: Context, uri: Uri) {
try {
context.contentResolver.openFileDescriptor(uri, "w")?.use {
FileWriter(it.fileDescriptor).use { writer ->
writer.write(Gson().toJson(myList.distinct()))
}
}
} catch (ex: Exception) {
Log.e(javaClass.name, "Exporting my list failed.", ex)
}
}
/**
* import my list from a (previously exported) json file
* @param context the current context
* @param uri the uri of the selected file
* @return 0 if import was successfull, else 1
*/
fun importMyList(context: Context, uri: Uri): Int {
try {
val text = context.contentResolver.openFileDescriptor(uri, "r")?.use {
FileReader(it.fileDescriptor).use { reader ->
reader.readText()
}
}
myList.clear()
myList.addAll(JsonParser.parseString(text).asJsonArray.map { it.asInt }.distinct())
// after the list has been imported also save it
saveMyList(context)
} catch (ex: Exception) {
myList.clear()
Log.e(javaClass.name, "Importing my list failed.", ex)
return 1
}
return 0
}
} }

View File

@ -4,9 +4,9 @@ import android.util.Log
import com.google.gson.JsonObject import com.google.gson.JsonObject
import com.google.gson.JsonParser import com.google.gson.JsonParser
import kotlinx.coroutines.* import kotlinx.coroutines.*
import org.mosad.teapod.util.DataTypes.MediaType
import java.net.URL import java.net.URL
import java.net.URLEncoder import java.net.URLEncoder
import org.mosad.teapod.util.DataTypes.MediaType
class TMDBApiController { class TMDBApiController {
@ -21,11 +21,15 @@ class TMDBApiController {
private val imageUrl = "https://image.tmdb.org/t/p/w500" private val imageUrl = "https://image.tmdb.org/t/p/w500"
suspend fun search(title: String, type: MediaType): TMDBResponse { suspend fun search(title: String, type: MediaType): TMDBResponse {
val searchTerm = title.replace("(Sub)", "").trim() // remove unneeded text from the media title before searching
val searchTerm = title.replace("(Sub)", "")
.replace(Regex("-?\\s?[0-9]+.\\s?(Staffel|Season)"), "")
.replace(Regex("(Staffel|Season)\\s?[0-9]+"), "")
.trim()
return when (type) { return when (type) {
MediaType.MOVIE -> searchMovie(searchTerm).await() MediaType.MOVIE -> searchMovie(searchTerm)
MediaType.TVSHOW -> searchTVShow(searchTerm).await() MediaType.TVSHOW -> searchTVShow(searchTerm)
else -> { else -> {
Log.e(javaClass.name, "Wrong Type: $type") Log.e(javaClass.name, "Wrong Type: $type")
TMDBResponse() TMDBResponse()
@ -34,62 +38,64 @@ class TMDBApiController {
} }
fun searchTVShow(title: String): Deferred<TMDBResponse> { @Suppress("BlockingMethodInNonBlockingContext")
private suspend fun searchTVShow(title: String): TMDBResponse = withContext(Dispatchers.IO) {
val url = URL("$searchTVUrl$preparedParameters&query=${URLEncoder.encode(title, "UTF-8")}") val url = URL("$searchTVUrl$preparedParameters&query=${URLEncoder.encode(title, "UTF-8")}")
val response = JsonParser.parseString(url.readText()).asJsonObject
// println(response)
return GlobalScope.async { val sortedResults = response.get("results").asJsonArray.toList().sortedBy {
val response = JsonParser.parseString(url.readText()).asJsonObject getStringNotNull(it.asJsonObject, "name")
//println(response) }
if (response.get("total_results").asInt > 0) { return@withContext if (sortedResults.isNotEmpty()) {
response.get("results").asJsonArray.first().asJsonObject.let { sortedResults.first().asJsonObject.let {
val id = getStringNotNull(it, "id").toInt() val id = getStringNotNull(it, "id").toInt()
val overview = getStringNotNull(it, "overview") val overview = getStringNotNull(it, "overview")
val posterPath = getStringNotNullPrefix(it, "poster_path", imageUrl) val posterPath = getStringNotNullPrefix(it, "poster_path", imageUrl)
val backdropPath = getStringNotNullPrefix(it, "backdrop_path", imageUrl) val backdropPath = getStringNotNullPrefix(it, "backdrop_path", imageUrl)
TMDBResponse(id, "", overview, posterPath, backdropPath) TMDBResponse(id, "", overview, posterPath, backdropPath)
}
} else {
TMDBResponse()
} }
} else {
TMDBResponse()
} }
} }
fun searchMovie(title: String): Deferred<TMDBResponse> { @Suppress("BlockingMethodInNonBlockingContext")
private suspend fun searchMovie(title: String): TMDBResponse = withContext(Dispatchers.IO) {
val url = URL("$searchMovieUrl$preparedParameters&query=${URLEncoder.encode(title, "UTF-8")}") val url = URL("$searchMovieUrl$preparedParameters&query=${URLEncoder.encode(title, "UTF-8")}")
val response = JsonParser.parseString(url.readText()).asJsonObject
// println(response)
return GlobalScope.async { val sortedResults = response.get("results").asJsonArray.toList().sortedBy {
val response = JsonParser.parseString(url.readText()).asJsonObject getStringNotNull(it.asJsonObject, "title")
//println(response) }
if (response.get("total_results").asInt > 0) { return@withContext if (sortedResults.isNotEmpty()) {
response.get("results").asJsonArray.first().asJsonObject.let { sortedResults.first().asJsonObject.let {
val id = getStringNotNull(it,"id").toInt() val id = getStringNotNull(it,"id").toInt()
val overview = getStringNotNull(it,"overview") val overview = getStringNotNull(it,"overview")
val posterPath = getStringNotNullPrefix(it, "poster_path", imageUrl) val posterPath = getStringNotNullPrefix(it, "poster_path", imageUrl)
val backdropPath = getStringNotNullPrefix(it, "backdrop_path", imageUrl) val backdropPath = getStringNotNullPrefix(it, "backdrop_path", imageUrl)
val runtime = getMovieRuntime(id) val runtime = getMovieRuntime(id)
TMDBResponse(id, "", overview, posterPath, backdropPath, runtime) TMDBResponse(id, "", overview, posterPath, backdropPath, runtime)
}
} else {
TMDBResponse()
} }
} else {
TMDBResponse()
} }
} }
/** /**
* currently only used for runtime, need a rework * currently only used for runtime, need a rework
*/ */
fun getMovieRuntime(id: Int): Int = runBlocking { @Suppress("BlockingMethodInNonBlockingContext")
suspend fun getMovieRuntime(id: Int): Int = withContext(Dispatchers.IO) {
val url = URL("$getMovieUrl/$id?api_key=$apiKey&language=$language") val url = URL("$getMovieUrl/$id?api_key=$apiKey&language=$language")
GlobalScope.async { val response = JsonParser.parseString(url.readText()).asJsonObject
val response = JsonParser.parseString(url.readText()).asJsonObject return@withContext getStringNotNull(response,"runtime").toInt()
return@async getStringNotNull(response,"runtime").toInt()
}.await()
} }
/** /**

View File

@ -49,14 +49,14 @@ class MediaItemAdapter(private val initMedia: List<ItemMedia>) : RecyclerView.Ad
inner class MediaFilter : Filter() { inner class MediaFilter : Filter() {
override fun performFiltering(constraint: CharSequence?): FilterResults { override fun performFiltering(constraint: CharSequence?): FilterResults {
val filterTerm = constraint.toString().toLowerCase(Locale.ROOT) val filterTerm = constraint.toString().lowercase(Locale.ROOT)
val results = FilterResults() val results = FilterResults()
val filteredList = if (filterTerm.isEmpty()) { val filteredList = if (filterTerm.isEmpty()) {
initMedia initMedia
} else { } else {
initMedia.filter { initMedia.filter {
it.title.toLowerCase(Locale.ROOT).contains(filterTerm) it.title.lowercase(Locale.ROOT).contains(filterTerm)
} }
} }

View File

@ -0,0 +1,6 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z"/>
<path android:fillColor="@android:color/white" android:pathData="M12.5,7H11v6l5.25,3.15 0.75,-1.23 -4.5,-2.67z"/>
</vector>

View File

@ -3,17 +3,18 @@
android:height="108dp" android:height="108dp"
android:viewportWidth="108" android:viewportWidth="108"
android:viewportHeight="108"> android:viewportHeight="108">
<group android:scaleX="0.051679686" <group
android:scaleY="0.051679686" android:scaleX="0.051679686"
android:translateX="27.54" android:scaleY="0.051679686"
android:translateY="38.90954"> android:translateX="27.54"
<path android:translateY="38.90954">
android:pathData="m850.19,372.71c87.88,-11.01 119.04,-84.97 123.1,-99.87 4.06,-14.89 24.91,-80.57 11.92,-129.36 -12.99,-48.79 -34.36,-72.36 -58.62,-77.25 -24.25,-4.9 -50.59,10.51 -65,32.81 -14.41,22.3 -14.68,45.14 -14.78,55.29 -0.11,10.15 0.76,23.2 -3.37,33.29 -4.13,10.09 3.23,25.71 6.04,35.23 2.81,9.52 9.67,82.62 5.78,115.57 -3.89,32.95 -5.07,34.29 -5.07,34.29zM0.4,23.58C55.81,77.29 56.45,120.86 56.08,132.92c-0.36,12.06 4.77,130.59 11.47,150.76 4.42,13.3 12.11,50.16 41.78,74.48 25.51,20.91 58.65,31.38 58.65,31.38 0,0 36.42,78.46 78.83,108.64 31.56,22.46 39.61,23.74 46.5,35.55 6.18,10.6 93.56,62.62 275.1,47.23 127.29,-10.79 138.56,-44.3 138.56,-44.3 0,0 49.41,-21.9 101.15,-80.43 12.87,-14.56 4.41,-13.21 28.57,-17.79 24.16,-4.58 138.01,-45.58 170.66,-154.36C1039.99,175.32 1017.81,96.01 994.52,69.12 971.23,42.22 931.6,24.18 912.25,24.93c-18.47,0.71 -44.78,4.24 -80.21,46.87 -35.43,42.62 -28.94,37.4 -39.36,41.73 -6.82,2.83 -5.68,3.91 -26.75,-11.65 -20.23,-14.93 -28.9,-21.24 -43.38,-27.24 -7.96,-3.3 2.05,-5.55 2.59,-19.48 0.54,-13.93 2.4,-23.51 -17.32,-23.77 -19.72,-0.26 -408.02,0.21 -408.02,0.21 0,0 -18.8,-1.29 -7.79,24.82 4.2,9.94 -1.45,6.43 -33.27,25.85 -31.82,19.42 -55.58,34.4 -72.28,66.09 -8.43,16 -22.91,23.02 -27.97,8.05C153.44,141.43 125.2,48.96 105.17,23.22 85.56,-1.97 77.8,0.26 77.8,0.26Z" <path
android:strokeLineJoin="miter" android:fillColor="#000000"
android:strokeWidth="0.41878" android:fillType="evenOdd"
android:fillColor="#000000" android:pathData="m850.19,372.71c87.88,-11.01 119.04,-84.97 123.1,-99.87 4.06,-14.89 24.91,-80.57 11.92,-129.36 -12.99,-48.79 -34.36,-72.36 -58.62,-77.25 -24.25,-4.9 -50.59,10.51 -65,32.81 -14.41,22.3 -14.68,45.14 -14.78,55.29 -0.11,10.15 0.76,23.2 -3.37,33.29 -4.13,10.09 3.23,25.71 6.04,35.23 2.81,9.52 9.67,82.62 5.78,115.57 -3.89,32.95 -5.07,34.29 -5.07,34.29zM0.4,23.58C55.81,77.29 56.45,120.86 56.08,132.92c-0.36,12.06 4.77,130.59 11.47,150.76 4.42,13.3 12.11,50.16 41.78,74.48 25.51,20.91 58.65,31.38 58.65,31.38 0,0 36.42,78.46 78.83,108.64 31.56,22.46 39.61,23.74 46.5,35.55 6.18,10.6 93.56,62.62 275.1,47.23 127.29,-10.79 138.56,-44.3 138.56,-44.3 0,0 49.41,-21.9 101.15,-80.43 12.87,-14.56 4.41,-13.21 28.57,-17.79 24.16,-4.58 138.01,-45.58 170.66,-154.36C1039.99,175.32 1017.81,96.01 994.52,69.12 971.23,42.22 931.6,24.18 912.25,24.93c-18.47,0.71 -44.78,4.24 -80.21,46.87 -35.43,42.62 -28.94,37.4 -39.36,41.73 -6.82,2.83 -5.68,3.91 -26.75,-11.65 -20.23,-14.93 -28.9,-21.24 -43.38,-27.24 -7.96,-3.3 2.05,-5.55 2.59,-19.48 0.54,-13.93 2.4,-23.51 -17.32,-23.77 -19.72,-0.26 -408.02,0.21 -408.02,0.21 0,0 -18.8,-1.29 -7.79,24.82 4.2,9.94 -1.45,6.43 -33.27,25.85 -31.82,19.42 -55.58,34.4 -72.28,66.09 -8.43,16 -22.91,23.02 -27.97,8.05C153.44,141.43 125.2,48.96 105.17,23.22 85.56,-1.97 77.8,0.26 77.8,0.26Z"
android:strokeColor="#000000" android:strokeWidth="0.41878"
android:fillType="evenOdd" android:strokeColor="#000000"
android:strokeLineCap="butt"/> android:strokeLineCap="butt"
</group> android:strokeLineJoin="miter" />
</group>
</vector> </vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#FFFFFF"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M18,15v3H6v-3H4v3c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2v-3H18zM17,11l-1.41,-1.41L13,12.17V4h-2v8.17L8.41,9.59L7,11l5,5L17,11z" />
</vector>

View File

@ -1,5 +1,10 @@
<vector android:height="24dp" android:tint="#FFFFFF" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:viewportHeight="24" android:viewportWidth="24" android:width="24dp"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> android:height="24dp"
<path android:fillColor="@android:color/white" android:pathData="M11,7h2v2h-2zM11,11h2v6h-2zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8z"/> android:tint="#FFFFFF"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M11,7h2v2h-2zM11,11h2v6h-2zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8z" />
</vector> </vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#FFFFFF"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M18,15v3H6v-3H4v3c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2v-3H18zM7,9l1.41,1.41L11,7.83V16h2V7.83l2.59,2.58L17,9l-5,-5L7,9z" />
</vector>

View File

@ -79,8 +79,52 @@
android:text="@string/account_login_desc" android:text="@string/account_login_desc"
android:textColor="?textSecondary" /> android:textColor="?textSecondary" />
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
<LinearLayout
android:id="@+id/linear_account_subscription"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:foreground="?android:selectableItemBackground"
android:gravity="center"
android:orientation="horizontal"
android:padding="7dp">
<ImageView
android:id="@+id/imageView6"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/account"
android:minWidth="48dp"
android:minHeight="48dp"
android:padding="9dp"
android:scaleType="fitXY"
android:src="@drawable/ic_baseline_access_time_24"
app:tint="?iconColor" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/text_account_subscription"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/account_subscription"
android:textSize="16sp" />
<TextView
android:id="@+id/text_account_subscription_desc"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/account_subscription_desc"
android:textColor="?textSecondary" />
</LinearLayout>
</LinearLayout>
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout
@ -176,7 +220,7 @@
android:padding="7dp"> android:padding="7dp">
<ImageView <ImageView
android:id="@+id/imageView4" android:id="@+id/image_autoplay"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:contentDescription="@string/settings_autoplay" android:contentDescription="@string/settings_autoplay"
@ -237,7 +281,7 @@
android:padding="7dp"> android:padding="7dp">
<ImageView <ImageView
android:id="@+id/imageViewTheme" android:id="@+id/image_theme"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:contentDescription="@string/account" android:contentDescription="@string/account"
@ -274,6 +318,118 @@
</LinearLayout> </LinearLayout>
<LinearLayout
android:id="@+id/linear_dev_settings"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:background="?themeSecondary"
android:clipToPadding="false"
android:elevation="5dp"
android:orientation="vertical">
<TextView
android:id="@+id/text_dev_settings"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="7dp"
android:paddingEnd="7dp"
android:text="@string/dev_settings"
android:textSize="16sp"
android:textStyle="bold" />
<LinearLayout
android:id="@+id/linear_export_data"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:foreground="?android:selectableItemBackground"
android:gravity="center"
android:orientation="horizontal"
android:padding="7dp">
<ImageView
android:id="@+id/image_export_data"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/info"
android:minWidth="48dp"
android:minHeight="48dp"
android:padding="9dp"
android:scaleType="fitXY"
app:srcCompat="@drawable/ic_outline_upload_24"
app:tint="?iconColor" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/text_export_data"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/export_data"
android:textSize="16sp" />
<TextView
android:id="@+id/text_export_data_desc"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/export_data_desc"
android:textColor="?textSecondary" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/linear_import_data"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:foreground="?android:selectableItemBackground"
android:gravity="center"
android:orientation="horizontal"
android:padding="7dp">
<ImageView
android:id="@+id/image_import_data"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/info"
android:minWidth="48dp"
android:minHeight="48dp"
android:padding="9dp"
android:scaleType="fitXY"
app:srcCompat="@drawable/ic_outline_download_24"
app:tint="?iconColor" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/text_import_data"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/import_data"
android:textSize="16sp" />
<TextView
android:id="@+id/text_import_data_desc"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/import_data_desc"
android:textColor="?textSecondary" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
<LinearLayout <LinearLayout
android:id="@+id/linear_info" android:id="@+id/linear_info"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@ -86,7 +86,7 @@
android:id="@+id/button_select" android:id="@+id/button_select"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/save" android:text="@string/apply"
android:textAllCaps="false" android:textAllCaps="false"
android:textColor="@color/themePrimaryDark" android:textColor="@color/themePrimaryDark"
android:textSize="16sp" android:textSize="16sp"

View File

@ -1,68 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<notices>
<notice>
<name>AndroidX</name>
<url>https://developer.android.com/jetpack/androidx</url>
<copyright>Copyright The Android Open Source Project</copyright>
<license>Apache Software License 2.0</license>
</notice>
<notice>
<name>Material Components for Android</name>
<url>https://github.com/material-components/material-components-android</url>
<copyright>Copyright The Android Open Source Project</copyright>
<license>Apache Software License 2.0</license>
</notice>
<notice>
<name>ExoPlayer</name>
<url>https://github.com/google/ExoPlayer</url>
<copyright>Copyright The Android Open Source Project</copyright>
<license>Apache Software License 2.0</license>
</notice>
<notice>
<name>Gson</name>
<url>https://github.com/google/gson</url>
<copyright>Copyright 2008 Google Inc.</copyright>
<license>Apache Software License 2.0</license>
</notice>
<notice>
<name>Material design icons</name>
<url>https://github.com/google/material-design-icons</url>
<copyright>Copyright Google Inc.</copyright>
<license>Apache Software License 2.0</license>
</notice>
<notice>
<name>Material Dialogs</name>
<url>https://github.com/afollestad/material-dialogs</url>
<copyright>Copyright Aidan Follestad</copyright>
<license>Apache Software License 2.0</license>
</notice>
<notice>
<name>Jsoup</name>
<url>https://jsoup.org/</url>
<copyright>Copyright 2009 - 2020 Jonathan Hedley</copyright>
<license>MIT License</license>
</notice>
<notice>
<name>kotlinx.coroutines</name>
<url>https://github.com/Kotlin/kotlinx.coroutines</url>
<copyright>Copyright 2016 - 2019 JetBrains</copyright>
<license>Apache Software License 2.0</license>
</notice>
<notice>
<name>Glide</name>
<url>https://github.com/bumptech/glide</url>
<copyright>Copyright Google, Inc</copyright>
<license>BSD 2-Clause License</license>
</notice>
<notice>
<name>Glide Transformations</name>
<url>https://github.com/wasabeef/glide-transformations</url>
<copyright>Copyright 2020 Wasabeef</copyright>
<license>Apache Software License 2.0</license>
</notice>
<notice>
<name>The Movie Database API</name>
<url>https://www.themoviedb.org</url>
<copyright>This product uses the TMDb API but is not endorsed or certified by TMDb</copyright>
</notice>
</notices>

View File

@ -22,7 +22,6 @@
<item quantity="one">%d Episode</item> <item quantity="one">%d Episode</item>
<item quantity="other">%d Episoden</item> <item quantity="other">%d Episoden</item>
</plurals> </plurals>
<string name="text_runtime">%1$d Minuten</string>
<plurals name="text_runtime"> <plurals name="text_runtime">
<item quantity="one">%d Minute</item> <item quantity="one">%d Minute</item>
<item quantity="other">%d Minuten</item> <item quantity="other">%d Minuten</item>
@ -34,6 +33,8 @@
<!-- settings fragment --> <!-- settings fragment -->
<string name="account">Account</string> <string name="account">Account</string>
<string name="account_login_desc">Zum bearbeiten tippen</string> <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="info">Info</string> <string name="info">Info</string>
<string name="info_about_desc">Version %1$s (%2$s)</string> <string name="info_about_desc">Version %1$s (%2$s)</string>
<string name="settings">Einstellungen</string> <string name="settings">Einstellungen</string>
@ -44,6 +45,12 @@
<string name="theme">Design</string> <string name="theme">Design</string>
<string name="theme_light">Hell</string> <string name="theme_light">Hell</string>
<string name="theme_dark">Dunkel</string> <string name="theme_dark">Dunkel</string>
<string name="dev_settings">Entwickler Einstellungen</string>
<string name="export_data">Daten exportieren</string>
<string name="export_data_desc">Speichere "Meine Liste" in eine Datei</string>
<string name="import_data">Daten importieren</string>
<string name="import_data_desc">Lade "Meine Liste" aus einer Datei</string>
<string name="import_data_success">"Meine Liste" erfolgreich importiert</string>
<!-- about fragment --> <!-- about fragment -->
<string name="version">Version</string> <string name="version">Version</string>
@ -53,6 +60,8 @@
<string name="about_info">Eine inoffizielle App für Anime on Demand.</string> <string name="about_info">Eine inoffizielle App für Anime on Demand.</string>
<string name="third_party_heading">Lizenzen von Drittanbietern</string> <string name="third_party_heading">Lizenzen von Drittanbietern</string>
<string name="third_party_component_desc">© %1$s %2$s unter %3$s</string> <string name="third_party_component_desc">© %1$s %2$s unter %3$s</string>
<string name="dev_settings_enabled">Du bist jetzt ein Entwickler</string>
<string name="dev_settings_already">Du bist schon ein Entwickler</string>
<!-- player --> <!-- player -->
<string name="close_player">Player schließen</string> <string name="close_player">Player schließen</string>
@ -75,8 +84,10 @@
<string name="on_login_failed">Login nicht erfolgreich! Stelle sicher das deine Login-Daten korrekt sind und versuche es erneut.</string> <string name="on_login_failed">Login nicht erfolgreich! Stelle sicher das deine Login-Daten korrekt sind und versuche es erneut.</string>
<!-- dialogs --> <!-- dialogs -->
<string name="save">speichern</string> <string name="save">Speichern</string>
<string name="apply">Übernehmen</string>
<string name="cancel">@android:string/cancel</string> <string name="cancel">@android:string/cancel</string>
<string name="loading">Lädt…</string>
<string name="dialog_timeout_head">Anmelden fehlgeschlagen</string> <string name="dialog_timeout_head">Anmelden fehlgeschlagen</string>
<string name="dialog_timeout_desc">Der Server scheint langsam zu antworten. Bitte versuche es später noch einmal.</string> <string name="dialog_timeout_desc">Der Server scheint langsam zu antworten. Bitte versuche es später noch einmal.</string>

View File

@ -29,7 +29,6 @@
<item quantity="one">%d episode</item> <item quantity="one">%d episode</item>
<item quantity="other">%d episodes</item> <item quantity="other">%d episodes</item>
</plurals> </plurals>
<string name="text_runtime">%1$d Minutes</string>
<plurals name="text_runtime"> <plurals name="text_runtime">
<item quantity="one">%d Minute</item> <item quantity="one">%d Minute</item>
<item quantity="other">%d Minutes</item> <item quantity="other">%d Minutes</item>
@ -44,6 +43,8 @@
<string name="account">Account</string> <string name="account">Account</string>
<string name="account_login_ex" translatable="false">user@example.com</string> <string name="account_login_ex" translatable="false">user@example.com</string>
<string name="account_login_desc">Tap to edit</string> <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="info">Info</string> <string name="info">Info</string>
<string name="info_about" translatable="false">Teapod by @Seil0</string> <string name="info_about" translatable="false">Teapod by @Seil0</string>
<string name="info_about_desc">Version %1$s (%2$s)</string> <string name="info_about_desc">Version %1$s (%2$s)</string>
@ -55,6 +56,13 @@
<string name="theme">Theme</string> <string name="theme">Theme</string>
<string name="theme_light">Light</string> <string name="theme_light">Light</string>
<string name="theme_dark">Dark</string> <string name="theme_dark">Dark</string>
<string name="dev_settings">Developer Settings</string>
<string name="export_data">export data</string>
<string name="export_data_desc">export "My list" to a file</string>
<string name="import_data">import data</string>
<string name="import_data_desc">import "My list" from a file</string>
<string name="import_data_success">imported "My list" successfully</string>
<!-- about fragment --> <!-- about fragment -->
<string name="version">Version</string> <string name="version">Version</string>
@ -69,6 +77,8 @@
<string name="tmdb_notice" translatable="false">This product uses the TMDb API but is not endorsed or certified by TMDb.</string> <string name="tmdb_notice" translatable="false">This product uses the TMDb API but is not endorsed or certified by TMDb.</string>
<string name="third_party_heading">Third Party Licenses</string> <string name="third_party_heading">Third Party Licenses</string>
<string name="third_party_component_desc">© %1$s %2$s under %3$s</string> <string name="third_party_component_desc">© %1$s %2$s under %3$s</string>
<string name="dev_settings_enabled">You are now a developer</string>
<string name="dev_settings_already">You are already a developer</string>
<!-- player --> <!-- player -->
<string name="close_player">close player</string> <string name="close_player">close player</string>
@ -95,8 +105,10 @@
<string name="on_login_failed">Could not login! Make sure Username and Password are correct and try again.</string> <string name="on_login_failed">Could not login! Make sure Username and Password are correct and try again.</string>
<!-- dialogs --> <!-- dialogs -->
<string name="save">save</string> <string name="save">Save</string>
<string name="cancel">@android:string/cancel</string> <string name="cancel">@android:string/cancel</string>
<string name="apply">Apply</string>
<string name="loading">Loading…</string>
<string name="dialog_timeout_head">Login failed</string> <string name="dialog_timeout_head">Login failed</string>
<string name="dialog_timeout_desc">Looks like the server is taking to long to respond. Please try again later.</string> <string name="dialog_timeout_desc">Looks like the server is taking to long to respond. Please try again later.</string>
@ -113,12 +125,10 @@
<string name="save_key_user_password" translatable="false">org.mosad.teapod.user_password</string> <string name="save_key_user_password" translatable="false">org.mosad.teapod.user_password</string>
<string name="save_key_prefer_secondary" translatable="false">org.mosad.teapod.prefer_secondary</string> <string name="save_key_prefer_secondary" translatable="false">org.mosad.teapod.prefer_secondary</string>
<string name="save_key_autoplay" translatable="false">org.mosad.teapod.autoplay</string> <string name="save_key_autoplay" translatable="false">org.mosad.teapod.autoplay</string>
<string name="save_key_dev_settings" translatable="false">org.mosad.teapod.dev.settings</string>
<string name="save_key_theme" translatable="false">org.mosad.teapod.theme</string> <string name="save_key_theme" translatable="false">org.mosad.teapod.theme</string>
<!-- intents & states --> <!-- intents & states -->
<string name="intent_media_id" translatable="false">intent_media_id</string> <string name="intent_media_id" translatable="false">intent_media_id</string>
<string name="intent_episode_id" translatable="false">intent_episode_id</string> <string name="intent_episode_id" translatable="false">intent_episode_id</string>
<string name="state_resume_window" translatable="false">state_resume_window</string>
<string name="state_resume_position" translatable="false">state_resume_position</string>
<string name="state_is_playing" translatable="false">state_is_playing</string>
</resources> </resources>

View File

@ -1,12 +1,12 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript { buildscript {
ext.kotlin_version = "1.4.31" ext.kotlin_version = "1.5.20"
repositories { repositories {
google() google()
jcenter() mavenCentral()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:4.1.2' classpath 'com.android.tools.build:gradle:4.2.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong // NOTE: Do not place your application dependencies here; they belong
@ -17,7 +17,7 @@ buildscript {
allprojects { allprojects {
repositories { repositories {
google() google()
jcenter() mavenCentral()
} }
} }

View File

@ -0,0 +1,5 @@
* Entwickleroptionen
* Export/Import für "Meine Liste"
* Der Picture in Picture Modus hat nun Controlls (#35)
* Teapod stürtzt nicht mehr ab, wenn ein Element aus "Meine List" nicht geladen werden konnte (#42)
* Staffel-Informationen im Title werden bei der Suche in tmdb ignoriert (#43)

View File

@ -0,0 +1,5 @@
* Developer options
* Export/Import for "My List"
* The Picture in Picture Modus now has Controlls (#35)
* Teapod deosn't crash, if a element from "My List" could not be loaded (#42)
* Season-Information in titles will be ignored, when searching in tmdb (#43)

Binary file not shown.

View File

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

2
gradlew vendored
View File

@ -130,7 +130,7 @@ fi
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"` APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"` JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath # We build the pattern for arguments to be converted via cygpath

21
gradlew.bat vendored
View File

@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1 %JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init if "%ERRORLEVEL%" == "0" goto execute
echo. echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@ -54,7 +54,7 @@ goto fail
set JAVA_HOME=%JAVA_HOME:"=% set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init if exist "%JAVA_EXE%" goto execute
echo. echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
@ -64,21 +64,6 @@ echo location of your Java installation.
goto fail goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute :execute
@rem Setup the command line @rem Setup the command line
@ -86,7 +71,7 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle @rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end :end
@rem End local scope for the variables with windows NT shell @rem End local scope for the variables with windows NT shell