40 Commits

Author SHA1 Message Date
a51f4ca490 „README.md“ ändern 2020-11-01 21:05:05 +01:00
4ec5d0fdc4 add fastlane metadata 2020-11-01 20:52:45 +01:00
8a516c640d add splash activity 2020-11-01 20:17:17 +01:00
49430e10bf update exoplayer to version 2.12.1 2020-10-30 10:03:10 +01:00
81b041ab61 added a app icon
closes #11
2020-10-25 20:04:48 +01:00
cf6a110455 set player rewind/forward to 10 sec 2020-10-23 11:51:09 +02:00
c138ab4587 add option to prefer the secondary stream, if present 2020-10-23 11:28:47 +02:00
f0ed6aa379 enable code shrink 2020-10-20 20:22:50 +02:00
a5fffd5d02 don't use gson.fromJson for a potentially unstable api 2020-10-20 20:07:59 +02:00
ff0727da22 fix movie parsing 2020-10-19 22:07:55 +02:00
ce84cb57a8 rework media parsing, parse secondary stream (sub/jap)
* use the secondary stream if no primary is present
2020-10-19 21:57:02 +02:00
4c274eb062 made AoDParser an object 2020-10-19 19:59:53 +02:00
a25ec81f6b added new episodes to home screen 2020-10-19 17:34:41 +02:00
aeb74dcb29 rework MediaItemAdapter to use ItemMedia instead of Media
This allows us to get the media onClick directly from the AoDParser. Media inforamtion are now only stored in the parsers mediaList.
2020-10-16 19:56:08 +02:00
2689c37af3 „README.md“ ändern 2020-10-16 18:31:32 +02:00
5458b43354 fix #9 & replace my list checkbox with layout for easier gui building 2020-10-16 18:24:34 +02:00
d912ed34a3 add a circular transparent background to the episode play icon 2020-10-16 14:08:17 +02:00
9f1717e646 update my list on home screen, when changed 2020-10-16 11:23:32 +02:00
085b2013ab play episode on poster click
closes #7
2020-10-16 10:05:11 +02:00
474b72df49 add favorite list to home screen 2020-10-15 21:00:31 +02:00
a8dc243d0e move all fragments into the fragments package 2020-10-15 19:01:37 +02:00
fa6419bb02 if a media was already fully loaded, don't load it again for
Since medias are cached in memory it is unnecessary to load them if they have been fully loaded once before
2020-10-15 18:57:58 +02:00
6100533c4d fix movie parsing
regression in 5b7d2cd26e
2020-10-15 18:51:29 +02:00
4ae23c4380 fix crash in episode count extraction 2020-10-15 16:23:52 +02:00
adf8a48251 replace GridView in library and search fragment with RecyclerView
closes #8
2020-10-15 13:00:44 +02:00
36c8678646 fix cancel text for german translation 2020-10-14 20:58:42 +02:00
442a02db70 update used libraries 2020-10-14 20:26:29 +02:00
5f80f1fabd show loading screen while loading media fragment
* use material components for shaped images and progress indicator
2020-10-14 20:22:20 +02:00
d2728405d1 redesign library and search fragment
* library/search now use a grid view with 2 columns
* media is now represented as card
* media details: poster and episodes have now rounded corners
2020-10-14 18:33:11 +02:00
87f9235b8a add why is it called teapod to readme 2020-10-14 01:24:51 +02:00
03cd42773d add Episode watched callback 2020-10-13 23:47:48 +02:00
cbfd186686 added licenses dialog 2020-10-13 21:27:05 +02:00
5b7d2cd26e added episode description and is watched status to MediaFragment 2020-10-13 20:23:55 +02:00
6fb8f56faf minor code clean up 2020-10-13 16:30:23 +02:00
dcaf64acde improved MediaFragment UI
* fix searchview not losing focus when media is selected
2020-10-13 15:56:07 +02:00
597271d4de use poster as backdrop if no backdrop is set, update to android studio
* update gradle to version 6.5
* update android gradle plugin to version 4.1
2020-10-13 12:27:13 +02:00
c947105a1f use material components button in media fragment 2020-10-13 00:14:03 +02:00
9ec4c24e21 verify login data on start, added german translation 2020-10-12 23:26:32 +02:00
00a6981ae5 improved tmdb data handling, added backdrop 2020-10-12 22:43:42 +02:00
ee063a5bbe „README.md“ ändern 2020-10-12 21:46:51 +02:00
79 changed files with 2135 additions and 628 deletions

View File

