Compare commits
40 Commits
0.1-alpha1
...
0.1.0
Author | SHA1 | Date | |
---|---|---|---|
a51f4ca490 | |||
4ec5d0fdc4
|
|||
8a516c640d
|
|||
49430e10bf
|
|||
81b041ab61
|
|||
cf6a110455
|
|||
c138ab4587
|
|||
f0ed6aa379
|
|||
a5fffd5d02
|
|||
ff0727da22
|
|||
ce84cb57a8
|
|||
4c274eb062
|
|||
a25ec81f6b
|
|||
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 |
38
README.md
@ -6,22 +6,36 @@ A unoffical App for Anime-on-Demand.
|
|||||||
* acces all media in the library
|
* acces all media in the library
|
||||||
* search the library
|
* search the library
|
||||||
* play movies/tv shows via integrated exoplayer
|
* play movies/tv shows via integrated exoplayer
|
||||||
|
* add movies/tv shows to you list, for easier access
|
||||||
|
|
||||||
|
### Missing Features
|
||||||
|
* a alternative/secondary stream is currently not supported (for dub titles the subtitle version is missing)
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
[<img src="https://www.mosad.xyz/images/Teapod/Teapod_Library.png" width=180>](https://www.mosad.xyz/images/Teapod/Teapod_Library.png)
|
[<img src="https://www.mosad.xyz/images/Teapod/Teapod_Home_200px.png" width=180>](https://www.mosad.xyz/images/Teapod/Teapod_Home.png)
|
||||||
[<img src="https://www.mosad.xyz/images/Teapod/Teapod_Media.png" width=180>](https://www.mosad.xyz/images/Teapod/Teapod_Media.png)
|
[<img src="https://www.mosad.xyz/images/Teapod/Teapod_Library_200px.png" width=180>](https://www.mosad.xyz/images/Teapod/Teapod_Library.png)
|
||||||
[<img src="https://www.mosad.xyz/images/Teapod/Teapod_Search.png" width=180>](https://www.mosad.xyz/images/Teapod/Teapod_Search.png)
|
[<img src="https://www.mosad.xyz/images/Teapod/Teapod_Media_200px.png" width=180>](https://www.mosad.xyz/images/Teapod/Teapod_Media.png)
|
||||||
|
[<img src="https://www.mosad.xyz/images/Teapod/Teapod_Search_200px.png" width=180>](https://www.mosad.xyz/images/Teapod/Teapod_Search.png)
|
||||||
|
|
||||||
## License
|
### License
|
||||||
This App is licensed under the treams and conditions of GPL3. This Project is not accosiated with Anime-on-Demand in anya way.
|
This App is licensed under the terms and conditions of GPL 3. This Project is not associated with Anime-on-Demand in any way.
|
||||||
|
|
||||||
|
### Known Issues
|
||||||
|
If a tv show is selected, the first episode will be marked as already watched. This is due to the difficulties of parsing. The Parser is designed to be as easy to maintain and fail safe as possible.
|
||||||
|
|
||||||
### Used Libraries
|
### Used Libraries
|
||||||
* gson: https://github.com/google/gson
|
* AndroidX: https://developer.android.com/jetpack/androidx
|
||||||
* exoplayer: https://github.com/google/ExoPlayer
|
* Material Components for Android: https://github.com/material-components/material-components-android
|
||||||
* jsoup: https://jsoup.org/
|
* ExoPlayer: https://github.com/google/ExoPlayer
|
||||||
* material-dialogs: https://github.com/afollestad/material-dialogs
|
* Gson: https://github.com/google/gson
|
||||||
* kotlin.coroutines: https://github.com/Kotlin/kotlinx.coroutines
|
|
||||||
* Material design icons: https://github.com/google/material-design-icons
|
* Material design icons: https://github.com/google/material-design-icons
|
||||||
* androidx libraries
|
* Material Dialogs: https://github.com/afollestad/material-dialogs
|
||||||
|
* Jsoup: https://jsoup.org
|
||||||
|
* kotlinx.coroutines: https://github.com/Kotlin/kotlinx.coroutines
|
||||||
|
* Glide: https://github.com/bumptech/glide
|
||||||
|
* Glide Transformations: https://github.com/wasabeef/glide-transformations
|
||||||
|
|
||||||
Teapod © 2020 [@Seil0](https://git.mosad.xyz/Seil0)
|
#### Why is it called Teapod?
|
||||||
|
Teapod is a Acronym for "The ultimate anime app on demand", hence this project is called Teapod and not Teapot.
|
||||||
|
|
||||||
|
Teapod © 2020 [@Seil0](https://git.mosad.xyz/Seil0)
|
||||||
|
@ -10,8 +10,8 @@ android {
|
|||||||
applicationId "org.mosad.teapod"
|
applicationId "org.mosad.teapod"
|
||||||
minSdkVersion 23
|
minSdkVersion 23
|
||||||
targetSdkVersion 30
|
targetSdkVersion 30
|
||||||
versionCode 1
|
versionCode 1000 //00.01.0000
|
||||||
versionName "0.1-alpha1"
|
versionName "0.1.0"
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
resValue "string", "build_time", buildTime()
|
resValue "string", "build_time", buildTime()
|
||||||
@ -20,7 +20,8 @@ android {
|
|||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
minifyEnabled false
|
minifyEnabled true
|
||||||
|
shrinkResources true
|
||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -40,25 +41,25 @@ dependencies {
|
|||||||
|
|
||||||
implementation 'androidx.core:core-ktx:1.3.2'
|
implementation 'androidx.core:core-ktx:1.3.2'
|
||||||
implementation 'androidx.appcompat:appcompat:1.2.0'
|
implementation 'androidx.appcompat:appcompat:1.2.0'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.2'
|
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
||||||
implementation 'androidx.navigation:navigation-fragment:2.3.0'
|
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.1'
|
||||||
implementation 'androidx.navigation:navigation-ui:2.3.0'
|
implementation 'androidx.navigation:navigation-ui-ktx:2.3.1'
|
||||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.0'
|
|
||||||
implementation 'androidx.navigation:navigation-ui-ktx:2.3.0'
|
|
||||||
implementation 'androidx.security:security-crypto:1.1.0-alpha02'
|
implementation 'androidx.security:security-crypto:1.1.0-alpha02'
|
||||||
|
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||||
|
|
||||||
implementation 'com.google.android.material:material:1.2.1'
|
implementation 'com.google.android.material:material:1.3.0-alpha03'
|
||||||
implementation 'com.google.code.gson:gson:2.8.6'
|
implementation 'com.google.code.gson:gson:2.8.6'
|
||||||
implementation 'com.google.android.exoplayer:exoplayer-core:2.12.0'
|
implementation 'com.google.android.exoplayer:exoplayer-core:2.12.1'
|
||||||
implementation 'com.google.android.exoplayer:exoplayer-hls:2.12.0'
|
implementation 'com.google.android.exoplayer:exoplayer-hls:2.12.1'
|
||||||
implementation 'com.google.android.exoplayer:exoplayer-dash:2.12.0'
|
implementation 'com.google.android.exoplayer:exoplayer-dash:2.12.1'
|
||||||
implementation 'com.google.android.exoplayer:exoplayer-ui:2.12.0'
|
implementation 'com.google.android.exoplayer:exoplayer-ui:2.12.1'
|
||||||
|
|
||||||
implementation 'org.jsoup:jsoup:1.13.1'
|
implementation 'org.jsoup:jsoup:1.13.1'
|
||||||
implementation 'com.github.bumptech.glide:glide:4.11.0'
|
implementation 'com.github.bumptech.glide:glide:4.11.0'
|
||||||
|
implementation 'jp.wasabeef:glide-transformations:4.3.0'
|
||||||
implementation 'com.afollestad.material-dialogs:core:3.3.0'
|
implementation 'com.afollestad.material-dialogs:core:3.3.0'
|
||||||
implementation 'com.afollestad.material-dialogs:bottomsheets:3.3.0'
|
implementation 'com.afollestad.material-dialogs:bottomsheets:3.3.0'
|
||||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
implementation 'de.psdev.licensesdialog:licensesdialog:2.1.0'
|
||||||
|
|
||||||
testImplementation 'junit:junit:4.12'
|
testImplementation 'junit:junit:4.12'
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
|
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
|
||||||
|
12
app/proguard-rules.pro
vendored
@ -15,7 +15,17 @@
|
|||||||
# Uncomment this to preserve the line number information for
|
# Uncomment this to preserve the line number information for
|
||||||
# debugging stack traces.
|
# debugging stack traces.
|
||||||
#-keepattributes SourceFile,LineNumberTable
|
#-keepattributes SourceFile,LineNumberTable
|
||||||
|
-dontobfuscate
|
||||||
|
|
||||||
# If you keep the line number information, uncomment this to
|
# If you keep the line number information, uncomment this to
|
||||||
# hide the original source file name.
|
# hide the original source file name.
|
||||||
#-renamesourcefileattribute SourceFile
|
#-renamesourcefileattribute SourceFile
|
||||||
|
-keep class org.mosad.teapod.util.** { <fields>; }
|
||||||
|
|
||||||
|
#Gson
|
||||||
|
-keepattributes Signature
|
||||||
|
-dontwarn sun.misc.**
|
||||||
|
|
||||||
|
#misc
|
||||||
|
-dontwarn java.lang.instrument.ClassFileTransformer
|
||||||
|
-dontwarn java.lang.ClassValue
|
||||||
|
@ -11,20 +11,25 @@
|
|||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/AppTheme">
|
android:theme="@style/AppTheme">
|
||||||
|
<activity
|
||||||
|
android:name=".SplashActivity"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:theme="@style/SplashTheme"
|
||||||
|
android:screenOrientation="portrait">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".PlayerActivity"
|
android:name=".PlayerActivity"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:configChanges="orientation|screenSize|layoutDirection"
|
android:theme="@style/PlayerTheme"
|
||||||
android:theme="@style/AppTheme.AppCompat.Light.NoActionBar.FullScreen" />
|
android:configChanges="orientation|screenSize|layoutDirection" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:screenOrientation="portrait">
|
android:screenOrientation="portrait">
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.MAIN" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
</activity>
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
|
BIN
app/src/main/ic_launcher-playstore.png
Normal file
After Width: | Height: | Size: 13 KiB |
@ -1,43 +1,68 @@
|
|||||||
|
/**
|
||||||
|
* Teapod
|
||||||
|
*
|
||||||
|
* Copyright 2020 <seil0@mosad.xyz>
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation; either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||||
|
* MA 02110-1301, USA.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
package org.mosad.teapod
|
package org.mosad.teapod
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import com.google.android.material.bottomnavigation.BottomNavigationView
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.commit
|
import androidx.fragment.app.commit
|
||||||
|
import com.google.android.material.bottomnavigation.BottomNavigationView
|
||||||
import kotlinx.android.synthetic.main.activity_main.*
|
import kotlinx.android.synthetic.main.activity_main.*
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import org.mosad.teapod.parser.AoDParser
|
import org.mosad.teapod.parser.AoDParser
|
||||||
import org.mosad.teapod.preferences.EncryptedPreferences
|
import org.mosad.teapod.preferences.EncryptedPreferences
|
||||||
import org.mosad.teapod.ui.MediaFragment
|
import org.mosad.teapod.preferences.Preferences
|
||||||
import org.mosad.teapod.ui.account.AccountFragment
|
|
||||||
import org.mosad.teapod.ui.components.LoginDialog
|
import org.mosad.teapod.ui.components.LoginDialog
|
||||||
import org.mosad.teapod.ui.home.HomeFragment
|
import org.mosad.teapod.ui.fragments.*
|
||||||
import org.mosad.teapod.ui.library.LibraryFragment
|
import org.mosad.teapod.util.StorageController
|
||||||
import org.mosad.teapod.ui.search.SearchFragment
|
|
||||||
import org.mosad.teapod.util.Media
|
|
||||||
import org.mosad.teapod.util.TMDBApiController
|
import org.mosad.teapod.util.TMDBApiController
|
||||||
|
import kotlin.system.measureTimeMillis
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemSelectedListener {
|
class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemSelectedListener {
|
||||||
|
|
||||||
private var activeFragment: Fragment = HomeFragment() // the currently active fragment, home at the start
|
private var activeBaseFragment: Fragment = HomeFragment() // the currently active fragment, home at the start
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_main)
|
setContentView(R.layout.activity_main)
|
||||||
val navView: BottomNavigationView = findViewById(R.id.nav_view)
|
nav_view.setOnNavigationItemSelectedListener(this)
|
||||||
navView.setOnNavigationItemSelectedListener(this)
|
|
||||||
|
|
||||||
load()
|
load()
|
||||||
|
|
||||||
|
supportFragmentManager.commit {
|
||||||
|
replace(R.id.nav_host_fragment, activeBaseFragment, activeBaseFragment.javaClass.simpleName)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBackPressed() {
|
override fun onBackPressed() {
|
||||||
if (supportFragmentManager.backStackEntryCount > 0) {
|
if (supportFragmentManager.backStackEntryCount > 0) {
|
||||||
supportFragmentManager.popBackStack()
|
supportFragmentManager.popBackStack()
|
||||||
} else {
|
} else {
|
||||||
if (activeFragment !is HomeFragment) {
|
if (activeBaseFragment !is HomeFragment) {
|
||||||
nav_view.selectedItemId = R.id.navigation_home
|
nav_view.selectedItemId = R.id.navigation_home
|
||||||
} else {
|
} else {
|
||||||
super.onBackPressed()
|
super.onBackPressed()
|
||||||
@ -46,52 +71,73 @@ class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemS
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onNavigationItemSelected(item: MenuItem): Boolean {
|
override fun onNavigationItemSelected(item: MenuItem): Boolean {
|
||||||
|
if (supportFragmentManager.backStackEntryCount > 0) {
|
||||||
|
supportFragmentManager.popBackStack()
|
||||||
|
}
|
||||||
|
|
||||||
val ret = when (item.itemId) {
|
val ret = when (item.itemId) {
|
||||||
R.id.navigation_home -> {
|
R.id.navigation_home -> {
|
||||||
activeFragment = HomeFragment()
|
activeBaseFragment = HomeFragment()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
R.id.navigation_library -> {
|
R.id.navigation_library -> {
|
||||||
activeFragment = LibraryFragment()
|
activeBaseFragment = LibraryFragment()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
R.id.navigation_search -> {
|
R.id.navigation_search -> {
|
||||||
activeFragment = SearchFragment()
|
activeBaseFragment = SearchFragment()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
R.id.navigation_account -> {
|
R.id.navigation_account -> {
|
||||||
activeFragment = AccountFragment()
|
activeBaseFragment = AccountFragment()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
|
|
||||||
supportFragmentManager.commit {
|
supportFragmentManager.commit {
|
||||||
replace(R.id.nav_host_fragment, activeFragment)
|
replace(R.id.nav_host_fragment, activeBaseFragment, activeBaseFragment.javaClass.simpleName)
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun load() {
|
private fun load() {
|
||||||
EncryptedPreferences.readCredentials(this)
|
// running login and list in parallel does not bring any speed improvements
|
||||||
|
val time = measureTimeMillis {
|
||||||
|
Preferences.load(this)
|
||||||
|
|
||||||
if (EncryptedPreferences.password.isEmpty()) {
|
// make sure credentials are set
|
||||||
Log.i(javaClass.name, "please login!")
|
EncryptedPreferences.readCredentials(this)
|
||||||
|
if (EncryptedPreferences.password.isEmpty()) {
|
||||||
|
showLoginDialog(true)
|
||||||
|
} else {
|
||||||
|
// try to login in, as most sites can only bee loaded once loged in
|
||||||
|
if (!AoDParser.login()) showLoginDialog(false)
|
||||||
|
}
|
||||||
|
|
||||||
LoginDialog(this).positiveButton {
|
StorageController.load(this)
|
||||||
EncryptedPreferences.saveCredentials(login, password, context)
|
AoDParser.initialLoading()
|
||||||
}.negativeButton {
|
|
||||||
Log.i(javaClass.name, "Login canceled, exiting.")
|
|
||||||
finish()
|
|
||||||
}.show()
|
|
||||||
}
|
}
|
||||||
|
Log.i(javaClass.name, "login and list in $time ms")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showDetailFragment(media: Media) {
|
/**
|
||||||
media.episodes = AoDParser().loadStreams(media) // load the streams for the selected media
|
* Show the media fragment for the selected media.
|
||||||
|
* While loading show the loading fragment.
|
||||||
|
* The loading and media fragment are not stored in activeBaseFragment,
|
||||||
|
* as the don't replace a fragment but are added on top of one.
|
||||||
|
*/
|
||||||
|
fun showMediaFragment(mediaId: Int) = GlobalScope.launch {
|
||||||
|
val loadingFragment = LoadingFragment()
|
||||||
|
supportFragmentManager.commit {
|
||||||
|
add(R.id.nav_host_fragment, loadingFragment, "MediaFragment")
|
||||||
|
show(loadingFragment)
|
||||||
|
}
|
||||||
|
|
||||||
val tmdb = TMDBApiController().search(media.title, media.type)
|
// load the streams for the selected media
|
||||||
|
val media = AoDParser.getMediaById(mediaId)
|
||||||
|
val tmdb = TMDBApiController().search(media.info.title, media.type)
|
||||||
|
|
||||||
val mediaFragment = MediaFragment(media, tmdb)
|
val mediaFragment = MediaFragment(media, tmdb)
|
||||||
supportFragmentManager.commit {
|
supportFragmentManager.commit {
|
||||||
@ -99,6 +145,10 @@ class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemS
|
|||||||
addToBackStack(null)
|
addToBackStack(null)
|
||||||
show(mediaFragment)
|
show(mediaFragment)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
supportFragmentManager.commit {
|
||||||
|
remove(loadingFragment)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startPlayer(streamUrl: String) {
|
fun startPlayer(streamUrl: String) {
|
||||||
@ -107,4 +157,18 @@ class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemS
|
|||||||
}
|
}
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showLoginDialog(firstTry: Boolean) {
|
||||||
|
LoginDialog(this, firstTry).positiveButton {
|
||||||
|
EncryptedPreferences.saveCredentials(login, password, context)
|
||||||
|
|
||||||
|
if (!AoDParser.login()) {
|
||||||
|
showLoginDialog(false)
|
||||||
|
Log.w(javaClass.name, "Login failed, please try again.")
|
||||||
|
}
|
||||||
|
}.negativeButton {
|
||||||
|
Log.i(javaClass.name, "Login canceled, exiting.")
|
||||||
|
finish()
|
||||||
|
}.show()
|
||||||
|
}
|
||||||
}
|
}
|
17
app/src/main/java/org/mosad/teapod/SplashActivity.kt
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package org.mosad.teapod
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
|
||||||
|
|
||||||
|
class SplashActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
val intent = Intent(this, MainActivity::class.java)
|
||||||
|
startActivity(intent)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,25 @@
|
|||||||
|
/**
|
||||||
|
* Teapod
|
||||||
|
*
|
||||||
|
* Copyright 2020 <seil0@mosad.xyz>
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation; either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||||
|
* MA 02110-1301, USA.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
package org.mosad.teapod.parser
|
package org.mosad.teapod.parser
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@ -6,28 +28,28 @@ import kotlinx.coroutines.*
|
|||||||
import org.jsoup.Connection
|
import org.jsoup.Connection
|
||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
import org.mosad.teapod.preferences.EncryptedPreferences
|
import org.mosad.teapod.preferences.EncryptedPreferences
|
||||||
|
import org.mosad.teapod.util.*
|
||||||
import org.mosad.teapod.util.DataTypes.MediaType
|
import org.mosad.teapod.util.DataTypes.MediaType
|
||||||
import org.mosad.teapod.util.Episode
|
import java.io.IOException
|
||||||
import org.mosad.teapod.util.Media
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.ArrayList
|
|
||||||
|
|
||||||
class AoDParser {
|
object AoDParser {
|
||||||
|
|
||||||
private val baseUrl = "https://www.anime-on-demand.de"
|
private const val baseUrl = "https://www.anime-on-demand.de"
|
||||||
private val loginPath = "/users/sign_in"
|
private const val loginPath = "/users/sign_in"
|
||||||
private val libraryPath = "/animes"
|
private const val libraryPath = "/animes"
|
||||||
|
|
||||||
companion object {
|
private const val userAgent = "Mozilla/5.0 (X11; Linux x86_64; rv:80.0) Gecko/20100101 Firefox/80.0"
|
||||||
private var sessionCookies = mutableMapOf<String, String>()
|
|
||||||
private var loginSuccess = false
|
|
||||||
|
|
||||||
val mediaList = arrayListOf<Media>()
|
private var sessionCookies = mutableMapOf<String, String>()
|
||||||
}
|
private var csrfToken: String = ""
|
||||||
|
private var loginSuccess = false
|
||||||
|
|
||||||
private fun login() = runBlocking {
|
private val mediaList = arrayListOf<Media>()
|
||||||
|
val itemMediaList = arrayListOf<ItemMedia>()
|
||||||
|
val newEpisodesList = arrayListOf<ItemMedia>()
|
||||||
|
|
||||||
val userAgent = "Mozilla/5.0 (X11; Linux x86_64; rv:80.0) Gecko/20100101 Firefox/80.0"
|
fun login(): Boolean = runBlocking {
|
||||||
|
|
||||||
withContext(Dispatchers.Default) {
|
withContext(Dispatchers.Default) {
|
||||||
// get the authenticity token
|
// get the authenticity token
|
||||||
@ -36,10 +58,10 @@ class AoDParser {
|
|||||||
.execute()
|
.execute()
|
||||||
|
|
||||||
val authenticityToken = resAuth.parse().select("meta[name=csrf-token]").attr("content")
|
val authenticityToken = resAuth.parse().select("meta[name=csrf-token]").attr("content")
|
||||||
println("Authenticity token is: $authenticityToken")
|
val authCookies = resAuth.cookies()
|
||||||
|
|
||||||
val cookies = resAuth.cookies()
|
//Log.d(javaClass.name, "Received authenticity token: $authenticityToken")
|
||||||
println("cookies: $cookies")
|
//Log.d(javaClass.name, "Received authenticity cookies: $authCookies")
|
||||||
|
|
||||||
val data = mapOf(
|
val data = mapOf(
|
||||||
Pair("user[login]", EncryptedPreferences.login),
|
Pair("user[login]", EncryptedPreferences.login),
|
||||||
@ -53,22 +75,78 @@ class AoDParser {
|
|||||||
.method(Connection.Method.POST)
|
.method(Connection.Method.POST)
|
||||||
.data(data)
|
.data(data)
|
||||||
.postDataCharset("UTF-8")
|
.postDataCharset("UTF-8")
|
||||||
.cookies(cookies)
|
.cookies(authCookies)
|
||||||
.execute()
|
.execute()
|
||||||
|
|
||||||
//println(resLogin.body())
|
//println(resLogin.body())
|
||||||
|
|
||||||
loginSuccess = resLogin.body().contains("Hallo, du bist jetzt angemeldet.")
|
|
||||||
println("Status: ${resLogin.statusCode()} (${resLogin.statusMessage()}), login successful: $loginSuccess")
|
|
||||||
|
|
||||||
sessionCookies = resLogin.cookies()
|
sessionCookies = resLogin.cookies()
|
||||||
|
loginSuccess = resLogin.body().contains("Hallo, du bist jetzt angemeldet.")
|
||||||
|
Log.i(javaClass.name, "Status: ${resLogin.statusCode()} (${resLogin.statusMessage()}), login successful: $loginSuccess")
|
||||||
|
|
||||||
|
loginSuccess
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* list all animes from the website
|
* initially load all media and home screen data
|
||||||
|
* -> blocking
|
||||||
*/
|
*/
|
||||||
fun listAnimes(): ArrayList<Media> = runBlocking {
|
fun initialLoading() = runBlocking {
|
||||||
|
val newEPJob = GlobalScope.async {
|
||||||
|
listNewEpisodes()
|
||||||
|
}
|
||||||
|
|
||||||
|
val listJob = GlobalScope.async {
|
||||||
|
listAnimes()
|
||||||
|
}
|
||||||
|
|
||||||
|
newEPJob.await()
|
||||||
|
listJob.await()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get a media by it's ID (int)
|
||||||
|
* @return Media
|
||||||
|
*/
|
||||||
|
fun getMediaById(mediaId: Int): Media {
|
||||||
|
val media = mediaList.first { it.id == mediaId }
|
||||||
|
|
||||||
|
if (media.episodes.isEmpty()) {
|
||||||
|
loadStreams(media)
|
||||||
|
}
|
||||||
|
|
||||||
|
return media
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO don't use jsoup here
|
||||||
|
fun sendCallback(callbackPath: String) = GlobalScope.launch {
|
||||||
|
val headers = mutableMapOf(
|
||||||
|
Pair("Accept", "application/json, text/javascript, */*; q=0.01"),
|
||||||
|
Pair("Accept-Language", "de,en-US;q=0.7,en;q=0.3"),
|
||||||
|
Pair("Accept-Encoding", "gzip, deflate, br"),
|
||||||
|
Pair("X-CSRF-Token", csrfToken),
|
||||||
|
Pair("X-Requested-With", "XMLHttpRequest"),
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
Jsoup.connect(baseUrl + callbackPath)
|
||||||
|
.ignoreContentType(true)
|
||||||
|
.cookies(sessionCookies)
|
||||||
|
.headers(headers)
|
||||||
|
.execute()
|
||||||
|
}
|
||||||
|
} catch (ex: IOException) {
|
||||||
|
Log.e(javaClass.name, "Callback for $callbackPath failed.", ex)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* load all media from aod into itemMediaList and mediaList
|
||||||
|
*/
|
||||||
|
private fun listAnimes() = runBlocking {
|
||||||
if (sessionCookies.isEmpty()) login()
|
if (sessionCookies.isEmpty()) login()
|
||||||
|
|
||||||
withContext(Dispatchers.Default) {
|
withContext(Dispatchers.Default) {
|
||||||
@ -78,6 +156,7 @@ class AoDParser {
|
|||||||
|
|
||||||
//println(resAnimes)
|
//println(resAnimes)
|
||||||
|
|
||||||
|
itemMediaList.clear()
|
||||||
mediaList.clear()
|
mediaList.clear()
|
||||||
resAnimes.select("div.animebox").forEach {
|
resAnimes.select("div.animebox").forEach {
|
||||||
val type = if (it.select("p.animebox-link").select("a").text().toLowerCase(Locale.ROOT) == "zur serie") {
|
val type = if (it.select("p.animebox-link").select("a").text().toLowerCase(Locale.ROOT) == "zur serie") {
|
||||||
@ -85,57 +164,176 @@ class AoDParser {
|
|||||||
} else {
|
} else {
|
||||||
MediaType.MOVIE
|
MediaType.MOVIE
|
||||||
}
|
}
|
||||||
|
val mediaTitle = it.select("h3.animebox-title").text()
|
||||||
|
val mediaLink = it.select("p.animebox-link").select("a").attr("href")
|
||||||
|
val mediaImage = it.select("p.animebox-image").select("img").attr("src")
|
||||||
|
val mediaShortText = it.select("p.animebox-shorttext").text()
|
||||||
|
val mediaId = mediaLink.substringAfterLast("/").toInt()
|
||||||
|
|
||||||
val media = Media(
|
itemMediaList.add(ItemMedia(mediaId, mediaTitle, mediaImage))
|
||||||
it.select("h3.animebox-title").text(),
|
mediaList.add(Media(mediaId, mediaLink, type).apply {
|
||||||
it.select("p.animebox-link").select("a").attr("href"),
|
info.title = mediaTitle
|
||||||
type,
|
info.posterUrl = mediaImage
|
||||||
it.select("p.animebox-image").select("img").attr("src"),
|
info.shortDesc = mediaShortText
|
||||||
it.select("p.animebox-shorttext").text()
|
})
|
||||||
)
|
|
||||||
mediaList.add(media)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
println("got ${mediaList.size} anime")
|
Log.i(javaClass.name, "Total library size is: ${mediaList.size}")
|
||||||
|
|
||||||
return@withContext mediaList
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* load streams for the media path
|
* load all new episodes from AoD into newEpisodesList
|
||||||
*/
|
*/
|
||||||
fun loadStreams(media: Media): List<Episode> = runBlocking {
|
private fun listNewEpisodes() = runBlocking {
|
||||||
|
if (sessionCookies.isEmpty()) login()
|
||||||
|
|
||||||
|
withContext(Dispatchers.Default) {
|
||||||
|
val resHome = Jsoup.connect(baseUrl)
|
||||||
|
.cookies(sessionCookies)
|
||||||
|
.get()
|
||||||
|
|
||||||
|
newEpisodesList.clear()
|
||||||
|
resHome.select("div.jcarousel-container-new").select("li").forEach {
|
||||||
|
if (it.select("span").hasClass("neweps")) {
|
||||||
|
val mediaId = it.select("a.thumbs").attr("href")
|
||||||
|
.substringAfterLast("/").toInt()
|
||||||
|
val mediaImage = it.select("a.thumbs > img").attr("src")
|
||||||
|
val mediaTitle = "${it.select("a").text()} - ${it.select("span.neweps").text()}"
|
||||||
|
|
||||||
|
newEpisodesList.add(ItemMedia(mediaId, mediaTitle, mediaImage))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* load streams for the media path, movies have one episode
|
||||||
|
* @param media is used as call ba reference
|
||||||
|
*/
|
||||||
|
private fun loadStreams(media: Media) = runBlocking {
|
||||||
if (sessionCookies.isEmpty()) login()
|
if (sessionCookies.isEmpty()) login()
|
||||||
|
|
||||||
if (!loginSuccess) {
|
if (!loginSuccess) {
|
||||||
println("please log in") // TODO
|
Log.w(javaClass.name, "Login, was not successful.")
|
||||||
return@runBlocking listOf()
|
return@runBlocking
|
||||||
}
|
}
|
||||||
|
|
||||||
withContext(Dispatchers.Default) {
|
withContext(Dispatchers.Default) {
|
||||||
|
|
||||||
|
// get the media page
|
||||||
val res = Jsoup.connect(baseUrl + media.link)
|
val res = Jsoup.connect(baseUrl + media.link)
|
||||||
.cookies(sessionCookies)
|
.cookies(sessionCookies)
|
||||||
.get()
|
.get()
|
||||||
|
|
||||||
//println(res)
|
//println(res)
|
||||||
|
|
||||||
val playlists = res.select("input.streamstarter_html5").eachAttr("data-playlist")
|
if (csrfToken.isEmpty()) {
|
||||||
val csrfToken = res.select("meta[name=csrf-token]").attr("content")
|
csrfToken = res.select("meta[name=csrf-token]").attr("content")
|
||||||
|
//Log.i(javaClass.name, "New csrf token is $csrfToken")
|
||||||
|
}
|
||||||
|
|
||||||
//println("first entry: ${playlists.first()}")
|
|
||||||
//println("csrf token is: $csrfToken")
|
|
||||||
|
|
||||||
return@withContext loadStreamInfo(playlists.first(), csrfToken, media.type)
|
val pl = res.select("input.streamstarter_html5").first()
|
||||||
|
val primary = pl.attr("data-playlist")
|
||||||
|
val secondary = pl.attr("data-otherplaylist")
|
||||||
|
val secondaryIsOmU = secondary.contains("OmU", true)
|
||||||
|
|
||||||
|
// load primary and secondary playlist
|
||||||
|
val primaryPlaylist = parsePlaylistAsync(primary)
|
||||||
|
val secondaryPlaylist = parsePlaylistAsync(secondary)
|
||||||
|
|
||||||
|
primaryPlaylist.await().playlist.forEach { ep ->
|
||||||
|
val epNumber = if (media.type == MediaType.TVSHOW) {
|
||||||
|
ep.title.substringAfter(", Ep. ").toInt()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
media.episodes.add(
|
||||||
|
Episode(
|
||||||
|
id = ep.mediaid,
|
||||||
|
priStreamUrl = ep.sources.first().file,
|
||||||
|
posterUrl = ep.image,
|
||||||
|
title = ep.title,
|
||||||
|
description = ep.description,
|
||||||
|
number = epNumber
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Log.i(javaClass.name, "Loading primary playlist finished")
|
||||||
|
|
||||||
|
secondaryPlaylist.await().playlist.forEach { ep ->
|
||||||
|
val episode = media.episodes.firstOrNull { it.id == ep.mediaid }
|
||||||
|
|
||||||
|
if (episode != null) {
|
||||||
|
episode.secStreamUrl = ep.sources.first().file
|
||||||
|
episode.secStreamOmU = secondaryIsOmU
|
||||||
|
} else {
|
||||||
|
val epNumber = if (media.type == MediaType.TVSHOW) {
|
||||||
|
ep.title.substringAfter(", Ep. ").toInt()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
media.episodes.add(
|
||||||
|
Episode(
|
||||||
|
id = ep.mediaid,
|
||||||
|
secStreamUrl = ep.sources.first().file,
|
||||||
|
secStreamOmU = secondaryIsOmU,
|
||||||
|
posterUrl = ep.image,
|
||||||
|
title = ep.title,
|
||||||
|
description = ep.description,
|
||||||
|
number = epNumber
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Log.i(javaClass.name, "Loading secondary plalyist finished")
|
||||||
|
|
||||||
|
// parse additional info from the media page
|
||||||
|
res.select("table.vertical-table").select("tr").forEach { row ->
|
||||||
|
when (row.select("th").text().toLowerCase(Locale.ROOT)) {
|
||||||
|
"produktionsjahr" -> media.info.year = row.select("td").text().toInt()
|
||||||
|
"fsk" -> media.info.age = row.select("td").text().toInt()
|
||||||
|
"episodenanzahl" -> {
|
||||||
|
media.info.episodesCount = row.select("td").text()
|
||||||
|
.substringBefore("/")
|
||||||
|
.filter{ it.isDigit() }
|
||||||
|
.toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse additional information for tv shows
|
||||||
|
if (media.type == MediaType.TVSHOW) {
|
||||||
|
res.select("div.three-box-container > div.episodebox").forEach { episodebox ->
|
||||||
|
val episodeId = episodebox.select("div.flip-front").attr("id").substringAfter("-").toInt()
|
||||||
|
val episodeShortDesc = episodebox.select("p.episodebox-shorttext").text()
|
||||||
|
val episodeWatched = episodebox.select("div.episodebox-icons > div").hasClass("status-icon-orange")
|
||||||
|
val episodeWatchedCallback = episodebox.select("input.streamstarter_html5").eachAttr("data-playlist").first()
|
||||||
|
|
||||||
|
media.episodes.firstOrNull { it.id == episodeId }?.apply {
|
||||||
|
shortDesc = episodeShortDesc
|
||||||
|
watched = episodeWatched
|
||||||
|
watchedCallback = episodeWatchedCallback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* load the playlist path and parse it, read the stream info from json
|
* don't use Gson().fromJson() as we don't have any control over the api and it may change
|
||||||
*/
|
*/
|
||||||
private fun loadStreamInfo(playlistPath: String, csrfToken: String, type: MediaType): List<Episode> = runBlocking {
|
private fun parsePlaylistAsync(playlistPath: String): Deferred<AoDObject> {
|
||||||
withContext(Dispatchers.Default) {
|
if (playlistPath == "[]") {
|
||||||
|
return CompletableDeferred(AoDObject(listOf()))
|
||||||
|
}
|
||||||
|
|
||||||
|
return GlobalScope.async {
|
||||||
val headers = mutableMapOf(
|
val headers = mutableMapOf(
|
||||||
Pair("Accept", "application/json, text/javascript, */*; q=0.01"),
|
Pair("Accept", "application/json, text/javascript, */*; q=0.01"),
|
||||||
Pair("Accept-Language", "de,en-US;q=0.7,en;q=0.3"),
|
Pair("Accept-Language", "de,en-US;q=0.7,en;q=0.3"),
|
||||||
@ -144,46 +342,28 @@ class AoDParser {
|
|||||||
Pair("X-Requested-With", "XMLHttpRequest"),
|
Pair("X-Requested-With", "XMLHttpRequest"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
//println("loading streaminfo with cstf: $csrfToken")
|
||||||
|
|
||||||
val res = Jsoup.connect(baseUrl + playlistPath)
|
val res = Jsoup.connect(baseUrl + playlistPath)
|
||||||
.ignoreContentType(true)
|
.ignoreContentType(true)
|
||||||
.cookies(sessionCookies)
|
.cookies(sessionCookies)
|
||||||
.headers(headers)
|
.headers(headers)
|
||||||
.execute()
|
.execute()
|
||||||
|
|
||||||
//println(res.body())
|
//Gson().fromJson(res.body(), AoDObject::class.java)
|
||||||
|
|
||||||
return@withContext when (type) {
|
return@async AoDObject(JsonParser.parseString(res.body()).asJsonObject
|
||||||
MediaType.MOVIE -> {
|
.get("playlist").asJsonArray.map {
|
||||||
val movie = JsonParser.parseString(res.body()).asJsonObject
|
Playlist(
|
||||||
.get("playlist").asJsonArray
|
sources = it.asJsonObject.get("sources").asJsonArray.map { source ->
|
||||||
|
Source(source.asJsonObject.get("file").asString)
|
||||||
movie.first().asJsonObject.get("sources").asJsonArray.toList().map {
|
},
|
||||||
Episode(streamUrl = it.asJsonObject.get("file").asString)
|
image = it.asJsonObject.get("image").asString,
|
||||||
}
|
title = it.asJsonObject.get("title").asString,
|
||||||
}
|
description = it.asJsonObject.get("description").asString,
|
||||||
MediaType.TVSHOW -> {
|
mediaid = it.asJsonObject.get("mediaid").asInt
|
||||||
val episodesJson = JsonParser.parseString(res.body()).asJsonObject
|
)
|
||||||
.get("playlist").asJsonArray
|
})
|
||||||
|
|
||||||
|
|
||||||
episodesJson.map {
|
|
||||||
val episodeStream = it.asJsonObject.get("sources").asJsonArray
|
|
||||||
.first().asJsonObject
|
|
||||||
.get("file").asString
|
|
||||||
val episodeTitle = it.asJsonObject.get("title").asString
|
|
||||||
|
|
||||||
Episode(
|
|
||||||
episodeTitle,
|
|
||||||
episodeStream
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
Log.e(javaClass.name, "Wrong Type, please report this issue.")
|
|
||||||
listOf()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,22 +1,40 @@
|
|||||||
package org.mosad.teapod.preferences
|
package org.mosad.teapod.preferences
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import org.mosad.teapod.R
|
||||||
|
|
||||||
object Preferences {
|
object Preferences {
|
||||||
|
|
||||||
var login = ""
|
var preferSecondary = false
|
||||||
internal set
|
|
||||||
var password = ""
|
|
||||||
internal set
|
internal set
|
||||||
|
|
||||||
|
fun savePreferSecondary(context: Context, preferSecondary: Boolean) {
|
||||||
|
val sharedPref = context.getSharedPreferences(
|
||||||
|
context.getString(R.string.preference_file_key),
|
||||||
|
Context.MODE_PRIVATE
|
||||||
|
)
|
||||||
|
|
||||||
fun saveCredentials(login: String, password: String) {
|
with(sharedPref.edit()) {
|
||||||
this.login = login
|
putBoolean(context.getString(R.string.save_key_prefer_secondary), preferSecondary)
|
||||||
this.password = password
|
apply()
|
||||||
|
}
|
||||||
|
|
||||||
// TODO save
|
this.preferSecondary = preferSecondary
|
||||||
}
|
}
|
||||||
|
|
||||||
fun load() {
|
/**
|
||||||
// TODO
|
* initially load the stored values
|
||||||
|
*/
|
||||||
|
fun load(context: Context) {
|
||||||
|
val sharedPref = context.getSharedPreferences(
|
||||||
|
context.getString(R.string.preference_file_key),
|
||||||
|
Context.MODE_PRIVATE
|
||||||
|
)
|
||||||
|
|
||||||
|
preferSecondary = sharedPref.getBoolean(
|
||||||
|
context.getString(R.string.save_key_prefer_secondary), false
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
@ -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,15 +1,19 @@
|
|||||||
package org.mosad.teapod.ui.account
|
package org.mosad.teapod.ui.fragments
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import com.afollestad.materialdialogs.MaterialDialog
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
|
import de.psdev.licensesdialog.LicensesDialog
|
||||||
import kotlinx.android.synthetic.main.fragment_account.*
|
import kotlinx.android.synthetic.main.fragment_account.*
|
||||||
import org.mosad.teapod.BuildConfig
|
import org.mosad.teapod.BuildConfig
|
||||||
import org.mosad.teapod.R
|
import org.mosad.teapod.R
|
||||||
|
import org.mosad.teapod.parser.AoDParser
|
||||||
import org.mosad.teapod.preferences.EncryptedPreferences
|
import org.mosad.teapod.preferences.EncryptedPreferences
|
||||||
|
import org.mosad.teapod.preferences.Preferences
|
||||||
import org.mosad.teapod.ui.components.LoginDialog
|
import org.mosad.teapod.ui.components.LoginDialog
|
||||||
|
|
||||||
class AccountFragment : Fragment() {
|
class AccountFragment : Fragment() {
|
||||||
@ -23,18 +27,14 @@ class AccountFragment : Fragment() {
|
|||||||
|
|
||||||
text_account_login.text = EncryptedPreferences.login
|
text_account_login.text = EncryptedPreferences.login
|
||||||
text_info_about_desc.text = getString(R.string.info_about_desc, BuildConfig.VERSION_NAME, getString(R.string.build_time))
|
text_info_about_desc.text = getString(R.string.info_about_desc, BuildConfig.VERSION_NAME, getString(R.string.build_time))
|
||||||
|
switch_secondary.isChecked = Preferences.preferSecondary
|
||||||
|
|
||||||
initActions()
|
initActions()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initActions() {
|
private fun initActions() {
|
||||||
linear_account_login.setOnClickListener {
|
linear_account_login.setOnClickListener {
|
||||||
LoginDialog(requireContext()).positiveButton {
|
showLoginDialog(true)
|
||||||
EncryptedPreferences.saveCredentials(login, password, context)
|
|
||||||
}.show {
|
|
||||||
login = EncryptedPreferences.login
|
|
||||||
password = ""
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
linear_about.setOnClickListener {
|
linear_about.setOnClickListener {
|
||||||
@ -43,5 +43,33 @@ class AccountFragment : Fragment() {
|
|||||||
.message(R.string.info_about_dialog)
|
.message(R.string.info_about_dialog)
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
text_licenses.setOnClickListener {
|
||||||
|
LicensesDialog.Builder(requireContext())
|
||||||
|
.setNotices(R.raw.notices)
|
||||||
|
.setTitle(R.string.licenses)
|
||||||
|
.setIncludeOwnLicense(true)
|
||||||
|
.setThemeResourceId(R.style.AppTheme)
|
||||||
|
.build()
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
switch_secondary.setOnClickListener {
|
||||||
|
Preferences.savePreferSecondary(requireContext(), switch_secondary.isChecked)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showLoginDialog(firstTry: Boolean) {
|
||||||
|
LoginDialog(requireContext(), firstTry).positiveButton {
|
||||||
|
EncryptedPreferences.saveCredentials(login, password, context)
|
||||||
|
|
||||||
|
if (!AoDParser.login()) {
|
||||||
|
showLoginDialog(false)
|
||||||
|
Log.w(javaClass.name, "Login failed, please try again.")
|
||||||
|
}
|
||||||
|
}.show {
|
||||||
|
login = EncryptedPreferences.login
|
||||||
|
password = ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -0,0 +1,70 @@
|
|||||||
|
package org.mosad.teapod.ui.fragments
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import kotlinx.android.synthetic.main.fragment_home.*
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.mosad.teapod.MainActivity
|
||||||
|
import org.mosad.teapod.R
|
||||||
|
import org.mosad.teapod.parser.AoDParser
|
||||||
|
import org.mosad.teapod.util.StorageController
|
||||||
|
import org.mosad.teapod.util.adapter.MediaItemAdapter
|
||||||
|
import org.mosad.teapod.util.decoration.MediaItemDecoration
|
||||||
|
|
||||||
|
class HomeFragment : Fragment() {
|
||||||
|
|
||||||
|
private lateinit var adapterMyList: MediaItemAdapter
|
||||||
|
private lateinit var adapterNewEpisodes: MediaItemAdapter
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
|
return inflater.inflate(R.layout.fragment_home, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
GlobalScope.launch {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
context?.let {
|
||||||
|
recycler_my_list.addItemDecoration(MediaItemDecoration(9))
|
||||||
|
|
||||||
|
updateMyListMedia()
|
||||||
|
|
||||||
|
adapterNewEpisodes = MediaItemAdapter(AoDParser.newEpisodesList)
|
||||||
|
recycler_new_episodes.adapter = adapterNewEpisodes
|
||||||
|
recycler_new_episodes.addItemDecoration(MediaItemDecoration(9))
|
||||||
|
|
||||||
|
initActions()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO recreating the adapter on list change is not a good solution
|
||||||
|
fun updateMyListMedia() {
|
||||||
|
val myListMedia = StorageController.myList.map { elementId ->
|
||||||
|
AoDParser.itemMediaList.first {
|
||||||
|
elementId == it.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
adapterMyList = MediaItemAdapter(myListMedia)
|
||||||
|
adapterMyList.onItemClick = { mediaId, _ ->
|
||||||
|
(activity as MainActivity).showMediaFragment(mediaId)
|
||||||
|
}
|
||||||
|
|
||||||
|
recycler_my_list.adapter = adapterMyList
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initActions() {
|
||||||
|
adapterNewEpisodes.onItemClick = { mediaId, _ ->
|
||||||
|
(activity as MainActivity).showMediaFragment(mediaId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package org.mosad.teapod.ui.library
|
package org.mosad.teapod.ui.fragments
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
@ -10,12 +10,12 @@ import kotlinx.coroutines.*
|
|||||||
import org.mosad.teapod.MainActivity
|
import org.mosad.teapod.MainActivity
|
||||||
import org.mosad.teapod.R
|
import org.mosad.teapod.R
|
||||||
import org.mosad.teapod.parser.AoDParser
|
import org.mosad.teapod.parser.AoDParser
|
||||||
import org.mosad.teapod.util.CustomAdapter
|
import org.mosad.teapod.util.decoration.MediaItemDecoration
|
||||||
import org.mosad.teapod.util.Media
|
import org.mosad.teapod.util.adapter.MediaItemAdapter
|
||||||
|
|
||||||
class LibraryFragment : Fragment() {
|
class LibraryFragment : Fragment() {
|
||||||
|
|
||||||
private lateinit var adapter : CustomAdapter
|
private lateinit var adapter: MediaItemAdapter
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
return inflater.inflate(R.layout.fragment_library, container, false)
|
return inflater.inflate(R.layout.fragment_library, container, false)
|
||||||
@ -24,31 +24,21 @@ class LibraryFragment : Fragment() {
|
|||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
// init async
|
||||||
GlobalScope.launch {
|
GlobalScope.launch {
|
||||||
if (AoDParser.mediaList.isEmpty()) {
|
|
||||||
AoDParser().listAnimes()
|
|
||||||
}
|
|
||||||
|
|
||||||
// create and set the adapter, needs context
|
// create and set the adapter, needs context
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
context?.let {
|
context?.let {
|
||||||
adapter = CustomAdapter(it, AoDParser.mediaList)
|
adapter = MediaItemAdapter(AoDParser.itemMediaList)
|
||||||
list_library.adapter = adapter
|
adapter.onItemClick = { mediaId, _ ->
|
||||||
|
(activity as MainActivity).showMediaFragment(mediaId)
|
||||||
|
}
|
||||||
|
|
||||||
|
recycler_media_library.adapter = adapter
|
||||||
|
recycler_media_library.addItemDecoration(MediaItemDecoration(9))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
initActions()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initActions() {
|
|
||||||
list_library.setOnItemClickListener { _, _, position, _ ->
|
|
||||||
val media = adapter.getItem(position) as Media
|
|
||||||
println("selected item is: ${media.title}")
|
|
||||||
|
|
||||||
val mainActivity = activity as MainActivity
|
|
||||||
mainActivity.showDetailFragment(media)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
148
app/src/main/java/org/mosad/teapod/ui/fragments/MediaFragment.kt
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
package org.mosad.teapod.ui.fragments
|
||||||
|
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.drawable.ColorDrawable
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import com.bumptech.glide.request.RequestOptions
|
||||||
|
import jp.wasabeef.glide.transformations.BlurTransformation
|
||||||
|
import kotlinx.android.synthetic.main.fragment_media.*
|
||||||
|
import org.mosad.teapod.MainActivity
|
||||||
|
import org.mosad.teapod.R
|
||||||
|
import org.mosad.teapod.parser.AoDParser
|
||||||
|
import org.mosad.teapod.preferences.Preferences
|
||||||
|
import org.mosad.teapod.util.DataTypes.MediaType
|
||||||
|
import org.mosad.teapod.util.Episode
|
||||||
|
import org.mosad.teapod.util.Media
|
||||||
|
import org.mosad.teapod.util.StorageController
|
||||||
|
import org.mosad.teapod.util.TMDBResponse
|
||||||
|
import org.mosad.teapod.util.adapter.EpisodeItemAdapter
|
||||||
|
|
||||||
|
class MediaFragment(private val media: Media, private val tmdb: TMDBResponse) : Fragment() {
|
||||||
|
|
||||||
|
private lateinit var adapterRecEpisodes: EpisodeItemAdapter
|
||||||
|
private lateinit var viewManager: RecyclerView.LayoutManager
|
||||||
|
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
|
return inflater.inflate(R.layout.fragment_media, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
initGUI()
|
||||||
|
initActions()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* if tmdb data is present, use it, else use the aod data
|
||||||
|
*/
|
||||||
|
private fun initGUI() {
|
||||||
|
// generic gui
|
||||||
|
val backdropUrl = if (tmdb.backdropUrl.isNotEmpty()) tmdb.backdropUrl else media.info.posterUrl
|
||||||
|
val posterUrl = if (tmdb.posterUrl.isNotEmpty()) tmdb.posterUrl else media.info.posterUrl
|
||||||
|
|
||||||
|
Glide.with(requireContext()).load(backdropUrl)
|
||||||
|
.apply(RequestOptions.placeholderOf(ColorDrawable(Color.DKGRAY)))
|
||||||
|
.apply(RequestOptions.bitmapTransform(BlurTransformation(20, 3)))
|
||||||
|
.into(image_backdrop)
|
||||||
|
|
||||||
|
Glide.with(requireContext()).load(posterUrl)
|
||||||
|
.into(image_poster)
|
||||||
|
|
||||||
|
text_title.text = media.info.title
|
||||||
|
text_year.text = media.info.year.toString()
|
||||||
|
text_age.text = media.info.age.toString()
|
||||||
|
text_overview.text = media.info.shortDesc
|
||||||
|
if (StorageController.myList.contains(media.id)) {
|
||||||
|
Glide.with(requireContext()).load(R.drawable.ic_baseline_check_24).into(image_my_list_action)
|
||||||
|
} else {
|
||||||
|
Glide.with(requireContext()).load(R.drawable.ic_baseline_add_24).into(image_my_list_action)
|
||||||
|
}
|
||||||
|
|
||||||
|
// specific gui
|
||||||
|
if (media.type == MediaType.TVSHOW) {
|
||||||
|
adapterRecEpisodes = EpisodeItemAdapter(media.episodes)
|
||||||
|
viewManager = LinearLayoutManager(context)
|
||||||
|
recycler_episodes.layoutManager = viewManager
|
||||||
|
recycler_episodes.adapter = adapterRecEpisodes
|
||||||
|
|
||||||
|
text_episodes_or_runtime.text = getString(R.string.text_episodes_count, media.info.episodesCount)
|
||||||
|
} else if (media.type == MediaType.MOVIE) {
|
||||||
|
recycler_episodes.visibility = View.GONE
|
||||||
|
|
||||||
|
if (tmdb.runtime > 0) {
|
||||||
|
text_episodes_or_runtime.text = getString(R.string.text_runtime, tmdb.runtime)
|
||||||
|
} else {
|
||||||
|
text_episodes_or_runtime.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initActions() {
|
||||||
|
button_play.setOnClickListener {
|
||||||
|
when (media.type) {
|
||||||
|
MediaType.MOVIE -> playStream(media.episodes.first())
|
||||||
|
MediaType.TVSHOW -> playStream(media.episodes.first())
|
||||||
|
else -> Log.e(javaClass.name, "Wrong Type: $media.type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add or remove media from myList
|
||||||
|
linear_my_list_action.setOnClickListener {
|
||||||
|
if (StorageController.myList.contains(media.id)) {
|
||||||
|
StorageController.myList.remove(media.id)
|
||||||
|
Glide.with(requireContext()).load(R.drawable.ic_baseline_add_24).into(image_my_list_action)
|
||||||
|
} else {
|
||||||
|
StorageController.myList.add(media.id)
|
||||||
|
Glide.with(requireContext()).load(R.drawable.ic_baseline_check_24).into(image_my_list_action)
|
||||||
|
}
|
||||||
|
StorageController.saveMyList(requireContext())
|
||||||
|
|
||||||
|
// notify home fragment on change
|
||||||
|
parentFragmentManager.findFragmentByTag("HomeFragment")?.let {
|
||||||
|
(it as HomeFragment).updateMyListMedia()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// set onItemClick only in adapter is initialized
|
||||||
|
if (this::adapterRecEpisodes.isInitialized) {
|
||||||
|
adapterRecEpisodes.onImageClick = { _, position ->
|
||||||
|
playStream(media.episodes[position])
|
||||||
|
|
||||||
|
// update watched state
|
||||||
|
AoDParser.sendCallback(media.episodes[position].watchedCallback)
|
||||||
|
adapterRecEpisodes.updateWatchedState(true, position)
|
||||||
|
adapterRecEpisodes.notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Play the media's stream
|
||||||
|
* If prefer secondary or primary is empty and secondary is present (secStreamOmU),
|
||||||
|
* use the secondary stream. Else, if the primary stream is set use the primary stream.
|
||||||
|
*/
|
||||||
|
private fun playStream(ep: Episode) {
|
||||||
|
val streamUrl = if ((Preferences.preferSecondary || ep.priStreamUrl.isEmpty()) && ep.secStreamOmU) {
|
||||||
|
ep.secStreamUrl
|
||||||
|
} else if (ep.priStreamUrl.isNotEmpty()) {
|
||||||
|
ep.priStreamUrl
|
||||||
|
} else {
|
||||||
|
Log.e(javaClass.name, "No stream url set.")
|
||||||
|
""
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(javaClass.name, "Playing stream: $streamUrl")
|
||||||
|
(activity as MainActivity).startPlayer(streamUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package org.mosad.teapod.ui.search
|
package org.mosad.teapod.ui.fragments
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
@ -11,13 +11,12 @@ import kotlinx.coroutines.*
|
|||||||
import org.mosad.teapod.MainActivity
|
import org.mosad.teapod.MainActivity
|
||||||
import org.mosad.teapod.R
|
import org.mosad.teapod.R
|
||||||
import org.mosad.teapod.parser.AoDParser
|
import org.mosad.teapod.parser.AoDParser
|
||||||
import org.mosad.teapod.util.CustomAdapter
|
import org.mosad.teapod.util.decoration.MediaItemDecoration
|
||||||
import org.mosad.teapod.util.Media
|
import org.mosad.teapod.util.adapter.MediaItemAdapter
|
||||||
|
|
||||||
class SearchFragment : Fragment() {
|
class SearchFragment : Fragment() {
|
||||||
|
|
||||||
private val instance = this
|
private var adapter : MediaItemAdapter? = null
|
||||||
private lateinit var adapter : CustomAdapter
|
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
return inflater.inflate(R.layout.fragment_search, container, false)
|
return inflater.inflate(R.layout.fragment_search, container, false)
|
||||||
@ -27,15 +26,17 @@ class SearchFragment : Fragment() {
|
|||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
GlobalScope.launch {
|
GlobalScope.launch {
|
||||||
if (AoDParser.mediaList.isEmpty()) {
|
|
||||||
AoDParser().listAnimes()
|
|
||||||
}
|
|
||||||
|
|
||||||
// create and set the adapter, needs context
|
// create and set the adapter, needs context
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
context?.let {
|
context?.let {
|
||||||
adapter = CustomAdapter(it, AoDParser.mediaList)
|
adapter = MediaItemAdapter(AoDParser.itemMediaList)
|
||||||
list_search.adapter = adapter
|
adapter!!.onItemClick = { mediaId, _ ->
|
||||||
|
search_text.clearFocus()
|
||||||
|
(activity as MainActivity).showMediaFragment(mediaId)
|
||||||
|
}
|
||||||
|
|
||||||
|
recycler_media_search.adapter = adapter
|
||||||
|
recycler_media_search.addItemDecoration(MediaItemDecoration(9))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -46,23 +47,16 @@ class SearchFragment : Fragment() {
|
|||||||
private fun initActions() {
|
private fun initActions() {
|
||||||
search_text.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
search_text.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||||
override fun onQueryTextSubmit(query: String?): Boolean {
|
override fun onQueryTextSubmit(query: String?): Boolean {
|
||||||
|
adapter?.filter?.filter(query)
|
||||||
|
adapter?.notifyDataSetChanged()
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onQueryTextChange(newText: String?): Boolean {
|
override fun onQueryTextChange(newText: String?): Boolean {
|
||||||
adapter.filter.filter(newText)
|
adapter?.filter?.filter(newText)
|
||||||
adapter.notifyDataSetChanged()
|
adapter?.notifyDataSetChanged()
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
list_search.setOnItemClickListener { _, _, position, _ ->
|
|
||||||
val media = adapter.getItem(position) as Media
|
|
||||||
|
|
||||||
println("selected item is: ${media.title}")
|
|
||||||
|
|
||||||
val mainActivity = activity as MainActivity
|
|
||||||
mainActivity.showDetailFragment(media)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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,74 @@ class DataTypes {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Media(val title: String, val link: String, val type: DataTypes.MediaType, val posterLink: String, val shortDesc : String, var episodes: List<Episode> = listOf()) {
|
/**
|
||||||
override fun toString(): String {
|
* this class is used to represent the item media
|
||||||
return title
|
* it is uses in the ItemMediaAdapter (RecyclerView)
|
||||||
}
|
*/
|
||||||
}
|
data class ItemMedia(
|
||||||
|
val id: Int,
|
||||||
|
val title: String,
|
||||||
|
val posterUrl: String
|
||||||
|
)
|
||||||
|
|
||||||
data class Episode(val title: String = "", val streamUrl: String = "", val posterLink: String = "", var watched: Boolean = false)
|
|
||||||
|
|
||||||
data class TMDBResponse(val title: String = "", val overview: String = "", val posterUrl: String = "", val backdropUrl: String = "")
|
/**
|
||||||
|
* TODO the episodes workflow could use a clean up/rework
|
||||||
|
*/
|
||||||
|
data class Media(
|
||||||
|
val id: Int,
|
||||||
|
val link: String,
|
||||||
|
val type: DataTypes.MediaType,
|
||||||
|
val info: Info = Info(),
|
||||||
|
var episodes: ArrayList<Episode> = arrayListOf()
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Info(
|
||||||
|
var title: String = "",
|
||||||
|
var posterUrl: String = "",
|
||||||
|
var shortDesc: String = "",
|
||||||
|
var description: String = "",
|
||||||
|
var year: Int = 0,
|
||||||
|
var age: Int = 0,
|
||||||
|
var episodesCount: Int = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* if secStreamOmU == true, then a secondary stream is present
|
||||||
|
*/
|
||||||
|
data class Episode(
|
||||||
|
val id: Int = 0,
|
||||||
|
var title: String = "",
|
||||||
|
var priStreamUrl: String = "",
|
||||||
|
var secStreamUrl: String = "",
|
||||||
|
var secStreamOmU: Boolean = false,
|
||||||
|
var posterUrl: String = "",
|
||||||
|
var description: String = "",
|
||||||
|
var shortDesc: String = "",
|
||||||
|
var number: Int = 0,
|
||||||
|
var watched: Boolean = false,
|
||||||
|
var watchedCallback: String = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
data class TMDBResponse(
|
||||||
|
val id: Int = 0,
|
||||||
|
val title: String = "",
|
||||||
|
val overview: String = "",
|
||||||
|
val posterUrl: String = "",
|
||||||
|
val backdropUrl: String = "",
|
||||||
|
var runtime: Int = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
data class AoDObject(val playlist: List<Playlist>)
|
||||||
|
|
||||||
|
data class Playlist(
|
||||||
|
val sources: List<Source>,
|
||||||
|
val image: String,
|
||||||
|
val title: String,
|
||||||
|
val description: String,
|
||||||
|
val mediaid: Int
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Source(
|
||||||
|
val file: String = ""
|
||||||
|
)
|
||||||
|
@ -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
@ -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,37 @@ class TMDBApiController {
|
|||||||
}.await()
|
}.await()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* currently only used for runtime, need a rework
|
||||||
|
*/
|
||||||
|
fun getMovieRuntime(id: Int): Int = runBlocking {
|
||||||
|
val url = URL("$getMovieUrl/$id?api_key=$apiKey&language=$language")
|
||||||
|
|
||||||
|
GlobalScope.async {
|
||||||
|
val response = JsonParser.parseString(url.readText()).asJsonObject
|
||||||
|
|
||||||
|
return@async getStringNotNull(response,"runtime").toInt()
|
||||||
|
}.await()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* return memberName as string if it's not JsonNull,
|
||||||
|
* else return an empty string
|
||||||
|
*/
|
||||||
|
private fun getStringNotNull(jsonObject: JsonObject, memberName: String): String {
|
||||||
|
return getStringNotNullPrefix(jsonObject, memberName, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* return memberName as string with a prefix if it's not JsonNull,
|
||||||
|
* else return an empty string
|
||||||
|
*/
|
||||||
|
private fun getStringNotNullPrefix(jsonObject: JsonObject, memberName: String, prefix: String): String {
|
||||||
|
return if (!jsonObject.get(memberName).isJsonNull) {
|
||||||
|
prefix + jsonObject.get(memberName).asString
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -0,0 +1,73 @@
|
|||||||
|
package org.mosad.teapod.util.adapter
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import com.bumptech.glide.request.RequestOptions
|
||||||
|
import jp.wasabeef.glide.transformations.RoundedCornersTransformation
|
||||||
|
import kotlinx.android.synthetic.main.item_episode.view.*
|
||||||
|
import org.mosad.teapod.R
|
||||||
|
import org.mosad.teapod.util.Episode
|
||||||
|
|
||||||
|
class EpisodeItemAdapter(private val episodes: List<Episode>) : RecyclerView.Adapter<EpisodeItemAdapter.MyViewHolder>() {
|
||||||
|
|
||||||
|
var onItemClick: ((String, Int) -> Unit)? = null
|
||||||
|
var onImageClick: ((String, Int) -> Unit)? = null
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
|
||||||
|
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_episode, parent, false)
|
||||||
|
|
||||||
|
return MyViewHolder(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
|
||||||
|
val context = holder.view.context
|
||||||
|
val ep = episodes[position]
|
||||||
|
|
||||||
|
val titleText = if (ep.priStreamUrl.isEmpty() && ep.secStreamOmU) {
|
||||||
|
context.getString(R.string.component_episode_title_sub, ep.number, ep.description)
|
||||||
|
} else {
|
||||||
|
context.getString(R.string.component_episode_title, ep.number, ep.description)
|
||||||
|
}
|
||||||
|
|
||||||
|
holder.view.text_episode_title.text = titleText
|
||||||
|
holder.view.text_episode_desc.text = ep.shortDesc
|
||||||
|
|
||||||
|
if (episodes[position].posterUrl.isNotEmpty()) {
|
||||||
|
Glide.with(context).load(ep.posterUrl)
|
||||||
|
.apply(RequestOptions.bitmapTransform(RoundedCornersTransformation(10, 0)))
|
||||||
|
.into(holder.view.image_episode)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ep.watched) {
|
||||||
|
holder.view.image_watched.setImageDrawable(
|
||||||
|
ContextCompat.getDrawable(context, R.drawable.ic_baseline_check_circle_24)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
holder.view.image_watched.setImageDrawable(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int {
|
||||||
|
return episodes.size
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateWatchedState(watched: Boolean, position: Int) {
|
||||||
|
episodes[position].watched = watched
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class MyViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
|
||||||
|
init {
|
||||||
|
view.setOnClickListener {
|
||||||
|
onItemClick?.invoke(episodes[adapterPosition].title, adapterPosition)
|
||||||
|
}
|
||||||
|
|
||||||
|
view.image_episode.setOnClickListener {
|
||||||
|
onImageClick?.invoke(episodes[adapterPosition].title, adapterPosition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,30 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:aapt="http://schemas.android.com/aapt"
|
|
||||||
android:width="108dp"
|
|
||||||
android:height="108dp"
|
|
||||||
android:viewportWidth="108"
|
|
||||||
android:viewportHeight="108">
|
|
||||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
|
||||||
<aapt:attr name="android:fillColor">
|
|
||||||
<gradient
|
|
||||||
android:endX="85.84757"
|
|
||||||
android:endY="92.4963"
|
|
||||||
android:startX="42.9492"
|
|
||||||
android:startY="49.59793"
|
|
||||||
android:type="linear">
|
|
||||||
<item
|
|
||||||
android:color="#44000000"
|
|
||||||
android:offset="0.0" />
|
|
||||||
<item
|
|
||||||
android:color="#00000000"
|
|
||||||
android:offset="1.0" />
|
|
||||||
</gradient>
|
|
||||||
</aapt:attr>
|
|
||||||
</path>
|
|
||||||
<path
|
|
||||||
android:fillColor="#FFFFFF"
|
|
||||||
android:fillType="nonZero"
|
|
||||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
|
||||||
android:strokeWidth="1"
|
|
||||||
android:strokeColor="#00000000" />
|
|
||||||
</vector>
|
|
@ -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>
|
12
app/src/main/res/drawable/bg_splash.xml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<item android:drawable="@android:color/black"/>
|
||||||
|
|
||||||
|
<item android:gravity="center" android:width="144dp" android:height="144dp">
|
||||||
|
<bitmap
|
||||||
|
android:gravity="fill_horizontal|fill_vertical"
|
||||||
|
android:src="@drawable/ic_splash_logo"/>
|
||||||
|
</item>
|
||||||
|
|
||||||
|
</layer-list>
|
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
@ -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
@ -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>
|
10
app/src/main/res/drawable/ic_baseline_subtitles_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="M20,4L4,4c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,6c0,-1.1 -0.9,-2 -2,-2zM4,12h4v2L4,14v-2zM14,18L4,18v-2h10v2zM20,18h-4v-2h4v2zM20,14L10,14v-2h10v2z"/>
|
||||||
|
</vector>
|
19
app/src/main/res/drawable/ic_launcher_foreground.xml
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="108"
|
||||||
|
android:viewportHeight="108">
|
||||||
|
<group android:scaleX="0.051679686"
|
||||||
|
android:scaleY="0.051679686"
|
||||||
|
android:translateX="27.54"
|
||||||
|
android:translateY="38.90954">
|
||||||
|
<path
|
||||||
|
android:pathData="m850.19,372.71c87.88,-11.01 119.04,-84.97 123.1,-99.87 4.06,-14.89 24.91,-80.57 11.92,-129.36 -12.99,-48.79 -34.36,-72.36 -58.62,-77.25 -24.25,-4.9 -50.59,10.51 -65,32.81 -14.41,22.3 -14.68,45.14 -14.78,55.29 -0.11,10.15 0.76,23.2 -3.37,33.29 -4.13,10.09 3.23,25.71 6.04,35.23 2.81,9.52 9.67,82.62 5.78,115.57 -3.89,32.95 -5.07,34.29 -5.07,34.29zM0.4,23.58C55.81,77.29 56.45,120.86 56.08,132.92c-0.36,12.06 4.77,130.59 11.47,150.76 4.42,13.3 12.11,50.16 41.78,74.48 25.51,20.91 58.65,31.38 58.65,31.38 0,0 36.42,78.46 78.83,108.64 31.56,22.46 39.61,23.74 46.5,35.55 6.18,10.6 93.56,62.62 275.1,47.23 127.29,-10.79 138.56,-44.3 138.56,-44.3 0,0 49.41,-21.9 101.15,-80.43 12.87,-14.56 4.41,-13.21 28.57,-17.79 24.16,-4.58 138.01,-45.58 170.66,-154.36C1039.99,175.32 1017.81,96.01 994.52,69.12 971.23,42.22 931.6,24.18 912.25,24.93c-18.47,0.71 -44.78,4.24 -80.21,46.87 -35.43,42.62 -28.94,37.4 -39.36,41.73 -6.82,2.83 -5.68,3.91 -26.75,-11.65 -20.23,-14.93 -28.9,-21.24 -43.38,-27.24 -7.96,-3.3 2.05,-5.55 2.59,-19.48 0.54,-13.93 2.4,-23.51 -17.32,-23.77 -19.72,-0.26 -408.02,0.21 -408.02,0.21 0,0 -18.8,-1.29 -7.79,24.82 4.2,9.94 -1.45,6.43 -33.27,25.85 -31.82,19.42 -55.58,34.4 -72.28,66.09 -8.43,16 -22.91,23.02 -27.97,8.05C153.44,141.43 125.2,48.96 105.17,23.22 85.56,-1.97 77.8,0.26 77.8,0.26Z"
|
||||||
|
android:strokeLineJoin="miter"
|
||||||
|
android:strokeWidth="0.41878"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:strokeColor="#000000"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:strokeLineCap="butt"/>
|
||||||
|
</group>
|
||||||
|
</vector>
|
BIN
app/src/main/res/drawable/ic_splash_logo.png
Normal file
After Width: | Height: | Size: 10 KiB |
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"
|
||||||
|
@ -12,13 +12,17 @@
|
|||||||
android:id="@+id/video_view"
|
android:id="@+id/video_view"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_gravity="center" />
|
android:layout_gravity="center"
|
||||||
|
app:fastforward_increment="10000"
|
||||||
|
app:rewind_increment="10000" />
|
||||||
<!-- app:controller_layout_id="@layout/player_custom_control"/>-->
|
<!-- app:controller_layout_id="@layout/player_custom_control"/>-->
|
||||||
|
|
||||||
<ProgressBar
|
<com.google.android.material.progressindicator.ProgressIndicator
|
||||||
android:id="@+id/loading"
|
android:id="@+id/loading"
|
||||||
|
style="@style/Widget.MaterialComponents.ProgressIndicator.Circular.Indeterminate"
|
||||||
android:layout_width="70dp"
|
android:layout_width="70dp"
|
||||||
android:layout_height="70dp"
|
android:layout_height="70dp"
|
||||||
android:layout_gravity="center" />
|
android:layout_gravity="center"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
</FrameLayout>
|
</FrameLayout>
|
@ -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">
|
||||||
|
|
||||||
@ -80,6 +81,91 @@
|
|||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/linear_settings"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
android:background="#ffffff"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_settings"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingStart="7dp"
|
||||||
|
android:paddingEnd="7dp"
|
||||||
|
android:text="@string/settings"
|
||||||
|
android:textColor="@android:color/primary_text_light"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/linear_settings_secondary"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_margin="7dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/imageView3"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:contentDescription="@string/account"
|
||||||
|
android:minWidth="48dp"
|
||||||
|
android:minHeight="48dp"
|
||||||
|
android:padding="5dp"
|
||||||
|
android:scaleType="fitXY"
|
||||||
|
android:src="@drawable/ic_baseline_subtitles_24" />
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/linearLayout"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/switch_secondary"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_settings_secondary"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/settings_secondary"
|
||||||
|
android:textColor="@android:color/primary_text_light"
|
||||||
|
android:textSize="16sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_settings_secondary_desc"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:maxLines="2"
|
||||||
|
android:text="@string/settings_secondary_desc"
|
||||||
|
android:textColor="@android:color/secondary_text_light" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||||
|
android:id="@+id/switch_secondary"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:checked="true"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/linear_info"
|
android:id="@+id/linear_info"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@ -103,6 +189,7 @@
|
|||||||
android:id="@+id/linear_about"
|
android:id="@+id/linear_about"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
android:layout_margin="7dp"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:orientation="horizontal">
|
android:orientation="horizontal">
|
||||||
|
|
||||||
@ -110,6 +197,7 @@
|
|||||||
android:id="@+id/imageView2"
|
android:id="@+id/imageView2"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:contentDescription="@string/info"
|
||||||
android:minWidth="48dp"
|
android:minWidth="48dp"
|
||||||
android:minHeight="48dp"
|
android:minHeight="48dp"
|
||||||
android:padding="5dp"
|
android:padding="5dp"
|
||||||
@ -140,7 +228,25 @@
|
|||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/divider"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:background="?android:attr/listDivider" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_licenses"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="7dp"
|
||||||
|
android:paddingStart="48dp"
|
||||||
|
android:paddingEnd="48dp"
|
||||||
|
android:text="@string/licenses"
|
||||||
|
android:textColor="@android:color/primary_text_light"
|
||||||
|
android:textSize="16sp" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
||||||
|
@ -5,19 +5,72 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:background="#f5f5f5"
|
android:background="#f5f5f5"
|
||||||
tools:context=".ui.home.HomeFragment">
|
tools:context=".ui.fragments.HomeFragment">
|
||||||
|
|
||||||
<TextView
|
<ScrollView
|
||||||
android:id="@+id/text_home"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="match_parent">
|
||||||
android:layout_marginStart="8dp"
|
|
||||||
android:layout_marginTop="8dp"
|
<LinearLayout
|
||||||
android:layout_marginEnd="8dp"
|
android:layout_width="match_parent"
|
||||||
android:textAlignment="center"
|
android:layout_height="wrap_content"
|
||||||
android:textSize="20sp"
|
android:orientation="vertical" >
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
<LinearLayout
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
android:layout_width="match_parent"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingBottom="7dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_my_list"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingStart="10dp"
|
||||||
|
android:paddingTop="15dp"
|
||||||
|
android:paddingEnd="5dp"
|
||||||
|
android:paddingBottom="5dp"
|
||||||
|
android:text="@string/my_list"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/recycler_my_list"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||||
|
tools:listitem="@layout/item_media" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingBottom="7dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_new_episodes"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingStart="10dp"
|
||||||
|
android:paddingTop="15dp"
|
||||||
|
android:paddingEnd="5dp"
|
||||||
|
android:paddingBottom="5dp"
|
||||||
|
android:text="@string/new_episodes"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/recycler_new_episodes"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||||
|
tools:listitem="@layout/item_media" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -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
@ -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">
|
||||||
|
|
||||||
<ImageView
|
<FrameLayout
|
||||||
android:id="@+id/image_poster"
|
android:layout_width="match_parent"
|
||||||
android:layout_width="wrap_content"
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/image_backdrop"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:adjustViewBounds="false"
|
||||||
|
android:maxHeight="231dp"
|
||||||
|
android:minHeight="220dp"
|
||||||
|
android:scaleType="centerCrop" />
|
||||||
|
|
||||||
|
<com.google.android.material.imageview.ShapeableImageView
|
||||||
|
android:id="@+id/image_poster"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="200dp"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
app:shapeAppearance="@style/ShapeAppearance.Teapod.RoundedPoster"
|
||||||
|
app:srcCompat="@drawable/ic_launcher_background" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/linear_media_info"
|
||||||
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:layout_marginTop="10dp"
|
||||||
android:layout_marginTop="20dp"
|
android:gravity="center"
|
||||||
android:minHeight="200dp"
|
android:orientation="horizontal">
|
||||||
android:src="@drawable/ic_launcher_background" />
|
|
||||||
|
|
||||||
<Button
|
<TextView
|
||||||
|
android:id="@+id/text_year"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:padding="2dp"
|
||||||
|
android:text="@string/text_year_ex" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_age"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="7dp"
|
||||||
|
android:background="@drawable/shape_rounden_corner"
|
||||||
|
android:paddingStart="3dp"
|
||||||
|
android:paddingTop="2dp"
|
||||||
|
android:paddingEnd="3dp"
|
||||||
|
android:paddingBottom="2dp"
|
||||||
|
android:text="@string/text_age_ex" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_episodes_or_runtime"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="7dp"
|
||||||
|
android:padding="2dp"
|
||||||
|
android:text="@string/text_episodes_count" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/button_play"
|
android:id="@+id/button_play"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
|
||||||
android:layout_marginStart="7dp"
|
android:layout_marginStart="7dp"
|
||||||
android:layout_marginTop="24dp"
|
android:layout_marginTop="6dp"
|
||||||
android:layout_marginEnd="7dp"
|
android:layout_marginEnd="7dp"
|
||||||
android:background="#4A4141"
|
android:gravity="center"
|
||||||
android:drawableStart="@drawable/ic_baseline_play_arrow_24"
|
|
||||||
android:drawablePadding="10dp"
|
|
||||||
android:drawableTint="#FFFFFF"
|
|
||||||
android:gravity="start|center_vertical"
|
|
||||||
android:paddingStart="160dp"
|
|
||||||
android:paddingEnd="160dp"
|
|
||||||
android:text="@string/button_play"
|
android:text="@string/button_play"
|
||||||
android:textAllCaps="false"
|
android:textAllCaps="false"
|
||||||
android:textColor="@android:color/primary_text_dark"
|
android:textColor="@android:color/primary_text_dark"
|
||||||
android:textSize="16sp" />
|
android:textSize="16sp"
|
||||||
|
app:backgroundTint="#4A4141"
|
||||||
|
app:icon="@drawable/ic_baseline_play_arrow_24"
|
||||||
|
app:iconGravity="textStart" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/text_title"
|
android:id="@+id/text_title"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="19dp"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
android:layout_marginStart="7dp"
|
android:layout_marginStart="7dp"
|
||||||
android:layout_marginTop="12dp"
|
android:layout_marginTop="12dp"
|
||||||
android:layout_marginEnd="7dp"
|
android:layout_marginEnd="7dp"
|
||||||
android:text="TextView"
|
android:text="@string/text_title_ex"
|
||||||
android:textStyle="bold" />
|
android:textStyle="bold" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/text_desc"
|
android:id="@+id/text_overview"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
android:layout_marginStart="7dp"
|
android:layout_marginStart="12dp"
|
||||||
android:layout_marginTop="10dp"
|
android:layout_marginTop="7dp"
|
||||||
android:layout_marginEnd="7dp"
|
android:layout_marginEnd="12dp"
|
||||||
android:text="TextView" />
|
android:text="@string/text_overview_ex" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/linear_actions"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:layout_marginTop="7dp"
|
||||||
|
android:layout_marginEnd="12dp"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/linear_my_list_action"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center_horizontal"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/image_my_list_action"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:src="@drawable/ic_baseline_add_24"
|
||||||
|
app:tint="#4A4141" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_my_list_action"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/my_list" />
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/recycler_episodes"
|
android:id="@+id/recycler_episodes"
|
||||||
@ -74,7 +151,8 @@
|
|||||||
android:layout_marginStart="7dp"
|
android:layout_marginStart="7dp"
|
||||||
android:layout_marginTop="17dp"
|
android:layout_marginTop="17dp"
|
||||||
android:layout_marginEnd="7dp"
|
android:layout_marginEnd="7dp"
|
||||||
tools:layout_editor_absoluteY="298dp" />
|
tools:layout_editor_absoluteY="298dp"
|
||||||
|
tools:listitem="@layout/item_episode" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</androidx.core.widget.NestedScrollView>
|
</androidx.core.widget.NestedScrollView>
|
||||||
|
|
||||||
|
@ -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
@ -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
@ -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>
|
|
@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@drawable/ic_launcher_background" />
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@drawable/ic_launcher_background" />
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 989 B |
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 7.2 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 4.3 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 10 KiB |
@ -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
@ -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>
|
42
app/src/main/res/values-de-rDE/strings.xml
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="title_home">Startseite</string>
|
||||||
|
<string name="title_library">Übersicht</string>
|
||||||
|
<string name="title_search">Suche</string>
|
||||||
|
<string name="title_account">Account</string>
|
||||||
|
|
||||||
|
<!-- home fragment -->
|
||||||
|
<string name="my_list">Meine Liste</string>
|
||||||
|
<string name="new_episodes">Neue Episoden</string>
|
||||||
|
|
||||||
|
<!-- search fragment -->
|
||||||
|
<string name="search_hint">Suche nach Filmen und Serien</string>
|
||||||
|
|
||||||
|
<!-- media fragment -->
|
||||||
|
<string name="button_play">Abspielen</string>
|
||||||
|
<string name="text_episodes_count">%1$d Episoden</string>
|
||||||
|
<string name="text_runtime">%1$d Minuten</string>
|
||||||
|
<string name="component_episode_title">Episode %1$d %2$s</string>
|
||||||
|
<string name="component_episode_title_sub">Episode %1$d %2$s (OmU)</string>
|
||||||
|
|
||||||
|
<!-- settings fragment -->
|
||||||
|
<string name="account">Account</string>
|
||||||
|
<string name="account_login_desc">Zum bearbeiten tippen</string>
|
||||||
|
<string name="info">Info</string>
|
||||||
|
<string name="info_about_desc">Version %1$s (%2$s)</string>
|
||||||
|
<string name="info_about_dialog">Diese App wird unter den Bedingungen der GNU GPL 3 oder höher zur Verfügung gestellt. Weiter Informationen findest du unter: \ngit.mosad.xyz/Seil0/teapod \n\n© 2020 seil0@mosad.xyz</string>
|
||||||
|
<string name="licenses">Lizenzen</string>
|
||||||
|
<string name="settings">Einstellungen</string>
|
||||||
|
<string name="settings_secondary">Bevorzuge alternativen Stream</string>
|
||||||
|
<string name="settings_secondary_desc">Untertitle-Stream verwenden, sofern vorhanden</string>
|
||||||
|
|
||||||
|
<!-- dialogs -->
|
||||||
|
<string name="save">speichern</string>
|
||||||
|
<string name="cancel">@android:string/cancel</string>
|
||||||
|
|
||||||
|
<!-- etc -->
|
||||||
|
<string name="login">Login</string>
|
||||||
|
<string name="login_desc">Um Teapod zu benutzen musst du eingeloggt sein. Die Login-Daten weden verschüsselt aud deinem Gerät gespeichert.</string>
|
||||||
|
<string name="login_failed_desc">Login nicht erfolgreich. Bitte versuche es erneut.</string>
|
||||||
|
<string name="password">Passwort</string>
|
||||||
|
</resources>
|
@ -3,4 +3,6 @@
|
|||||||
<color name="colorPrimary">#6200EE</color>
|
<color name="colorPrimary">#6200EE</color>
|
||||||
<color name="colorPrimaryDark">#3700B3</color>
|
<color name="colorPrimaryDark">#3700B3</color>
|
||||||
<color name="colorAccent">#03DAC5</color>
|
<color name="colorAccent">#03DAC5</color>
|
||||||
|
|
||||||
|
<color name="ic_launcher_background">#FFFFFF</color>
|
||||||
</resources>
|
</resources>
|
@ -1,24 +1,43 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">Teapod</string>
|
<string name="app_name" translatable="false">Teapod</string>
|
||||||
<string name="title_home">Home</string>
|
<string name="title_home">Home</string>
|
||||||
<string name="title_library">Library</string>
|
<string name="title_library">Library</string>
|
||||||
<string name="title_search">Search</string>
|
<string name="title_search">Search</string>
|
||||||
<string name="title_account">Account</string>
|
<string name="title_account">Account</string>
|
||||||
|
|
||||||
|
<!-- home fragment -->
|
||||||
|
<string name="my_list">My list</string>
|
||||||
|
<string name="new_episodes">New episodes</string>
|
||||||
|
|
||||||
<!-- search fragment -->
|
<!-- search fragment -->
|
||||||
<string name="search_hint">Search for movies and series</string>
|
<string name="search_hint">Search for movies and series</string>
|
||||||
|
<string name="media_poster_desc" translatable="false">poster</string>
|
||||||
|
|
||||||
<!-- media fragment -->
|
<!-- media fragment -->
|
||||||
<string name="button_play">Play</string>
|
<string name="button_play">Play</string>
|
||||||
|
<string name="text_title_ex" translatable="false">A Silent Voice</string>
|
||||||
|
<string name="text_overview_ex" translatable="false">Shouya Ishida starts bullying the new girl in class …</string>
|
||||||
|
<string name="text_year_ex" translatable="false">2016</string>
|
||||||
|
<string name="text_age_ex" translatable="false">6</string>
|
||||||
|
<string name="text_episodes_count">%1$d episodes</string>
|
||||||
|
<string name="text_runtime">%1$d Minutes</string>
|
||||||
|
<string name="component_episode_title">Episode %1$d %2$s</string>
|
||||||
|
<string name="component_episode_title_sub">Episode %1$d %2$s (Sub)</string>
|
||||||
|
<string name="component_poster_desc" translatable="false">episode poster</string>
|
||||||
|
<string name="component_watched_desc" translatable="false">already watched</string>
|
||||||
|
|
||||||
<!-- settings fragment -->
|
<!-- settings fragment -->
|
||||||
<string name="account">Account</string>
|
<string name="account">Account</string>
|
||||||
<string name="account_login_ex">user@example.com</string>
|
<string name="account_login_ex" translatable="false">user@example.com</string>
|
||||||
<string name="account_login_desc">Tap to edit</string>
|
<string name="account_login_desc">Tap to edit</string>
|
||||||
<string name="info">Info</string>
|
<string name="info">Info</string>
|
||||||
<string name="info_about">Teapod by @Seil0</string>
|
<string name="info_about" translatable="false">Teapod by @Seil0</string>
|
||||||
<string name="info_about_desc" translatable="false">Version %1$s (%2$s)</string>
|
<string name="info_about_desc">Version %1$s (%2$s)</string>
|
||||||
<string name="info_about_dialog" translatable="false">This software is published under the terms and conditions of GPL 3. For further information visit git.mosad.xyz/Seil0 \n\n© 2020 seil0@mosad.xyz</string>
|
<string name="info_about_dialog">This app is published under the terms and conditions of the GNU GPL 3 or later. For further information visit: \ngit.mosad.xyz/Seil0/teapod \n\n© 2020 seil0@mosad.xyz</string>
|
||||||
|
<string name="licenses">Licenses</string>
|
||||||
|
<string name="settings">Settings</string>
|
||||||
|
<string name="settings_secondary">Prefer secondary (sub) stream</string>
|
||||||
|
<string name="settings_secondary_desc">Use the subtitles stream if present</string>
|
||||||
|
|
||||||
<!-- dialogs -->
|
<!-- dialogs -->
|
||||||
<string name="save">save</string>
|
<string name="save">save</string>
|
||||||
@ -26,13 +45,16 @@
|
|||||||
|
|
||||||
<!-- etc -->
|
<!-- etc -->
|
||||||
<string name="login">Login</string>
|
<string name="login">Login</string>
|
||||||
<string name="login_desc">You need to login before you can use Teapod. Your Login-Data will be stored encrypted on your device.</string>
|
<string name="login_desc">You need to login before you can use Teapod. The Login-Data will be stored encrypted on your device.</string>
|
||||||
|
<string name="login_failed_desc">Could not login. Please try again.</string>
|
||||||
<string name="password">Password</string>
|
<string name="password">Password</string>
|
||||||
|
|
||||||
<!-- save keys -->
|
<!-- save keys -->
|
||||||
<string name="encrypted_preference_file_key" translatable="false">org.mosad.teapod.encrypted_preferences</string>
|
<string name="encrypted_preference_file_key" translatable="false">org.mosad.teapod.encrypted_preferences</string>
|
||||||
|
<string name="preference_file_key" translatable="false">org.mosad.teapod.preferences</string>
|
||||||
<string name="save_key_user_login" translatable="false">org.mosad.teapod.user_login</string>
|
<string name="save_key_user_login" translatable="false">org.mosad.teapod.user_login</string>
|
||||||
<string name="save_key_user_password" translatable="false">org.mosad.teapod.user_password</string>
|
<string name="save_key_user_password" translatable="false">org.mosad.teapod.user_password</string>
|
||||||
|
<string name="save_key_prefer_secondary" translatable="false">org.mosad.teapod.prefer_secondary</string>
|
||||||
|
|
||||||
<!-- intents & states -->
|
<!-- intents & states -->
|
||||||
<string name="intent_stream_url" translatable="false">intent_stream_url</string>
|
<string name="intent_stream_url" translatable="false">intent_stream_url</string>
|
||||||
|
@ -1,18 +1,27 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<!-- Base application theme. -->
|
<!-- Base application theme. -->
|
||||||
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
|
<style name="AppTheme" parent="Theme.MaterialComponents.Light.NoActionBar">
|
||||||
<!-- Customize your theme here. -->
|
<!-- Customize your theme here. -->
|
||||||
<item name="colorPrimary">@color/colorPrimary</item>
|
<item name="colorPrimary">@color/colorPrimary</item>
|
||||||
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||||
<item name="colorAccent">@color/colorAccent</item>
|
<item name="colorAccent">@color/colorAccent</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="AppTheme.AppCompat.Light.NoActionBar.FullScreen" parent="@style/Theme.AppCompat.Light.NoActionBar">
|
<style name="PlayerTheme" parent="Theme.MaterialComponents.Light.NoActionBar">
|
||||||
<item name="android:windowNoTitle">true</item>
|
<item name="android:windowNoTitle">true</item>
|
||||||
<item name="android:windowActionBar">false</item>
|
<item name="android:windowActionBar">false</item>
|
||||||
<item name="android:windowFullscreen">true</item>
|
<item name="android:windowFullscreen">true</item>
|
||||||
<item name="android:windowContentOverlay">@null</item>
|
<item name="android:windowContentOverlay">@null</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style name="SplashTheme" parent="Theme.AppCompat.NoActionBar">
|
||||||
|
<item name="android:windowBackground">@drawable/bg_splash</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<!-- shapes -->
|
||||||
|
<style name="ShapeAppearance.Teapod.RoundedPoster" parent="ShapeAppearance.MaterialComponents.LargeComponent">
|
||||||
|
<item name="cornerFamily">rounded</item>
|
||||||
|
<item name="cornerSize">5dp</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
@ -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
|
||||||
|
342
etc/drawable_resources/icon.svg
Normal file
After Width: | Height: | Size: 548 KiB |
1
fastlane/metadata/android/de-DE/changelogs/1000.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
Version 0.1 ist der erste öffentliche Release von Teapod.
|
11
fastlane/metadata/android/de-DE/full_description.txt
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
Teapod ist eine inoffizielle App für Anime-on-Demand (AoD).
|
||||||
|
|
||||||
|
* Schau dir alle Title von AoD auf deinem Android Gerät an
|
||||||
|
* Nativer Player auf Basis des ExoPayers
|
||||||
|
* Bevorzuge die OmU Version über die App-Einstellungen
|
||||||
|
* Speicher deine lieblings Anime in "Meine Liste"
|
||||||
|
|
||||||
|
Zur Verfügung gestellt unter den Bedingungen der GNU GPL 3 oder höher.
|
||||||
|
Dieses Projekt ist in keiner Weise mit Anime-on-Demand verbunden.
|
||||||
|
|
||||||
|
Bitte melde Fehler und Probleme an support@mosad.xyz
|
After Width: | Height: | Size: 848 KiB |
After Width: | Height: | Size: 1.5 MiB |
After Width: | Height: | Size: 572 KiB |
After Width: | Height: | Size: 1.2 MiB |
1
fastlane/metadata/android/de-DE/short_description.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
Android App für AoD
|
1
fastlane/metadata/android/de-DE/title.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
Teapod
|
11
fastlane/metadata/android/en-US/full_description.txt
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
Teapod is a unoffical App for Anime-on-Demand (AoD).
|
||||||
|
|
||||||
|
* Watch all animes from AoD on your Android device
|
||||||
|
* Native Player based on ExoPayer
|
||||||
|
* Prefer the OmU version via the app settings
|
||||||
|
* Save your favorite animes to "My List"
|
||||||
|
|
||||||
|
Licensed under the terms and conditions of GPL 3.
|
||||||
|
This Project is not associated with Anime-on-Demand in any way.
|
||||||
|
|
||||||
|
Please report bugs and issues to support@mosad.xyz
|
1
fastlane/metadata/android/en-US/short_description.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
Android App for AoD
|
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
|
||||||
|