Compare commits
27 Commits
0.1-alpha1
...
0.1-alpha3
Author | SHA1 | Date | |
---|---|---|---|
aeb74dcb29
|
|||
2689c37af3 | |||
5458b43354
|
|||
d912ed34a3
|
|||
9f1717e646
|
|||
085b2013ab
|
|||
474b72df49
|
|||
a8dc243d0e
|
|||
fa6419bb02
|
|||
6100533c4d
|
|||
4ae23c4380
|
|||
adf8a48251
|
|||
36c8678646
|
|||
442a02db70
|
|||
5f80f1fabd
|
|||
d2728405d1
|
|||
87f9235b8a | |||
03cd42773d
|
|||
cbfd186686
|
|||
5b7d2cd26e
|
|||
6fb8f56faf
|
|||
dcaf64acde
|
|||
597271d4de
|
|||
c947105a1f
|
|||
9ec4c24e21
|
|||
00a6981ae5
|
|||
ee063a5bbe |
29
README.md
29
README.md
@ -6,22 +6,35 @@ 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_Library.png" width=180>](https://www.mosad.xyz/images/Teapod/Teapod_Library.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_Media.png" width=180>](https://www.mosad.xyz/images/Teapod/Teapod_Media.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_Search.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
|
||||||
|
|
||||||
|
#### 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)
|
Teapod © 2020 [@Seil0](https://git.mosad.xyz/Seil0)
|
@ -11,7 +11,7 @@ android {
|
|||||||
minSdkVersion 23
|
minSdkVersion 23
|
||||||
targetSdkVersion 30
|
targetSdkVersion 30
|
||||||
versionCode 1
|
versionCode 1
|
||||||
versionName "0.1-alpha1"
|
versionName "0.1-alpha3"
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
resValue "string", "build_time", buildTime()
|
resValue "string", "build_time", buildTime()
|
||||||
@ -41,13 +41,12 @@ 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.2'
|
||||||
implementation 'androidx.navigation:navigation-fragment:2.3.0'
|
|
||||||
implementation 'androidx.navigation:navigation-ui:2.3.0'
|
|
||||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.0'
|
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.0'
|
||||||
implementation 'androidx.navigation:navigation-ui-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.0'
|
||||||
implementation 'com.google.android.exoplayer:exoplayer-hls:2.12.0'
|
implementation 'com.google.android.exoplayer:exoplayer-hls:2.12.0'
|
||||||
@ -56,9 +55,10 @@ dependencies {
|
|||||||
|
|
||||||
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'
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
android:name=".PlayerActivity"
|
android:name=".PlayerActivity"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:configChanges="orientation|screenSize|layoutDirection"
|
android:configChanges="orientation|screenSize|layoutDirection"
|
||||||
android:theme="@style/AppTheme.AppCompat.Light.NoActionBar.FullScreen" />
|
android:theme="@style/AppTheme.MaterialComponents.Light.NoActionBar.FullScreen" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
|
@ -1,43 +1,72 @@
|
|||||||
|
/**
|
||||||
|
* 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.ui.fragments.MediaFragment
|
||||||
import org.mosad.teapod.ui.account.AccountFragment
|
import org.mosad.teapod.ui.fragments.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.HomeFragment
|
||||||
import org.mosad.teapod.ui.library.LibraryFragment
|
import org.mosad.teapod.ui.fragments.LibraryFragment
|
||||||
import org.mosad.teapod.ui.search.SearchFragment
|
import org.mosad.teapod.ui.fragments.SearchFragment
|
||||||
import org.mosad.teapod.util.Media
|
import org.mosad.teapod.ui.fragments.LoadingFragment
|
||||||
|
import org.mosad.teapod.util.StorageController
|
||||||
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 +75,75 @@ 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() {
|
||||||
|
// running login and list in parallel does not bring any speed improvements
|
||||||
|
val time = measureTimeMillis {
|
||||||
|
// make sure credentials are set
|
||||||
EncryptedPreferences.readCredentials(this)
|
EncryptedPreferences.readCredentials(this)
|
||||||
|
|
||||||
if (EncryptedPreferences.password.isEmpty()) {
|
if (EncryptedPreferences.password.isEmpty()) {
|
||||||
Log.i(javaClass.name, "please login!")
|
showLoginDialog(true)
|
||||||
|
} else {
|
||||||
LoginDialog(this).positiveButton {
|
// try to login in, as most sites can only bee loaded once loged in
|
||||||
EncryptedPreferences.saveCredentials(login, password, context)
|
if (!AoDParser().login()) showLoginDialog(false)
|
||||||
}.negativeButton {
|
|
||||||
Log.i(javaClass.name, "Login canceled, exiting.")
|
|
||||||
finish()
|
|
||||||
}.show()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showDetailFragment(media: Media) {
|
StorageController.load(this)
|
||||||
media.episodes = AoDParser().loadStreams(media) // load the streams for the selected media
|
|
||||||
|
|
||||||
val tmdb = TMDBApiController().search(media.title, media.type)
|
// initially load all media
|
||||||
|
AoDParser().listAnimes()
|
||||||
|
|
||||||
|
// TODO load home screen, can be parallel to listAnimes
|
||||||
|
}
|
||||||
|
Log.i(javaClass.name, "login and list in $time ms")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 +151,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 +163,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()
|
||||||
|
}
|
||||||
}
|
}
|
@ -8,26 +8,32 @@ import org.jsoup.Jsoup
|
|||||||
import org.mosad.teapod.preferences.EncryptedPreferences
|
import org.mosad.teapod.preferences.EncryptedPreferences
|
||||||
import org.mosad.teapod.util.DataTypes.MediaType
|
import org.mosad.teapod.util.DataTypes.MediaType
|
||||||
import org.mosad.teapod.util.Episode
|
import org.mosad.teapod.util.Episode
|
||||||
|
import org.mosad.teapod.util.ItemMedia
|
||||||
import org.mosad.teapod.util.Media
|
import org.mosad.teapod.util.Media
|
||||||
|
import java.io.IOException
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.ArrayList
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* maybe AoDParser as object would be useful
|
||||||
|
*/
|
||||||
class AoDParser {
|
class AoDParser {
|
||||||
|
|
||||||
private val baseUrl = "https://www.anime-on-demand.de"
|
private val baseUrl = "https://www.anime-on-demand.de"
|
||||||
private val loginPath = "/users/sign_in"
|
private val loginPath = "/users/sign_in"
|
||||||
private val libraryPath = "/animes"
|
private val libraryPath = "/animes"
|
||||||
|
|
||||||
|
private val userAgent = "Mozilla/5.0 (X11; Linux x86_64; rv:80.0) Gecko/20100101 Firefox/80.0"
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
private var csrfToken: String = ""
|
||||||
private var sessionCookies = mutableMapOf<String, String>()
|
private var sessionCookies = mutableMapOf<String, String>()
|
||||||
private var loginSuccess = false
|
private var loginSuccess = false
|
||||||
|
|
||||||
val mediaList = arrayListOf<Media>()
|
val mediaList = arrayListOf<Media>()
|
||||||
|
val itemMediaList = arrayListOf<ItemMedia>()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun login() = runBlocking {
|
fun login(): Boolean = runBlocking {
|
||||||
|
|
||||||
val userAgent = "Mozilla/5.0 (X11; Linux x86_64; rv:80.0) Gecko/20100101 Firefox/80.0"
|
|
||||||
|
|
||||||
withContext(Dispatchers.Default) {
|
withContext(Dispatchers.Default) {
|
||||||
// get the authenticity token
|
// get the authenticity token
|
||||||
@ -36,10 +42,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,15 +59,16 @@ 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,32 +92,46 @@ 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
|
return@withContext mediaList
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getMediaById(mediaId: Int): Media {
|
||||||
|
val media = mediaList.first { it.id == mediaId }
|
||||||
|
|
||||||
|
if (media.episodes.isEmpty()) {
|
||||||
|
loadStreams(media)
|
||||||
|
}
|
||||||
|
|
||||||
|
return media
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* load streams for the media path
|
* load streams for the media path, movies have one episode
|
||||||
|
* @param media is used as call ba reference
|
||||||
*/
|
*/
|
||||||
fun loadStreams(media: Media): List<Episode> = runBlocking {
|
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) {
|
||||||
@ -121,20 +142,60 @@ class AoDParser {
|
|||||||
|
|
||||||
//println(res)
|
//println(res)
|
||||||
|
|
||||||
|
// 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
|
||||||
|
media.episodes = when (media.type) {
|
||||||
|
MediaType.MOVIE -> listOf(Episode())
|
||||||
|
MediaType.TVSHOW -> {
|
||||||
|
res.select("div.three-box-container > div.episodebox").map { 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()
|
||||||
|
|
||||||
|
Episode(
|
||||||
|
id = episodeId,
|
||||||
|
shortDesc = episodeShortDesc,
|
||||||
|
watched = episodeWatched,
|
||||||
|
watchedCallback = episodeWatchedCallback
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MediaType.OTHER -> listOf()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (csrfToken.isEmpty()) {
|
||||||
|
csrfToken = res.select("meta[name=csrf-token]").attr("content")
|
||||||
|
//Log.i(javaClass.name, "New csrf token is $csrfToken")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO has attr data-lag (ger or jap)
|
||||||
val playlists = res.select("input.streamstarter_html5").eachAttr("data-playlist")
|
val playlists = res.select("input.streamstarter_html5").eachAttr("data-playlist")
|
||||||
val csrfToken = res.select("meta[name=csrf-token]").attr("content")
|
|
||||||
|
|
||||||
//println("first entry: ${playlists.first()}")
|
if (playlists.size > 0) {
|
||||||
//println("csrf token is: $csrfToken")
|
loadPlaylist(playlists.first(), csrfToken, media.type, media.episodes)
|
||||||
|
}
|
||||||
return@withContext loadStreamInfo(playlists.first(), csrfToken, media.type)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* load the playlist path and parse it, read the stream info from json
|
* load the playlist path and parse it, read the stream info from json
|
||||||
|
* @param episodes is used as call ba reference
|
||||||
*/
|
*/
|
||||||
private fun loadStreamInfo(playlistPath: String, csrfToken: String, type: MediaType): List<Episode> = runBlocking {
|
private fun loadPlaylist(playlistPath: String, csrfToken: String, type: MediaType, episodes: List<Episode>) = runBlocking {
|
||||||
withContext(Dispatchers.Default) {
|
withContext(Dispatchers.Default) {
|
||||||
val headers = mutableMapOf(
|
val headers = mutableMapOf(
|
||||||
Pair("Accept", "application/json, text/javascript, */*; q=0.01"),
|
Pair("Accept", "application/json, text/javascript, */*; q=0.01"),
|
||||||
@ -144,6 +205,8 @@ 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)
|
||||||
@ -152,39 +215,70 @@ class AoDParser {
|
|||||||
|
|
||||||
//println(res.body())
|
//println(res.body())
|
||||||
|
|
||||||
return@withContext when (type) {
|
when (type) {
|
||||||
MediaType.MOVIE -> {
|
MediaType.MOVIE -> {
|
||||||
val movie = JsonParser.parseString(res.body()).asJsonObject
|
val movie = JsonParser.parseString(res.body()).asJsonObject
|
||||||
.get("playlist").asJsonArray
|
.get("playlist").asJsonArray
|
||||||
|
.first().asJsonObject
|
||||||
|
|
||||||
movie.first().asJsonObject.get("sources").asJsonArray.toList().map {
|
movie.get("sources").asJsonArray.first().apply {
|
||||||
Episode(streamUrl = it.asJsonObject.get("file").asString)
|
episodes.first().streamUrl = this.asJsonObject.get("file").asString
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MediaType.TVSHOW -> {
|
MediaType.TVSHOW -> {
|
||||||
val episodesJson = JsonParser.parseString(res.body()).asJsonObject
|
val episodesJson = JsonParser.parseString(res.body()).asJsonObject
|
||||||
.get("playlist").asJsonArray
|
.get("playlist").asJsonArray
|
||||||
|
|
||||||
|
episodesJson.forEach { jsonElement ->
|
||||||
episodesJson.map {
|
val episodeId = jsonElement.asJsonObject.get("mediaid")
|
||||||
val episodeStream = it.asJsonObject.get("sources").asJsonArray
|
val episodeStream = jsonElement.asJsonObject.get("sources").asJsonArray
|
||||||
.first().asJsonObject
|
.first().asJsonObject
|
||||||
.get("file").asString
|
.get("file").asString
|
||||||
val episodeTitle = it.asJsonObject.get("title").asString
|
val episodeTitle = jsonElement.asJsonObject.get("title").asString
|
||||||
|
val episodePoster = jsonElement.asJsonObject.get("image").asString
|
||||||
|
val episodeDescription = jsonElement.asJsonObject.get("description").asString
|
||||||
|
val episodeNumber = episodeTitle.substringAfter(", Ep. ").toInt()
|
||||||
|
|
||||||
Episode(
|
episodes.first { it.id == episodeId.asInt }.apply {
|
||||||
episodeTitle,
|
this.title = episodeTitle
|
||||||
episodeStream
|
this.posterUrl = episodePoster
|
||||||
)
|
this.streamUrl = episodeStream
|
||||||
|
this.description = episodeDescription
|
||||||
|
this.number = episodeNumber
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
Log.e(javaClass.name, "Wrong Type, please report this issue.")
|
Log.e(javaClass.name, "Wrong Type, please report this issue.")
|
||||||
listOf()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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)
|
||||||
|
@ -1,14 +1,17 @@
|
|||||||
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.ui.components.LoginDialog
|
import org.mosad.teapod.ui.components.LoginDialog
|
||||||
|
|
||||||
@ -29,12 +32,7 @@ class AccountFragment : Fragment() {
|
|||||||
|
|
||||||
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 +41,29 @@ 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -0,0 +1,66 @@
|
|||||||
|
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 androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
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 adapter: MediaItemAdapter
|
||||||
|
private lateinit var layoutManager: LinearLayoutManager
|
||||||
|
|
||||||
|
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 {
|
||||||
|
if (AoDParser.mediaList.isEmpty()) {
|
||||||
|
AoDParser().listAnimes()
|
||||||
|
}
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
context?.let {
|
||||||
|
layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
|
||||||
|
recycler_my_list.layoutManager = layoutManager
|
||||||
|
recycler_my_list.addItemDecoration(MediaItemDecoration(9))
|
||||||
|
|
||||||
|
updateMyListMedia()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
adapter = MediaItemAdapter(myListMedia)
|
||||||
|
adapter.onItemClick = { mediaId, _ ->
|
||||||
|
(activity as MainActivity).showMediaFragment(mediaId)
|
||||||
|
}
|
||||||
|
|
||||||
|
recycler_my_list.adapter = adapter
|
||||||
|
}
|
||||||
|
}
|
@ -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,6 +24,7 @@ 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()) {
|
if (AoDParser.mediaList.isEmpty()) {
|
||||||
AoDParser().listAnimes()
|
AoDParser().listAnimes()
|
||||||
@ -32,23 +33,16 @@ class LibraryFragment : Fragment() {
|
|||||||
// 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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
132
app/src/main/java/org/mosad/teapod/ui/fragments/MediaFragment.kt
Normal file
132
app/src/main/java/org/mosad/teapod/ui/fragments/MediaFragment.kt
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
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.util.DataTypes.MediaType
|
||||||
|
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().streamUrl)
|
||||||
|
MediaType.TVSHOW -> playStream(media.episodes.first().streamUrl)
|
||||||
|
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].streamUrl)
|
||||||
|
|
||||||
|
// update watched state
|
||||||
|
AoDParser().sendCallback(media.episodes[position].watchedCallback)
|
||||||
|
adapterRecEpisodes.updateWatchedState(true, position)
|
||||||
|
adapterRecEpisodes.notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun playStream(url: String) {
|
||||||
|
Log.d(javaClass.name, "Playing stream: $url")
|
||||||
|
(activity as MainActivity).startPlayer(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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)
|
||||||
@ -34,8 +33,14 @@ class SearchFragment : Fragment() {
|
|||||||
// 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 +51,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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
@ -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()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -8,12 +8,55 @@ 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: List<Episode> = listOf()
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Episode(
|
||||||
|
val id: Int = 0,
|
||||||
|
var title: String = "",
|
||||||
|
var streamUrl: String = "",
|
||||||
|
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
|
||||||
|
)
|
||||||
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
38
app/src/main/java/org/mosad/teapod/util/StorageController.kt
Normal file
38
app/src/main/java/org/mosad/teapod/util/StorageController.kt
Normal 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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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,42 @@ 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
|
||||||
|
//println(response)
|
||||||
|
|
||||||
|
val runtime = getStringNotNull(response,"runtime").toInt()
|
||||||
|
println(runtime)
|
||||||
|
|
||||||
|
|
||||||
|
return@async runtime
|
||||||
|
}.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 {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -0,0 +1,70 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
holder.view.text_episode_title.text = context.getString(
|
||||||
|
R.string.component_episode_title,
|
||||||
|
episodes[position].number,
|
||||||
|
episodes[position].description
|
||||||
|
)
|
||||||
|
holder.view.text_episode_desc.text = episodes[position].shortDesc
|
||||||
|
|
||||||
|
if (episodes[position].posterUrl.isNotEmpty()) {
|
||||||
|
Glide.with(context).load(episodes[position].posterUrl)
|
||||||
|
.apply(RequestOptions.bitmapTransform(RoundedCornersTransformation(10, 0)))
|
||||||
|
.into(holder.view.image_episode)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (episodes[position].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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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>
|
10
app/src/main/res/drawable/ic_baseline_add_24.xml
Normal file
10
app/src/main/res/drawable/ic_baseline_add_24.xml
Normal 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>
|
10
app/src/main/res/drawable/ic_baseline_check_24.xml
Normal file
10
app/src/main/res/drawable/ic_baseline_check_24.xml
Normal 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>
|
10
app/src/main/res/drawable/ic_baseline_check_circle_24.xml
Normal file
10
app/src/main/res/drawable/ic_baseline_check_circle_24.xml
Normal 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>
|
5
app/src/main/res/drawable/shape_rounden_corner.xml
Normal file
5
app/src/main/res/drawable/shape_rounden_corner.xml
Normal 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>
|
@ -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"
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<FrameLayout 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"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
@ -15,10 +14,12 @@
|
|||||||
android:layout_gravity="center" />
|
android:layout_gravity="center" />
|
||||||
<!-- 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>
|
@ -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>
|
|
@ -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">
|
||||||
|
|
||||||
@ -103,6 +104,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 +112,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,6 +143,23 @@
|
|||||||
</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="Licenses"
|
||||||
|
android:textColor="@android:color/primary_text_light"
|
||||||
|
android:textSize="16sp" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
@ -5,7 +5,45 @@
|
|||||||
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">
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical" >
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textView"
|
||||||
|
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>
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/text_home"
|
android:id="@+id/text_home"
|
||||||
|
@ -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>
|
17
app/src/main/res/layout/fragment_loading.xml
Normal file
17
app/src/main/res/layout/fragment_loading.xml
Normal 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>
|
@ -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">
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
<ImageView
|
<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:id="@+id/image_poster"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="200dp"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
android:layout_marginTop="20dp"
|
app:shapeAppearance="@style/ShapeAppearance.Teapod.RoundedPoster"
|
||||||
android:minHeight="200dp"
|
app:srcCompat="@drawable/ic_launcher_background" />
|
||||||
android:src="@drawable/ic_launcher_background" />
|
|
||||||
|
|
||||||
<Button
|
</FrameLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/linear_media_info"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="10dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
@ -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>
|
64
app/src/main/res/layout/item_episode.xml
Normal file
64
app/src/main/res/layout/item_episode.xml
Normal 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>
|
42
app/src/main/res/layout/item_media.xml
Normal file
42
app/src/main/res/layout/item_media.xml
Normal 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>
|
@ -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>
|
|
@ -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" />
|
||||||
|
|
||||||
|
63
app/src/main/res/raw/notices.xml
Normal file
63
app/src/main/res/raw/notices.xml
Normal 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>
|
37
app/src/main/res/values-de-rDE/strings.xml
Normal file
37
app/src/main/res/values-de-rDE/strings.xml
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<?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>
|
||||||
|
|
||||||
|
<!-- 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>
|
||||||
|
|
||||||
|
<!-- 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>
|
||||||
|
|
||||||
|
<!-- 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>
|
@ -1,24 +1,38 @@
|
|||||||
<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>
|
||||||
|
|
||||||
<!-- 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_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>
|
||||||
|
|
||||||
<!-- dialogs -->
|
<!-- dialogs -->
|
||||||
<string name="save">save</string>
|
<string name="save">save</string>
|
||||||
@ -26,7 +40,8 @@
|
|||||||
|
|
||||||
<!-- 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 -->
|
||||||
|
@ -1,18 +1,23 @@
|
|||||||
<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="AppTheme.MaterialComponents.Light.NoActionBar.FullScreen" parent="@style/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>
|
||||||
|
|
||||||
|
<!-- shapes -->
|
||||||
|
<style name="ShapeAppearance.Teapod.RoundedPoster" parent="ShapeAppearance.MaterialComponents.LargeComponent">
|
||||||
|
<item name="cornerFamily">rounded</item>
|
||||||
|
<item name="cornerSize">5dp</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
@ -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
|
||||||
|
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
@ -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
|
||||||
|
Reference in New Issue
Block a user