@ -6,22 +6,36 @@ A unoffical App for Anime-on-Demand.
* acces all media in the library * acces all media in the library
* search the library * search the library
* play movies/tv shows via integrated exoplayer * play movies/tv shows via integrated exoplayer
* add movies/tv shows to you list, for easier access
### Missing Features
* a alternative/secondary stream is currently not supported (for dub titles the subtitle version is missing)
## Screenshots ## Screenshots
[<img src="https://www.mosad.xyz/images/Teapod/Teapod_Library.png" width=180>](https://www.mosad.xyz/images/Teapod/Teapod_Library.png) [<img src="https://www.mosad.xyz/images/Teapod/Teapod_Home_200px.png" width=180>](https://www.mosad.xyz/images/Teapod/Teapod_Home.png)
[<img src="https://www.mosad.xyz/images/Teapod/Teapod_Media.png" width=180>](https://www.mosad.xyz/images/Teapod/Teapod_Media.png) [<img src="https://www.mosad.xyz/images/Teapod/Teapod_Library_200px.png" width=180>](https://www.mosad.xyz/images/Teapod/Teapod_Library.png)
[<img src="https://www.mosad.xyz/images/Teapod/Teapod_Search.png" width=180>](https://www.mosad.xyz/images/Teapod/Teapod_Search.png) [<img src="https://www.mosad.xyz/images/Teapod/Teapod_Media_200px.png" width=180>](https://www.mosad.xyz/images/Teapod/Teapod_Media.png)
[<img src="https://www.mosad.xyz/images/Teapod/Teapod_Search_200px.png" width=180>](https://www.mosad.xyz/images/Teapod/Teapod_Search.png)
## License ### License
This App is licensed under the treams and conditions of GPL3. This Project is not accosiated with Anime-on-Demand in anya way. This App is licensed under the terms and conditions of GPL 3. This Project is not associated with Anime-on-Demand in any way.
### Known Issues
If a tv show is selected, the first episode will be marked as already watched. This is due to the difficulties of parsing. The Parser is designed to be as easy to maintain and fail safe as possible.
### Used Libraries ### Used Libraries
* gson: https://github.com/google/gson * AndroidX: https://developer.android.com/jetpack/androidx
* exoplayer: https://github.com/google/ExoPlayer * Material Components for Android: https://github.com/material-components/material-components-android
* jsoup: https://jsoup.org/ * ExoPlayer: https://github.com/google/ExoPlayer
* material-dialogs: https://github.com/afollestad/material-dialogs * Gson: https://github.com/google/gson
* kotlin.coroutines: https://github.com/Kotlin/kotlinx.coroutines
* Material design icons: https://github.com/google/material-design-icons * Material design icons: https://github.com/google/material-design-icons
* androidx libraries * Material Dialogs: https://github.com/afollestad/material-dialogs
* Jsoup: https://jsoup.org
* kotlinx.coroutines: https://github.com/Kotlin/kotlinx.coroutines
* Glide: https://github.com/bumptech/glide
* Glide Transformations: https://github.com/wasabeef/glide-transformations
Teapod © 2020 [@Seil0](https://git.mosad.xyz/Seil0) #### 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.
Teapod © 2020 [@Seil0](https://git.mosad.xyz/Seil0)

View File

@ -10,8 +10,8 @@ android {
applicationId "org.mosad.teapod" applicationId "org.mosad.teapod"
minSdkVersion 23 minSdkVersion 23
targetSdkVersion 30 targetSdkVersion 30
versionCode 1 versionCode 1000 //00.01.0000
versionName "0.1-alpha1" versionName "0.1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resValue "string", "build_time", buildTime() resValue "string", "build_time", buildTime()
@ -20,7 +20,8 @@ android {
buildTypes { buildTypes {
release { release {
minifyEnabled false minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
} }
} }
@ -40,25 +41,25 @@ dependencies {
implementation 'androidx.core:core-ktx:1.3.2' implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.2' implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'androidx.navigation:navigation-fragment:2.3.0' implementation 'androidx.navigation:navigation-fragment-ktx:2.3.1'
implementation 'androidx.navigation:navigation-ui:2.3.0' implementation 'androidx.navigation:navigation-ui-ktx:2.3.1'
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.0'
implementation 'androidx.navigation:navigation-ui-ktx:2.3.0'
implementation 'androidx.security:security-crypto:1.1.0-alpha02' implementation 'androidx.security:security-crypto:1.1.0-alpha02'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'com.google.android.material:material:1.2.1' implementation 'com.google.android.material:material:1.3.0-alpha03'
implementation 'com.google.code.gson:gson:2.8.6' implementation 'com.google.code.gson:gson:2.8.6'
implementation 'com.google.android.exoplayer:exoplayer-core:2.12.0' implementation 'com.google.android.exoplayer:exoplayer-core:2.12.1'
implementation 'com.google.android.exoplayer:exoplayer-hls:2.12.0' implementation 'com.google.android.exoplayer:exoplayer-hls:2.12.1'
implementation 'com.google.android.exoplayer:exoplayer-dash:2.12.0' implementation 'com.google.android.exoplayer:exoplayer-dash:2.12.1'
implementation 'com.google.android.exoplayer:exoplayer-ui:2.12.0' implementation 'com.google.android.exoplayer:exoplayer-ui:2.12.1'
implementation 'org.jsoup:jsoup:1.13.1' implementation 'org.jsoup:jsoup:1.13.1'
implementation 'com.github.bumptech.glide:glide:4.11.0' implementation 'com.github.bumptech.glide:glide:4.11.0'
implementation 'jp.wasabeef:glide-transformations:4.3.0'
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'
implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'de.psdev.licensesdialog:licensesdialog:2.1.0'
testImplementation 'junit:junit:4.12' testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.2' androidTestImplementation 'androidx.test.ext:junit:1.1.2'

View File

@ -15,7 +15,17 @@
# Uncomment this to preserve the line number information for # Uncomment this to preserve the line number information for
# debugging stack traces. # debugging stack traces.
#-keepattributes SourceFile,LineNumberTable #-keepattributes SourceFile,LineNumberTable
-dontobfuscate
# If you keep the line number information, uncomment this to # If you keep the line number information, uncomment this to
# hide the original source file name. # hide the original source file name.
#-renamesourcefileattribute SourceFile #-renamesourcefileattribute SourceFile
-keep class org.mosad.teapod.util.** { <fields>; }
#Gson
-keepattributes Signature
-dontwarn sun.misc.**
#misc
-dontwarn java.lang.instrument.ClassFileTransformer
-dontwarn java.lang.ClassValue

View File

@ -11,20 +11,25 @@
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/AppTheme"> android:theme="@style/AppTheme">
<activity
android:name=".SplashActivity"
android:label="@string/app_name"
android:theme="@style/SplashTheme"
android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity <activity
android:name=".PlayerActivity" android:name=".PlayerActivity"
android:label="@string/app_name" android:label="@string/app_name"
android:configChanges="orientation|screenSize|layoutDirection" android:theme="@style/PlayerTheme"
android:theme="@style/AppTheme.AppCompat.Light.NoActionBar.FullScreen" /> android:configChanges="orientation|screenSize|layoutDirection" />
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:label="@string/app_name" android:label="@string/app_name"
android:screenOrientation="portrait"> android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity> </activity>
</application> </application>

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -1,43 +1,68 @@
/**
* Teapod
*
* Copyright 2020 <seil0@mosad.xyz>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*
*/
package org.mosad.teapod package org.mosad.teapod
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.MenuItem import android.view.MenuItem
import com.google.android.material.bottomnavigation.BottomNavigationView
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.commit import androidx.fragment.app.commit
import com.google.android.material.bottomnavigation.BottomNavigationView
import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
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.ui.MediaFragment import org.mosad.teapod.preferences.Preferences
import org.mosad.teapod.ui.account.AccountFragment
import org.mosad.teapod.ui.components.LoginDialog import org.mosad.teapod.ui.components.LoginDialog
import org.mosad.teapod.ui.home.HomeFragment import org.mosad.teapod.ui.fragments.*
import org.mosad.teapod.ui.library.LibraryFragment import org.mosad.teapod.util.StorageController
import org.mosad.teapod.ui.search.SearchFragment
import org.mosad.teapod.util.Media
import org.mosad.teapod.util.TMDBApiController import org.mosad.teapod.util.TMDBApiController
import kotlin.system.measureTimeMillis
class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemSelectedListener { class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemSelectedListener {
private var activeFragment: Fragment = HomeFragment() // the currently active fragment, home at the start private var activeBaseFragment: Fragment = HomeFragment() // the currently active fragment, home at the start
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main) setContentView(R.layout.activity_main)
val navView: BottomNavigationView = findViewById(R.id.nav_view) nav_view.setOnNavigationItemSelectedListener(this)
navView.setOnNavigationItemSelectedListener(this)
load() load()
supportFragmentManager.commit {
replace(R.id.nav_host_fragment, activeBaseFragment, activeBaseFragment.javaClass.simpleName)
}
} }
override fun onBackPressed() { override fun onBackPressed() {
if (supportFragmentManager.backStackEntryCount > 0) { if (supportFragmentManager.backStackEntryCount > 0) {
supportFragmentManager.popBackStack() supportFragmentManager.popBackStack()
} else { } else {
if (activeFragment !is HomeFragment) { if (activeBaseFragment !is HomeFragment) {
nav_view.selectedItemId = R.id.navigation_home nav_view.selectedItemId = R.id.navigation_home
} else { } else {
super.onBackPressed() super.onBackPressed()
@ -46,52 +71,73 @@ class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemS
} }
override fun onNavigationItemSelected(item: MenuItem): Boolean { override fun onNavigationItemSelected(item: MenuItem): Boolean {
if (supportFragmentManager.backStackEntryCount > 0) {
supportFragmentManager.popBackStack()
}
val ret = when (item.itemId) { val ret = when (item.itemId) {
R.id.navigation_home -> { R.id.navigation_home -> {
activeFragment = HomeFragment() activeBaseFragment = HomeFragment()
true true
} }
R.id.navigation_library -> { R.id.navigation_library -> {
activeFragment = LibraryFragment() activeBaseFragment = LibraryFragment()
true true
} }
R.id.navigation_search -> { R.id.navigation_search -> {
activeFragment = SearchFragment() activeBaseFragment = SearchFragment()
true true
} }
R.id.navigation_account -> { R.id.navigation_account -> {
activeFragment = AccountFragment() activeBaseFragment = AccountFragment()
true true
} }
else -> false else -> false
} }
supportFragmentManager.commit { supportFragmentManager.commit {
replace(R.id.nav_host_fragment, activeFragment) replace(R.id.nav_host_fragment, activeBaseFragment, activeBaseFragment.javaClass.simpleName)
} }
return ret return ret
} }
private fun load() { private fun load() {
EncryptedPreferences.readCredentials(this) // running login and list in parallel does not bring any speed improvements
val time = measureTimeMillis {
Preferences.load(this)
if (EncryptedPreferences.password.isEmpty()) { // make sure credentials are set
Log.i(javaClass.name, "please login!") EncryptedPreferences.readCredentials(this)
if (EncryptedPreferences.password.isEmpty()) {
showLoginDialog(true)
} else {
// try to login in, as most sites can only bee loaded once loged in
if (!AoDParser.login()) showLoginDialog(false)
}
LoginDialog(this).positiveButton { StorageController.load(this)
EncryptedPreferences.saveCredentials(login, password, context) AoDParser.initialLoading()
}.negativeButton {
Log.i(javaClass.name, "Login canceled, exiting.")
finish()
}.show()
} }
Log.i(javaClass.name, "login and list in $time ms")
} }
fun showDetailFragment(media: Media) { /**
media.episodes = AoDParser().loadStreams(media) // load the streams for the selected media * Show the media fragment for the selected media.
* While loading show the loading fragment.
* The loading and media fragment are not stored in activeBaseFragment,
* as the don't replace a fragment but are added on top of one.
*/
fun showMediaFragment(mediaId: Int) = GlobalScope.launch {
val loadingFragment = LoadingFragment()
supportFragmentManager.commit {
add(R.id.nav_host_fragment, loadingFragment, "MediaFragment")
show(loadingFragment)
}
val tmdb = TMDBApiController().search(media.title, media.type) // load the streams for the selected media
val media = AoDParser.getMediaById(mediaId)
val tmdb = TMDBApiController().search(media.info.title, media.type)
val mediaFragment = MediaFragment(media, tmdb) val mediaFragment = MediaFragment(media, tmdb)
supportFragmentManager.commit { supportFragmentManager.commit {
@ -99,6 +145,10 @@ class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemS
addToBackStack(null) addToBackStack(null)
show(mediaFragment) show(mediaFragment)
} }
supportFragmentManager.commit {
remove(loadingFragment)
}
} }
fun startPlayer(streamUrl: String) { fun startPlayer(streamUrl: String) {
@ -107,4 +157,18 @@ class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemS
} }
startActivity(intent) startActivity(intent)
} }
private fun showLoginDialog(firstTry: Boolean) {
LoginDialog(this, firstTry).positiveButton {
EncryptedPreferences.saveCredentials(login, password, context)
if (!AoDParser.login()) {
showLoginDialog(false)
Log.w(javaClass.name, "Login failed, please try again.")
}
}.negativeButton {
Log.i(javaClass.name, "Login canceled, exiting.")
finish()
}.show()
}
} }

View File

@ -0,0 +1,17 @@
package org.mosad.teapod
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
class SplashActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val intent = Intent(this, MainActivity::class.java)
startActivity(intent)
finish()
}
}

View File

@ -1,3 +1,25 @@
/**
* Teapod
*
* Copyright 2020 <seil0@mosad.xyz>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*
*/
package org.mosad.teapod.parser package org.mosad.teapod.parser
import android.util.Log import android.util.Log
@ -6,28 +28,28 @@ import kotlinx.coroutines.*
import org.jsoup.Connection import org.jsoup.Connection
import org.jsoup.Jsoup import org.jsoup.Jsoup
import org.mosad.teapod.preferences.EncryptedPreferences import org.mosad.teapod.preferences.EncryptedPreferences
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 java.io.IOException
import org.mosad.teapod.util.Media
import java.util.* import java.util.*
import kotlin.collections.ArrayList
class AoDParser { object AoDParser {
private val baseUrl = "https://www.anime-on-demand.de" private const val baseUrl = "https://www.anime-on-demand.de"
private val loginPath = "/users/sign_in" private const val loginPath = "/users/sign_in"
private val libraryPath = "/animes" private const val libraryPath = "/animes"
companion object { private const val userAgent = "Mozilla/5.0 (X11; Linux x86_64; rv:80.0) Gecko/20100101 Firefox/80.0"
private var sessionCookies = mutableMapOf<String, String>()
private var loginSuccess = false
val mediaList = arrayListOf<Media>() private var sessionCookies = mutableMapOf<String, String>()
} private var csrfToken: String = ""
private var loginSuccess = false
private fun login() = runBlocking { private val mediaList = arrayListOf<Media>()
val itemMediaList = arrayListOf<ItemMedia>()
val newEpisodesList = arrayListOf<ItemMedia>()
val userAgent = "Mozilla/5.0 (X11; Linux x86_64; rv:80.0) Gecko/20100101 Firefox/80.0" fun login(): Boolean = runBlocking {
withContext(Dispatchers.Default) { withContext(Dispatchers.Default) {
// get the authenticity token // get the authenticity token
@ -36,10 +58,10 @@ class AoDParser {
.execute() .execute()
val authenticityToken = resAuth.parse().select("meta[name=csrf-token]").attr("content") val authenticityToken = resAuth.parse().select("meta[name=csrf-token]").attr("content")
println("Authenticity token is: $authenticityToken") val authCookies = resAuth.cookies()
val cookies = resAuth.cookies() //Log.d(javaClass.name, "Received authenticity token: $authenticityToken")
println("cookies: $cookies") //Log.d(javaClass.name, "Received authenticity cookies: $authCookies")
val data = mapOf( val data = mapOf(
Pair("user[login]", EncryptedPreferences.login), Pair("user[login]", EncryptedPreferences.login),
@ -53,22 +75,78 @@ class AoDParser {
.method(Connection.Method.POST) .method(Connection.Method.POST)
.data(data) .data(data)
.postDataCharset("UTF-8") .postDataCharset("UTF-8")
.cookies(cookies) .cookies(authCookies)
.execute() .execute()
//println(resLogin.body()) //println(resLogin.body())
loginSuccess = resLogin.body().contains("Hallo, du bist jetzt angemeldet.")
println("Status: ${resLogin.statusCode()} (${resLogin.statusMessage()}), login successful: $loginSuccess")
sessionCookies = resLogin.cookies() sessionCookies = resLogin.cookies()
loginSuccess = resLogin.body().contains("Hallo, du bist jetzt angemeldet.")
Log.i(javaClass.name, "Status: ${resLogin.statusCode()} (${resLogin.statusMessage()}), login successful: $loginSuccess")
loginSuccess
} }
} }
/** /**
* list all animes from the website * initially load all media and home screen data
* -> blocking
*/ */
fun listAnimes(): ArrayList<Media> = runBlocking { fun initialLoading() = runBlocking {
val newEPJob = GlobalScope.async {
listNewEpisodes()
}
val listJob = GlobalScope.async {
listAnimes()
}
newEPJob.await()
listJob.await()
}
/**
* get a media by it's ID (int)
* @return Media
*/
fun getMediaById(mediaId: Int): Media {
val media = mediaList.first { it.id == mediaId }
if (media.episodes.isEmpty()) {
loadStreams(media)
}
return media
}
// TODO don't use jsoup here
fun sendCallback(callbackPath: String) = GlobalScope.launch {
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"),
Pair("Accept-Encoding", "gzip, deflate, br"),
Pair("X-CSRF-Token", csrfToken),
Pair("X-Requested-With", "XMLHttpRequest"),
)
try {
withContext(Dispatchers.IO) {
Jsoup.connect(baseUrl + callbackPath)
.ignoreContentType(true)
.cookies(sessionCookies)
.headers(headers)
.execute()
}
} catch (ex: IOException) {
Log.e(javaClass.name, "Callback for $callbackPath failed.", ex)
}
}
/**
* load all media from aod into itemMediaList and mediaList
*/
private fun listAnimes() = runBlocking {
if (sessionCookies.isEmpty()) login() if (sessionCookies.isEmpty()) login()
withContext(Dispatchers.Default) { withContext(Dispatchers.Default) {
@ -78,6 +156,7 @@ class AoDParser {
//println(resAnimes) //println(resAnimes)
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().toLowerCase(Locale.ROOT) == "zur serie") {
@ -85,57 +164,176 @@ class AoDParser {
} 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()
val media = Media( itemMediaList.add(ItemMedia(mediaId, mediaTitle, mediaImage))
it.select("h3.animebox-title").text(), mediaList.add(Media(mediaId, mediaLink, type).apply {
it.select("p.animebox-link").select("a").attr("href"), info.title = mediaTitle
type, info.posterUrl = mediaImage
it.select("p.animebox-image").select("img").attr("src"), info.shortDesc = mediaShortText
it.select("p.animebox-shorttext").text() })
)
mediaList.add(media)
} }
println("got ${mediaList.size} anime") Log.i(javaClass.name, "Total library size is: ${mediaList.size}")
return@withContext mediaList
} }
} }
/** /**
* load streams for the media path * load all new episodes from AoD into newEpisodesList
*/ */
fun loadStreams(media: Media): List<Episode> = runBlocking { private fun listNewEpisodes() = runBlocking {
if (sessionCookies.isEmpty()) login()
withContext(Dispatchers.Default) {
val resHome = Jsoup.connect(baseUrl)
.cookies(sessionCookies)
.get()
newEpisodesList.clear()
resHome.select("div.jcarousel-container-new").select("li").forEach {
if (it.select("span").hasClass("neweps")) {
val mediaId = it.select("a.thumbs").attr("href")
.substringAfterLast("/").toInt()
val mediaImage = it.select("a.thumbs > img").attr("src")
val mediaTitle = "${it.select("a").text()} - ${it.select("span.neweps").text()}"
newEpisodesList.add(ItemMedia(mediaId, mediaTitle, mediaImage))
}
}
}
}
/**
* load streams for the media path, movies have one episode
* @param media is used as call ba reference
*/
private fun loadStreams(media: Media) = runBlocking {
if (sessionCookies.isEmpty()) login() if (sessionCookies.isEmpty()) login()
if (!loginSuccess) { if (!loginSuccess) {
println("please log in") // TODO Log.w(javaClass.name, "Login, was not successful.")
return@runBlocking listOf() return@runBlocking
} }
withContext(Dispatchers.Default) { withContext(Dispatchers.Default) {
// get the media page
val res = Jsoup.connect(baseUrl + media.link) val res = Jsoup.connect(baseUrl + media.link)
.cookies(sessionCookies) .cookies(sessionCookies)
.get() .get()
//println(res) //println(res)
val playlists = res.select("input.streamstarter_html5").eachAttr("data-playlist") if (csrfToken.isEmpty()) {
val csrfToken = res.select("meta[name=csrf-token]").attr("content") csrfToken = res.select("meta[name=csrf-token]").attr("content")
//Log.i(javaClass.name, "New csrf token is $csrfToken")
}
//println("first entry: ${playlists.first()}")
//println("csrf token is: $csrfToken")
return@withContext loadStreamInfo(playlists.first(), csrfToken, media.type) val pl = res.select("input.streamstarter_html5").first()
val primary = pl.attr("data-playlist")
val secondary = pl.attr("data-otherplaylist")
val secondaryIsOmU = secondary.contains("OmU", true)
// load primary and secondary playlist
val primaryPlaylist = parsePlaylistAsync(primary)
val secondaryPlaylist = parsePlaylistAsync(secondary)
primaryPlaylist.await().playlist.forEach { ep ->
val epNumber = if (media.type == MediaType.TVSHOW) {
ep.title.substringAfter(", Ep. ").toInt()
} else {
0
}
media.episodes.add(
Episode(
id = ep.mediaid,
priStreamUrl = ep.sources.first().file,
posterUrl = ep.image,
title = ep.title,
description = ep.description,
number = epNumber
)
)
}
Log.i(javaClass.name, "Loading primary playlist finished")
secondaryPlaylist.await().playlist.forEach { ep ->
val episode = media.episodes.firstOrNull { it.id == ep.mediaid }
if (episode != null) {
episode.secStreamUrl = ep.sources.first().file
episode.secStreamOmU = secondaryIsOmU
} else {
val epNumber = if (media.type == MediaType.TVSHOW) {
ep.title.substringAfter(", Ep. ").toInt()
} else {
0
}
media.episodes.add(
Episode(
id = ep.mediaid,
secStreamUrl = ep.sources.first().file,
secStreamOmU = secondaryIsOmU,
posterUrl = ep.image,
title = ep.title,
description = ep.description,
number = epNumber
)
)
}
}
Log.i(javaClass.name, "Loading secondary plalyist finished")
// parse 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()
"fsk" -> media.info.age = row.select("td").text().toInt()
"episodenanzahl" -> {
media.info.episodesCount = row.select("td").text()
.substringBefore("/")
.filter{ it.isDigit() }
.toInt()
}
}
}
// parse additional information for tv shows
if (media.type == MediaType.TVSHOW) {
res.select("div.three-box-container > div.episodebox").forEach { episodebox ->
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
}
}
}
} }
} }
/** /**
* load the playlist path and parse it, read the stream info from json * don't use Gson().fromJson() as we don't have any control over the api and it may change
*/ */
private fun loadStreamInfo(playlistPath: String, csrfToken: String, type: MediaType): List<Episode> = runBlocking { private fun parsePlaylistAsync(playlistPath: String): Deferred<AoDObject> {
withContext(Dispatchers.Default) { if (playlistPath == "[]") {
return CompletableDeferred(AoDObject(listOf()))
}
return GlobalScope.async {
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"),
@ -144,46 +342,28 @@ class AoDParser {
Pair("X-Requested-With", "XMLHttpRequest"), Pair("X-Requested-With", "XMLHttpRequest"),
) )
//println("loading streaminfo with cstf: $csrfToken")
val res = Jsoup.connect(baseUrl + playlistPath) val res = Jsoup.connect(baseUrl + playlistPath)
.ignoreContentType(true) .ignoreContentType(true)
.cookies(sessionCookies) .cookies(sessionCookies)
.headers(headers) .headers(headers)
.execute() .execute()
//println(res.body()) //Gson().fromJson(res.body(), AoDObject::class.java)
return@withContext when (type) { return@async AoDObject(JsonParser.parseString(res.body()).asJsonObject
MediaType.MOVIE -> { .get("playlist").asJsonArray.map {
val movie = JsonParser.parseString(res.body()).asJsonObject Playlist(
.get("playlist").asJsonArray sources = it.asJsonObject.get("sources").asJsonArray.map { source ->
Source(source.asJsonObject.get("file").asString)
movie.first().asJsonObject.get("sources").asJsonArray.toList().map { },
Episode(streamUrl = it.asJsonObject.get("file").asString) image = it.asJsonObject.get("image").asString,
} title = it.asJsonObject.get("title").asString,
} description = it.asJsonObject.get("description").asString,
MediaType.TVSHOW -> { mediaid = it.asJsonObject.get("mediaid").asInt
val episodesJson = JsonParser.parseString(res.body()).asJsonObject )
.get("playlist").asJsonArray })
episodesJson.map {
val episodeStream = it.asJsonObject.get("sources").asJsonArray
.first().asJsonObject
.get("file").asString
val episodeTitle = it.asJsonObject.get("title").asString
Episode(
episodeTitle,
episodeStream
)
}
}
else -> {
Log.e(javaClass.name, "Wrong Type, please report this issue.")
listOf()
}
}
} }
} }

View File

@ -1,22 +1,40 @@
package org.mosad.teapod.preferences package org.mosad.teapod.preferences
import android.content.Context
import org.mosad.teapod.R
object Preferences { object Preferences {
var login = "" var preferSecondary = false
internal set
var password = ""
internal set internal set
fun savePreferSecondary(context: Context, preferSecondary: Boolean) {
val sharedPref = context.getSharedPreferences(
context.getString(R.string.preference_file_key),
Context.MODE_PRIVATE
)
fun saveCredentials(login: String, password: String) { with(sharedPref.edit()) {
this.login = login putBoolean(context.getString(R.string.save_key_prefer_secondary), preferSecondary)
this.password = password apply()
}
// TODO save this.preferSecondary = preferSecondary
} }
fun load() { /**
// TODO * initially load the stored values
*/
fun load(context: Context) {
val sharedPref = context.getSharedPreferences(
context.getString(R.string.preference_file_key),
Context.MODE_PRIVATE
)
preferSecondary = sharedPref.getBoolean(
context.getString(R.string.save_key_prefer_secondary), false
)
} }
} }

View File

@ -1,87 +0,0 @@
package org.mosad.teapod.ui
import android.os.Bundle
import android.util.Log
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import kotlinx.android.synthetic.main.fragment_media.*
import org.mosad.teapod.MainActivity
import org.mosad.teapod.R
import org.mosad.teapod.util.DataTypes.MediaType
import org.mosad.teapod.util.EpisodesAdapter
import org.mosad.teapod.util.Media
import org.mosad.teapod.util.TMDBResponse
class MediaFragment(private val media: Media, private val tmdb: TMDBResponse) : Fragment() {
private lateinit var adapterRecEpisodes: EpisodesAdapter
private lateinit var viewManager: RecyclerView.LayoutManager
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_media, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// generic gui
text_title.text = media.title
if (tmdb.posterUrl.isNotEmpty()) {
Glide.with(requireContext()).load(tmdb.posterUrl).into(image_poster)
text_desc.text = tmdb.overview
Log.d(javaClass.name, "TMDB data present")
} else {
Glide.with(requireContext()).load(media.posterLink).into(image_poster)
text_desc.text = media.shortDesc
Log.d(javaClass.name, "No TMDB data present, using Aod")
}
// specific gui
if (media.type == MediaType.TVSHOW) {
val episodeTitles = media.episodes.map { it.title }
adapterRecEpisodes = EpisodesAdapter(episodeTitles)
viewManager = LinearLayoutManager(context)
recycler_episodes.layoutManager = viewManager
recycler_episodes.adapter = adapterRecEpisodes
} else if (media.type == MediaType.MOVIE) {
recycler_episodes.visibility = View.GONE
}
println("media streams: ${media.episodes}")
initActions()
}
private fun initActions() {
button_play.setOnClickListener {
when (media.type) {
MediaType.MOVIE -> playStream(media.episodes.first().streamUrl)
MediaType.TVSHOW -> playStream(media.episodes.first().streamUrl)
MediaType.OTHER -> Log.e(javaClass.name, "Wrong Type, please report this issue.")
}
}
// set onItemClick only in adapter is initialized
if (this::adapterRecEpisodes.isInitialized) {
adapterRecEpisodes.onItemClick = { item, position ->
playStream(media.episodes[position].streamUrl)
}
}
}
private fun playStream(url: String) {
val mainActivity = activity as MainActivity
mainActivity.startPlayer(url)
}
}

View File

@ -31,7 +31,7 @@ import com.afollestad.materialdialogs.customview.customView
import com.afollestad.materialdialogs.customview.getCustomView import com.afollestad.materialdialogs.customview.getCustomView
import org.mosad.teapod.R import org.mosad.teapod.R
class LoginDialog(val context: Context) { class LoginDialog(val context: Context, firstTry: Boolean) {
private val dialog = MaterialDialog(context, BottomSheet()) private val dialog = MaterialDialog(context, BottomSheet())
@ -43,7 +43,7 @@ class LoginDialog(val context: Context) {
init { init {
dialog.title(R.string.login) dialog.title(R.string.login)
.message(R.string.login_desc) .message(if (firstTry) R.string.login_desc else R.string.login_failed_desc)
.customView(R.layout.dialog_login) .customView(R.layout.dialog_login)
.positiveButton(R.string.save) .positiveButton(R.string.save)
.negativeButton(R.string.cancel) .negativeButton(R.string.cancel)

View File

@ -1,15 +1,19 @@
package org.mosad.teapod.ui.account package org.mosad.teapod.ui.fragments
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
import de.psdev.licensesdialog.LicensesDialog
import kotlinx.android.synthetic.main.fragment_account.* import kotlinx.android.synthetic.main.fragment_account.*
import org.mosad.teapod.BuildConfig import org.mosad.teapod.BuildConfig
import org.mosad.teapod.R import org.mosad.teapod.R
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.ui.components.LoginDialog import org.mosad.teapod.ui.components.LoginDialog
class AccountFragment : Fragment() { class AccountFragment : Fragment() {
@ -23,18 +27,14 @@ class AccountFragment : Fragment() {
text_account_login.text = EncryptedPreferences.login text_account_login.text = EncryptedPreferences.login
text_info_about_desc.text = getString(R.string.info_about_desc, BuildConfig.VERSION_NAME, getString(R.string.build_time)) text_info_about_desc.text = getString(R.string.info_about_desc, BuildConfig.VERSION_NAME, getString(R.string.build_time))
switch_secondary.isChecked = Preferences.preferSecondary
initActions() initActions()
} }
private fun initActions() { private fun initActions() {
linear_account_login.setOnClickListener { linear_account_login.setOnClickListener {
LoginDialog(requireContext()).positiveButton { showLoginDialog(true)
EncryptedPreferences.saveCredentials(login, password, context)
}.show {
login = EncryptedPreferences.login
password = ""
}
} }
linear_about.setOnClickListener { linear_about.setOnClickListener {
@ -43,5 +43,33 @@ class AccountFragment : Fragment() {
.message(R.string.info_about_dialog) .message(R.string.info_about_dialog)
.show() .show()
} }
text_licenses.setOnClickListener {
LicensesDialog.Builder(requireContext())
.setNotices(R.raw.notices)
.setTitle(R.string.licenses)
.setIncludeOwnLicense(true)
.setThemeResourceId(R.style.AppTheme)
.build()
.show()
}
switch_secondary.setOnClickListener {
Preferences.savePreferSecondary(requireContext(), switch_secondary.isChecked)
}
}
private fun showLoginDialog(firstTry: Boolean) {
LoginDialog(requireContext(), firstTry).positiveButton {
EncryptedPreferences.saveCredentials(login, password, context)
if (!AoDParser.login()) {
showLoginDialog(false)
Log.w(javaClass.name, "Login failed, please try again.")
}
}.show {
login = EncryptedPreferences.login
password = ""
}
} }
} }

View File

@ -0,0 +1,70 @@
package org.mosad.teapod.ui.fragments
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import kotlinx.android.synthetic.main.fragment_home.*
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.R
import org.mosad.teapod.parser.AoDParser
import org.mosad.teapod.util.StorageController
import org.mosad.teapod.util.adapter.MediaItemAdapter
import org.mosad.teapod.util.decoration.MediaItemDecoration
class HomeFragment : Fragment() {
private lateinit var adapterMyList: MediaItemAdapter
private lateinit var adapterNewEpisodes: MediaItemAdapter
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_home, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
GlobalScope.launch {
withContext(Dispatchers.Main) {
context?.let {
recycler_my_list.addItemDecoration(MediaItemDecoration(9))
updateMyListMedia()
adapterNewEpisodes = MediaItemAdapter(AoDParser.newEpisodesList)
recycler_new_episodes.adapter = adapterNewEpisodes
recycler_new_episodes.addItemDecoration(MediaItemDecoration(9))
initActions()
}
}
}
}
// TODO recreating the adapter on list change is not a good solution
fun updateMyListMedia() {
val myListMedia = StorageController.myList.map { elementId ->
AoDParser.itemMediaList.first {
elementId == it.id
}
}
adapterMyList = MediaItemAdapter(myListMedia)
adapterMyList.onItemClick = { mediaId, _ ->
(activity as MainActivity).showMediaFragment(mediaId)
}
recycler_my_list.adapter = adapterMyList
}
private fun initActions() {
adapterNewEpisodes.onItemClick = { mediaId, _ ->
(activity as MainActivity).showMediaFragment(mediaId)
}
}
}

View File

@ -1,4 +1,4 @@
package org.mosad.teapod.ui.library package org.mosad.teapod.ui.fragments
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
@ -10,12 +10,12 @@ import kotlinx.coroutines.*
import org.mosad.teapod.MainActivity import org.mosad.teapod.MainActivity
import org.mosad.teapod.R import org.mosad.teapod.R
import org.mosad.teapod.parser.AoDParser import org.mosad.teapod.parser.AoDParser
import org.mosad.teapod.util.CustomAdapter import org.mosad.teapod.util.decoration.MediaItemDecoration
import org.mosad.teapod.util.Media import org.mosad.teapod.util.adapter.MediaItemAdapter
class LibraryFragment : Fragment() { class LibraryFragment : Fragment() {
private lateinit var adapter : CustomAdapter private lateinit var adapter: MediaItemAdapter
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_library, container, false) return inflater.inflate(R.layout.fragment_library, container, false)
@ -24,31 +24,21 @@ class LibraryFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
// init async
GlobalScope.launch { GlobalScope.launch {
if (AoDParser.mediaList.isEmpty()) {
AoDParser().listAnimes()
}
// create and set the adapter, needs context // create and set the adapter, needs context
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
context?.let { context?.let {
adapter = CustomAdapter(it, AoDParser.mediaList) adapter = MediaItemAdapter(AoDParser.itemMediaList)
list_library.adapter = adapter adapter.onItemClick = { mediaId, _ ->
(activity as MainActivity).showMediaFragment(mediaId)
}
recycler_media_library.adapter = adapter
recycler_media_library.addItemDecoration(MediaItemDecoration(9))
} }
} }
}
initActions()
}
private fun initActions() {
list_library.setOnItemClickListener { _, _, position, _ ->
val media = adapter.getItem(position) as Media
println("selected item is: ${media.title}")
val mainActivity = activity as MainActivity
mainActivity.showDetailFragment(media)
} }
} }
} }

View File

@ -0,0 +1,16 @@
package org.mosad.teapod.ui.fragments
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.R
class LoadingFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_loading, container, false)
}
}

View File

@ -0,0 +1,148 @@
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.android.synthetic.main.fragment_media.*
import org.mosad.teapod.MainActivity
import org.mosad.teapod.R
import org.mosad.teapod.parser.AoDParser
import org.mosad.teapod.preferences.Preferences
import org.mosad.teapod.util.DataTypes.MediaType
import org.mosad.teapod.util.Episode
import org.mosad.teapod.util.Media
import org.mosad.teapod.util.StorageController
import org.mosad.teapod.util.TMDBResponse
import org.mosad.teapod.util.adapter.EpisodeItemAdapter
class MediaFragment(private val media: Media, private val tmdb: TMDBResponse) : Fragment() {
private lateinit var adapterRecEpisodes: EpisodeItemAdapter
private lateinit var viewManager: RecyclerView.LayoutManager
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_media, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initGUI()
initActions()
}
/**
* if tmdb data is present, use it, else use the aod data
*/
private fun initGUI() {
// 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(image_backdrop)
Glide.with(requireContext()).load(posterUrl)
.into(image_poster)
text_title.text = media.info.title
text_year.text = media.info.year.toString()
text_age.text = media.info.age.toString()
text_overview.text = media.info.shortDesc
if (StorageController.myList.contains(media.id)) {
Glide.with(requireContext()).load(R.drawable.ic_baseline_check_24).into(image_my_list_action)
} else {
Glide.with(requireContext()).load(R.drawable.ic_baseline_add_24).into(image_my_list_action)
}
// specific gui
if (media.type == MediaType.TVSHOW) {
adapterRecEpisodes = EpisodeItemAdapter(media.episodes)
viewManager = LinearLayoutManager(context)
recycler_episodes.layoutManager = viewManager
recycler_episodes.adapter = adapterRecEpisodes
text_episodes_or_runtime.text = getString(R.string.text_episodes_count, media.info.episodesCount)
} else if (media.type == MediaType.MOVIE) {
recycler_episodes.visibility = View.GONE
if (tmdb.runtime > 0) {
text_episodes_or_runtime.text = getString(R.string.text_runtime, tmdb.runtime)
} else {
text_episodes_or_runtime.visibility = View.GONE
}
}
}
private fun initActions() {
button_play.setOnClickListener {
when (media.type) {
MediaType.MOVIE -> playStream(media.episodes.first())
MediaType.TVSHOW -> playStream(media.episodes.first())
else -> Log.e(javaClass.name, "Wrong Type: $media.type")
}
}
// add or remove media from myList
linear_my_list_action.setOnClickListener {
if (StorageController.myList.contains(media.id)) {
StorageController.myList.remove(media.id)
Glide.with(requireContext()).load(R.drawable.ic_baseline_add_24).into(image_my_list_action)
} else {
StorageController.myList.add(media.id)
Glide.with(requireContext()).load(R.drawable.ic_baseline_check_24).into(image_my_list_action)
}
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 ->
playStream(media.episodes[position])
// update watched state
AoDParser.sendCallback(media.episodes[position].watchedCallback)
adapterRecEpisodes.updateWatchedState(true, position)
adapterRecEpisodes.notifyDataSetChanged()
}
}
}
/**
* Play the media's stream
* If prefer secondary or primary is empty and secondary is present (secStreamOmU),
* use the secondary stream. Else, if the primary stream is set use the primary stream.
*/
private fun playStream(ep: Episode) {
val streamUrl = if ((Preferences.preferSecondary || ep.priStreamUrl.isEmpty()) && ep.secStreamOmU) {
ep.secStreamUrl
} else if (ep.priStreamUrl.isNotEmpty()) {
ep.priStreamUrl
} else {
Log.e(javaClass.name, "No stream url set.")
""
}
Log.d(javaClass.name, "Playing stream: $streamUrl")
(activity as MainActivity).startPlayer(streamUrl)
}
}

