Compare commits
18 Commits
0.1-alpha2
...
0.1-alpha3
Author | SHA1 | Date | |
---|---|---|---|
aeb74dcb29
|
|||
2689c37af3 | |||
5458b43354
|
|||
d912ed34a3
|
|||
9f1717e646
|
|||
085b2013ab
|
|||
474b72df49
|
|||
a8dc243d0e
|
|||
fa6419bb02
|
|||
6100533c4d
|
|||
4ae23c4380
|
|||
adf8a48251
|
|||
36c8678646
|
|||
442a02db70
|
|||
5f80f1fabd
|
|||
d2728405d1
|
|||
87f9235b8a | |||
03cd42773d
|
33
README.md
33
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 terms and conditions of GPL 3. This Project is not associated with Anime-on-Demand in any 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.
|
||||||
|
|
||||||
### Used Libraries
|
### Known Issues
|
||||||
* gson: https://github.com/google/gson
|
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.
|
||||||
* exoplayer: https://github.com/google/ExoPlayer
|
|
||||||
* jsoup: https://jsoup.org/
|
|
||||||
* material-dialogs: https://github.com/afollestad/material-dialogs
|
|
||||||
* kotlin.coroutines: https://github.com/Kotlin/kotlinx.coroutines
|
|
||||||
* Material design icons: https://github.com/google/material-design-icons
|
|
||||||
* androidx libraries
|
|
||||||
|
|
||||||
Teapod © 2020 [@Seil0](https://git.mosad.xyz/Seil0)
|
### Used Libraries
|
||||||
|
* AndroidX: https://developer.android.com/jetpack/androidx
|
||||||
|
* Material Components for Android: https://github.com/material-components/material-components-android
|
||||||
|
* ExoPlayer: https://github.com/google/ExoPlayer
|
||||||
|
* Gson: https://github.com/google/gson
|
||||||
|
* Material design icons: https://github.com/google/material-design-icons
|
||||||
|
* 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)
|
||||||
|
@ -11,7 +11,7 @@ android {
|
|||||||
minSdkVersion 23
|
minSdkVersion 23
|
||||||
targetSdkVersion 30
|
targetSdkVersion 30
|
||||||
versionCode 1
|
versionCode 1
|
||||||
versionName "0.1-alpha2"
|
versionName "0.1-alpha3"
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
resValue "string", "build_time", buildTime()
|
resValue "string", "build_time", buildTime()
|
||||||
@ -46,7 +46,7 @@ dependencies {
|
|||||||
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 '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'
|
||||||
|
@ -26,42 +26,47 @@ 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.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
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()
|
||||||
@ -70,51 +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() {
|
||||||
EncryptedPreferences.readCredentials(this)
|
// running login and list in parallel does not bring any speed improvements
|
||||||
|
val time = measureTimeMillis {
|
||||||
|
// make sure credentials are set
|
||||||
|
EncryptedPreferences.readCredentials(this)
|
||||||
|
if (EncryptedPreferences.password.isEmpty()) {
|
||||||
|
showLoginDialog(true)
|
||||||
|
} else {
|
||||||
|
// try to login in, as most sites can only bee loaded once loged in
|
||||||
|
if (!AoDParser().login()) showLoginDialog(false)
|
||||||
|
}
|
||||||
|
|
||||||
// make sure credentials are set and valid
|
StorageController.load(this)
|
||||||
if (EncryptedPreferences.password.isEmpty()) {
|
|
||||||
showLoginDialog(true)
|
// initially load all media
|
||||||
} else if (!AoDParser().login()) {
|
AoDParser().listAnimes()
|
||||||
showLoginDialog(false)
|
|
||||||
|
// TODO load home screen, can be parallel to listAnimes
|
||||||
}
|
}
|
||||||
|
Log.i(javaClass.name, "login and list in $time ms")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO show loading fragment
|
* 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 showDetailFragment(media: Media) = GlobalScope.launch {
|
fun showMediaFragment(mediaId: Int) = GlobalScope.launch {
|
||||||
media.episodes = AoDParser().loadStreams(media) // load the streams for the selected media
|
val loadingFragment = LoadingFragment()
|
||||||
|
supportFragmentManager.commit {
|
||||||
|
add(R.id.nav_host_fragment, loadingFragment, "MediaFragment")
|
||||||
|
show(loadingFragment)
|
||||||
|
}
|
||||||
|
|
||||||
val tmdb = TMDBApiController().search(media.title, media.type)
|
// load the streams for the selected media
|
||||||
|
val media = AoDParser().getMediaById(mediaId)
|
||||||
|
val tmdb = TMDBApiController().search(media.info.title, media.type)
|
||||||
|
|
||||||
val mediaFragment = MediaFragment(media, tmdb)
|
val mediaFragment = MediaFragment(media, tmdb)
|
||||||
supportFragmentManager.commit {
|
supportFragmentManager.commit {
|
||||||
@ -122,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) {
|
||||||
|
@ -8,27 +8,33 @@ 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>()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun login(): Boolean = 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
|
||||||
val resAuth = Jsoup.connect(baseUrl + loginPath)
|
val resAuth = Jsoup.connect(baseUrl + loginPath)
|
||||||
@ -38,8 +44,8 @@ class AoDParser {
|
|||||||
val authenticityToken = resAuth.parse().select("meta[name=csrf-token]").attr("content")
|
val authenticityToken = resAuth.parse().select("meta[name=csrf-token]").attr("content")
|
||||||
val authCookies = resAuth.cookies()
|
val authCookies = resAuth.cookies()
|
||||||
|
|
||||||
Log.i(javaClass.name, "Received authenticity token: $authenticityToken")
|
//Log.d(javaClass.name, "Received authenticity token: $authenticityToken")
|
||||||
Log.i(javaClass.name, "Received authenticity cookies: $authCookies")
|
//Log.d(javaClass.name, "Received authenticity cookies: $authCookies")
|
||||||
|
|
||||||
val data = mapOf(
|
val data = mapOf(
|
||||||
Pair("user[login]", EncryptedPreferences.login),
|
Pair("user[login]", EncryptedPreferences.login),
|
||||||
@ -57,9 +63,9 @@ class AoDParser {
|
|||||||
.execute()
|
.execute()
|
||||||
|
|
||||||
//println(resLogin.body())
|
//println(resLogin.body())
|
||||||
|
|
||||||
sessionCookies = resLogin.cookies()
|
sessionCookies = resLogin.cookies()
|
||||||
loginSuccess = resLogin.body().contains("Hallo, du bist jetzt angemeldet.")
|
loginSuccess = resLogin.body().contains("Hallo, du bist jetzt angemeldet.")
|
||||||
|
|
||||||
Log.i(javaClass.name, "Status: ${resLogin.statusCode()} (${resLogin.statusMessage()}), login successful: $loginSuccess")
|
Log.i(javaClass.name, "Status: ${resLogin.statusCode()} (${resLogin.statusMessage()}), login successful: $loginSuccess")
|
||||||
|
|
||||||
loginSuccess
|
loginSuccess
|
||||||
@ -86,16 +92,18 @@ 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
|
||||||
)
|
info.shortDesc = mediaShortText
|
||||||
media.info.posterLink = it.select("p.animebox-image").select("img").attr("src")
|
})
|
||||||
media.info.shortDesc = it.select("p.animebox-shorttext").text()
|
|
||||||
|
|
||||||
mediaList.add(media)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.i(javaClass.name, "Total library size is: ${mediaList.size}")
|
Log.i(javaClass.name, "Total library size is: ${mediaList.size}")
|
||||||
@ -104,15 +112,26 @@ class AoDParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
Log.w(javaClass.name, "Login, was not successful.")
|
Log.w(javaClass.name, "Login, was not successful.")
|
||||||
return@runBlocking listOf()
|
return@runBlocking
|
||||||
}
|
}
|
||||||
|
|
||||||
withContext(Dispatchers.Default) {
|
withContext(Dispatchers.Default) {
|
||||||
@ -124,50 +143,59 @@ class AoDParser {
|
|||||||
//println(res)
|
//println(res)
|
||||||
|
|
||||||
// parse additional info from the media page
|
// parse additional info from the media page
|
||||||
res.select("table.vertical-table").select("tr").forEach {
|
res.select("table.vertical-table").select("tr").forEach { row ->
|
||||||
when (it.select("th").text().toLowerCase(Locale.ROOT)) {
|
when (row.select("th").text().toLowerCase(Locale.ROOT)) {
|
||||||
"produktionsjahr" -> media.info.year = it.select("td").text().toInt()
|
"produktionsjahr" -> media.info.year = row.select("td").text().toInt()
|
||||||
"fsk" -> media.info.age = it.select("td").text().toInt()
|
"fsk" -> media.info.age = row.select("td").text().toInt()
|
||||||
"episodenanzahl" -> media.info.episodesCount = it.select("td").text().toInt()
|
"episodenanzahl" -> {
|
||||||
|
media.info.episodesCount = row.select("td").text()
|
||||||
|
.substringBefore("/")
|
||||||
|
.filter{ it.isDigit() }
|
||||||
|
.toInt()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// parse additional information for tv shows
|
||||||
* TODO tv show specific for each episode (div.episodebox)
|
media.episodes = when (media.type) {
|
||||||
* * watchedCallback
|
MediaType.MOVIE -> listOf(Episode())
|
||||||
*/
|
MediaType.TVSHOW -> {
|
||||||
val episodes = if (media.type == MediaType.TVSHOW) {
|
res.select("div.three-box-container > div.episodebox").map { episodebox ->
|
||||||
res.select("div.three-box-container > div.episodebox").map { episodebox ->
|
val episodeId = episodebox.select("div.flip-front").attr("id").substringAfter("-").toInt()
|
||||||
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 episodeWatched = episodebox.select("div.episodebox-icons > div").hasClass("status-icon-orange")
|
||||||
val episodeShortDesc = episodebox.select("p.episodebox-shorttext").text()
|
val episodeWatchedCallback = episodebox.select("input.streamstarter_html5").eachAttr("data-playlist").first()
|
||||||
|
|
||||||
Episode(id = episodeId, watched = episodeWatched, shortDesc = episodeShortDesc)
|
Episode(
|
||||||
|
id = episodeId,
|
||||||
|
shortDesc = episodeShortDesc,
|
||||||
|
watched = episodeWatched,
|
||||||
|
watchedCallback = episodeWatchedCallback
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
MediaType.OTHER -> listOf()
|
||||||
listOf(Episode())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// has attr data-lag (ger or jap)
|
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 if (playlists.size > 0) {
|
|
||||||
loadStreamInfo(playlists.first(), csrfToken, media.type, episodes)
|
|
||||||
} else {
|
|
||||||
listOf()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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, additionally it is passed a return value
|
* @param episodes is used as call ba reference
|
||||||
*/
|
*/
|
||||||
private fun loadStreamInfo(playlistPath: String, csrfToken: String, type: MediaType, episodes: List<Episode>): 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"),
|
||||||
@ -177,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)
|
||||||
@ -189,9 +219,10 @@ class AoDParser {
|
|||||||
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().forEach {
|
movie.get("sources").asJsonArray.first().apply {
|
||||||
episodes.first().streamUrl = it.asJsonObject.get("file").asString
|
episodes.first().streamUrl = this.asJsonObject.get("file").asString
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -199,7 +230,6 @@ class AoDParser {
|
|||||||
val episodesJson = JsonParser.parseString(res.body()).asJsonObject
|
val episodesJson = JsonParser.parseString(res.body()).asJsonObject
|
||||||
.get("playlist").asJsonArray
|
.get("playlist").asJsonArray
|
||||||
|
|
||||||
|
|
||||||
episodesJson.forEach { jsonElement ->
|
episodesJson.forEach { jsonElement ->
|
||||||
val episodeId = jsonElement.asJsonObject.get("mediaid")
|
val episodeId = jsonElement.asJsonObject.get("mediaid")
|
||||||
val episodeStream = jsonElement.asJsonObject.get("sources").asJsonArray
|
val episodeStream = jsonElement.asJsonObject.get("sources").asJsonArray
|
||||||
@ -212,7 +242,7 @@ class AoDParser {
|
|||||||
|
|
||||||
episodes.first { it.id == episodeId.asInt }.apply {
|
episodes.first { it.id == episodeId.asInt }.apply {
|
||||||
this.title = episodeTitle
|
this.title = episodeTitle
|
||||||
this.posterLink = episodePoster
|
this.posterUrl = episodePoster
|
||||||
this.streamUrl = episodeStream
|
this.streamUrl = episodeStream
|
||||||
this.description = episodeDescription
|
this.description = episodeDescription
|
||||||
this.number = episodeNumber
|
this.number = episodeNumber
|
||||||
@ -225,9 +255,30 @@ class AoDParser {
|
|||||||
Log.e(javaClass.name, "Wrong Type, please report this issue.")
|
Log.e(javaClass.name, "Wrong Type, please report this issue.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return@withContext episodes
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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,4 +1,4 @@
|
|||||||
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.util.Log
|
||||||
@ -6,7 +6,7 @@ import android.view.LayoutInflater
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
import de.psdev.licensesdialog.LicensesDialog
|
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
|
||||||
@ -36,9 +36,9 @@ class AccountFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
linear_about.setOnClickListener {
|
linear_about.setOnClickListener {
|
||||||
MaterialAlertDialogBuilder(requireContext())
|
MaterialDialog(requireContext())
|
||||||
.setTitle(R.string.info_about)
|
.title(R.string.info_about)
|
||||||
.setMessage(R.string.info_about_dialog)
|
.message(R.string.info_about_dialog)
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,13 +1,13 @@
|
|||||||
package org.mosad.teapod.ui
|
package org.mosad.teapod.ui.fragments
|
||||||
|
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.graphics.drawable.ColorDrawable
|
import android.graphics.drawable.ColorDrawable
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
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.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
@ -16,14 +16,16 @@ import jp.wasabeef.glide.transformations.BlurTransformation
|
|||||||
import kotlinx.android.synthetic.main.fragment_media.*
|
import kotlinx.android.synthetic.main.fragment_media.*
|
||||||
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.util.DataTypes.MediaType
|
import org.mosad.teapod.util.DataTypes.MediaType
|
||||||
import org.mosad.teapod.util.EpisodesAdapter
|
|
||||||
import org.mosad.teapod.util.Media
|
import org.mosad.teapod.util.Media
|
||||||
|
import org.mosad.teapod.util.StorageController
|
||||||
import org.mosad.teapod.util.TMDBResponse
|
import org.mosad.teapod.util.TMDBResponse
|
||||||
|
import org.mosad.teapod.util.adapter.EpisodeItemAdapter
|
||||||
|
|
||||||
class MediaFragment(private val media: Media, private val tmdb: TMDBResponse) : Fragment() {
|
class MediaFragment(private val media: Media, private val tmdb: TMDBResponse) : Fragment() {
|
||||||
|
|
||||||
private lateinit var adapterRecEpisodes: EpisodesAdapter
|
private lateinit var adapterRecEpisodes: EpisodeItemAdapter
|
||||||
private lateinit var viewManager: RecyclerView.LayoutManager
|
private lateinit var viewManager: RecyclerView.LayoutManager
|
||||||
|
|
||||||
|
|
||||||
@ -43,25 +45,30 @@ class MediaFragment(private val media: Media, private val tmdb: TMDBResponse) :
|
|||||||
*/
|
*/
|
||||||
private fun initGUI() {
|
private fun initGUI() {
|
||||||
// generic gui
|
// generic gui
|
||||||
val backdropUrl = if (tmdb.backdropUrl.isNotEmpty()) tmdb.backdropUrl else media.info.posterLink
|
val backdropUrl = if (tmdb.backdropUrl.isNotEmpty()) tmdb.backdropUrl else media.info.posterUrl
|
||||||
val posterUrl = if (tmdb.posterUrl.isNotEmpty()) tmdb.posterUrl else media.info.posterLink
|
val posterUrl = if (tmdb.posterUrl.isNotEmpty()) tmdb.posterUrl else media.info.posterUrl
|
||||||
|
|
||||||
Glide.with(requireContext()).load(backdropUrl)
|
Glide.with(requireContext()).load(backdropUrl)
|
||||||
.apply(RequestOptions.placeholderOf(ColorDrawable(Color.DKGRAY)))
|
.apply(RequestOptions.placeholderOf(ColorDrawable(Color.DKGRAY)))
|
||||||
.apply(RequestOptions.bitmapTransform(BlurTransformation(25, 3)))
|
.apply(RequestOptions.bitmapTransform(BlurTransformation(20, 3)))
|
||||||
.into(image_backdrop)
|
.into(image_backdrop)
|
||||||
|
|
||||||
Glide.with(requireContext()).load(posterUrl)
|
Glide.with(requireContext()).load(posterUrl)
|
||||||
.into(image_poster)
|
.into(image_poster)
|
||||||
|
|
||||||
text_title.text = media.title
|
text_title.text = media.info.title
|
||||||
text_year.text = media.info.year.toString()
|
text_year.text = media.info.year.toString()
|
||||||
text_age.text = media.info.age.toString()
|
text_age.text = media.info.age.toString()
|
||||||
text_overview.text = media.info.shortDesc //if (tmdb.overview.isNotEmpty()) tmdb.overview else media.shortDesc
|
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
|
// specific gui
|
||||||
if (media.type == MediaType.TVSHOW) {
|
if (media.type == MediaType.TVSHOW) {
|
||||||
adapterRecEpisodes = EpisodesAdapter(media.episodes, requireContext())
|
adapterRecEpisodes = EpisodeItemAdapter(media.episodes)
|
||||||
viewManager = LinearLayoutManager(context)
|
viewManager = LinearLayoutManager(context)
|
||||||
recycler_episodes.layoutManager = viewManager
|
recycler_episodes.layoutManager = viewManager
|
||||||
recycler_episodes.adapter = adapterRecEpisodes
|
recycler_episodes.adapter = adapterRecEpisodes
|
||||||
@ -87,17 +94,39 @@ class MediaFragment(private val media: Media, private val tmdb: TMDBResponse) :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
// set onItemClick only in adapter is initialized
|
||||||
if (this::adapterRecEpisodes.isInitialized) {
|
if (this::adapterRecEpisodes.isInitialized) {
|
||||||
adapterRecEpisodes.onItemClick = { _, position ->
|
adapterRecEpisodes.onImageClick = { _, position ->
|
||||||
playStream(media.episodes[position].streamUrl)
|
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) {
|
private fun playStream(url: String) {
|
||||||
val mainActivity = activity as MainActivity
|
Log.d(javaClass.name, "Playing stream: $url")
|
||||||
mainActivity.startPlayer(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,12 +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 var adapter : CustomAdapter? = null
|
private var adapter : MediaItemAdapter? = null
|
||||||
|
|
||||||
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)
|
||||||
@ -33,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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -56,16 +62,5 @@ class SearchFragment : Fragment() {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
list_search.setOnItemClickListener { _, _, position, _ ->
|
|
||||||
search_text.clearFocus() // remove focus from the SearchView
|
|
||||||
|
|
||||||
runBlocking {
|
|
||||||
val media = adapter?.getItem(position) as Media
|
|
||||||
println("selected item is: ${media.title}")
|
|
||||||
|
|
||||||
(activity as MainActivity).showDetailFragment(media).join()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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].info.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,14 +8,31 @@ class DataTypes {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Media(val title: String, val link: String, val type: DataTypes.MediaType, val info : Info = Info(), 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
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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(
|
data class Info(
|
||||||
var posterLink: String = "",
|
var title: String = "",
|
||||||
|
var posterUrl: String = "",
|
||||||
var shortDesc: String = "",
|
var shortDesc: String = "",
|
||||||
var description: String = "",
|
var description: String = "",
|
||||||
var year: Int = 0,
|
var year: Int = 0,
|
||||||
@ -27,11 +44,12 @@ data class Episode(
|
|||||||
val id: Int = 0,
|
val id: Int = 0,
|
||||||
var title: String = "",
|
var title: String = "",
|
||||||
var streamUrl: String = "",
|
var streamUrl: String = "",
|
||||||
var posterLink: String = "",
|
var posterUrl: String = "",
|
||||||
var description: String = "",
|
var description: String = "",
|
||||||
var shortDesc: String = "",
|
var shortDesc: String = "",
|
||||||
var number: Int = 0,
|
var number: Int = 0,
|
||||||
var watched: Boolean = false
|
var watched: Boolean = false,
|
||||||
|
var watchedCallback: String = ""
|
||||||
)
|
)
|
||||||
|
|
||||||
data class TMDBResponse(
|
data class TMDBResponse(
|
||||||
|
@ -1,50 +0,0 @@
|
|||||||
package org.mosad.teapod.util
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import com.bumptech.glide.Glide
|
|
||||||
import kotlinx.android.synthetic.main.component_episode.view.*
|
|
||||||
import org.mosad.teapod.R
|
|
||||||
|
|
||||||
class EpisodesAdapter(private val episodes: List<Episode>, private val context: Context) : 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 = 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].posterLink.isNotEmpty()) {
|
|
||||||
Glide.with(context).load(episodes[position].posterLink).into(holder.view.image_episode)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!episodes[position].watched) {
|
|
||||||
holder.view.image_watched.setImageDrawable(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemCount(): Int {
|
|
||||||
return episodes.size
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class MyViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
|
|
||||||
init {
|
|
||||||
view.setOnClickListener {
|
|
||||||
onItemClick?.invoke(episodes[adapterPosition].title, 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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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>
|
@ -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>
|
@ -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"
|
||||||
|
@ -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"
|
||||||
@ -30,13 +30,13 @@
|
|||||||
android:minHeight="220dp"
|
android:minHeight="220dp"
|
||||||
android:scaleType="centerCrop" />
|
android:scaleType="centerCrop" />
|
||||||
|
|
||||||
<ImageView
|
<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:minHeight="200dp"
|
app:shapeAppearance="@style/ShapeAppearance.Teapod.RoundedPoster"
|
||||||
android:src="@drawable/ic_launcher_background" />
|
app:srcCompat="@drawable/ic_launcher_background" />
|
||||||
|
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|
||||||
@ -113,6 +113,37 @@
|
|||||||
android:layout_marginEnd="12dp"
|
android:layout_marginEnd="12dp"
|
||||||
android:text="@string/text_overview_ex" />
|
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"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@ -120,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>
|
@ -14,12 +14,28 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="horizontal">
|
android:orientation="horizontal">
|
||||||
|
|
||||||
<ImageView
|
<FrameLayout
|
||||||
android:id="@+id/image_episode"
|
android:layout_width="wrap_content"
|
||||||
android:layout_width="128dp"
|
android:layout_height="wrap_content">
|
||||||
android:layout_height="72dp"
|
|
||||||
android:contentDescription="@string/component_poster_desc"
|
<com.google.android.material.imageview.ShapeableImageView
|
||||||
app:srcCompat="@drawable/ic_baseline_account_box_24" />
|
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
|
<TextView
|
||||||
android:id="@+id/text_episode_title"
|
android:id="@+id/text_episode_title"
|
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" />
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
</notice>
|
</notice>
|
||||||
<notice>
|
<notice>
|
||||||
<name>ExoPlayer</name>
|
<name>ExoPlayer</name>
|
||||||
<url>https://github.com/material-components/material-components-android</url>
|
<url>https://github.com/google/ExoPlayer</url>
|
||||||
<copyright>Copyright The Android Open Source Project</copyright>
|
<copyright>Copyright The Android Open Source Project</copyright>
|
||||||
<license>Apache Software License 2.0</license>
|
<license>Apache Software License 2.0</license>
|
||||||
</notice>
|
</notice>
|
||||||
|
@ -5,6 +5,9 @@
|
|||||||
<string name="title_search">Suche</string>
|
<string name="title_search">Suche</string>
|
||||||
<string name="title_account">Account</string>
|
<string name="title_account">Account</string>
|
||||||
|
|
||||||
|
<!-- home fragment -->
|
||||||
|
<string name="my_list">Meine Liste</string>
|
||||||
|
|
||||||
<!-- search fragment -->
|
<!-- search fragment -->
|
||||||
<string name="search_hint">Suche nach Filmen und Serien</string>
|
<string name="search_hint">Suche nach Filmen und Serien</string>
|
||||||
|
|
||||||
@ -24,7 +27,7 @@
|
|||||||
|
|
||||||
<!-- dialogs -->
|
<!-- dialogs -->
|
||||||
<string name="save">speichern</string>
|
<string name="save">speichern</string>
|
||||||
<string name="cancel">\@android:string/cancel</string>
|
<string name="cancel">@android:string/cancel</string>
|
||||||
|
|
||||||
<!-- etc -->
|
<!-- etc -->
|
||||||
<string name="login">Login</string>
|
<string name="login">Login</string>
|
||||||
|
@ -5,8 +5,12 @@
|
|||||||
<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>
|
||||||
|
@ -14,5 +14,10 @@
|
|||||||
<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>
|
Reference in New Issue
Block a user