diff --git a/README.md b/README.md
index c1af88c..57c2a97 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,8 @@
-# teapod
+# Teapod
-A unofficial App for Anime-on-Demand.
-Teapod is a unofficial App for Anime-on-Demand (AoD). It allows you to watch all your favourite animes from AoD on your Android Device.
+Teapod is a unofficial App for Anime on Demand (AoD). It allows you to watch all your favourite animes from AoD on your android device. To use Teapod you need to have a subscription to AoD.
[](https://f-droid.org/de/packages/org.mosad.teapod/)
-[](https://apt.izzysoft.de/fdroid/index/apk/org.mosad.teapod)
## Features
* Watch all animes from AoD on your Android device
@@ -19,13 +17,12 @@ Teapod is a unofficial App for Anime-on-Demand (AoD). It allows you to watch all
[](https://www.mosad.xyz/images/Teapod/Teapod_Search.webp)
### License
-This Project is not associated with Anime-on-Demand in any way. Using this app may violates the ToS of AoD. Teapod is licensed under the terms and conditions of GPL 3.
-
-### Known Issues
-If a tv show is selected, the first episode will be marked as watched. This is due to parsing the website.
+Teapod is licensed under the terms and conditions of GPL 3. This Project is not associated with Anime on Demand in any way. But they allow open source apps for their service.
### Contributing
-If you want to contribute to Teapod and need an account on this gitea instance, please write me an email: seil0@mosad.xyz
+Currentl you need to have an AoD account to contrtibut to Teapod. Contributing without on is kind of impossible.If you want to contribute to Teapod and need an account on this gitea instance, please write me an email.
+
+### [FAQ](https://git.mosad.xyz/Seil0/teapod/wiki#hilfe)
#### Why is it called Teapod?
Teapod is a Acronym for "The ultimate anime app on demand", hence this project is called Teapod and not Teapot.
diff --git a/app/build.gradle b/app/build.gradle
index 110fbdb..bab39ce 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -10,8 +10,8 @@ android {
applicationId "org.mosad.teapod"
minSdkVersion 23
targetSdkVersion 30
- versionCode 3000 //00.03.000
- versionName "0.3.0"
+ versionCode 4000 //00.04.000
+ versionName "0.4.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resValue "string", "build_time", buildTime()
@@ -46,20 +46,20 @@ dependencies {
implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
- implementation 'androidx.navigation:navigation-fragment-ktx:2.3.2'
- implementation 'androidx.navigation:navigation-ui-ktx:2.3.2'
+ implementation 'androidx.navigation:navigation-fragment-ktx:2.3.3'
+ implementation 'androidx.navigation:navigation-ui-ktx:2.3.3'
implementation 'androidx.security:security-crypto:1.1.0-alpha03'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
- implementation 'com.google.android.material:material:1.3.0-beta01'
+ implementation 'com.google.android.material:material:1.3.0'
implementation 'com.google.code.gson:gson:2.8.6'
- implementation 'com.google.android.exoplayer:exoplayer-core:2.12.2'
- implementation 'com.google.android.exoplayer:exoplayer-hls:2.12.2'
- implementation 'com.google.android.exoplayer:exoplayer-dash:2.12.2'
- implementation 'com.google.android.exoplayer:exoplayer-ui:2.12.2'
+ implementation 'com.google.android.exoplayer:exoplayer-core:2.13.2'
+ implementation 'com.google.android.exoplayer:exoplayer-hls:2.13.2'
+ implementation 'com.google.android.exoplayer:exoplayer-dash:2.13.2'
+ implementation 'com.google.android.exoplayer:exoplayer-ui:2.13.2'
implementation 'org.jsoup:jsoup:1.13.1'
- implementation 'com.github.bumptech.glide:glide:4.11.0'
+ implementation 'com.github.bumptech.glide:glide:4.12.0'
implementation 'jp.wasabeef:glide-transformations:4.3.0'
implementation 'com.afollestad.material-dialogs:core:3.3.0'
implementation 'com.afollestad.material-dialogs:bottomsheets:3.3.0'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 1a62b38..9cd30f9 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,5 +1,6 @@
@@ -10,9 +11,9 @@
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
- android:theme="@style/AppTheme.Light">
+ android:theme="@style/AppTheme.Dark">
@@ -22,15 +23,28 @@
+ android:screenOrientation="portrait"
+ android:launchMode="singleTop"
+ android:windowSoftInputMode="adjustPan">
+
+
\ No newline at end of file
diff --git a/app/src/main/java/org/mosad/teapod/parser/AoDParser.kt b/app/src/main/java/org/mosad/teapod/parser/AoDParser.kt
index 46d73b0..defa271 100644
--- a/app/src/main/java/org/mosad/teapod/parser/AoDParser.kt
+++ b/app/src/main/java/org/mosad/teapod/parser/AoDParser.kt
@@ -41,7 +41,7 @@ object AoDParser {
private const val loginPath = "/users/sign_in"
private const val libraryPath = "/animes"
- private const val userAgent = "Mozilla/5.0 (X11; Linux x86_64; rv:80.0) Gecko/20100101 Firefox/80.0"
+ private const val userAgent = "Mozilla/5.0 (X11; Linux x86_64; rv:84.0) Gecko/20100101 Firefox/84.0"
private var sessionCookies = mutableMapOf()
private var csrfToken: String = ""
@@ -53,10 +53,11 @@ object AoDParser {
val newEpisodesList = arrayListOf()
val newSimulcastsList = arrayListOf()
val newTitlesList = arrayListOf()
+ val topTenList = arrayListOf()
fun login(): Boolean = runBlocking {
- withContext(Dispatchers.Default) {
+ withContext(Dispatchers.IO) {
// get the authenticity token
val resAuth = Jsoup.connect(baseUrl + loginPath)
.header("User-Agent", userAgent)
@@ -78,7 +79,7 @@ object AoDParser {
val resLogin = Jsoup.connect(baseUrl + loginPath)
.method(Connection.Method.POST)
- .timeout(60000) // login can take some time
+ .timeout(60000) // login can take some time default is 60000 (60 sec)
.data(data)
.postDataCharset("UTF-8")
.cookies(authCookies)
@@ -96,20 +97,11 @@ object AoDParser {
/**
* initially load all media and home screen data
- * -> blocking
*/
- fun initialLoading() = runBlocking {
- val loadHomeJob = GlobalScope.async {
- loadHome()
- }
-
- val listJob = GlobalScope.async {
+ fun initialLoading() = listOf(
+ loadHome(),
listAnimes()
- }
-
- loadHomeJob.await()
- listJob.await()
- }
+ )
/**
* get a media by it's ID (int)
@@ -134,7 +126,7 @@ object AoDParser {
}
// TODO don't use jsoup here
- fun sendCallback(callbackPath: String) = GlobalScope.launch(Dispatchers.IO) {
+ private fun sendCallback(callbackPath: String) = GlobalScope.launch(Dispatchers.IO) {
val headers = mutableMapOf(
Pair("Accept", "application/json, text/javascript, */*; q=0.01"),
Pair("Accept-Language", "de,en-US;q=0.7,en;q=0.3"),
@@ -158,112 +150,112 @@ object AoDParser {
/**
* load all media from aod into itemMediaList and mediaList
*/
- private fun listAnimes() = runBlocking {
- if (sessionCookies.isEmpty()) login()
+ private fun listAnimes() = GlobalScope.launch(Dispatchers.IO) {
+ val resAnimes = Jsoup.connect(baseUrl + libraryPath).get()
+ //println(resAnimes)
- withContext(Dispatchers.Default) {
- val resAnimes = Jsoup.connect(baseUrl + libraryPath)
- .cookies(sessionCookies)
- .get()
-
- //println(resAnimes)
-
- itemMediaList.clear()
- mediaList.clear()
- resAnimes.select("div.animebox").forEach {
- val type = if (it.select("p.animebox-link").select("a").text().toLowerCase(Locale.ROOT) == "zur serie") {
- MediaType.TVSHOW
- } else {
- 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
- })
+ itemMediaList.clear()
+ mediaList.clear()
+ resAnimes.select("div.animebox").forEach {
+ val type = if (it.select("p.animebox-link").select("a").text().toLowerCase(Locale.ROOT) == "zur serie") {
+ MediaType.TVSHOW
+ } else {
+ 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()
- Log.i(javaClass.name, "Total library size is: ${mediaList.size}")
+ itemMediaList.add(ItemMedia(mediaId, mediaTitle, mediaImage))
+ 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
*/
- private fun loadHome() = runBlocking {
- if (sessionCookies.isEmpty()) login()
+ private fun loadHome() = GlobalScope.launch(Dispatchers.IO) {
+ val resHome = Jsoup.connect(baseUrl).get()
- withContext(Dispatchers.Default) {
- val resHome = Jsoup.connect(baseUrl)
- .cookies(sessionCookies)
- .get()
+ // get highlights from AoD
+ highlightsList.clear()
+ resHome.select("#aod-highlights").select("div.news-item").forEach {
+ val mediaId = it.select("div.news-item-text").select("a.serienlink")
+ .attr("href").substringAfterLast("/").toIntOrNull()
+ val mediaTitle = it.select("div.news-title").select("h2").text()
+ val mediaImage = it.select("img").attr("src")
- // get highlights from AoD
- highlightsList.clear()
- resHome.select("#aod-highlights").select("div.news-item").forEach {
- val mediaId = it.select("div.news-item-text").select("a.serienlink")
- .attr("href").substringAfterLast("/").toIntOrNull()
- val mediaTitle = it.select("div.news-title").select("h2").text()
- val mediaImage = it.select("img").attr("src")
-
- if (mediaId != null) {
- highlightsList.add(ItemMedia(mediaId, mediaTitle, mediaImage))
- }
+ if (mediaId != null) {
+ highlightsList.add(ItemMedia(mediaId, mediaTitle, mediaImage))
}
+ }
- // get all new episodes from AoD
- newEpisodesList.clear()
- resHome.select("h2:contains(Neue Episoden)").next().select("li").forEach {
- 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()} - ${it.select("span.neweps").text()}"
+ // get all new episodes from AoD
+ newEpisodesList.clear()
+ resHome.select("h2:contains(Neue Episoden)").next().select("li").forEach {
+ 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()} - ${it.select("span.neweps").text()}"
- if (mediaId != null) {
- newEpisodesList.add(ItemMedia(mediaId, mediaTitle, mediaImage))
- }
+ if (mediaId != null) {
+ newEpisodesList.add(ItemMedia(mediaId, mediaTitle, mediaImage))
}
+ }
- // get new simulcasts from AoD
- newSimulcastsList.clear()
- resHome.select("h2:contains(Neue Simulcasts)").next().select("li").forEach {
- 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()
+ // get new simulcasts from AoD
+ newSimulcastsList.clear()
+ resHome.select("h2:contains(Neue Simulcasts)").next().select("li").forEach {
+ 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) {
- newSimulcastsList.add(ItemMedia(mediaId, mediaTitle, mediaImage))
- }
+ if (mediaId != null) {
+ newSimulcastsList.add(ItemMedia(mediaId, mediaTitle, mediaImage))
}
+ }
- // get new titles from AoD
- newTitlesList.clear()
- resHome.select("h2:contains(Neue Anime-Titel)").next().select("li").forEach {
- 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()
+ // get new titles from AoD
+ newTitlesList.clear()
+ resHome.select("h2:contains(Neue Anime-Titel)").next().select("li").forEach {
+ 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) {
- newTitlesList.add(ItemMedia(mediaId, mediaTitle, mediaImage))
- }
+ if (mediaId != null) {
+ newTitlesList.add(ItemMedia(mediaId, mediaTitle, mediaImage))
}
+ }
- // if highlights is empty, add a random new title
- if (highlightsList.isEmpty()) {
- if (newTitlesList.isNotEmpty()) {
- highlightsList.add(newTitlesList[Random.nextInt(0, newTitlesList.size)])
- } else {
- highlightsList.add(ItemMedia(0,"", ""))
- }
+ // get top ten from AoD
+ topTenList.clear()
+ resHome.select("h2:contains(Anime Top 10)").next().select("li").forEach {
+ 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) {
+ topTenList.add(ItemMedia(mediaId, mediaTitle, mediaImage))
+ }
+ }
+
+ // if highlights is empty, add a random new title
+ if (highlightsList.isEmpty()) {
+ if (newTitlesList.isNotEmpty()) {
+ highlightsList.add(newTitlesList[Random.nextInt(0, newTitlesList.size)])
+ } else {
+ highlightsList.add(ItemMedia(0,"", ""))
}
}
}
@@ -272,7 +264,7 @@ object AoDParser {
* load streams for the media path, movies have one episode
* @param media is used as call ba reference
*/
- private suspend fun loadStreams(media: Media) = GlobalScope.launch(Dispatchers.IO) {
+ private fun loadStreams(media: Media) = GlobalScope.launch(Dispatchers.IO) {
if (sessionCookies.isEmpty()) login()
if (!loginSuccess) {
@@ -331,7 +323,7 @@ object AoDParser {
}
Log.i(javaClass.name, "Loaded playlists successfully")
- // parse additional info from the media page
+ // additional info from the media page
res.select("table.vertical-table").select("tr").forEach { row ->
when (row.select("th").text().toLowerCase(Locale.ROOT)) {
"produktionsjahr" -> media.info.year = row.select("td").text().toInt()
@@ -345,7 +337,21 @@ object AoDParser {
}
}
- // parse additional information for tv shows the episode title (description) is loaded from the "api"
+ // 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
@@ -363,6 +369,7 @@ object AoDParser {
}
}
}
+ Log.i(javaClass.name, "media loaded successfully")
}
/**
diff --git a/app/src/main/java/org/mosad/teapod/preferences/Preferences.kt b/app/src/main/java/org/mosad/teapod/preferences/Preferences.kt
index 172fb92..0c8b0c2 100644
--- a/app/src/main/java/org/mosad/teapod/preferences/Preferences.kt
+++ b/app/src/main/java/org/mosad/teapod/preferences/Preferences.kt
@@ -11,7 +11,7 @@ object Preferences {
internal set
var autoplay = true
internal set
- var theme = DataTypes.Theme.LIGHT
+ var theme = DataTypes.Theme.DARK
internal set
private fun getSharedPref(context: Context): SharedPreferences {
@@ -62,8 +62,8 @@ object Preferences {
)
theme = DataTypes.Theme.valueOf(
sharedPref.getString(
- context.getString(R.string.save_key_theme), DataTypes.Theme.LIGHT.toString()
- ) ?: DataTypes.Theme.LIGHT.toString()
+ context.getString(R.string.save_key_theme), DataTypes.Theme.DARK.toString()
+ ) ?: DataTypes.Theme.DARK.toString()
)
}
diff --git a/app/src/main/java/org/mosad/teapod/SplashActivity.kt b/app/src/main/java/org/mosad/teapod/ui/activity/SplashActivity.kt
similarity index 80%
rename from app/src/main/java/org/mosad/teapod/SplashActivity.kt
rename to app/src/main/java/org/mosad/teapod/ui/activity/SplashActivity.kt
index 54515b2..c7dfa49 100644
--- a/app/src/main/java/org/mosad/teapod/SplashActivity.kt
+++ b/app/src/main/java/org/mosad/teapod/ui/activity/SplashActivity.kt
@@ -1,8 +1,9 @@
-package org.mosad.teapod
+package org.mosad.teapod.ui.activity
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
+import org.mosad.teapod.ui.activity.main.MainActivity
class SplashActivity : AppCompatActivity() {
diff --git a/app/src/main/java/org/mosad/teapod/MainActivity.kt b/app/src/main/java/org/mosad/teapod/ui/activity/main/MainActivity.kt
similarity index 68%
rename from app/src/main/java/org/mosad/teapod/MainActivity.kt
rename to app/src/main/java/org/mosad/teapod/ui/activity/main/MainActivity.kt
index 5e74c0a..19dd297 100644
--- a/app/src/main/java/org/mosad/teapod/MainActivity.kt
+++ b/app/src/main/java/org/mosad/teapod/ui/activity/main/MainActivity.kt
@@ -20,7 +20,7 @@
*
*/
-package org.mosad.teapod
+package org.mosad.teapod.ui.activity.main
import android.content.Intent
import android.os.Bundle
@@ -29,16 +29,27 @@ import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.fragment.app.commit
+import com.afollestad.materialdialogs.MaterialDialog
+import com.afollestad.materialdialogs.callbacks.onDismiss
import com.google.android.material.bottomnavigation.BottomNavigationView
+import kotlinx.coroutines.joinAll
+import kotlinx.coroutines.runBlocking
+import org.mosad.teapod.R
import org.mosad.teapod.databinding.ActivityMainBinding
import org.mosad.teapod.parser.AoDParser
-import org.mosad.teapod.player.PlayerActivity
+import org.mosad.teapod.ui.activity.player.PlayerActivity
import org.mosad.teapod.preferences.EncryptedPreferences
import org.mosad.teapod.preferences.Preferences
import org.mosad.teapod.ui.components.LoginDialog
-import org.mosad.teapod.ui.fragments.*
+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.LibraryFragment
+import org.mosad.teapod.ui.activity.main.fragments.SearchFragment
+import org.mosad.teapod.ui.activity.onboarding.OnboardingActivity
import org.mosad.teapod.util.DataTypes
import org.mosad.teapod.util.StorageController
+import org.mosad.teapod.util.exitAndRemoveTask
+import java.net.SocketTimeoutException
import kotlin.system.measureTimeMillis
class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemSelectedListener {
@@ -48,6 +59,11 @@ class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemS
companion object {
var wasInitialized = false
+ lateinit var instance: MainActivity
+ }
+
+ init {
+ instance = this
}
override fun onCreate(savedInstanceState: Bundle?) {
@@ -111,39 +127,57 @@ class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemS
private fun getThemeResource(): Int {
return when (Preferences.theme) {
- DataTypes.Theme.DARK -> R.style.AppTheme_Dark
- else -> R.style.AppTheme_Light
+ DataTypes.Theme.LIGHT -> R.style.AppTheme_Light
+ else -> R.style.AppTheme_Dark
}
}
+ /**
+ * initial loading and login are run in parallel, as initial loading doesn't require
+ * any login cookies
+ */
private fun load() {
- // running login and list in parallel does not bring any speed improvements
val time = measureTimeMillis {
- Preferences.load(this)
+ val loadingJob = AoDParser.initialLoading() // start the initial loading
- // make sure credentials are set, run's async
+ // load all saved stuff here
+ Preferences.load(this)
EncryptedPreferences.readCredentials(this)
+ StorageController.load(this)
+
+ // show onbaording
if (EncryptedPreferences.password.isEmpty()) {
- showLoginDialog(true)
+ showOnboarding()
} else {
- // try to login in, as most sites can only bee loaded once loged in
- if (!AoDParser.login()) showLoginDialog(false)
+ try {
+ if (!AoDParser.login()) {
+ showLoginDialog()
+ }
+ } catch (ex: SocketTimeoutException) {
+ Log.w(javaClass.name, "Timeout during login!")
+
+ // show waring dialog before finishing
+ MaterialDialog(this).show {
+ title(R.string.dialog_timeout_head)
+ message(R.string.dialog_timeout_desc)
+ onDismiss { exitAndRemoveTask() }
+ }
+ }
}
- StorageController.load(this)
- AoDParser.initialLoading()
-
- wasInitialized = true
+ runBlocking { loadingJob.joinAll() } // wait for initial loading to finish
}
- Log.i(javaClass.name, "login and list in $time ms")
+ Log.i(javaClass.name, "loading and login in $time ms")
+
+ wasInitialized = true
}
- private fun showLoginDialog(firstTry: Boolean) {
- LoginDialog(this, firstTry).positiveButton {
+ private fun showLoginDialog() {
+ LoginDialog(this, false).positiveButton {
EncryptedPreferences.saveCredentials(login, password, context)
if (!AoDParser.login()) {
- showLoginDialog(false)
+ showLoginDialog()
Log.w(javaClass.name, "Login failed, please try again.")
}
}.negativeButton {
@@ -153,18 +187,16 @@ class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemS
}
/**
- * Show a fragment on top of the current fragment.
- * The current fragment is replaced and the new one is added
- * to the back stack.
+ * start the onboarding activity and finish the main activity
*/
- fun showFragment(fragment: Fragment) {
- supportFragmentManager.commit {
- replace(R.id.nav_host_fragment, fragment, fragment.javaClass.simpleName)
- addToBackStack(fragment.javaClass.name)
- show(fragment)
- }
+ private fun showOnboarding() {
+ startActivity(Intent(this, OnboardingActivity::class.java))
+ finish()
}
+ /**
+ * start the player as new activity
+ */
fun startPlayer(mediaId: Int, episodeId: Int) {
val intent = Intent(this, PlayerActivity::class.java).apply {
putExtra(getString(R.string.intent_media_id), mediaId)
@@ -183,5 +215,4 @@ class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemS
startActivity(restartIntent)
}
-
}
\ No newline at end of file
diff --git a/app/src/main/java/org/mosad/teapod/ui/fragments/AboutFragment.kt b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/AboutFragment.kt
similarity index 84%
rename from app/src/main/java/org/mosad/teapod/ui/fragments/AboutFragment.kt
rename to app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/AboutFragment.kt
index b1c7fb2..a30d129 100644
--- a/app/src/main/java/org/mosad/teapod/ui/fragments/AboutFragment.kt
+++ b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/AboutFragment.kt
@@ -1,5 +1,7 @@
-package org.mosad.teapod.ui.fragments
+package org.mosad.teapod.ui.activity.main.fragments
+import android.content.Intent
+import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
@@ -17,6 +19,7 @@ import java.lang.StringBuilder
class AboutFragment : Fragment() {
+ private val teapodRepoUrl = "https://git.mosad.xyz/Seil0/teapod"
private lateinit var binding: FragmentAboutBinding
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
@@ -27,7 +30,7 @@ class AboutFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
- binding.textVersion.text = getString(R.string.info_about_desc, BuildConfig.VERSION_NAME, getString(R.string.build_time))
+ binding.textVersionDesc.text = getString(R.string.version_desc, BuildConfig.VERSION_NAME, getString(R.string.build_time))
getThirdPartyComponents().forEach { thirdParty ->
val componentBinding = ItemComponentBinding.inflate(layoutInflater) //(R.layout.item_component, container, false)
@@ -44,10 +47,25 @@ class AboutFragment : Fragment() {
binding.linearThirdParty.addView(componentBinding.root)
}
+
+ initActions()
+ }
+
+ private fun initActions() {
+ binding.linearSource.setOnClickListener {
+ startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(teapodRepoUrl)))
+ }
+
+ binding.linearLicense.setOnClickListener {
+ MaterialDialog(requireContext())
+ .title(text = License.GPL3.long)
+ .message(text = parseLicense(R.raw.gpl_3_full))
+ .show()
+ }
}
private fun getThirdPartyComponents(): List {
- return listOf(
+ return listOf(
ThirdPartyComponent("AndroidX", "", "The Android Open Source Project",
"https://developer.android.com/jetpack/androidx", License.APACHE2),
ThirdPartyComponent("Material Components for Android", "2020", "The Android Open Source Project",
@@ -79,13 +97,10 @@ class AboutFragment : Fragment() {
License.MIT -> parseLicense(R.raw.mit_full)
}
- println("showing: ${license.long}")
-
MaterialDialog(requireContext())
.title(text = license.long)
.message(text = licenseText)
.show()
-
}
private fun parseLicense(@RawRes id: Int): String {
diff --git a/app/src/main/java/org/mosad/teapod/ui/fragments/AccountFragment.kt b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/AccountFragment.kt
similarity index 94%
rename from app/src/main/java/org/mosad/teapod/ui/fragments/AccountFragment.kt
rename to app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/AccountFragment.kt
index b87a753..61ca637 100644
--- a/app/src/main/java/org/mosad/teapod/ui/fragments/AccountFragment.kt
+++ b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/AccountFragment.kt
@@ -1,4 +1,4 @@
-package org.mosad.teapod.ui.fragments
+package org.mosad.teapod.ui.activity.main.fragments
import android.os.Bundle
import android.util.Log
@@ -9,7 +9,7 @@ import androidx.fragment.app.Fragment
import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.list.listItemsSingleChoice
import org.mosad.teapod.BuildConfig
-import org.mosad.teapod.MainActivity
+import org.mosad.teapod.ui.activity.main.MainActivity
import org.mosad.teapod.R
import org.mosad.teapod.databinding.FragmentAccountBinding
import org.mosad.teapod.parser.AoDParser
@@ -17,6 +17,7 @@ import org.mosad.teapod.preferences.EncryptedPreferences
import org.mosad.teapod.preferences.Preferences
import org.mosad.teapod.ui.components.LoginDialog
import org.mosad.teapod.util.DataTypes.Theme
+import org.mosad.teapod.util.showFragment
class AccountFragment : Fragment() {
@@ -53,7 +54,7 @@ class AccountFragment : Fragment() {
}
binding.linearInfo.setOnClickListener {
- (activity as MainActivity).showFragment(AboutFragment())
+ activity?.showFragment(AboutFragment())
}
binding.switchSecondary.setOnClickListener {
diff --git a/app/src/main/java/org/mosad/teapod/ui/fragments/HomeFragment.kt b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/HomeFragment.kt
similarity index 74%
rename from app/src/main/java/org/mosad/teapod/ui/fragments/HomeFragment.kt
rename to app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/HomeFragment.kt
index c63ed15..6e19d11 100644
--- a/app/src/main/java/org/mosad/teapod/ui/fragments/HomeFragment.kt
+++ b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/HomeFragment.kt
@@ -1,27 +1,25 @@
-package org.mosad.teapod.ui.fragments
+package org.mosad.teapod.ui.activity.main.fragments
-import android.graphics.drawable.Drawable
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
-import android.widget.TextView
import androidx.fragment.app.Fragment
import com.bumptech.glide.Glide
-import com.bumptech.glide.request.target.CustomTarget
-import com.bumptech.glide.request.transition.Transition
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
-import org.mosad.teapod.MainActivity
import org.mosad.teapod.R
+import org.mosad.teapod.ui.activity.main.MainActivity
import org.mosad.teapod.databinding.FragmentHomeBinding
import org.mosad.teapod.parser.AoDParser
import org.mosad.teapod.util.ItemMedia
import org.mosad.teapod.util.StorageController
import org.mosad.teapod.util.adapter.MediaItemAdapter
import org.mosad.teapod.util.decoration.MediaItemDecoration
+import org.mosad.teapod.util.setDrawableTop
+import org.mosad.teapod.util.showFragment
class HomeFragment : Fragment() {
@@ -30,6 +28,7 @@ class HomeFragment : Fragment() {
private lateinit var adapterNewEpisodes: MediaItemAdapter
private lateinit var adapterNewSimulcasts: MediaItemAdapter
private lateinit var adapterNewTitles: MediaItemAdapter
+ private lateinit var adapterTopTen: MediaItemAdapter
private lateinit var highlightMedia: ItemMedia
@@ -59,9 +58,9 @@ class HomeFragment : Fragment() {
.into(binding.imageHighlight)
if (StorageController.myList.contains(highlightMedia.id)) {
- loadIntoCompoundDrawable(R.drawable.ic_baseline_check_24, binding.textHighlightMyList)
+ binding.textHighlightMyList.setDrawableTop(R.drawable.ic_baseline_check_24)
} else {
- loadIntoCompoundDrawable(R.drawable.ic_baseline_add_24, binding.textHighlightMyList)
+ binding.textHighlightMyList.setDrawableTop(R.drawable.ic_baseline_add_24)
}
}
}
@@ -71,6 +70,7 @@ class HomeFragment : Fragment() {
binding.recyclerNewEpisodes.addItemDecoration(MediaItemDecoration(9))
binding.recyclerNewSimulcasts.addItemDecoration(MediaItemDecoration(9))
binding.recyclerNewTitles.addItemDecoration(MediaItemDecoration(9))
+ binding.recyclerTopTen.addItemDecoration(MediaItemDecoration(9))
// my list
val myListMedia = StorageController.myList.map { elementId ->
@@ -79,9 +79,6 @@ class HomeFragment : Fragment() {
}
}
adapterMyList = MediaItemAdapter(myListMedia)
- adapterMyList.onItemClick = { mediaId, _ ->
- (activity as MainActivity).showFragment(MediaFragment(mediaId))
- }
binding.recyclerMyList.adapter = adapterMyList
// new episodes
@@ -95,6 +92,10 @@ class HomeFragment : Fragment() {
// new titles
adapterNewTitles = MediaItemAdapter(AoDParser.newTitlesList)
binding.recyclerNewTitles.adapter = adapterNewTitles
+
+ // top ten
+ adapterTopTen = MediaItemAdapter(AoDParser.topTenList)
+ binding.recyclerTopTen.adapter = adapterTopTen
}
private fun initActions() {
@@ -111,10 +112,10 @@ class HomeFragment : Fragment() {
binding.textHighlightMyList.setOnClickListener {
if (StorageController.myList.contains(highlightMedia.id)) {
StorageController.myList.remove(highlightMedia.id)
- loadIntoCompoundDrawable(R.drawable.ic_baseline_add_24, binding.textHighlightMyList)
+ binding.textHighlightMyList.setDrawableTop(R.drawable.ic_baseline_add_24)
} else {
StorageController.myList.add(highlightMedia.id)
- loadIntoCompoundDrawable(R.drawable.ic_baseline_check_24, binding.textHighlightMyList)
+ binding.textHighlightMyList.setDrawableTop(R.drawable.ic_baseline_check_24)
}
StorageController.saveMyList(requireContext())
@@ -122,19 +123,27 @@ class HomeFragment : Fragment() {
}
binding.textHighlightInfo.setOnClickListener {
- (activity as MainActivity).showFragment(MediaFragment(highlightMedia.id))
+ activity?.showFragment(MediaFragment(highlightMedia.id))
+ }
+
+ adapterMyList.onItemClick = { mediaId, _ ->
+ activity?.showFragment(MediaFragment(mediaId))
}
adapterNewEpisodes.onItemClick = { mediaId, _ ->
- (activity as MainActivity).showFragment(MediaFragment(mediaId))
+ activity?.showFragment(MediaFragment(mediaId))
}
adapterNewSimulcasts.onItemClick = { mediaId, _ ->
- (activity as MainActivity).showFragment(MediaFragment(mediaId))
+ activity?.showFragment(MediaFragment(mediaId))
}
adapterNewTitles.onItemClick = { mediaId, _ ->
- (activity as MainActivity).showFragment(MediaFragment(mediaId))
+ activity?.showFragment(MediaFragment(mediaId))
+ }
+
+ adapterTopTen.onItemClick = { mediaId, _ ->
+ activity?.showFragment(MediaFragment(mediaId))
}
}
@@ -155,18 +164,4 @@ class HomeFragment : Fragment() {
adapterMyList.notifyDataSetChanged()
}
- private fun loadIntoCompoundDrawable(drawable: Int, textView: TextView) {
- Glide.with(requireContext())
- .load(drawable)
- .into(object : CustomTarget(48, 48) {
- override fun onLoadCleared(drawable: Drawable?) {
- textView.setCompoundDrawablesWithIntrinsicBounds(null, drawable, null, null)
- }
-
- override fun onResourceReady(res: Drawable, transition: Transition?) {
- textView.setCompoundDrawablesWithIntrinsicBounds(null, res, null, null)
- }
-
- })
- }
}
\ No newline at end of file
diff --git a/app/src/main/java/org/mosad/teapod/ui/fragments/LibraryFragment.kt b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/LibraryFragment.kt
similarity index 90%
rename from app/src/main/java/org/mosad/teapod/ui/fragments/LibraryFragment.kt
rename to app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/LibraryFragment.kt
index 4429e6a..01ab191 100644
--- a/app/src/main/java/org/mosad/teapod/ui/fragments/LibraryFragment.kt
+++ b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/LibraryFragment.kt
@@ -1,4 +1,4 @@
-package org.mosad.teapod.ui.fragments
+package org.mosad.teapod.ui.activity.main.fragments
import android.os.Bundle
import android.view.LayoutInflater
@@ -9,11 +9,11 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
-import org.mosad.teapod.MainActivity
import org.mosad.teapod.databinding.FragmentLibraryBinding
import org.mosad.teapod.parser.AoDParser
import org.mosad.teapod.util.adapter.MediaItemAdapter
import org.mosad.teapod.util.decoration.MediaItemDecoration
+import org.mosad.teapod.util.showFragment
class LibraryFragment : Fragment() {
@@ -35,7 +35,7 @@ class LibraryFragment : Fragment() {
context?.let {
adapter = MediaItemAdapter(AoDParser.itemMediaList)
adapter.onItemClick = { mediaId, _ ->
- (activity as MainActivity).showFragment(MediaFragment(mediaId))
+ activity?.showFragment(MediaFragment(mediaId))
}
binding.recyclerMediaLibrary.adapter = adapter
diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/MediaFragment.kt b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/MediaFragment.kt
new file mode 100644
index 0000000..39c86b8
--- /dev/null
+++ b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/MediaFragment.kt
@@ -0,0 +1,210 @@
+package org.mosad.teapod.ui.activity.main.fragments
+
+import android.graphics.Color
+import android.graphics.drawable.ColorDrawable
+import android.os.Bundle
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentActivity
+import androidx.fragment.app.activityViewModels
+import androidx.viewpager2.adapter.FragmentStateAdapter
+import com.bumptech.glide.Glide
+import com.bumptech.glide.request.RequestOptions
+import com.google.android.material.appbar.AppBarLayout
+import com.google.android.material.tabs.TabLayoutMediator
+import jp.wasabeef.glide.transformations.BlurTransformation
+import kotlinx.coroutines.*
+import org.mosad.teapod.R
+import org.mosad.teapod.databinding.FragmentMediaBinding
+import org.mosad.teapod.ui.activity.main.MainActivity
+import org.mosad.teapod.ui.activity.main.viewmodel.MediaFragmentViewModel
+import org.mosad.teapod.util.*
+import org.mosad.teapod.util.DataTypes.MediaType
+
+
+/**
+ * The media detail fragment.
+ * Note: the fragment is created only once, when selecting a similar title etc.
+ * therefore fragments may be not empty and model may be the old one
+ */
+class MediaFragment(private val mediaId: Int) : Fragment() {
+
+ private lateinit var binding: FragmentMediaBinding
+ private lateinit var pagerAdapter: FragmentStateAdapter
+
+ private val fragments = arrayListOf()
+
+ private val model: MediaFragmentViewModel by activityViewModels()
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
+ binding = FragmentMediaBinding.inflate(inflater, container, false)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ binding.frameLoading.visibility = View.VISIBLE
+
+ // tab layout and pager TODO
+ pagerAdapter = ScreenSlidePagerAdapter(requireActivity())
+ // fix material components issue #1878, if more tabs are added increase
+ binding.pagerEpisodesSimilar.offscreenPageLimit = 2
+ binding.pagerEpisodesSimilar.adapter = pagerAdapter
+ TabLayoutMediator(binding.tabEpisodesSimilar, binding.pagerEpisodesSimilar) { tab, position ->
+ tab.text = if (model.media.type == MediaType.TVSHOW && position == 0) {
+ getString(R.string.episodes)
+ } else {
+ getString(R.string.similar_titles)
+ }
+ }.attach()
+
+ GlobalScope.launch(Dispatchers.Main) {
+ model.load(mediaId) // load the streams and tmdb for the selected media
+
+ if (this@MediaFragment.isAdded) {
+ updateGUI()
+ initActions()
+ }
+ }
+ }
+
+ override fun onResume() {
+ super.onResume()
+
+ // update the next ep text if there is one, since it may have changed
+ if (model.nextEpisode.title.isNotEmpty()) {
+ binding.textTitle.text = model.nextEpisode.title
+ }
+ }
+
+ /**
+ * if tmdb data is present, use it, else use the aod data
+ */
+ private fun updateGUI() = with(model) {
+ // generic gui
+ val backdropUrl = if (tmdb.backdropUrl.isNotEmpty()) tmdb.backdropUrl else media.info.posterUrl
+ val posterUrl = if (tmdb.posterUrl.isNotEmpty()) tmdb.posterUrl else media.info.posterUrl
+
+ Glide.with(requireContext()).load(backdropUrl)
+ .apply(RequestOptions.placeholderOf(ColorDrawable(Color.DKGRAY)))
+ .apply(RequestOptions.bitmapTransform(BlurTransformation(20, 3)))
+ .into(binding.imageBackdrop)
+
+ Glide.with(requireContext()).load(posterUrl)
+ .into(binding.imagePoster)
+
+ binding.textTitle.text = media.info.title
+ binding.textYear.text = media.info.year.toString()
+ binding.textAge.text = media.info.age.toString()
+ binding.textOverview.text = media.info.shortDesc
+ if (StorageController.myList.contains(media.id)) {
+ Glide.with(requireContext()).load(R.drawable.ic_baseline_check_24).into(binding.imageMyListAction)
+ } else {
+ Glide.with(requireContext()).load(R.drawable.ic_baseline_add_24).into(binding.imageMyListAction)
+ }
+
+ // clear fragments, since it lives in onCreate scope (don't do this in onPause/onStop -> FragmentManager transaction)
+ fragments.clear()
+ pagerAdapter.notifyDataSetChanged()
+
+ // specific gui
+ if (media.type == MediaType.TVSHOW) {
+ // get next episode
+ nextEpisode = if (media.episodes.firstOrNull{ !it.watched } != null) {
+ media.episodes.first{ !it.watched }
+ } else {
+ media.episodes.first()
+ }
+
+ // title is the next episodes title
+ binding.textTitle.text = nextEpisode.title
+
+ // episodes count
+ binding.textEpisodesOrRuntime.text = resources.getQuantityString(
+ R.plurals.text_episodes_count,
+ media.info.episodesCount,
+ media.info.episodesCount
+ )
+
+ // episodes
+ fragments.add(MediaFragmentEpisodes())
+ pagerAdapter.notifyDataSetChanged()
+ } else if (media.type == MediaType.MOVIE) {
+
+ if (tmdb.runtime > 0) {
+ binding.textEpisodesOrRuntime.text = resources.getQuantityString(
+ R.plurals.text_runtime,
+ tmdb.runtime,
+ tmdb.runtime
+ )
+ } else {
+ binding.textEpisodesOrRuntime.visibility = View.GONE
+ }
+ }
+
+ // if has similar titles
+ if (media.info.similar.isNotEmpty()) {
+ fragments.add(MediaFragmentSimilar())
+ pagerAdapter.notifyDataSetChanged()
+ }
+
+ // disable scrolling on appbar, if no tabs where added
+ if(fragments.isEmpty()) {
+ val params = binding.linearMedia.layoutParams as AppBarLayout.LayoutParams
+ params.scrollFlags = 0 // clear all scroll flags
+ }
+
+ binding.frameLoading.visibility = View.GONE // hide loading indicator
+ }
+
+ private fun initActions() = with(model) {
+ binding.buttonPlay.setOnClickListener {
+ when (media.type) {
+ MediaType.MOVIE -> playEpisode(media.episodes.first())
+ MediaType.TVSHOW -> playEpisode(nextEpisode)
+ else -> Log.e(javaClass.name, "Wrong Type: $media.type")
+ }
+ }
+
+ // add or remove media from myList
+ binding.linearMyListAction.setOnClickListener {
+ if (StorageController.myList.contains(media.id)) {
+ StorageController.myList.remove(media.id)
+ Glide.with(requireContext()).load(R.drawable.ic_baseline_add_24).into(binding.imageMyListAction)
+ } else {
+ StorageController.myList.add(media.id)
+ Glide.with(requireContext()).load(R.drawable.ic_baseline_check_24).into(binding.imageMyListAction)
+ }
+ StorageController.saveMyList(requireContext())
+
+ // notify home fragment on change
+ parentFragmentManager.findFragmentByTag("HomeFragment")?.let {
+ (it as HomeFragment).updateMyListMedia()
+ }
+ }
+ }
+
+ /**
+ * play the current episode
+ * TODO this is also used in MediaFragmentEpisode, we should only have on implementation
+ */
+ private fun playEpisode(ep: Episode) {
+ (activity as MainActivity).startPlayer(model.media.id, ep.id)
+ Log.d(javaClass.name, "Started Player with episodeId: ${ep.id}")
+
+ model.updateNextEpisode(ep) // set the correct next episode
+ }
+
+ /**
+ * A simple pager adapter
+ */
+ private inner class ScreenSlidePagerAdapter(fa: FragmentActivity) : FragmentStateAdapter(fa) {
+ override fun getItemCount(): Int = fragments.size
+
+ override fun createFragment(position: Int): Fragment = fragments[position]
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/MediaFragmentEpisodes.kt b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/MediaFragmentEpisodes.kt
new file mode 100644
index 0000000..78f480f
--- /dev/null
+++ b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/MediaFragmentEpisodes.kt
@@ -0,0 +1,61 @@
+package org.mosad.teapod.ui.activity.main.fragments
+
+import android.os.Bundle
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import org.mosad.teapod.ui.activity.main.MainActivity
+import org.mosad.teapod.ui.activity.main.viewmodel.MediaFragmentViewModel
+import org.mosad.teapod.databinding.FragmentMediaEpisodesBinding
+import org.mosad.teapod.util.Episode
+import org.mosad.teapod.util.adapter.EpisodeItemAdapter
+
+class MediaFragmentEpisodes : Fragment() {
+
+ private lateinit var binding: FragmentMediaEpisodesBinding
+ private lateinit var adapterRecEpisodes: EpisodeItemAdapter
+
+ private val model: MediaFragmentViewModel by activityViewModels()
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
+ binding = FragmentMediaEpisodesBinding.inflate(inflater, container, false)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ adapterRecEpisodes = EpisodeItemAdapter(model.media.episodes)
+ binding.recyclerEpisodes.adapter = adapterRecEpisodes
+
+ // set onItemClick only in adapter is initialized
+ if (this::adapterRecEpisodes.isInitialized) {
+ adapterRecEpisodes.onImageClick = { _, position ->
+ playEpisode(model.media.episodes[position])
+ }
+ }
+ }
+
+ override fun onResume() {
+ super.onResume()
+
+ // if adapterRecEpisodes is initialized, update the watched state for the episodes
+ if (this::adapterRecEpisodes.isInitialized) {
+ model.media.episodes.forEachIndexed { index, episode ->
+ adapterRecEpisodes.updateWatchedState(episode.watched, index)
+ }
+ adapterRecEpisodes.notifyDataSetChanged()
+ }
+ }
+
+ private fun playEpisode(ep: Episode) {
+ (activity as MainActivity).startPlayer(model.media.id, ep.id)
+ Log.d(javaClass.name, "Started Player with episodeId: ${ep.id}")
+
+ model.updateNextEpisode(ep) // set the correct next episode
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/MediaFragmentSimilar.kt b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/MediaFragmentSimilar.kt
new file mode 100644
index 0000000..db6d519
--- /dev/null
+++ b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/MediaFragmentSimilar.kt
@@ -0,0 +1,41 @@
+package org.mosad.teapod.ui.activity.main.fragments
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import org.mosad.teapod.databinding.FragmentMediaSimilarBinding
+import org.mosad.teapod.ui.activity.main.viewmodel.MediaFragmentViewModel
+import org.mosad.teapod.util.adapter.MediaItemAdapter
+import org.mosad.teapod.util.decoration.MediaItemDecoration
+import org.mosad.teapod.util.showFragment
+
+class MediaFragmentSimilar : Fragment() {
+
+ private lateinit var binding: FragmentMediaSimilarBinding
+ private val model: MediaFragmentViewModel by activityViewModels()
+
+ private lateinit var adapterSimilar: MediaItemAdapter
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
+ binding = FragmentMediaSimilarBinding.inflate(inflater, container, false)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ adapterSimilar = MediaItemAdapter(model.media.info.similar)
+ binding.recyclerMediaSimilar.adapter = adapterSimilar
+ binding.recyclerMediaSimilar.addItemDecoration(MediaItemDecoration(9))
+
+ // set onItemClick only in adapter is initialized
+ if (this::adapterSimilar.isInitialized) {
+ adapterSimilar.onItemClick = { mediaId, _ ->
+ activity?.showFragment(MediaFragment(mediaId))
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/mosad/teapod/ui/fragments/SearchFragment.kt b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/SearchFragment.kt
similarity index 92%
rename from app/src/main/java/org/mosad/teapod/ui/fragments/SearchFragment.kt
rename to app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/SearchFragment.kt
index 6810030..57c43b1 100644
--- a/app/src/main/java/org/mosad/teapod/ui/fragments/SearchFragment.kt
+++ b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/SearchFragment.kt
@@ -1,4 +1,4 @@
-package org.mosad.teapod.ui.fragments
+package org.mosad.teapod.ui.activity.main.fragments
import android.os.Bundle
import android.view.LayoutInflater
@@ -7,11 +7,11 @@ import android.view.ViewGroup
import android.widget.SearchView
import androidx.fragment.app.Fragment
import kotlinx.coroutines.*
-import org.mosad.teapod.MainActivity
import org.mosad.teapod.databinding.FragmentSearchBinding
import org.mosad.teapod.parser.AoDParser
import org.mosad.teapod.util.decoration.MediaItemDecoration
import org.mosad.teapod.util.adapter.MediaItemAdapter
+import org.mosad.teapod.util.showFragment
class SearchFragment : Fragment() {
@@ -33,7 +33,7 @@ class SearchFragment : Fragment() {
adapter = MediaItemAdapter(AoDParser.itemMediaList)
adapter!!.onItemClick = { mediaId, _ ->
binding.searchText.clearFocus()
- (activity as MainActivity).showFragment(MediaFragment(mediaId))
+ activity?.showFragment(MediaFragment(mediaId))
}
binding.recyclerMediaSearch.adapter = adapter
diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/main/viewmodel/MediaFragmentViewModel.kt b/app/src/main/java/org/mosad/teapod/ui/activity/main/viewmodel/MediaFragmentViewModel.kt
new file mode 100644
index 0000000..c2ba21d
--- /dev/null
+++ b/app/src/main/java/org/mosad/teapod/ui/activity/main/viewmodel/MediaFragmentViewModel.kt
@@ -0,0 +1,48 @@
+package org.mosad.teapod.ui.activity.main.viewmodel
+
+import android.app.Application
+import androidx.lifecycle.AndroidViewModel
+import org.mosad.teapod.parser.AoDParser
+import org.mosad.teapod.util.*
+import org.mosad.teapod.util.DataTypes.MediaType
+
+/**
+ * handle media, next ep and tmdb
+ */
+class MediaFragmentViewModel(application: Application) : AndroidViewModel(application) {
+
+ var media = Media(-1, "", MediaType.OTHER)
+ internal set
+ var nextEpisode = Episode()
+ internal set
+ var tmdb = TMDBResponse()
+ internal set
+
+ /**
+ * set media, tmdb and nextEpisode
+ */
+ suspend fun load(mediaId: Int) {
+ media = AoDParser.getMediaById(mediaId)
+ tmdb = TMDBApiController().search(media.info.title, media.type)
+
+ if (media.type == MediaType.TVSHOW) {
+ nextEpisode = if (media.episodes.firstOrNull{ !it.watched } != null) {
+ media.episodes.first{ !it.watched }
+ } else {
+ media.episodes.first()
+ }
+ }
+ }
+
+ /**
+ * get the next episode based on episode number (the true next episode)
+ * if no matching is found, use first episode
+ */
+ fun updateNextEpisode(currentEp: Episode) {
+ if (media.type == MediaType.MOVIE) return // return if movie
+
+ nextEpisode = media.episodes.firstOrNull{ it.number > currentEp.number }
+ ?: media.episodes.first()
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/onboarding/OnLoginFragment.kt b/app/src/main/java/org/mosad/teapod/ui/activity/onboarding/OnLoginFragment.kt
new file mode 100644
index 0000000..f65ab30
--- /dev/null
+++ b/app/src/main/java/org/mosad/teapod/ui/activity/onboarding/OnLoginFragment.kt
@@ -0,0 +1,54 @@
+package org.mosad.teapod.ui.activity.onboarding
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import kotlinx.coroutines.*
+import org.mosad.teapod.R
+import org.mosad.teapod.databinding.FragmentOnLoginBinding
+import org.mosad.teapod.parser.AoDParser
+import org.mosad.teapod.preferences.EncryptedPreferences
+
+class OnLoginFragment: Fragment() {
+
+ private lateinit var binding: FragmentOnLoginBinding
+ private var loginJob: Job? = null
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
+ binding = FragmentOnLoginBinding.inflate(inflater, container, false)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ initActions()
+ }
+
+ private fun initActions() {
+ binding.buttonLogin.setOnClickListener {
+ // get login credentials from gui
+ val email = binding.editTextLogin.text.toString()
+ val password = binding.editTextPassword.text.toString()
+
+ EncryptedPreferences.saveCredentials(email, password, requireContext()) // save the credentials
+
+ binding.buttonLogin.isClickable = false
+ loginJob = GlobalScope.launch {
+ if (AoDParser.login()) {
+ // if login was successful, switch to main
+ if (activity is OnboardingActivity) {
+ (activity as OnboardingActivity).launchMainActivity()
+ }
+ } else {
+ withContext(Dispatchers.Main) {
+ binding.textLoginDesc.text = getString(R.string.on_login_failed)
+ binding.buttonLogin.isClickable = true
+ }
+ }
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/onboarding/OnWelcomeFragment.kt b/app/src/main/java/org/mosad/teapod/ui/activity/onboarding/OnWelcomeFragment.kt
new file mode 100644
index 0000000..b51e6ed
--- /dev/null
+++ b/app/src/main/java/org/mosad/teapod/ui/activity/onboarding/OnWelcomeFragment.kt
@@ -0,0 +1,31 @@
+package org.mosad.teapod.ui.activity.onboarding
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import org.mosad.teapod.databinding.FragmentOnWelcomeBinding
+
+class OnWelcomeFragment: Fragment() {
+
+ private lateinit var binding: FragmentOnWelcomeBinding
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
+ binding = FragmentOnWelcomeBinding.inflate(inflater, container, false)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ initActions()
+ }
+
+ private fun initActions() {
+ binding.buttonGetStarted.setOnClickListener {
+ if (activity is OnboardingActivity) {
+ (activity as OnboardingActivity).nextFragment()
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/onboarding/OnboardingActivity.kt b/app/src/main/java/org/mosad/teapod/ui/activity/onboarding/OnboardingActivity.kt
new file mode 100644
index 0000000..8087e98
--- /dev/null
+++ b/app/src/main/java/org/mosad/teapod/ui/activity/onboarding/OnboardingActivity.kt
@@ -0,0 +1,79 @@
+package org.mosad.teapod.ui.activity.onboarding
+
+import android.content.Intent
+import android.os.Bundle
+import android.view.View
+import androidx.appcompat.app.AppCompatActivity
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentActivity
+import androidx.viewpager2.adapter.FragmentStateAdapter
+import com.google.android.material.tabs.TabLayoutMediator
+import org.mosad.teapod.ui.activity.main.MainActivity
+import org.mosad.teapod.databinding.ActivityOnboardingBinding
+
+class OnboardingActivity : AppCompatActivity() {
+
+ private lateinit var binding: ActivityOnboardingBinding
+ private lateinit var pagerAdapter: FragmentStateAdapter
+
+ private val fragments = arrayOf(OnLoginFragment())
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ binding = ActivityOnboardingBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+
+ pagerAdapter = ScreenSlidePagerAdapter(this)
+ binding.viewPager.adapter = pagerAdapter
+ TabLayoutMediator(binding.tabLayout, binding.viewPager) { _, _ -> }.attach()
+
+ // we don't use the skip button, instead we use the start button to skip the last fragment
+ binding.buttonSkip.visibility = View.GONE
+
+ // hide tab layout if only one tab is displayed
+ if (fragments.size <= 1) {
+ binding.tabLayout.visibility = View.GONE
+ }
+ }
+
+ override fun onBackPressed() {
+ if (binding.viewPager.currentItem == 0) {
+ super.onBackPressed()
+ } else {
+ binding.viewPager.currentItem = binding.viewPager.currentItem - 1
+ }
+ }
+
+ fun nextFragment() {
+ if (binding.viewPager.currentItem < fragments.size - 1) {
+ binding.viewPager.currentItem++
+ } else {
+ launchMainActivity()
+ }
+ }
+
+ fun btnNextClick(@Suppress("UNUSED_PARAMETER")v: View) {
+ //nextFragment() // currently not used in Teapod
+ }
+
+ fun btnSkipClick(@Suppress("UNUSED_PARAMETER")v: View) {
+ //launchMainActivity() // currently not used in Teapod
+ }
+
+ fun launchMainActivity() {
+ startActivity(Intent(this, MainActivity::class.java))
+ finish()
+ }
+
+ /**
+ * A simple pager adapter
+ */
+ private inner class ScreenSlidePagerAdapter(fa: FragmentActivity) : FragmentStateAdapter(fa) {
+ override fun getItemCount(): Int = fragments.size
+
+ override fun createFragment(position: Int): Fragment = fragments[position]
+ }
+
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/mosad/teapod/player/PlayerActivity.kt b/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerActivity.kt
similarity index 78%
rename from app/src/main/java/org/mosad/teapod/player/PlayerActivity.kt
rename to app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerActivity.kt
index d5b76e9..9c28001 100644
--- a/app/src/main/java/org/mosad/teapod/player/PlayerActivity.kt
+++ b/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerActivity.kt
@@ -1,13 +1,21 @@
-package org.mosad.teapod.player
+package org.mosad.teapod.ui.activity.player
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.annotation.SuppressLint
+import android.app.PictureInPictureParams
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.content.res.Configuration
import android.os.Build
import android.os.Bundle
import android.util.Log
-import android.view.*
+import android.util.Rational
+import android.view.GestureDetector
+import android.view.MotionEvent
+import android.view.View
import androidx.activity.viewModels
+import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.GestureDetectorCompat
import androidx.core.view.isVisible
@@ -26,6 +34,9 @@ import org.mosad.teapod.preferences.Preferences
import org.mosad.teapod.ui.components.EpisodesListPlayer
import org.mosad.teapod.ui.components.LanguageSettingsPlayer
import org.mosad.teapod.util.DataTypes
+import org.mosad.teapod.util.hideBars
+import org.mosad.teapod.util.isInPiPMode
+import org.mosad.teapod.util.navToLauncherTask
import java.util.*
import java.util.concurrent.TimeUnit
import kotlin.concurrent.scheduleAtFixedRate
@@ -38,6 +49,7 @@ class PlayerActivity : AppCompatActivity() {
private lateinit var gestureDetector: GestureDetectorCompat
private lateinit var timerUpdates: TimerTask
+ private var wasInPiP = false
private var playWhenReady = true
private var currentWindow = 0
private var playbackPosition: Long = 0
@@ -73,6 +85,11 @@ class PlayerActivity : AppCompatActivity() {
initActions()
}
+ /**
+ * once minimum is android 7.0 this can be simplified
+ * only onStart and onStop should be needed then
+ * see: https://developer.android.com/guide/topics/ui/picture-in-picture#continuing_playback
+ */
override fun onStart() {
super.onStart()
if (Util.SDK_INT > 23) {
@@ -83,6 +100,8 @@ class PlayerActivity : AppCompatActivity() {
override fun onResume() {
super.onResume()
+ if (isInPiPMode()) { return }
+
if (Util.SDK_INT <= 23) {
initPlayer()
video_view?.onResume()
@@ -91,6 +110,8 @@ class PlayerActivity : AppCompatActivity() {
override fun onPause() {
super.onPause()
+ if (isInPiPMode()) { return }
+
if (Util.SDK_INT <= 23) {
video_view?.onPause()
releasePlayer()
@@ -103,6 +124,11 @@ class PlayerActivity : AppCompatActivity() {
video_view?.onPause()
releasePlayer()
}
+
+ // if the player was in pip, it's on a different task
+ if (wasInPiP) { navToLauncherTask() }
+ // if the player is in pip, remove the task, else we'll get a zombie
+ if (isInPiPMode()) { finishAndRemoveTask() }
}
override fun onSaveInstanceState(outState: Bundle) {
@@ -112,6 +138,57 @@ class PlayerActivity : AppCompatActivity() {
super.onSaveInstanceState(outState)
}
+ /**
+ * used, when the player is in pip and the user selects a new media
+ */
+ override fun onNewIntent(intent: Intent?) {
+ super.onNewIntent(intent)
+
+ // when the intent changed, lead the new media and play it
+ intent?.let {
+ model.loadMedia(
+ it.getIntExtra(getString(R.string.intent_media_id), 0),
+ it.getIntExtra(getString(R.string.intent_episode_id), 0)
+ )
+ model.playEpisode(model.currentEpisode, replace = true)
+ }
+ }
+
+ /**
+ * previous to android n, don't override
+ */
+ @RequiresApi(Build.VERSION_CODES.N)
+ override fun onUserLeaveHint() {
+ super.onUserLeaveHint()
+
+ // start pip mode, if supported
+ if (packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
+ @Suppress("deprecation")
+ enterPictureInPictureMode()
+ } else {
+ val width = model.player.videoFormat?.width ?: 0
+ val height = model.player.videoFormat?.height ?: 0
+ val params = PictureInPictureParams.Builder()
+ .setAspectRatio(Rational(width, height))
+ .build()
+ enterPictureInPictureMode(params)
+ }
+
+ wasInPiP = isInPiPMode()
+ }
+ }
+
+ override fun onPictureInPictureModeChanged(
+ isInPictureInPictureMode: Boolean,
+ newConfig: Configuration?
+ ) {
+ super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
+
+ // Hide the full-screen UI (controls, etc.) while in picture-in-picture mode.
+ video_view.useController = !isInPictureInPictureMode
+ }
+
private fun initPlayer() {
if (model.media.id < 0) {
Log.e(javaClass.name, "No media was set.")
@@ -122,8 +199,8 @@ class PlayerActivity : AppCompatActivity() {
initTimeUpdates()
// if the player is ready or buffering we can simply play the file again, else do nothing
- if ((model.player.playbackState == ExoPlayer.STATE_READY || model.player.playbackState == ExoPlayer.STATE_BUFFERING)
- ) {
+ val playbackState = model.player.playbackState
+ if ((playbackState == ExoPlayer.STATE_READY || playbackState == ExoPlayer.STATE_BUFFERING)) {
model.player.play()
}
}
@@ -178,7 +255,9 @@ class PlayerActivity : AppCompatActivity() {
}
private fun initActions() {
- exo_close_player.setOnClickListener { this.finish() }
+ exo_close_player.setOnClickListener {
+ this.finish()
+ }
rwd_10.setOnButtonClickListener { rewind() }
ffwd_10.setOnButtonClickListener { fastForward() }
button_next_ep.setOnClickListener { playNextEpisode() }
@@ -213,8 +292,8 @@ class PlayerActivity : AppCompatActivity() {
}
if (remainingTime in 1..20000) {
- // if the next ep button is not visible, make it visible
- if (!btnNextEpIsVisible && model.nextEpisode != null && Preferences.autoplay) {
+ // 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()) {
withContext(Dispatchers.Main) { showButtonNextEp() }
}
} else if (btnNextEpIsVisible) {
@@ -229,7 +308,7 @@ class PlayerActivity : AppCompatActivity() {
}
}
- private fun releasePlayer(){
+ private fun releasePlayer() {
playbackPosition = model.player.currentPosition
currentWindow = model.player.currentWindowIndex
playWhenReady = model.player.playWhenReady
@@ -260,11 +339,19 @@ class PlayerActivity : AppCompatActivity() {
private fun onMediaChanged() {
exo_text_title.text = model.getMediaTitle()
+ // hide the next ep button, if there is none
button_next_ep_c.visibility = if (model.nextEpisode == null) {
View.GONE
} else {
View.VISIBLE
}
+
+ // hide the episodes button, if the media type changed
+ button_episodes.visibility = if (model.media.type == DataTypes.MediaType.MOVIE) {
+ View.GONE
+ } else {
+ View.VISIBLE
+ }
}
/**
@@ -312,27 +399,6 @@ class PlayerActivity : AppCompatActivity() {
hideButtonNextEp()
}
- /**
- * hide the status and navigation bar
- */
- private fun hideBars() {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
- window.setDecorFitsSystemWindows(false)
- window.insetsController?.apply {
- hide(WindowInsets.Type.statusBars() or WindowInsets.Type.navigationBars())
- systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_BARS_BY_SWIPE
- }
- } else {
- @Suppress("deprecation")
- window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_IMMERSIVE
- or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
- or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
- or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
- or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
- or View.SYSTEM_UI_FLAG_FULLSCREEN)
- }
- }
-
/**
* show the next episode button
* TODO improve the show animation
@@ -393,7 +459,10 @@ class PlayerActivity : AppCompatActivity() {
* on single tap hide or show the controls
*/
override fun onSingleTapConfirmed(e: MotionEvent?): Boolean {
- if (controller.isVisible) controller.hide() else controller.show()
+ if (!isInPiPMode()) {
+ if (controller.isVisible) controller.hide() else controller.show()
+ }
+
return true
}
diff --git a/app/src/main/java/org/mosad/teapod/player/PlayerViewModel.kt b/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerViewModel.kt
similarity index 98%
rename from app/src/main/java/org/mosad/teapod/player/PlayerViewModel.kt
rename to app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerViewModel.kt
index 35117f4..4efb7c4 100644
--- a/app/src/main/java/org/mosad/teapod/player/PlayerViewModel.kt
+++ b/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerViewModel.kt
@@ -1,4 +1,4 @@
-package org.mosad.teapod.player
+package org.mosad.teapod.ui.activity.player
import android.app.Application
import android.net.Uri
@@ -15,7 +15,6 @@ import kotlinx.coroutines.runBlocking
import org.mosad.teapod.R
import org.mosad.teapod.parser.AoDParser
import org.mosad.teapod.preferences.Preferences
-import org.mosad.teapod.ui.fragments.MediaFragment
import org.mosad.teapod.util.DataTypes
import org.mosad.teapod.util.Episode
import org.mosad.teapod.util.Media
diff --git a/app/src/main/java/org/mosad/teapod/ui/components/EpisodesListPlayer.kt b/app/src/main/java/org/mosad/teapod/ui/components/EpisodesListPlayer.kt
index 9f41056..cb51deb 100644
--- a/app/src/main/java/org/mosad/teapod/ui/components/EpisodesListPlayer.kt
+++ b/app/src/main/java/org/mosad/teapod/ui/components/EpisodesListPlayer.kt
@@ -6,7 +6,7 @@ import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.LinearLayout
import org.mosad.teapod.databinding.PlayerEpisodesListBinding
-import org.mosad.teapod.player.PlayerViewModel
+import org.mosad.teapod.ui.activity.player.PlayerViewModel
import org.mosad.teapod.util.adapter.PlayerEpisodeItemAdapter
class EpisodesListPlayer @JvmOverloads constructor(
diff --git a/app/src/main/java/org/mosad/teapod/ui/components/LanguageSettingsPlayer.kt b/app/src/main/java/org/mosad/teapod/ui/components/LanguageSettingsPlayer.kt
index ff4510a..404ba7e 100644
--- a/app/src/main/java/org/mosad/teapod/ui/components/LanguageSettingsPlayer.kt
+++ b/app/src/main/java/org/mosad/teapod/ui/components/LanguageSettingsPlayer.kt
@@ -13,7 +13,7 @@ import android.widget.TextView
import androidx.core.view.children
import org.mosad.teapod.R
import org.mosad.teapod.databinding.PlayerLanguageSettingsBinding
-import org.mosad.teapod.player.PlayerViewModel
+import org.mosad.teapod.ui.activity.player.PlayerViewModel
import java.util.*
class LanguageSettingsPlayer @JvmOverloads constructor(
diff --git a/app/src/main/java/org/mosad/teapod/ui/fragments/MediaFragment.kt b/app/src/main/java/org/mosad/teapod/ui/fragments/MediaFragment.kt
deleted file mode 100644
index aeb6fa4..0000000
--- a/app/src/main/java/org/mosad/teapod/ui/fragments/MediaFragment.kt
+++ /dev/null
@@ -1,177 +0,0 @@
-package org.mosad.teapod.ui.fragments
-
-import android.graphics.Color
-import android.graphics.drawable.ColorDrawable
-import android.os.Bundle
-import android.util.Log
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import androidx.fragment.app.Fragment
-import androidx.recyclerview.widget.LinearLayoutManager
-import androidx.recyclerview.widget.RecyclerView
-import com.bumptech.glide.Glide
-import com.bumptech.glide.request.RequestOptions
-import jp.wasabeef.glide.transformations.BlurTransformation
-import kotlinx.coroutines.*
-import org.mosad.teapod.MainActivity
-import org.mosad.teapod.R
-import org.mosad.teapod.databinding.FragmentMediaBinding
-import org.mosad.teapod.parser.AoDParser
-import org.mosad.teapod.util.*
-import org.mosad.teapod.util.DataTypes.MediaType
-import org.mosad.teapod.util.adapter.EpisodeItemAdapter
-
-class MediaFragment(private val mediaId: Int) : Fragment() {
-
- private lateinit var binding: FragmentMediaBinding
- private lateinit var adapterRecEpisodes: EpisodeItemAdapter
- private lateinit var viewManager: RecyclerView.LayoutManager
-
- private lateinit var media: Media
- private lateinit var tmdb: TMDBResponse
- private lateinit var nextEpisode: Episode
-
- override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
- binding = FragmentMediaBinding.inflate(inflater, container, false)
- return binding.root
- }
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
- binding.frameLoading.visibility = View.VISIBLE
-
- GlobalScope.launch(Dispatchers.Main) {
- // load the streams for the selected media
- media = AoDParser.getMediaById(mediaId)
- tmdb = TMDBApiController().search(media.info.title, media.type)
-
- if (this@MediaFragment.isAdded) {
- updateGUI()
- initActions()
- }
- }
- }
-
- override fun onResume() {
- super.onResume()
-
- // only notify adapter, if initialized
- if (this::adapterRecEpisodes.isInitialized) {
- // TODO find a better solution for this
- media.episodes.forEachIndexed() { index, episode ->
- adapterRecEpisodes.updateWatchedState(episode.watched, index)
- }
- adapterRecEpisodes.notifyDataSetChanged()
- }
- }
-
- /**
- * if tmdb data is present, use it, else use the aod data
- */
- private fun updateGUI() = with(binding) {
- // generic gui
- val backdropUrl = if (tmdb.backdropUrl.isNotEmpty()) tmdb.backdropUrl else media.info.posterUrl
- val posterUrl = if (tmdb.posterUrl.isNotEmpty()) tmdb.posterUrl else media.info.posterUrl
-
- Glide.with(requireContext()).load(backdropUrl)
- .apply(RequestOptions.placeholderOf(ColorDrawable(Color.DKGRAY)))
- .apply(RequestOptions.bitmapTransform(BlurTransformation(20, 3)))
- .into(imageBackdrop)
-
- Glide.with(requireContext()).load(posterUrl)
- .into(imagePoster)
-
- textTitle.text = media.info.title
- textYear.text = media.info.year.toString()
- textAge.text = media.info.age.toString()
- textOverview.text = media.info.shortDesc
- if (StorageController.myList.contains(media.id)) {
- Glide.with(requireContext()).load(R.drawable.ic_baseline_check_24).into(imageMyListAction)
- } else {
- Glide.with(requireContext()).load(R.drawable.ic_baseline_add_24).into(imageMyListAction)
- }
-
- // specific gui
- if (media.type == MediaType.TVSHOW) {
- adapterRecEpisodes = EpisodeItemAdapter(media.episodes)
- viewManager = LinearLayoutManager(context)
- recyclerEpisodes.layoutManager = viewManager
- recyclerEpisodes.adapter = adapterRecEpisodes
-
- binding.textEpisodesOrRuntime.text = getString(R.string.text_episodes_count, media.info.episodesCount)
-
- // get next episode
- nextEpisode = if (media.episodes.firstOrNull{ !it.watched } != null) {
- media.episodes.first{ !it.watched }
- } else {
- media.episodes.first()
- }
-
- // title is the next episodes title
- textTitle.text = nextEpisode.title
- } else if (media.type == MediaType.MOVIE) {
- recyclerEpisodes.visibility = View.GONE
-
- if (tmdb.runtime > 0) {
- textEpisodesOrRuntime.text = getString(R.string.text_runtime, tmdb.runtime)
- } else {
- textEpisodesOrRuntime.visibility = View.GONE
- }
- }
-
- frameLoading.visibility = View.GONE // hide loading indicator
- }
-
- private fun initActions() {
- binding.buttonPlay.setOnClickListener {
- when (media.type) {
- MediaType.MOVIE -> playStream(media.episodes.first())
- MediaType.TVSHOW -> playEpisode(nextEpisode)
- else -> Log.e(javaClass.name, "Wrong Type: $media.type")
- }
- }
-
- // add or remove media from myList
- binding.linearMyListAction.setOnClickListener {
- if (StorageController.myList.contains(media.id)) {
- StorageController.myList.remove(media.id)
- Glide.with(requireContext()).load(R.drawable.ic_baseline_add_24).into(binding.imageMyListAction)
- } else {
- StorageController.myList.add(media.id)
- Glide.with(requireContext()).load(R.drawable.ic_baseline_check_24).into(binding.imageMyListAction)
- }
- StorageController.saveMyList(requireContext())
-
- // notify home fragment on change
- parentFragmentManager.findFragmentByTag("HomeFragment")?.let {
- (it as HomeFragment).updateMyListMedia()
- }
- }
-
- // set onItemClick only in adapter is initialized
- if (this::adapterRecEpisodes.isInitialized) {
- adapterRecEpisodes.onImageClick = { _, position ->
- playEpisode(media.episodes[position])
- }
- }
- }
-
- private fun playEpisode(ep: Episode) {
- playStream(ep)
-
- // update nextEpisode
- nextEpisode = if (media.episodes.firstOrNull{ !it.watched } != null) {
- media.episodes.first{ !it.watched }
- } else {
- media.episodes.first()
- }
- binding.textTitle.text = nextEpisode.title
- }
-
- private fun playStream(ep: Episode) {
- Log.d(javaClass.name, "Starting Player with mediaId: ${media.id}")
- (activity as MainActivity).startPlayer(media.id, ep.id)
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/org/mosad/teapod/util/ActivityUtils.kt b/app/src/main/java/org/mosad/teapod/util/ActivityUtils.kt
new file mode 100644
index 0000000..283d2e7
--- /dev/null
+++ b/app/src/main/java/org/mosad/teapod/util/ActivityUtils.kt
@@ -0,0 +1,82 @@
+package org.mosad.teapod.util
+
+import android.app.Activity
+import android.app.ActivityManager
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import android.view.View
+import android.view.WindowInsets
+import android.view.WindowInsetsController
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentActivity
+import androidx.fragment.app.commit
+import org.mosad.teapod.R
+import kotlin.system.exitProcess
+
+/**
+ * Show a fragment on top of the current fragment.
+ * The current fragment is replaced and the new one is added
+ * to the back stack.
+ */
+fun FragmentActivity.showFragment(fragment: Fragment) {
+ supportFragmentManager.commit {
+ replace(R.id.nav_host_fragment, fragment, fragment.javaClass.simpleName)
+ addToBackStack(fragment.javaClass.name)
+ show(fragment)
+ }
+}
+
+/**
+ * hide the status and navigation bar
+ */
+fun Activity.hideBars() {
+ window.apply {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ setDecorFitsSystemWindows(false)
+ insetsController?.apply {
+ hide(WindowInsets.Type.statusBars() or WindowInsets.Type.navigationBars())
+ systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_BARS_BY_SWIPE
+ }
+ } else {
+ @Suppress("deprecation")
+ decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_IMMERSIVE
+ or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
+ or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
+ or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+ or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
+ or View.SYSTEM_UI_FLAG_FULLSCREEN)
+ }
+ }
+}
+
+fun Activity.isInPiPMode(): Boolean {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ isInPictureInPictureMode
+ } else {
+ false // pip mode not supported
+ }
+}
+
+/**
+ * Bring up launcher task to front
+ */
+fun Activity.navToLauncherTask() {
+ val activityManager = (getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager)
+ activityManager.appTasks.forEach { task ->
+ val baseIntent = task.taskInfo.baseIntent
+ val categories = baseIntent.categories
+ if (categories != null && categories.contains(Intent.CATEGORY_LAUNCHER)) {
+ task.moveToFront()
+ return
+ }
+ }
+}
+
+/**
+ * exit and remove the app from tasks
+ */
+fun Activity.exitAndRemoveTask() {
+ finishAndRemoveTask()
+ exitProcess(0)
+}
diff --git a/app/src/main/java/org/mosad/teapod/util/DataTypes.kt b/app/src/main/java/org/mosad/teapod/util/DataTypes.kt
index bc27719..cf936bd 100644
--- a/app/src/main/java/org/mosad/teapod/util/DataTypes.kt
+++ b/app/src/main/java/org/mosad/teapod/util/DataTypes.kt
@@ -55,6 +55,7 @@ data class Media(
fun getEpisodeById(id: Int) = episodes.first { it.id == id }
}
+// TODO all val?
data class Info(
var title: String = "",
var posterUrl: String = "",
@@ -62,7 +63,8 @@ data class Info(
var description: String = "",
var year: Int = 0,
var age: Int = 0,
- var episodesCount: Int = 0
+ var episodesCount: Int = 0,
+ var similar: List = listOf()
)
/**
@@ -96,6 +98,7 @@ data class Stream(
/**
* this class is used for tmdb responses
+ * TODO why is runtime var?
*/
data class TMDBResponse(
val id: Int = 0,
diff --git a/app/src/main/java/org/mosad/teapod/util/Utils.kt b/app/src/main/java/org/mosad/teapod/util/Utils.kt
new file mode 100644
index 0000000..1b9e7f8
--- /dev/null
+++ b/app/src/main/java/org/mosad/teapod/util/Utils.kt
@@ -0,0 +1,7 @@
+package org.mosad.teapod.util
+
+import android.widget.TextView
+
+fun TextView.setDrawableTop(drawable: Int) {
+ this.setCompoundDrawablesWithIntrinsicBounds(0, drawable, 0, 0)
+}
diff --git a/app/src/main/java/org/mosad/teapod/util/adapter/EpisodeItemAdapter.kt b/app/src/main/java/org/mosad/teapod/util/adapter/EpisodeItemAdapter.kt
index 9438dc8..6eb467c 100644
--- a/app/src/main/java/org/mosad/teapod/util/adapter/EpisodeItemAdapter.kt
+++ b/app/src/main/java/org/mosad/teapod/util/adapter/EpisodeItemAdapter.kt
@@ -1,5 +1,7 @@
package org.mosad.teapod.util.adapter
+import android.graphics.Color
+import android.graphics.drawable.ColorDrawable
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.content.ContextCompat
@@ -34,6 +36,7 @@ class EpisodeItemAdapter(private val episodes: List) : RecyclerView.Ada
if (episodes[position].posterUrl.isNotEmpty()) {
Glide.with(context).load(ep.posterUrl)
+ .apply(RequestOptions.placeholderOf(ColorDrawable(Color.DKGRAY)))
.apply(RequestOptions.bitmapTransform(RoundedCornersTransformation(10, 0)))
.into(holder.binding.imageEpisode)
}
@@ -52,7 +55,8 @@ class EpisodeItemAdapter(private val episodes: List) : RecyclerView.Ada
}
fun updateWatchedState(watched: Boolean, position: Int) {
- episodes[position].watched = watched
+ // use getOrNull as there could be a index out of bound when running this in onResume()
+ episodes.getOrNull(position)?.watched = watched
}
inner class EpisodeViewHolder(val binding: ItemEpisodeBinding) : RecyclerView.ViewHolder(binding.root) {
diff --git a/app/src/main/res/drawable/dot_default.xml b/app/src/main/res/drawable/dot_default.xml
new file mode 100644
index 0000000..cb7aef7
--- /dev/null
+++ b/app/src/main/res/drawable/dot_default.xml
@@ -0,0 +1,12 @@
+
+
+ -
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/dot_selected.xml b/app/src/main/res/drawable/dot_selected.xml
new file mode 100644
index 0000000..078fd8f
--- /dev/null
+++ b/app/src/main/res/drawable/dot_selected.xml
@@ -0,0 +1,12 @@
+
+
+ -
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/dot_tab_selector.xml b/app/src/main/res/drawable/dot_tab_selector.xml
new file mode 100644
index 0000000..b32d447
--- /dev/null
+++ b/app/src/main/res/drawable/dot_tab_selector.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_baseline_code_24.xml b/app/src/main/res/drawable/ic_baseline_code_24.xml
new file mode 100644
index 0000000..2f812a4
--- /dev/null
+++ b/app/src/main/res/drawable/ic_baseline_code_24.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_baseline_description_24.xml b/app/src/main/res/drawable/ic_baseline_description_24.xml
new file mode 100644
index 0000000..e7ef3d4
--- /dev/null
+++ b/app/src/main/res/drawable/ic_baseline_description_24.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_baseline_info_24.xml b/app/src/main/res/drawable/ic_baseline_info_24.xml
deleted file mode 100644
index 17255b7..0000000
--- a/app/src/main/res/drawable/ic_baseline_info_24.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
diff --git a/app/src/main/res/drawable/ic_baseline_people_24.xml b/app/src/main/res/drawable/ic_baseline_people_24.xml
new file mode 100644
index 0000000..6fc325a
--- /dev/null
+++ b/app/src/main/res/drawable/ic_baseline_people_24.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_outline_info_24.xml b/app/src/main/res/drawable/ic_outline_info_24.xml
new file mode 100644
index 0000000..24bd840
--- /dev/null
+++ b/app/src/main/res/drawable/ic_outline_info_24.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/layout/activity_onboarding.xml b/app/src/main/res/layout/activity_onboarding.xml
new file mode 100644
index 0000000..1bdea86
--- /dev/null
+++ b/app/src/main/res/layout/activity_onboarding.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_player.xml b/app/src/main/res/layout/activity_player.xml
index 57ffcec..5a8f6bd 100644
--- a/app/src/main/res/layout/activity_player.xml
+++ b/app/src/main/res/layout/activity_player.xml
@@ -7,7 +7,7 @@
android:layout_height="match_parent"
android:background="#000000"
android:keepScreenOn="true"
- tools:context=".player.PlayerActivity">
+ tools:context=".ui.activity.player.PlayerActivity">
+ android:contentDescription="@string/rewind_10" />
-
+ tools:context=".ui.activity.main.fragments.AboutFragment">
@@ -30,23 +32,13 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
- android:layout_marginTop="7dp"
+ android:layout_marginTop="12dp"
android:layout_marginEnd="5dp"
android:text="@string/app_name"
android:textAlignment="center"
android:textSize="20sp"
android:textStyle="bold" />
-
-
+ android:layout_marginBottom="12dp"
+ android:text="@string/about_info" />
-
+ android:layout_height="match_parent"
+ android:foreground="?android:selectableItemBackground"
+ android:gravity="center"
+ android:orientation="horizontal"
+ android:padding="7dp">
-
+
+
+
+
+
+
+
+
+
+
+ android:layout_height="match_parent"
+ android:foreground="?android:selectableItemBackground"
+ android:gravity="center"
+ android:orientation="horizontal"
+ android:padding="7dp">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -107,5 +257,18 @@
android:orientation="vertical" />
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_account.xml b/app/src/main/res/layout/fragment_account.xml
index 56994bb..20ed76c 100644
--- a/app/src/main/res/layout/fragment_account.xml
+++ b/app/src/main/res/layout/fragment_account.xml
@@ -5,7 +5,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?themePrimary"
- tools:context=".ui.fragments.AccountFragment">
+ tools:context=".ui.activity.main.fragments.AccountFragment">
+ tools:context=".ui.activity.main.fragments.HomeFragment">
+ app:drawableTopCompat="@drawable/ic_outline_info_24" />
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_library.xml b/app/src/main/res/layout/fragment_library.xml
index 7fb8198..011314d 100644
--- a/app/src/main/res/layout/fragment_library.xml
+++ b/app/src/main/res/layout/fragment_library.xml
@@ -5,7 +5,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?themePrimary"
- tools:context=".ui.fragments.LibraryFragment">
+ tools:context=".ui.activity.main.fragments.LibraryFragment">
-
+ tools:context=".ui.activity.main.fragments.MediaFragment">
-
+ android:layout_height="match_parent">
-
+ android:background="?themePrimary">
-
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ app:layout_scrollFlags="scroll">
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ android:layout_height="wrap_content">
+ android:id="@+id/image_backdrop"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:adjustViewBounds="false"
+ android:contentDescription="@string/media_poster_backdrop_desc"
+ android:maxHeight="231dp"
+ android:minHeight="220dp"
+ android:scaleType="centerCrop" />
+
+
+
+
+
+
-
-
+ android:padding="2dp"
+ android:text="@string/text_year_ex" />
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_media_episodes.xml b/app/src/main/res/layout/fragment_media_episodes.xml
new file mode 100644
index 0000000..eb4485d
--- /dev/null
+++ b/app/src/main/res/layout/fragment_media_episodes.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_media_similar.xml b/app/src/main/res/layout/fragment_media_similar.xml
new file mode 100644
index 0000000..6837edb
--- /dev/null
+++ b/app/src/main/res/layout/fragment_media_similar.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_on_login.xml b/app/src/main/res/layout/fragment_on_login.xml
new file mode 100644
index 0000000..b1a106f
--- /dev/null
+++ b/app/src/main/res/layout/fragment_on_login.xml
@@ -0,0 +1,90 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_on_welcome.xml b/app/src/main/res/layout/fragment_on_welcome.xml
new file mode 100644
index 0000000..fb8ada8
--- /dev/null
+++ b/app/src/main/res/layout/fragment_on_welcome.xml
@@ -0,0 +1,74 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_search.xml b/app/src/main/res/layout/fragment_search.xml
index 548aa84..6427560 100644
--- a/app/src/main/res/layout/fragment_search.xml
+++ b/app/src/main/res/layout/fragment_search.xml
@@ -5,7 +5,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?themePrimary"
- tools:context=".ui.fragments.SearchFragment">
+ tools:context=".ui.activity.main.fragments.SearchFragment">
diff --git a/app/src/main/res/values-de-rDE/strings.xml b/app/src/main/res/values-de-rDE/strings.xml
index 034486b..c958865 100644
--- a/app/src/main/res/values-de-rDE/strings.xml
+++ b/app/src/main/res/values-de-rDE/strings.xml
@@ -11,14 +11,23 @@
Neue Episoden
Neue Simulcasts
Neue Titel
+ Top 10
Suche nach Filmen und Serien
Abspielen
- %1$d Episoden
+
+ - %d Episode
+ - %d Episoden
+
%1$d Minuten
+
+ - %d Minute
+ - %d Minuten
+
+ Ähnliche Titel
Flg. %1$d %2$s
Flg. %1$d %2$s (OmU)
@@ -37,12 +46,11 @@
Dunkel
-
- Teapod ist eine inoffizielle App für Anime on Demand.
- Sie wird unter den Bedingungen der GNU GPL 3 oder höher zur Verfügung gestellt.
- \n\n
- © 2020-2021 seil0@mosad.xyz
-
+ Version
+ Autor
+ Quellcode
+ Lizenz
+ Eine inoffizielle App für Anime on Demand.
Lizenzen von Drittanbietern
© %1$s %2$s unter %3$s
@@ -56,9 +64,21 @@
Folgen
Folge
+
+ Überspringen
+ Weiter
+ Fertig
+ Willkommen!\nTeapod ist eine inoffizielle App für AoD.
+ Los geht\'s
+ Login
+ Um Teapod verwenden zu können musst du dich mit deinem AoD Account anmelden. Deine Login-Daten werden verschlüsselt auf deinem Gerät gespeichert.
+ Login nicht erfolgreich! Stelle sicher das deine Login-Daten korrekt sind und versuche es erneut.
+
speichern
@android:string/cancel
+ Anmelden fehlgeschlagen
+ Der Server scheint langsam zu antworten. Bitte versuche es später noch einmal.
Login
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
deleted file mode 100644
index ef95257..0000000
--- a/app/src/main/res/values/dimens.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
- 16dp
- 16dp
- 16dp
-
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 551fa05..fe2da5e 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -11,10 +11,12 @@
New episodes
New simulcasts
New titles
+ Top 10
Search for movies and series
poster
+ poster backdrop
Play
@@ -22,8 +24,17 @@
Shouya Ishida starts bullying the new girl in class …
2016
6
- %1$d episodes
+ 1$d episodes
+
+ - %d episode
+ - %d episodes
+
%1$d Minutes
+
+ - %d Minute
+ - %d Minutes
+
+ Similar titles
Ep. %1$d %2$s
Ep. %1$d %2$s (Sub)
episode poster
@@ -46,14 +57,16 @@
Dark
-
- Teapod is an unofficial app for anime on demand.
- It is published under the terms and conditions of the GNU GPL 3 or later.
- \n\n
- © 2020-2021 seil0@mosad.xyz
-
- This product uses the TMDb API but is not endorsed or certified by TMDb.
+ Version
+ %1$s (%2$s)
+ Author
+ seil0@mosad.xyz
+ Source code
git.mosad.xyz/Seil0/teapod
+ License
+ GNU General Public License 3
+ An unofficial app for anime on demand.
+ This product uses the TMDb API but is not endorsed or certified by TMDb.
Third Party Licenses
© %1$s %2$s under %3$s
@@ -71,14 +84,26 @@
Episodes
Episode
+
+ Skip
+ Next
+ Start
+ Welcome!\nTeapod is an unofficial App for AoD.
+ Get started
+ Login
+ To use Teapod you need to log in with your AoD account. Your Login-Data will be stored encrypted on your device.
+ Could not login! Make sure Username and Password are correct and try again.
+
save
@android:string/cancel
+ Login failed
+ Looks like the server is taking to long to respond. Please try again later.
Login
You need to login before you can use Teapod. The Login-Data will be stored encrypted on your device.
- Could not login. Please try again.
+ Please try again.
Password
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index afed577..bc39363 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -19,6 +19,9 @@
- @color/buttonBackgroundLight
- @color/themeSecondaryLight
- @color/textSecondaryLight
+
+
+ - @color/textSecondaryLight
-
-