View File

@ -1,4 +1,4 @@
package org.mosad.teapod.ui.search package org.mosad.teapod.ui.fragments
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
@ -11,13 +11,12 @@ import kotlinx.coroutines.*
import org.mosad.teapod.MainActivity import org.mosad.teapod.MainActivity
import org.mosad.teapod.R import org.mosad.teapod.R
import org.mosad.teapod.parser.AoDParser import org.mosad.teapod.parser.AoDParser
import org.mosad.teapod.util.CustomAdapter import org.mosad.teapod.util.decoration.MediaItemDecoration
import org.mosad.teapod.util.Media import org.mosad.teapod.util.adapter.MediaItemAdapter
class SearchFragment : Fragment() { class SearchFragment : Fragment() {
private val instance = this private var adapter : MediaItemAdapter? = null
private lateinit var adapter : CustomAdapter
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_search, container, false) return inflater.inflate(R.layout.fragment_search, container, false)
@ -27,15 +26,17 @@ class SearchFragment : Fragment() {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
GlobalScope.launch { GlobalScope.launch {
if (AoDParser.mediaList.isEmpty()) {
AoDParser().listAnimes()
}
// create and set the adapter, needs context // create and set the adapter, needs context
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
context?.let { context?.let {
adapter = CustomAdapter(it, AoDParser.mediaList) adapter = MediaItemAdapter(AoDParser.itemMediaList)
list_search.adapter = adapter adapter!!.onItemClick = { mediaId, _ ->
search_text.clearFocus()
(activity as MainActivity).showMediaFragment(mediaId)
}
recycler_media_search.adapter = adapter
recycler_media_search.addItemDecoration(MediaItemDecoration(9))
} }
} }
} }
@ -46,23 +47,16 @@ class SearchFragment : Fragment() {
private fun initActions() { private fun initActions() {
search_text.setOnQueryTextListener(object : SearchView.OnQueryTextListener { search_text.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean { override fun onQueryTextSubmit(query: String?): Boolean {
adapter?.filter?.filter(query)
adapter?.notifyDataSetChanged()
return false return false
} }
override fun onQueryTextChange(newText: String?): Boolean { override fun onQueryTextChange(newText: String?): Boolean {
adapter.filter.filter(newText) adapter?.filter?.filter(newText)
adapter.notifyDataSetChanged() adapter?.notifyDataSetChanged()
return false return false
} }
}) })
list_search.setOnItemClickListener { _, _, position, _ ->
val media = adapter.getItem(position) as Media
println("selected item is: ${media.title}")
val mainActivity = activity as MainActivity
mainActivity.showDetailFragment(media)
}
} }
} }

View File

@ -1,23 +0,0 @@
package org.mosad.teapod.ui.home
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import kotlinx.android.synthetic.main.fragment_home.*
import org.mosad.teapod.R
class HomeFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_home, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
text_home.text = "This is the home fragment"
}
}

View File

@ -1,71 +0,0 @@
package org.mosad.teapod.util
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.*
import com.bumptech.glide.Glide
import org.mosad.teapod.R
import java.util.*
class CustomAdapter(val context: Context, private val originalMedia: ArrayList<Media>) : BaseAdapter(), Filterable {
private var filteredMedia = originalMedia.map { it.copy() }
private val customFilter = CustomFilter()
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val view = convertView ?: LayoutInflater.from(context).inflate(R.layout.linear_media, parent, false)
val textTitle = view.findViewById<TextView>(R.id.text_title)
val imagePoster = view.findViewById<ImageView>(R.id.image_poster)
textTitle.text = filteredMedia[position].title
Glide.with(context).load(filteredMedia[position].posterLink).into(imagePoster)
return view
}
override fun getFilter(): Filter {
return customFilter
}
override fun getCount(): Int {
return filteredMedia.size
}
override fun getItem(position: Int): Any {
return filteredMedia[position]
}
override fun getItemId(position: Int): Long {
return position.toLong()
}
inner class CustomFilter : Filter() {
override fun performFiltering(constraint: CharSequence?): FilterResults {
val filterTerm = constraint.toString().toLowerCase(Locale.ROOT)
val results = FilterResults()
val filteredList = if (filterTerm.isEmpty()) {
originalMedia
} else {
originalMedia.filter {
it.title.toLowerCase(Locale.ROOT).contains(filterTerm)
}
}
results.values = filteredList
results.count = filteredList.size
return results
}
override fun publishResults(constraint: CharSequence?, results: FilterResults?) {
filteredMedia = results?.values as ArrayList<Media>
notifyDataSetChanged()
}
}
}

View File

@ -8,12 +8,74 @@ class DataTypes {
} }
} }
data class Media(val title: String, val link: String, val type: DataTypes.MediaType, val posterLink: String, val shortDesc : String, var episodes: List<Episode> = listOf()) { /**
override fun toString(): String { * this class is used to represent the item media
return title * it is uses in the ItemMediaAdapter (RecyclerView)
} */
} data class ItemMedia(
val id: Int,
val title: String,
val posterUrl: String
)
data class Episode(val title: String = "", val streamUrl: String = "", val posterLink: String = "", var watched: Boolean = false)
data class TMDBResponse(val title: String = "", val overview: String = "", val posterUrl: String = "", val backdropUrl: String = "") /**
* TODO the episodes workflow could use a clean up/rework
*/
data class Media(
val id: Int,
val link: String,
val type: DataTypes.MediaType,
val info: Info = Info(),
var episodes: ArrayList<Episode> = arrayListOf()
)
data class Info(
var title: String = "",
var posterUrl: String = "",
var shortDesc: String = "",
var description: String = "",
var year: Int = 0,
var age: Int = 0,
var episodesCount: Int = 0
)
/**
* if secStreamOmU == true, then a secondary stream is present
*/
data class Episode(
val id: Int = 0,
var title: String = "",
var priStreamUrl: String = "",
var secStreamUrl: String = "",
var secStreamOmU: Boolean = false,
var posterUrl: String = "",
var description: String = "",
var shortDesc: String = "",
var number: Int = 0,
var watched: Boolean = false,
var watchedCallback: String = ""
)
data class TMDBResponse(
val id: Int = 0,
val title: String = "",
val overview: String = "",
val posterUrl: String = "",
val backdropUrl: String = "",
var runtime: Int = 0
)
data class AoDObject(val playlist: List<Playlist>)
data class Playlist(
val sources: List<Source>,
val image: String,
val title: String,
val description: String,
val mediaid: Int
)
data class Source(
val file: String = ""
)

View File

@ -1,35 +0,0 @@
package org.mosad.teapod.util
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.component_episode.view.*
import org.mosad.teapod.R
class EpisodesAdapter(private val data: List<String>) : RecyclerView.Adapter<EpisodesAdapter.MyViewHolder>() {
var onItemClick: ((String, Int) -> Unit)? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.component_episode, parent, false)
return MyViewHolder(view)
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
holder.view .text_episode_title.text = data[position]
}
override fun getItemCount(): Int {
return data.size
}
inner class MyViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
init {
view.setOnClickListener {
onItemClick?.invoke(data[adapterPosition], adapterPosition)
}
}
}
}

View File

@ -0,0 +1,38 @@
package org.mosad.teapod.util
import android.content.Context
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import kotlinx.coroutines.*
import java.io.File
/**
* This controller contains the logic for permanently saved data.
* On load, it loads the saved files into the variables
*/
object StorageController {
private const val fileNameMyList = "my_list.json"
val myList = ArrayList<Int>() // a list of saved mediaIds
fun load(context: Context) {
val file = File(context.filesDir, fileNameMyList)
if (!file.exists()) runBlocking { saveMyList(context).join() }
myList.clear()
myList.addAll(
GsonBuilder().create().fromJson(file.readText(), ArrayList<Int>().javaClass)
)
}
fun saveMyList(context: Context): Job {
val file = File(context.filesDir, fileNameMyList)
return GlobalScope.launch(Dispatchers.IO) {
file.writeText(Gson().toJson(myList))
}
}
}

View File

@ -1,6 +1,7 @@
package org.mosad.teapod.util package org.mosad.teapod.util
import android.util.Log import android.util.Log
import com.google.gson.JsonObject
import com.google.gson.JsonParser import com.google.gson.JsonParser
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async import kotlinx.coroutines.async
@ -14,26 +15,21 @@ class TMDBApiController {
private val apiUrl = "https://api.themoviedb.org/3" private val apiUrl = "https://api.themoviedb.org/3"
private val searchMovieUrl = "$apiUrl/search/movie" private val searchMovieUrl = "$apiUrl/search/movie"
private val searchTVUrl = "$apiUrl/search/tv" private val searchTVUrl = "$apiUrl/search/tv"
private val getMovieUrl = "$apiUrl/movie"
private val apiKey = "de959cf9c07a08b5ca7cb51cda9a40c2" private val apiKey = "de959cf9c07a08b5ca7cb51cda9a40c2"
private val language = "de" private val language = "de"
private val preparedParamters = "?api_key=$apiKey&language=$language" private val preparedParameters = "?api_key=$apiKey&language=$language"
private val imageUrl = "https://image.tmdb.org/t/p/w500" private val imageUrl = "https://image.tmdb.org/t/p/w500"
fun search(title: String, type: MediaType): TMDBResponse { fun search(title: String, type: MediaType): TMDBResponse {
val searchTerm = title.replace("(Sub)", "").trim()
return when (type) { return when (type) {
MediaType.MOVIE -> { MediaType.MOVIE -> searchMovie(searchTerm)
val test = searchMovie(title) MediaType.TVSHOW -> searchTVShow(searchTerm)
println("test: $test") else -> {
test Log.e(javaClass.name, "Wrong Type: $type")
}
MediaType.TVSHOW -> {
val test = searchTVShow(title)
println("test: $test")
test
}
MediaType.OTHER -> {
Log.e(javaClass.name, "Error")
TMDBResponse() TMDBResponse()
} }
} }
@ -41,19 +37,20 @@ class TMDBApiController {
} }
fun searchTVShow(title: String) = runBlocking { fun searchTVShow(title: String) = runBlocking {
val url = URL("$searchTVUrl$preparedParamters&query=${URLEncoder.encode(title, "UTF-8")}") val url = URL("$searchTVUrl$preparedParameters&query=${URLEncoder.encode(title, "UTF-8")}")
GlobalScope.async { GlobalScope.async {
val response = JsonParser.parseString(url.readText()).asJsonObject val response = JsonParser.parseString(url.readText()).asJsonObject
println(response) //println(response)
return@async if (response.get("total_results").asInt > 0) { return@async if (response.get("total_results").asInt > 0) {
response.get("results").asJsonArray.first().let { response.get("results").asJsonArray.first().asJsonObject.let {
val overview = it.asJsonObject.get("overview").asString val id = getStringNotNull(it,"id").toInt()
val posterPath = imageUrl + it.asJsonObject.get("poster_path").asString val overview = getStringNotNull(it,"overview")
val backdropPath = imageUrl + it.asJsonObject.get("backdrop_path").asString val posterPath = getStringNotNullPrefix(it, "poster_path", imageUrl)
val backdropPath = getStringNotNullPrefix(it, "backdrop_path", imageUrl)
TMDBResponse("", overview, posterPath, backdropPath) TMDBResponse(id, "", overview, posterPath, backdropPath)
} }
} else { } else {
TMDBResponse() TMDBResponse()
@ -63,19 +60,21 @@ class TMDBApiController {
} }
fun searchMovie(title: String) = runBlocking { fun searchMovie(title: String) = runBlocking {
val url = URL("$searchMovieUrl$preparedParamters&query=${URLEncoder.encode(title, "UTF-8")}") val url = URL("$searchMovieUrl$preparedParameters&query=${URLEncoder.encode(title, "UTF-8")}")
GlobalScope.async { GlobalScope.async {
val response = JsonParser.parseString(url.readText()).asJsonObject val response = JsonParser.parseString(url.readText()).asJsonObject
println(response) //println(response)
return@async if (response.get("total_results").asInt > 0) { return@async if (response.get("total_results").asInt > 0) {
response.get("results").asJsonArray.first().let { response.get("results").asJsonArray.first().asJsonObject.let {
val overview = it.asJsonObject.get("overview").asString val id = getStringNotNull(it,"id").toInt()
val posterPath = imageUrl + it.asJsonObject.get("poster_path").asString val overview = getStringNotNull(it,"overview")
val backdropPath = imageUrl + it.asJsonObject.get("backdrop_path").asString val posterPath = getStringNotNullPrefix(it, "poster_path", imageUrl)
val backdropPath = getStringNotNullPrefix(it, "backdrop_path", imageUrl)
val runtime = getMovieRuntime(id)
TMDBResponse("", overview, posterPath, backdropPath) TMDBResponse(id, "", overview, posterPath, backdropPath, runtime)
} }
} else { } else {
TMDBResponse() TMDBResponse()
@ -85,4 +84,37 @@ class TMDBApiController {
}.await() }.await()
} }
/**
* currently only used for runtime, need a rework
*/
fun getMovieRuntime(id: Int): Int = runBlocking {
val url = URL("$getMovieUrl/$id?api_key=$apiKey&language=$language")
GlobalScope.async {
val response = JsonParser.parseString(url.readText()).asJsonObject
return@async getStringNotNull(response,"runtime").toInt()
}.await()
}
/**
* return memberName as string if it's not JsonNull,
* else return an empty string
*/
private fun getStringNotNull(jsonObject: JsonObject, memberName: String): String {
return getStringNotNullPrefix(jsonObject, memberName, "")
}
/**
* return memberName as string with a prefix if it's not JsonNull,
* else return an empty string
*/
private fun getStringNotNullPrefix(jsonObject: JsonObject, memberName: String, prefix: String): String {
return if (!jsonObject.get(memberName).isJsonNull) {
prefix + jsonObject.get(memberName).asString
} else {
""
}
}
} }

View File

@ -0,0 +1,73 @@
package org.mosad.teapod.util.adapter
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
import jp.wasabeef.glide.transformations.RoundedCornersTransformation
import kotlinx.android.synthetic.main.item_episode.view.*
import org.mosad.teapod.R
import org.mosad.teapod.util.Episode
class EpisodeItemAdapter(private val episodes: List<Episode>) : RecyclerView.Adapter<EpisodeItemAdapter.MyViewHolder>() {
var onItemClick: ((String, Int) -> Unit)? = null
var onImageClick: ((String, Int) -> Unit)? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_episode, parent, false)
return MyViewHolder(view)
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
val context = holder.view.context
val ep = episodes[position]
val titleText = if (ep.priStreamUrl.isEmpty() && ep.secStreamOmU) {
context.getString(R.string.component_episode_title_sub, ep.number, ep.description)
} else {
context.getString(R.string.component_episode_title, ep.number, ep.description)
}
holder.view.text_episode_title.text = titleText
holder.view.text_episode_desc.text = ep.shortDesc
if (episodes[position].posterUrl.isNotEmpty()) {
Glide.with(context).load(ep.posterUrl)
.apply(RequestOptions.bitmapTransform(RoundedCornersTransformation(10, 0)))
.into(holder.view.image_episode)
}
if (ep.watched) {
holder.view.image_watched.setImageDrawable(
ContextCompat.getDrawable(context, R.drawable.ic_baseline_check_circle_24)
)
} else {
holder.view.image_watched.setImageDrawable(null)
}
}
override fun getItemCount(): Int {
return episodes.size
}
fun updateWatchedState(watched: Boolean, position: Int) {
episodes[position].watched = watched
}
inner class MyViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
init {
view.setOnClickListener {
onItemClick?.invoke(episodes[adapterPosition].title, adapterPosition)
}
view.image_episode.setOnClickListener {
onImageClick?.invoke(episodes[adapterPosition].title, adapterPosition)
}
}
}
}

View File

@ -0,0 +1,79 @@
package org.mosad.teapod.util.adapter
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Filter
import android.widget.Filterable
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import kotlinx.android.synthetic.main.item_media.view.*
import org.mosad.teapod.R
import org.mosad.teapod.util.ItemMedia
import java.util.*
class MediaItemAdapter(private val media: List<ItemMedia>) : RecyclerView.Adapter<MediaItemAdapter.ViewHolder>(), Filterable {
var onItemClick: ((Int, Int) -> Unit)? = null
private val filter = MediaFilter()
private var filteredMedia = media.map { it.copy() }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MediaItemAdapter.ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_media, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: MediaItemAdapter.ViewHolder, position: Int) {
holder.view.apply {
text_title.text = filteredMedia[position].title
Glide.with(context).load(filteredMedia[position].posterUrl).into(image_poster)
}
}
override fun getItemCount(): Int {
return filteredMedia.size
}
override fun getFilter(): Filter {
return filter
}
inner class ViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
init {
view.setOnClickListener {
onItemClick?.invoke(filteredMedia[adapterPosition].id, adapterPosition)
}
}
}
inner class MediaFilter : Filter() {
override fun performFiltering(constraint: CharSequence?): FilterResults {
val filterTerm = constraint.toString().toLowerCase(Locale.ROOT)
val results = FilterResults()
val filteredList = if (filterTerm.isEmpty()) {
media
} else {
media.filter {
it.title.toLowerCase(Locale.ROOT).contains(filterTerm)
}
}
results.values = filteredList
results.count = filteredList.size
return results
}
@Suppress("unchecked_cast")
/**
* suppressing unchecked cast is safe, since we only use Media
*/
override fun publishResults(constraint: CharSequence?, results: FilterResults?) {
filteredMedia = results?.values as List<ItemMedia>
notifyDataSetChanged()
}
}
}

View File

@ -0,0 +1,16 @@
package org.mosad.teapod.util.decoration
import android.graphics.Rect
import android.view.View
import androidx.recyclerview.widget.RecyclerView
class MediaItemDecoration(private val spacing: Int) : RecyclerView.ItemDecoration() {
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
outRect.left = spacing
outRect.right = spacing
outRect.bottom = spacing
outRect.top = spacing
}
}

View File

@ -1,30 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<size android:width="24dp"
android:height="24dp"/>
<solid android:color="#81000000"/>
</shape>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/black"/>
<item android:gravity="center" android:width="144dp" android:height="144dp">
<bitmap
android:gravity="fill_horizontal|fill_vertical"
android:src="@drawable/ic_splash_logo"/>
</item>
</layer-list>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM10,17l-5,-5 1.41,-1.41L10,14.17l7.59,-7.59L19,8l-9,9z"/>
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M20,4L4,4c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,6c0,-1.1 -0.9,-2 -2,-2zM4,12h4v2L4,14v-2zM14,18L4,18v-2h10v2zM20,18h-4v-2h4v2zM20,14L10,14v-2h10v2z"/>
</vector>

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#B0B0B0"/>
<corners android:radius="3dp"/>
</shape>

View File

@ -17,7 +17,7 @@
app:layout_constraintRight_toRightOf="parent" app:layout_constraintRight_toRightOf="parent"
app:menu="@menu/bottom_nav_menu" /> app:menu="@menu/bottom_nav_menu" />
<fragment <androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment" android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment" android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@ -12,13 +12,17 @@
android:id="@+id/video_view" android:id="@+id/video_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_gravity="center" /> android:layout_gravity="center"
app:fastforward_increment="10000"
app:rewind_increment="10000" />
<!-- app:controller_layout_id="@layout/player_custom_control"/>--> <!-- app:controller_layout_id="@layout/player_custom_control"/>-->
<ProgressBar <com.google.android.material.progressindicator.ProgressIndicator
android:id="@+id/loading" android:id="@+id/loading"
style="@style/Widget.MaterialComponents.ProgressIndicator.Circular.Indeterminate"
android:layout_width="70dp" android:layout_width="70dp"
android:layout_height="70dp" android:layout_height="70dp"
android:layout_gravity="center" /> android:layout_gravity="center"
tools:visibility="visible" />
</FrameLayout> </FrameLayout>

View File

@ -1,39 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="5dp"
android:paddingTop="7dp"
android:paddingEnd="5dp"
android:paddingBottom="7dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView
android:id="@+id/image_episode"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:minWidth="48dp"
app:srcCompat="@drawable/ic_baseline_account_box_24" />
<TextView
android:id="@+id/text_episode_title"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="7dp"
android:layout_weight="1"
android:text="TextView"
android:textSize="16sp" />
</LinearLayout>
<TextView
android:id="@+id/text_episode_desc"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>

View File

@ -5,7 +5,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="#f5f5f5" android:background="#f5f5f5"
tools:context=".ui.account.AccountFragment"> tools:context=".ui.fragments.AccountFragment">
<ScrollView <ScrollView
android:layout_width="match_parent" android:layout_width="match_parent"
@ -39,6 +39,7 @@
android:id="@+id/linear_account_login" android:id="@+id/linear_account_login"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_margin="7dp"
android:gravity="center" android:gravity="center"
android:orientation="horizontal"> android:orientation="horizontal">
@ -80,6 +81,91 @@
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
<LinearLayout
android:id="@+id/linear_settings"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="12dp"
android:background="#ffffff"
android:orientation="vertical">
<TextView
android:id="@+id/text_settings"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="7dp"
android:paddingEnd="7dp"
android:text="@string/settings"
android:textColor="@android:color/primary_text_light"
android:textSize="16sp"
android:textStyle="bold" />
<LinearLayout
android:id="@+id/linear_settings_secondary"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="7dp"
android:gravity="center"
android:orientation="horizontal">
<ImageView
android:id="@+id/imageView3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/account"
android:minWidth="48dp"
android:minHeight="48dp"
android:padding="5dp"
android:scaleType="fitXY"
android:src="@drawable/ic_baseline_subtitles_24" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:id="@+id/linearLayout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/switch_secondary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/text_settings_secondary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/settings_secondary"
android:textColor="@android:color/primary_text_light"
android:textSize="16sp" />
<TextView
android:id="@+id/text_settings_secondary_desc"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:maxLines="2"
android:text="@string/settings_secondary_desc"
android:textColor="@android:color/secondary_text_light" />
</LinearLayout>
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/switch_secondary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
</LinearLayout>
<LinearLayout <LinearLayout
android:id="@+id/linear_info" android:id="@+id/linear_info"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -103,6 +189,7 @@
android:id="@+id/linear_about" android:id="@+id/linear_about"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_margin="7dp"
android:gravity="center" android:gravity="center"
android:orientation="horizontal"> android:orientation="horizontal">
@ -110,6 +197,7 @@
android:id="@+id/imageView2" android:id="@+id/imageView2"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:contentDescription="@string/info"
android:minWidth="48dp" android:minWidth="48dp"
android:minHeight="48dp" android:minHeight="48dp"
android:padding="5dp" android:padding="5dp"
@ -140,7 +228,25 @@
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
<View
android:id="@+id/divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?android:attr/listDivider" />
<TextView
android:id="@+id/text_licenses"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="7dp"
android:paddingStart="48dp"
android:paddingEnd="48dp"
android:text="@string/licenses"
android:textColor="@android:color/primary_text_light"
android:textSize="16sp" />
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
</ScrollView> </ScrollView>

View File

@ -5,19 +5,72 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="#f5f5f5" android:background="#f5f5f5"
tools:context=".ui.home.HomeFragment"> tools:context=".ui.fragments.HomeFragment">
<TextView <ScrollView
android:id="@+id/text_home"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent">
android:layout_marginStart="8dp"
android:layout_marginTop="8dp" <LinearLayout
android:layout_marginEnd="8dp" android:layout_width="match_parent"
android:textAlignment="center" android:layout_height="wrap_content"
android:textSize="20sp" android:orientation="vertical" >
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" <LinearLayout
app:layout_constraintStart_toStartOf="parent" android:layout_width="match_parent"
app:layout_constraintTop_toTopOf="parent" /> android:layout_height="match_parent"
android:orientation="vertical"
android:paddingBottom="7dp">
<TextView
android:id="@+id/text_my_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="10dp"
android:paddingTop="15dp"
android:paddingEnd="5dp"
android:paddingBottom="5dp"
android:text="@string/my_list"
android:textSize="16sp"
android:textStyle="bold" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_my_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_media" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingBottom="7dp">
<TextView
android:id="@+id/text_new_episodes"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="10dp"
android:paddingTop="15dp"
android:paddingEnd="5dp"
android:paddingBottom="5dp"
android:text="@string/new_episodes"
android:textSize="16sp"
android:textStyle="bold" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_new_episodes"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_media" />
</LinearLayout>
</LinearLayout>
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -5,15 +5,21 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="#f5f5f5" android:background="#f5f5f5"
tools:context=".ui.library.LibraryFragment"> tools:context=".ui.fragments.LibraryFragment">
<ListView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/list_library" android:id="@+id/recycler_media_library"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:clipToPadding="false"
android:padding="3dp"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:spanCount="2"
tools:listitem="@layout/item_media" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#f5f5f5"
android:clickable="true"
android:focusable="true">
<com.google.android.material.progressindicator.ProgressIndicator
android:id="@+id/progressBar2"
style="@style/Widget.MaterialComponents.ProgressIndicator.Circular.Indeterminate"
android:layout_width="70dp"
android:layout_height="70dp"
android:layout_gravity="center"
tools:visibility="visible" />
</FrameLayout>

View File

@ -5,7 +5,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="#f5f5f5" android:background="#f5f5f5"
tools:context=".ui.MediaFragment"> tools:context=".ui.fragments.MediaFragment">
<androidx.core.widget.NestedScrollView <androidx.core.widget.NestedScrollView
android:layout_width="match_parent" android:layout_width="match_parent"
@ -15,57 +15,134 @@
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" > android:orientation="vertical">
<ImageView <FrameLayout
android:id="@+id/image_poster" android:layout_width="match_parent"
android:layout_width="wrap_content" android:layout_height="wrap_content">
<ImageView
android:id="@+id/image_backdrop"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:adjustViewBounds="false"
android:maxHeight="231dp"
android:minHeight="220dp"
android:scaleType="centerCrop" />
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/image_poster"
android:layout_width="wrap_content"
android:layout_height="200dp"
android:layout_gravity="center"
app:shapeAppearance="@style/ShapeAppearance.Teapod.RoundedPoster"
app:srcCompat="@drawable/ic_launcher_background" />
</FrameLayout>
<LinearLayout
android:id="@+id/linear_media_info"
android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_marginTop="10dp"
android:layout_marginTop="20dp" android:gravity="center"
android:minHeight="200dp" android:orientation="horizontal">
android:src="@drawable/ic_launcher_background" />
<Button <TextView
android:id="@+id/text_year"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="2dp"
android:text="@string/text_year_ex" />
<TextView
android:id="@+id/text_age"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="7dp"
android:background="@drawable/shape_rounden_corner"
android:paddingStart="3dp"
android:paddingTop="2dp"
android:paddingEnd="3dp"
android:paddingBottom="2dp"
android:text="@string/text_age_ex" />
<TextView
android:id="@+id/text_episodes_or_runtime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="7dp"
android:padding="2dp"
android:text="@string/text_episodes_count" />
</LinearLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/button_play" android:id="@+id/button_play"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginStart="7dp" android:layout_marginStart="7dp"
android:layout_marginTop="24dp" android:layout_marginTop="6dp"
android:layout_marginEnd="7dp" android:layout_marginEnd="7dp"
android:background="#4A4141" android:gravity="center"
android:drawableStart="@drawable/ic_baseline_play_arrow_24"
android:drawablePadding="10dp"
android:drawableTint="#FFFFFF"
android:gravity="start|center_vertical"
android:paddingStart="160dp"
android:paddingEnd="160dp"
android:text="@string/button_play" android:text="@string/button_play"
android:textAllCaps="false" android:textAllCaps="false"
android:textColor="@android:color/primary_text_dark" android:textColor="@android:color/primary_text_dark"
android:textSize="16sp" /> android:textSize="16sp"
app:backgroundTint="#4A4141"
app:icon="@drawable/ic_baseline_play_arrow_24"
app:iconGravity="textStart" />
<TextView <TextView
android:id="@+id/text_title" android:id="@+id/text_title"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="19dp" android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_gravity="center"
android:layout_marginStart="7dp" android:layout_marginStart="7dp"
android:layout_marginTop="12dp" android:layout_marginTop="12dp"
android:layout_marginEnd="7dp" android:layout_marginEnd="7dp"
android:text="TextView" android:text="@string/text_title_ex"
android:textStyle="bold" /> android:textStyle="bold" />
<TextView <TextView
android:id="@+id/text_desc" android:id="@+id/text_overview"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_gravity="center"
android:layout_marginStart="7dp" android:layout_marginStart="12dp"
android:layout_marginTop="10dp" android:layout_marginTop="7dp"
android:layout_marginEnd="7dp" android:layout_marginEnd="12dp"
android:text="TextView" /> android:text="@string/text_overview_ex" />
<LinearLayout
android:id="@+id/linear_actions"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="12dp"
android:layout_marginTop="7dp"
android:layout_marginEnd="12dp"
android:orientation="horizontal">
<LinearLayout
android:id="@+id/linear_my_list_action"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:orientation="vertical">
<ImageView
android:id="@+id/image_my_list_action"
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@drawable/ic_baseline_add_24"
app:tint="#4A4141" />
<TextView
android:id="@+id/text_my_list_action"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/my_list" />
</LinearLayout>
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_episodes" android:id="@+id/recycler_episodes"
@ -74,7 +151,8 @@
android:layout_marginStart="7dp" android:layout_marginStart="7dp"
android:layout_marginTop="17dp" android:layout_marginTop="17dp"
android:layout_marginEnd="7dp" android:layout_marginEnd="7dp"
tools:layout_editor_absoluteY="298dp" /> tools:layout_editor_absoluteY="298dp"
tools:listitem="@layout/item_episode" />
</LinearLayout> </LinearLayout>
</androidx.core.widget.NestedScrollView> </androidx.core.widget.NestedScrollView>

View File

@ -5,16 +5,15 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="#f5f5f5" android:background="#f5f5f5"
tools:context=".ui.search.SearchFragment"> tools:context=".ui.fragments.SearchFragment">
<SearchView <SearchView
android:id="@+id/search_text" android:id="@+id/search_text"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="48dp" android:layout_height="0dp"
android:background="#FFFFFF"
android:elevation="8dp"
android:iconifiedByDefault="false" android:iconifiedByDefault="false"
android:paddingStart="5dp"
android:paddingTop="5dp"
android:paddingEnd="5dp"
android:paddingBottom="5dp" android:paddingBottom="5dp"
android:queryHint="@string/search_hint" android:queryHint="@string/search_hint"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
@ -23,13 +22,21 @@
</SearchView> </SearchView>
<ListView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/list_search" android:id="@+id/recycler_media_search"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
android:clipToPadding="false"
android:padding="3dp"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/search_text" /> app:layout_constraintTop_toBottomOf="@+id/search_text"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:spanCount="2"
tools:listitem="@layout/item_media">
</androidx.recyclerview.widget.RecyclerView>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="5dp"
android:paddingTop="7dp"
android:paddingEnd="5dp"
android:paddingBottom="7dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/image_episode"
android:layout_width="128dp"
android:layout_height="72dp"
android:contentDescription="@string/component_poster_desc"
app:shapeAppearance="@style/ShapeAppearance.Teapod.RoundedPoster"
app:srcCompat="@color/md_disabled_text_dark_theme" />
<ImageView
android:id="@+id/image_episode_play"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="@drawable/bg_circle__black_transparent_24dp"
android:contentDescription="@string/button_play"
app:srcCompat="@drawable/ic_baseline_play_arrow_24"
app:tint="#FFFFFF" />
</FrameLayout>
<TextView
android:id="@+id/text_episode_title"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginStart="7dp"
android:layout_weight="1"
android:text="@string/component_episode_title"
android:textSize="16sp" />
<ImageView
android:id="@+id/image_watched"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_margin="2dp"
android:contentDescription="@string/component_watched_desc"
app:srcCompat="@drawable/ic_baseline_check_circle_24" />
</LinearLayout>
<TextView
android:id="@+id/text_episode_desc"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxLines="2"
android:ellipsize="end"/>
</LinearLayout>

View File

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="195dp"
android:layout_height="wrap_content"
android:backgroundTint="#FFFFFF"
android:visibility="visible"
app:cardCornerRadius="7dp"
app:cardElevation="4dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/image_poster"
android:layout_width="0dp"
android:layout_height="0dp"
android:contentDescription="@string/media_poster_desc"
android:scaleType="centerCrop"
app:layout_constraintBottom_toTopOf="@+id/text_title"
app:layout_constraintDimensionRatio="H,16:9"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@color/md_disabled_text_dark_theme" />
<TextView
android:id="@+id/text_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:lines="2"
android:maxLines="2"
android:padding="3dp"
android:text="@string/text_title_ex"
android:textAlignment="center"
android:textSize="15sp"
app:layout_constraintTop_toBottomOf="@+id/image_poster" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>

View File

@ -1,29 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/linear_media"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="7dp"
android:paddingTop="3dp"
android:paddingEnd="7dp"
android:paddingBottom="5dp">
<TextView
android:id="@+id/text_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="TextView"
android:textAlignment="center"
android:textSize="18sp"
android:textStyle="bold" />
<ImageView
android:id="@+id/image_poster"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="223dp"
tools:srcCompat="@drawable/ic_launcher_background" />
</LinearLayout>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" /> <background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground" /> <foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon> </adaptive-icon>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" /> <background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground" /> <foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon> </adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 989 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -7,25 +7,25 @@
<fragment <fragment
android:id="@+id/navigation_home" android:id="@+id/navigation_home"
android:name="org.mosad.teapod.ui.home.HomeFragment" android:name="org.mosad.teapod.ui.fragments.HomeFragment"
android:label="@string/title_home" android:label="@string/title_home"
tools:layout="@layout/fragment_home" /> tools:layout="@layout/fragment_home" />
<fragment <fragment
android:id="@+id/navigation_library" android:id="@+id/navigation_library"
android:name="org.mosad.teapod.ui.library.LibraryFragment" android:name="org.mosad.teapod.ui.fragments.LibraryFragment"
android:label="@string/title_library" android:label="@string/title_library"
tools:layout="@layout/fragment_library" /> tools:layout="@layout/fragment_library" />
<fragment <fragment
android:id="@+id/navigation_search" android:id="@+id/navigation_search"
android:name="org.mosad.teapod.ui.search.SearchFragment" android:name="org.mosad.teapod.ui.fragments.SearchFragment"
android:label="@string/title_search" android:label="@string/title_search"
tools:layout="@layout/fragment_search" /> tools:layout="@layout/fragment_search" />
<fragment <fragment
android:id="@+id/navigation_account" android:id="@+id/navigation_account"
android:name="org.mosad.teapod.ui.account.AccountFragment" android:name="org.mosad.teapod.ui.fragments.AccountFragment"
android:label="@string/title_account" android:label="@string/title_account"
tools:layout="@layout/fragment_account" /> tools:layout="@layout/fragment_account" />

View File

@ -0,0 +1,63 @@
<?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 3-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>
</notices>

View File

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="title_home">Startseite</string>
<string name="title_library">Übersicht</string>
<string name="title_search">Suche</string>
<string name="title_account">Account</string>
<!-- home fragment -->
<string name="my_list">Meine Liste</string>
<string name="new_episodes">Neue Episoden</string>
<!-- search fragment -->
<string name="search_hint">Suche nach Filmen und Serien</string>
<!-- media fragment -->
<string name="button_play">Abspielen</string>
<string name="text_episodes_count">%1$d Episoden</string>
<string name="text_runtime">%1$d Minuten</string>
<string name="component_episode_title">Episode %1$d %2$s</string>
<string name="component_episode_title_sub">Episode %1$d %2$s (OmU)</string>
<!-- settings fragment -->
<string name="account">Account</string>
<string name="account_login_desc">Zum bearbeiten tippen</string>
<string name="info">Info</string>
<string name="info_about_desc">Version %1$s (%2$s)</string>
<string name="info_about_dialog">Diese App wird unter den Bedingungen der GNU GPL 3 oder höher zur Verfügung gestellt. Weiter Informationen findest du unter: \ngit.mosad.xyz/Seil0/teapod \n\n© 2020 seil0@mosad.xyz</string>
<string name="licenses">Lizenzen</string>
<string name="settings">Einstellungen</string>
<string name="settings_secondary">Bevorzuge alternativen Stream</string>
<string name="settings_secondary_desc">Untertitle-Stream verwenden, sofern vorhanden</string>
<!-- dialogs -->
<string name="save">speichern</string>
<string name="cancel">@android:string/cancel</string>
<!-- etc -->
<string name="login">Login</string>
<string name="login_desc">Um Teapod zu benutzen musst du eingeloggt sein. Die Login-Daten weden verschüsselt aud deinem Gerät gespeichert.</string>
<string name="login_failed_desc">Login nicht erfolgreich. Bitte versuche es erneut.</string>
<string name="password">Passwort</string>
</resources>

View File

@ -3,4 +3,6 @@
<color name="colorPrimary">#6200EE</color> <color name="colorPrimary">#6200EE</color>
<color name="colorPrimaryDark">#3700B3</color> <color name="colorPrimaryDark">#3700B3</color>
<color name="colorAccent">#03DAC5</color> <color name="colorAccent">#03DAC5</color>
<color name="ic_launcher_background">#FFFFFF</color>
</resources> </resources>

View File

@ -1,24 +1,43 @@
<resources> <resources>
<string name="app_name">Teapod</string> <string name="app_name" translatable="false">Teapod</string>
<string name="title_home">Home</string> <string name="title_home">Home</string>
<string name="title_library">Library</string> <string name="title_library">Library</string>
<string name="title_search">Search</string> <string name="title_search">Search</string>
<string name="title_account">Account</string> <string name="title_account">Account</string>
<!-- home fragment -->
<string name="my_list">My list</string>
<string name="new_episodes">New episodes</string>
<!-- search fragment --> <!-- search fragment -->
<string name="search_hint">Search for movies and series</string> <string name="search_hint">Search for movies and series</string>
<string name="media_poster_desc" translatable="false">poster</string>
<!-- media fragment --> <!-- media fragment -->
<string name="button_play">Play</string> <string name="button_play">Play</string>
<string name="text_title_ex" translatable="false">A Silent Voice</string>
<string name="text_overview_ex" translatable="false">Shouya Ishida starts bullying the new girl in class …</string>
<string name="text_year_ex" translatable="false">2016</string>
<string name="text_age_ex" translatable="false">6</string>
<string name="text_episodes_count">%1$d episodes</string>
<string name="text_runtime">%1$d Minutes</string>
<string name="component_episode_title">Episode %1$d %2$s</string>
<string name="component_episode_title_sub">Episode %1$d %2$s (Sub)</string>
<string name="component_poster_desc" translatable="false">episode poster</string>
<string name="component_watched_desc" translatable="false">already watched</string>
<!-- settings fragment --> <!-- settings fragment -->
<string name="account">Account</string> <string name="account">Account</string>
<string name="account_login_ex">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="info">Info</string> <string name="info">Info</string>
<string name="info_about">Teapod by @Seil0</string> <string name="info_about" translatable="false">Teapod by @Seil0</string>
<string name="info_about_desc" translatable="false">Version %1$s (%2$s)</string> <string name="info_about_desc">Version %1$s (%2$s)</string>
<string name="info_about_dialog" translatable="false">This software is published under the terms and conditions of GPL 3. For further information visit git.mosad.xyz/Seil0 \n\n© 2020 seil0@mosad.xyz</string> <string name="info_about_dialog">This app is published under the terms and conditions of the GNU GPL 3 or later. For further information visit: \ngit.mosad.xyz/Seil0/teapod \n\n© 2020 seil0@mosad.xyz</string>
<string name="licenses">Licenses</string>
<string name="settings">Settings</string>
<string name="settings_secondary">Prefer secondary (sub) stream</string>
<string name="settings_secondary_desc">Use the subtitles stream if present</string>
<!-- dialogs --> <!-- dialogs -->
<string name="save">save</string> <string name="save">save</string>
@ -26,13 +45,16 @@
<!-- etc --> <!-- etc -->
<string name="login">Login</string> <string name="login">Login</string>
<string name="login_desc">You need to login before you can use Teapod. Your Login-Data will be stored encrypted on your device.</string> <string name="login_desc">You need to login before you can use Teapod. The Login-Data will be stored encrypted on your device.</string>
<string name="login_failed_desc">Could not login. Please try again.</string>
<string name="password">Password</string> <string name="password">Password</string>
<!-- save keys --> <!-- save keys -->
<string name="encrypted_preference_file_key" translatable="false">org.mosad.teapod.encrypted_preferences</string> <string name="encrypted_preference_file_key" translatable="false">org.mosad.teapod.encrypted_preferences</string>
<string name="preference_file_key" translatable="false">org.mosad.teapod.preferences</string>
<string name="save_key_user_login" translatable="false">org.mosad.teapod.user_login</string> <string name="save_key_user_login" translatable="false">org.mosad.teapod.user_login</string>
<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>
<!-- intents & states --> <!-- intents & states -->
<string name="intent_stream_url" translatable="false">intent_stream_url</string> <string name="intent_stream_url" translatable="false">intent_stream_url</string>

View File

@ -1,18 +1,27 @@
<resources> <resources>
<!-- Base application theme. --> <!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar"> <style name="AppTheme" parent="Theme.MaterialComponents.Light.NoActionBar">
<!-- Customize your theme here. --> <!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item> <item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item> <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item> <item name="colorAccent">@color/colorAccent</item>
</style> </style>
<style name="AppTheme.AppCompat.Light.NoActionBar.FullScreen" parent="@style/Theme.AppCompat.Light.NoActionBar"> <style name="PlayerTheme" parent="Theme.MaterialComponents.Light.NoActionBar">
<item name="android:windowNoTitle">true</item> <item name="android:windowNoTitle">true</item>
<item name="android:windowActionBar">false</item> <item name="android:windowActionBar">false</item>
<item name="android:windowFullscreen">true</item> <item name="android:windowFullscreen">true</item>
<item name="android:windowContentOverlay">@null</item> <item name="android:windowContentOverlay">@null</item>
</style> </style>
<style name="SplashTheme" parent="Theme.AppCompat.NoActionBar">
<item name="android:windowBackground">@drawable/bg_splash</item>
</style>
<!-- shapes -->
<style name="ShapeAppearance.Teapod.RoundedPoster" parent="ShapeAppearance.MaterialComponents.LargeComponent">
<item name="cornerFamily">rounded</item>
<item name="cornerSize">5dp</item>
</style>
</resources> </resources>

View File

@ -6,7 +6,7 @@ buildscript {
jcenter() jcenter()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:4.0.2' classpath 'com.android.tools.build:gradle:4.1.0'
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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 548 KiB

View File

@ -0,0 +1 @@
Version 0.1 ist der erste öffentliche Release von Teapod.

View File

@ -0,0 +1,11 @@
Teapod ist eine inoffizielle App für Anime-on-Demand (AoD).
* Schau dir alle Title von AoD auf deinem Android Gerät an
* Nativer Player auf Basis des ExoPayers
* Bevorzuge die OmU Version über die App-Einstellungen
* Speicher deine lieblings Anime in "Meine Liste"
Zur Verfügung gestellt unter den Bedingungen der GNU GPL 3 oder höher.
Dieses Projekt ist in keiner Weise mit Anime-on-Demand verbunden.
Bitte melde Fehler und Probleme an support@mosad.xyz

Binary file not shown.

After

Width:  |  Height:  |  Size: 848 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 572 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

@ -0,0 +1 @@
Android App für AoD

View File

@ -0,0 +1 @@
Teapod

View File

@ -0,0 +1,11 @@
Teapod is a unoffical App for Anime-on-Demand (AoD).
* Watch all animes from AoD on your Android device
* Native Player based on ExoPayer
* Prefer the OmU version via the app settings
* Save your favorite animes to "My List"
Licensed under the terms and conditions of GPL 3.
This Project is not associated with Anime-on-Demand in any way.
Please report bugs and issues to support@mosad.xyz

View File

@ -0,0 +1 @@
Android App for AoD

View File

@ -1,6 +1,6 @@
#Thu Oct 08 16:06:13 CEST 2020 #Tue Oct 13 12:04:29 CEST 2020
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip