Compare commits
82 Commits
0.1-alpha1
...
0.2.1
Author | SHA1 | Date | |
---|---|---|---|
77e657d37c
|
|||
20407d9cac
|
|||
dbd4b26a65
|
|||
ac5aee20de
|
|||
32844223fc
|
|||
d01e87bf14
|
|||
bb8c8ca85a
|
|||
3ed55ca3c9
|
|||
dfaf359952
|
|||
78d9f3cfa5
|
|||
db5758edf9
|
|||
2de1419d36
|
|||
7df99ea0cc
|
|||
8d1c3d9a3f
|
|||
c0c5cb9110
|
|||
21b6e358e7
|
|||
0e5c697bce
|
|||
830f7e753b
|
|||
71079ddc92
|
|||
57897077ab
|
|||
dcd6ebccea
|
|||
91c9b6d716
|
|||
256c32aa3c
|
|||
3880b3ab75
|
|||
0f0573e5bd
|
|||
6ce263832b
|
|||
fd099e97e6
|
|||
d4fa726f9c
|
|||
c8d80ddc9f
|
|||
14377c3f18
|
|||
23713fc1e6
|
|||
353ae6937a
|
|||
2e0a114a80
|
|||
0e9500e39d
|
|||
27e8e1c3c2
|
|||
e51fb0b290
|
|||
d3f078c661
|
|||
6526b8868e | |||
1118c8339c
|
|||
1595ef52bc
|
|||
406434809f | |||
1523e0235a
|
|||
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
@ -1,27 +1,39 @@
|
|||||||
# teapod
|
# teapod
|
||||||
|
|
||||||
A unoffical App for Anime-on-Demand.
|
A unofficial App for Anime-on-Demand.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
* 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 "My List", for easier access
|
||||||
|
* prefer the OmU version via the app settings
|
||||||
|
|
||||||
## 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,17 +10,22 @@ android {
|
|||||||
applicationId "org.mosad.teapod"
|
applicationId "org.mosad.teapod"
|
||||||
minSdkVersion 23
|
minSdkVersion 23
|
||||||
targetSdkVersion 30
|
targetSdkVersion 30
|
||||||
versionCode 1
|
versionCode 2100 //00.02.100
|
||||||
versionName "0.1-alpha1"
|
versionName "0.2.1"
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
resValue "string", "build_time", buildTime()
|
resValue "string", "build_time", buildTime()
|
||||||
setProperty("archivesBaseName", "teapod-$versionName")
|
setProperty("archivesBaseName", "teapod-$versionName")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buildFeatures {
|
||||||
|
viewBinding true
|
||||||
|
}
|
||||||
|
|
||||||
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 +45,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-alpha04'
|
||||||
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
|
||||||
|
@ -10,21 +10,26 @@
|
|||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
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.Light">
|
||||||
<activity
|
<activity
|
||||||
android:name=".PlayerActivity"
|
android:name=".SplashActivity"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:configChanges="orientation|screenSize|layoutDirection"
|
android:theme="@style/SplashTheme"
|
||||||
android:theme="@style/AppTheme.AppCompat.Light.NoActionBar.FullScreen" />
|
android:screenOrientation="portrait">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name=".player.PlayerActivity"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:theme="@style/PlayerTheme"
|
||||||
|
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,44 +1,79 @@
|
|||||||
|
/**
|
||||||
|
* 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 kotlinx.android.synthetic.main.activity_main.*
|
import com.google.android.material.bottomnavigation.BottomNavigationView
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.mosad.teapod.databinding.ActivityMainBinding
|
||||||
import org.mosad.teapod.parser.AoDParser
|
import org.mosad.teapod.parser.AoDParser
|
||||||
|
import org.mosad.teapod.player.PlayerActivity
|
||||||
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.DataTypes
|
||||||
import org.mosad.teapod.ui.search.SearchFragment
|
import org.mosad.teapod.util.StorageController
|
||||||
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 lateinit var binding: ActivityMainBinding
|
||||||
|
private var activeBaseFragment: Fragment = HomeFragment() // the currently active fragment, home at the start
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
var wasInitialized = false
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_main)
|
|
||||||
val navView: BottomNavigationView = findViewById(R.id.nav_view)
|
|
||||||
navView.setOnNavigationItemSelectedListener(this)
|
|
||||||
|
|
||||||
load()
|
if (!wasInitialized) { load() }
|
||||||
|
theme.applyStyle(getThemeResource(), true)
|
||||||
|
|
||||||
|
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||||
|
binding.navView.setOnNavigationItemSelectedListener(this)
|
||||||
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
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
|
binding.navView.selectedItemId = R.id.navigation_home
|
||||||
} else {
|
} else {
|
||||||
super.onBackPressed()
|
super.onBackPressed()
|
||||||
}
|
}
|
||||||
@ -46,54 +81,87 @@ 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 getThemeResource(): Int {
|
||||||
EncryptedPreferences.readCredentials(this)
|
return when (Preferences.theme) {
|
||||||
|
DataTypes.Theme.DARK -> R.style.AppTheme_Dark
|
||||||
if (EncryptedPreferences.password.isEmpty()) {
|
else -> R.style.AppTheme_Light
|
||||||
Log.i(javaClass.name, "please login!")
|
|
||||||
|
|
||||||
LoginDialog(this).positiveButton {
|
|
||||||
EncryptedPreferences.saveCredentials(login, password, context)
|
|
||||||
}.negativeButton {
|
|
||||||
Log.i(javaClass.name, "Login canceled, exiting.")
|
|
||||||
finish()
|
|
||||||
}.show()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showDetailFragment(media: Media) {
|
private fun load() {
|
||||||
media.episodes = AoDParser().loadStreams(media) // load the streams for the selected media
|
// running login and list in parallel does not bring any speed improvements
|
||||||
|
val time = measureTimeMillis {
|
||||||
|
Preferences.load(this)
|
||||||
|
|
||||||
val tmdb = TMDBApiController().search(media.title, media.type)
|
// make sure credentials are set
|
||||||
|
EncryptedPreferences.readCredentials(this)
|
||||||
|
if (EncryptedPreferences.password.isEmpty()) {
|
||||||
|
showLoginDialog(true)
|
||||||
|
} else {
|
||||||
|
// try to login in, as most sites can only bee loaded once loged in
|
||||||
|
if (!AoDParser.login()) showLoginDialog(false)
|
||||||
|
}
|
||||||
|
|
||||||
val mediaFragment = MediaFragment(media, tmdb)
|
StorageController.load(this)
|
||||||
|
AoDParser.initialLoading()
|
||||||
|
|
||||||
|
wasInitialized = true
|
||||||
|
}
|
||||||
|
Log.i(javaClass.name, "login and list in $time ms")
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the media fragment for the selected media.
|
||||||
|
* The media fragment is not stored in activeBaseFragment,
|
||||||
|
* as it doesn't replace a fragment but is added on top of one.
|
||||||
|
*/
|
||||||
|
fun showMediaFragment(mediaId: Int) = GlobalScope.launch {
|
||||||
|
val mediaFragment = MediaFragment(mediaId)
|
||||||
supportFragmentManager.commit {
|
supportFragmentManager.commit {
|
||||||
add(R.id.nav_host_fragment, mediaFragment, "MediaFragment")
|
add(R.id.nav_host_fragment, mediaFragment, "MediaFragment")
|
||||||
addToBackStack(null)
|
addToBackStack(null)
|
||||||
@ -101,10 +169,23 @@ class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemS
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startPlayer(streamUrl: String) {
|
fun startPlayer(mediaId: Int, episodeId: Int) {
|
||||||
val intent = Intent(this, PlayerActivity::class.java).apply {
|
val intent = Intent(this, PlayerActivity::class.java).apply {
|
||||||
putExtra(getString(R.string.intent_stream_url), streamUrl)
|
putExtra(getString(R.string.intent_media_id), mediaId)
|
||||||
|
putExtra(getString(R.string.intent_episode_id), episodeId)
|
||||||
}
|
}
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* use custom restart instead of recreate(), since it has animations
|
||||||
|
*/
|
||||||
|
fun restart() {
|
||||||
|
val restartIntent = intent
|
||||||
|
restartIntent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
|
||||||
|
finish()
|
||||||
|
startActivity(restartIntent)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
@ -1,153 +0,0 @@
|
|||||||
package org.mosad.teapod
|
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.util.Log
|
|
||||||
import android.view.View
|
|
||||||
import android.view.WindowInsets
|
|
||||||
import android.view.WindowInsetsController
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import com.google.android.exoplayer2.ExoPlayer
|
|
||||||
import com.google.android.exoplayer2.MediaItem
|
|
||||||
import com.google.android.exoplayer2.Player
|
|
||||||
import com.google.android.exoplayer2.SimpleExoPlayer
|
|
||||||
import com.google.android.exoplayer2.source.hls.HlsMediaSource
|
|
||||||
import com.google.android.exoplayer2.upstream.DataSource
|
|
||||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory
|
|
||||||
import com.google.android.exoplayer2.util.Util
|
|
||||||
import kotlinx.android.synthetic.main.activity_player.*
|
|
||||||
|
|
||||||
|
|
||||||
class PlayerActivity : AppCompatActivity() {
|
|
||||||
|
|
||||||
private lateinit var player: SimpleExoPlayer
|
|
||||||
private lateinit var dataSourceFactory: DataSource.Factory
|
|
||||||
|
|
||||||
private var streamUrl = ""
|
|
||||||
|
|
||||||
private var playWhenReady = true
|
|
||||||
private var currentWindow = 0
|
|
||||||
private var playbackPosition: Long = 0
|
|
||||||
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
setContentView(R.layout.activity_player)
|
|
||||||
hideBars() // Initial hide the bars
|
|
||||||
|
|
||||||
savedInstanceState?.let {
|
|
||||||
currentWindow = it.getInt(getString(R.string.state_resume_window))
|
|
||||||
playbackPosition = it.getLong(getString(R.string.state_resume_position))
|
|
||||||
playWhenReady = it.getBoolean(getString(R.string.state_is_playing))
|
|
||||||
}
|
|
||||||
|
|
||||||
streamUrl = intent.getStringExtra(getString(R.string.intent_stream_url)).toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
override fun onStart() {
|
|
||||||
super.onStart()
|
|
||||||
if (Util.SDK_INT > 23) {
|
|
||||||
initPlayer()
|
|
||||||
if (video_view != null) video_view.onResume()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
if (Util.SDK_INT <= 23) {
|
|
||||||
initPlayer()
|
|
||||||
if (video_view != null) video_view.onResume()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPause() {
|
|
||||||
super.onPause()
|
|
||||||
if (Util.SDK_INT <= 23) {
|
|
||||||
if (video_view != null) video_view.onPause()
|
|
||||||
releasePlayer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStop() {
|
|
||||||
super.onStop()
|
|
||||||
if (Util.SDK_INT > 23) {
|
|
||||||
if (video_view != null) video_view.onPause()
|
|
||||||
releasePlayer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
|
||||||
outState.putInt(getString(R.string.state_resume_window), currentWindow)
|
|
||||||
outState.putLong(getString(R.string.state_resume_position), playbackPosition)
|
|
||||||
outState.putBoolean(getString(R.string.state_is_playing), playWhenReady)
|
|
||||||
super.onSaveInstanceState(outState)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initPlayer() {
|
|
||||||
if (streamUrl.isEmpty()) {
|
|
||||||
Log.e(javaClass.name, "No stream url was set.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
player = SimpleExoPlayer.Builder(this).build()
|
|
||||||
dataSourceFactory = DefaultDataSourceFactory(this, Util.getUserAgent(this, "Teapod"))
|
|
||||||
|
|
||||||
val mediaSource = HlsMediaSource.Factory(dataSourceFactory)
|
|
||||||
.createMediaSource(MediaItem.fromUri(Uri.parse(streamUrl)))
|
|
||||||
|
|
||||||
player.playWhenReady = playWhenReady
|
|
||||||
player.setMediaSource(mediaSource)
|
|
||||||
player.seekTo(playbackPosition)
|
|
||||||
player.prepare()
|
|
||||||
|
|
||||||
|
|
||||||
player.addListener(object : Player.EventListener {
|
|
||||||
override fun onPlaybackStateChanged(state: Int) {
|
|
||||||
super.onPlaybackStateChanged(state)
|
|
||||||
|
|
||||||
loading.visibility = when (state) {
|
|
||||||
ExoPlayer.STATE_READY -> View.GONE
|
|
||||||
ExoPlayer.STATE_BUFFERING -> View.VISIBLE
|
|
||||||
else -> View.GONE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// when the player controls get hidden, hide the bars too
|
|
||||||
video_view.setControllerVisibilityListener {
|
|
||||||
if (it == View.GONE) hideBars()
|
|
||||||
}
|
|
||||||
|
|
||||||
video_view.player = player
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun releasePlayer(){
|
|
||||||
playbackPosition = player.currentPosition
|
|
||||||
currentWindow = player.currentWindowIndex
|
|
||||||
playWhenReady = player.playWhenReady
|
|
||||||
player.release()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* hide the status and navigation bar
|
|
||||||
*/
|
|
||||||
private fun hideBars() {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
||||||
window.setDecorFitsSystemWindows(false)
|
|
||||||
window.insetsController?.apply {
|
|
||||||
hide(WindowInsets.Type.statusBars() or WindowInsets.Type.navigationBars())
|
|
||||||
systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_BARS_BY_SWIPE
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
@Suppress("deprecation")
|
|
||||||
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_IMMERSIVE
|
|
||||||
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
|
||||||
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
|
||||||
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
|
||||||
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
|
||||||
or View.SYSTEM_UI_FLAG_FULLSCREEN)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
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),
|
||||||
@ -51,24 +73,79 @@ class AoDParser {
|
|||||||
|
|
||||||
val resLogin = Jsoup.connect(baseUrl + loginPath)
|
val resLogin = Jsoup.connect(baseUrl + loginPath)
|
||||||
.method(Connection.Method.POST)
|
.method(Connection.Method.POST)
|
||||||
|
.timeout(60000) // login can take some time
|
||||||
.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
|
||||||
|
*/
|
||||||
|
suspend fun getMediaById(mediaId: Int): Media {
|
||||||
|
val media = mediaList.first { it.id == mediaId }
|
||||||
|
|
||||||
|
if (media.episodes.isEmpty()) {
|
||||||
|
loadStreams(media).join()
|
||||||
|
}
|
||||||
|
|
||||||
|
return media
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO don't use jsoup here
|
||||||
|
fun sendCallback(callbackPath: String) = GlobalScope.launch(Dispatchers.IO) {
|
||||||
|
val headers = mutableMapOf(
|
||||||
|
Pair("Accept", "application/json, text/javascript, */*; q=0.01"),
|
||||||
|
Pair("Accept-Language", "de,en-US;q=0.7,en;q=0.3"),
|
||||||
|
Pair("Accept-Encoding", "gzip, deflate, br"),
|
||||||
|
Pair("X-CSRF-Token", csrfToken),
|
||||||
|
Pair("X-Requested-With", "XMLHttpRequest"),
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
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 +155,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 +163,174 @@ 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()
|
if (sessionCookies.isEmpty()) login()
|
||||||
|
|
||||||
if (!loginSuccess) {
|
|
||||||
println("please log in") // TODO
|
|
||||||
return@runBlocking listOf()
|
|
||||||
}
|
|
||||||
|
|
||||||
withContext(Dispatchers.Default) {
|
withContext(Dispatchers.Default) {
|
||||||
|
val resHome = Jsoup.connect(baseUrl)
|
||||||
val res = Jsoup.connect(baseUrl + media.link)
|
|
||||||
.cookies(sessionCookies)
|
.cookies(sessionCookies)
|
||||||
.get()
|
.get()
|
||||||
|
|
||||||
//println(res)
|
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()}"
|
||||||
|
|
||||||
val playlists = res.select("input.streamstarter_html5").eachAttr("data-playlist")
|
newEpisodesList.add(ItemMedia(mediaId, mediaTitle, mediaImage))
|
||||||
val csrfToken = res.select("meta[name=csrf-token]").attr("content")
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//println("first entry: ${playlists.first()}")
|
|
||||||
//println("csrf token is: $csrfToken")
|
|
||||||
|
|
||||||
return@withContext loadStreamInfo(playlists.first(), csrfToken, media.type)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* load the playlist path and parse it, read the stream info from json
|
* load streams for the media path, movies have one episode
|
||||||
|
* @param media is used as call ba reference
|
||||||
*/
|
*/
|
||||||
private fun loadStreamInfo(playlistPath: String, csrfToken: String, type: MediaType): List<Episode> = runBlocking {
|
private suspend fun loadStreams(media: Media) = GlobalScope.launch(Dispatchers.IO) {
|
||||||
withContext(Dispatchers.Default) {
|
if (sessionCookies.isEmpty()) login()
|
||||||
|
|
||||||
|
if (!loginSuccess) {
|
||||||
|
Log.w(javaClass.name, "Login, was not successful.")
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the media page
|
||||||
|
val res = Jsoup.connect(baseUrl + media.link)
|
||||||
|
.cookies(sessionCookies)
|
||||||
|
.get()
|
||||||
|
|
||||||
|
//println(res)
|
||||||
|
|
||||||
|
if (csrfToken.isEmpty()) {
|
||||||
|
csrfToken = res.select("meta[name=csrf-token]").attr("content")
|
||||||
|
//Log.i(javaClass.name, "New csrf token is $csrfToken")
|
||||||
|
}
|
||||||
|
|
||||||
|
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 playlist 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 the episode title (description) is loaded from the "api"
|
||||||
|
if (media.type == MediaType.TVSHOW) {
|
||||||
|
res.select("div.three-box-container > div.episodebox").forEach { episodebox ->
|
||||||
|
// make sure the episode has a streaming link
|
||||||
|
if (episodebox.select("input.streamstarter_html5").isNotEmpty()) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* don't use Gson().fromJson() as we don't have any control over the api and it may change
|
||||||
|
*/
|
||||||
|
private fun parsePlaylistAsync(playlistPath: String): Deferred<AoDObject> {
|
||||||
|
if (playlistPath == "[]") {
|
||||||
|
return CompletableDeferred(AoDObject(listOf()))
|
||||||
|
}
|
||||||
|
|
||||||
|
return GlobalScope.async(Dispatchers.IO) {
|
||||||
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 +339,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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
447
app/src/main/java/org/mosad/teapod/player/PlayerActivity.kt
Normal file
@ -0,0 +1,447 @@
|
|||||||
|
package org.mosad.teapod.player
|
||||||
|
|
||||||
|
import android.animation.Animator
|
||||||
|
import android.animation.AnimatorListenerAdapter
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.*
|
||||||
|
import androidx.activity.viewModels
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.view.GestureDetectorCompat
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import com.google.android.exoplayer2.ExoPlayer
|
||||||
|
import com.google.android.exoplayer2.MediaItem
|
||||||
|
import com.google.android.exoplayer2.Player
|
||||||
|
import com.google.android.exoplayer2.SimpleExoPlayer
|
||||||
|
import com.google.android.exoplayer2.source.hls.HlsMediaSource
|
||||||
|
import com.google.android.exoplayer2.ui.StyledPlayerControlView
|
||||||
|
import com.google.android.exoplayer2.upstream.DataSource
|
||||||
|
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory
|
||||||
|
import com.google.android.exoplayer2.util.Util
|
||||||
|
import kotlinx.android.synthetic.main.activity_player.*
|
||||||
|
import kotlinx.android.synthetic.main.player_controls.*
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.mosad.teapod.R
|
||||||
|
import org.mosad.teapod.preferences.Preferences
|
||||||
|
import org.mosad.teapod.util.DataTypes
|
||||||
|
import org.mosad.teapod.util.Episode
|
||||||
|
import java.util.*
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import kotlin.concurrent.scheduleAtFixedRate
|
||||||
|
|
||||||
|
class PlayerActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
private val model: PlayerViewModel by viewModels()
|
||||||
|
|
||||||
|
private lateinit var player: SimpleExoPlayer
|
||||||
|
private lateinit var dataSourceFactory: DataSource.Factory
|
||||||
|
private lateinit var controller: StyledPlayerControlView
|
||||||
|
private lateinit var gestureDetector: GestureDetectorCompat
|
||||||
|
private lateinit var timerUpdates: TimerTask
|
||||||
|
|
||||||
|
private var nextEpManually = false
|
||||||
|
private var playWhenReady = true
|
||||||
|
private var currentWindow = 0
|
||||||
|
private var playbackPosition: Long = 0
|
||||||
|
private var remainingTime: Long = 0
|
||||||
|
|
||||||
|
private val rwdTime = 10000
|
||||||
|
private val fwdTime = 10000
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(R.layout.activity_player)
|
||||||
|
hideBars() // Initial hide the bars
|
||||||
|
|
||||||
|
savedInstanceState?.let {
|
||||||
|
currentWindow = it.getInt(getString(R.string.state_resume_window))
|
||||||
|
playbackPosition = it.getLong(getString(R.string.state_resume_position))
|
||||||
|
playWhenReady = it.getBoolean(getString(R.string.state_is_playing))
|
||||||
|
}
|
||||||
|
|
||||||
|
model.loadMedia(
|
||||||
|
intent.getIntExtra(getString(R.string.intent_media_id), 0),
|
||||||
|
intent.getIntExtra(getString(R.string.intent_episode_id), 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
gestureDetector = GestureDetectorCompat(this, PlayerGestureListener())
|
||||||
|
|
||||||
|
initActions()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun onStart() {
|
||||||
|
super.onStart()
|
||||||
|
if (Util.SDK_INT > 23) {
|
||||||
|
initPlayer()
|
||||||
|
video_view?.onResume()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
if (Util.SDK_INT <= 23) {
|
||||||
|
initPlayer()
|
||||||
|
video_view?.onResume()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
super.onPause()
|
||||||
|
if (Util.SDK_INT <= 23) {
|
||||||
|
video_view?.onPause()
|
||||||
|
releasePlayer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStop() {
|
||||||
|
super.onStop()
|
||||||
|
if (Util.SDK_INT > 23) {
|
||||||
|
video_view?.onPause()
|
||||||
|
releasePlayer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
outState.putInt(getString(R.string.state_resume_window), currentWindow)
|
||||||
|
outState.putLong(getString(R.string.state_resume_position), playbackPosition)
|
||||||
|
outState.putBoolean(getString(R.string.state_is_playing), playWhenReady)
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initPlayer() {
|
||||||
|
if (model.mediaId <= 0) {
|
||||||
|
Log.e(javaClass.name, "No media id was set.")
|
||||||
|
this.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
initExoPlayer()
|
||||||
|
initVideoView()
|
||||||
|
initTimeUpdates()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initExoPlayer() {
|
||||||
|
player = SimpleExoPlayer.Builder(this).build()
|
||||||
|
dataSourceFactory = DefaultDataSourceFactory(this, Util.getUserAgent(this, "Teapod"))
|
||||||
|
controller = video_view.findViewById(R.id.exo_controller)
|
||||||
|
|
||||||
|
controller.isAnimationEnabled = false // disable controls (time-bar) animation
|
||||||
|
|
||||||
|
player.playWhenReady = playWhenReady
|
||||||
|
player.addListener(object : Player.EventListener {
|
||||||
|
override fun onPlaybackStateChanged(state: Int) {
|
||||||
|
super.onPlaybackStateChanged(state)
|
||||||
|
|
||||||
|
loading.visibility = when (state) {
|
||||||
|
ExoPlayer.STATE_READY -> View.GONE
|
||||||
|
ExoPlayer.STATE_BUFFERING -> View.VISIBLE
|
||||||
|
else -> View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
exo_play_pause.visibility = when (loading.visibility) {
|
||||||
|
View.GONE -> View.VISIBLE
|
||||||
|
View.VISIBLE -> View.INVISIBLE
|
||||||
|
else -> View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state == ExoPlayer.STATE_ENDED && model.nextEpisode != null && Preferences.autoplay) {
|
||||||
|
if (nextEpManually) {
|
||||||
|
nextEpManually = false
|
||||||
|
} else {
|
||||||
|
playNextEpisode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
playCurrentMedia(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
|
private fun initVideoView() {
|
||||||
|
video_view.player = player
|
||||||
|
|
||||||
|
// when the player controls get hidden, hide the bars too
|
||||||
|
video_view.setControllerVisibilityListener {
|
||||||
|
when (it) {
|
||||||
|
View.GONE -> hideBars()
|
||||||
|
View.VISIBLE -> updateControls()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
video_view.setOnTouchListener { _, event ->
|
||||||
|
gestureDetector.onTouchEvent(event)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initActions() {
|
||||||
|
exo_close_player.setOnClickListener { this.finish() }
|
||||||
|
rwd_10.setOnButtonClickListener { rewind() }
|
||||||
|
ffwd_10.setOnButtonClickListener { fastForward() }
|
||||||
|
button_next_ep.setOnClickListener { playNextEpisode() }
|
||||||
|
button_next_ep_c.setOnClickListener { playNextEpisode() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initTimeUpdates() {
|
||||||
|
if (this::timerUpdates.isInitialized) {
|
||||||
|
timerUpdates.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
timerUpdates = Timer().scheduleAtFixedRate(0, 500) {
|
||||||
|
GlobalScope.launch {
|
||||||
|
var btnNextEpIsVisible: Boolean
|
||||||
|
var controlsVisible: Boolean
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
remainingTime = player.duration - player.currentPosition
|
||||||
|
remainingTime = if (remainingTime < 0) 0 else remainingTime
|
||||||
|
|
||||||
|
btnNextEpIsVisible = button_next_ep.isVisible
|
||||||
|
controlsVisible = controller.isVisible
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remainingTime in 1..20000) {
|
||||||
|
// if the next ep button is not visible, make it visible
|
||||||
|
if (!btnNextEpIsVisible && model.nextEpisode != null && Preferences.autoplay) {
|
||||||
|
withContext(Dispatchers.Main) { showButtonNextEp() }
|
||||||
|
}
|
||||||
|
} else if (btnNextEpIsVisible) {
|
||||||
|
withContext(Dispatchers.Main) { hideButtonNextEp() }
|
||||||
|
}
|
||||||
|
|
||||||
|
// if controls are visible, update them
|
||||||
|
if (controlsVisible) {
|
||||||
|
withContext(Dispatchers.Main) { updateControls() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun releasePlayer(){
|
||||||
|
playbackPosition = player.currentPosition
|
||||||
|
currentWindow = player.currentWindowIndex
|
||||||
|
playWhenReady = player.playWhenReady
|
||||||
|
player.release()
|
||||||
|
timerUpdates.cancel()
|
||||||
|
|
||||||
|
Log.d(javaClass.name, "Released player")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* update the custom controls
|
||||||
|
*/
|
||||||
|
private fun updateControls() {
|
||||||
|
// update remaining time label
|
||||||
|
val hours = TimeUnit.MILLISECONDS.toHours(remainingTime) % 24
|
||||||
|
val minutes = TimeUnit.MILLISECONDS.toMinutes(remainingTime) % 60
|
||||||
|
val seconds = TimeUnit.MILLISECONDS.toSeconds(remainingTime) % 60
|
||||||
|
|
||||||
|
// if remaining time is below 60 minutes, don't show hours
|
||||||
|
exo_remaining.text = if (TimeUnit.MILLISECONDS.toMinutes(remainingTime) < 60) {
|
||||||
|
getString(R.string.time_min_sec, minutes, seconds)
|
||||||
|
} else {
|
||||||
|
getString(R.string.time_hour_min_sec, hours, minutes, seconds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO set position of rewind/fast forward indicators programmatically
|
||||||
|
*/
|
||||||
|
|
||||||
|
private fun rewind() {
|
||||||
|
player.seekTo(player.currentPosition - rwdTime)
|
||||||
|
|
||||||
|
// hide/show needed components
|
||||||
|
exo_double_tap_indicator.visibility = View.VISIBLE
|
||||||
|
ffwd_10_indicator.visibility = View.INVISIBLE
|
||||||
|
rwd_10.visibility = View.INVISIBLE
|
||||||
|
|
||||||
|
rwd_10_indicator.onAnimationEndCallback = {
|
||||||
|
exo_double_tap_indicator.visibility = View.GONE
|
||||||
|
ffwd_10_indicator.visibility = View.VISIBLE
|
||||||
|
rwd_10.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
// run animation
|
||||||
|
rwd_10_indicator.runOnClickAnimation()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fastForward() {
|
||||||
|
player.seekTo(player.currentPosition + fwdTime)
|
||||||
|
|
||||||
|
// hide/show needed components
|
||||||
|
exo_double_tap_indicator.visibility = View.VISIBLE
|
||||||
|
rwd_10_indicator.visibility = View.INVISIBLE
|
||||||
|
ffwd_10.visibility = View.INVISIBLE
|
||||||
|
|
||||||
|
ffwd_10_indicator.onAnimationEndCallback = {
|
||||||
|
exo_double_tap_indicator.visibility = View.GONE
|
||||||
|
rwd_10_indicator.visibility = View.VISIBLE
|
||||||
|
ffwd_10.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
// run animation
|
||||||
|
ffwd_10_indicator.runOnClickAnimation()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun togglePausePlay() {
|
||||||
|
if (player.isPlaying) {
|
||||||
|
player.pause()
|
||||||
|
} else {
|
||||||
|
player.play()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun playNextEpisode() = model.nextEpisode?.let {
|
||||||
|
model.nextEpisode() // current = next, next = new or null
|
||||||
|
hideButtonNextEp()
|
||||||
|
|
||||||
|
nextEpManually = true
|
||||||
|
playCurrentMedia(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* start playing a episode
|
||||||
|
* Note: movies are episodes too!
|
||||||
|
*/
|
||||||
|
private fun playCurrentMedia(seekToPosition: Boolean) {
|
||||||
|
// update the gui
|
||||||
|
exo_text_title.text = if (model.media.type == DataTypes.MediaType.TVSHOW) {
|
||||||
|
getString(R.string.component_episode_title, model.currentEpisode.number, model.currentEpisode.description)
|
||||||
|
} else {
|
||||||
|
model.currentEpisode.title
|
||||||
|
}
|
||||||
|
|
||||||
|
if (model.nextEpisode == null) {
|
||||||
|
button_next_ep_c.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
player.clearMediaItems() //remove previous item
|
||||||
|
val mediaSource = HlsMediaSource.Factory(dataSourceFactory).createMediaSource(
|
||||||
|
MediaItem.fromUri(Uri.parse(autoSelectStream(model.currentEpisode)))
|
||||||
|
)
|
||||||
|
if (seekToPosition) player.seekTo(playbackPosition)
|
||||||
|
player.setMediaSource(mediaSource)
|
||||||
|
player.prepare()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If preferSecondary or priStreamUrl is empty and secondary is present (secStreamOmU),
|
||||||
|
* use the secondary stream. Else, if the primary stream is set use the primary stream.
|
||||||
|
* If no stream is present, close the activity.
|
||||||
|
*/
|
||||||
|
private fun autoSelectStream(episode: Episode): String {
|
||||||
|
return if ((Preferences.preferSecondary || episode.priStreamUrl.isEmpty()) && episode.secStreamOmU) {
|
||||||
|
episode.secStreamUrl
|
||||||
|
} else if (episode.priStreamUrl.isNotEmpty()) {
|
||||||
|
episode.priStreamUrl
|
||||||
|
} else {
|
||||||
|
Log.e(javaClass.name, "No stream url set.")
|
||||||
|
this.finish()
|
||||||
|
""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* hide the status and navigation bar
|
||||||
|
*/
|
||||||
|
private fun hideBars() {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
window.setDecorFitsSystemWindows(false)
|
||||||
|
window.insetsController?.apply {
|
||||||
|
hide(WindowInsets.Type.statusBars() or WindowInsets.Type.navigationBars())
|
||||||
|
systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_BARS_BY_SWIPE
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
@Suppress("deprecation")
|
||||||
|
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_IMMERSIVE
|
||||||
|
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||||
|
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||||
|
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||||
|
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||||
|
or View.SYSTEM_UI_FLAG_FULLSCREEN)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* show the next episode button
|
||||||
|
* TODO improve the show animation
|
||||||
|
*/
|
||||||
|
private fun showButtonNextEp() {
|
||||||
|
button_next_ep.visibility = View.VISIBLE
|
||||||
|
button_next_ep.alpha = 0.0f
|
||||||
|
|
||||||
|
button_next_ep.animate()
|
||||||
|
.alpha(1.0f)
|
||||||
|
.setListener(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* hide the next episode button
|
||||||
|
* TODO improve the hide animation
|
||||||
|
*/
|
||||||
|
private fun hideButtonNextEp() {
|
||||||
|
button_next_ep.animate()
|
||||||
|
.alpha(0.0f)
|
||||||
|
.setListener(object : AnimatorListenerAdapter() {
|
||||||
|
override fun onAnimationEnd(animation: Animator?) {
|
||||||
|
super.onAnimationEnd(animation)
|
||||||
|
button_next_ep.visibility = View.GONE
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class PlayerGestureListener : GestureDetector.SimpleOnGestureListener() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* on single tap hide or show the controls
|
||||||
|
*/
|
||||||
|
override fun onSingleTapConfirmed(e: MotionEvent?): Boolean {
|
||||||
|
if (controller.isVisible) controller.hide() else controller.show()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* on double tap rewind or forward
|
||||||
|
*/
|
||||||
|
override fun onDoubleTap(e: MotionEvent?): Boolean {
|
||||||
|
val eventPosX = e?.x?.toInt() ?: 0
|
||||||
|
val viewCenterX = video_view.measuredWidth / 2
|
||||||
|
|
||||||
|
// if the event position is on the left side rewind, if it's on the right forward
|
||||||
|
if (eventPosX < viewCenterX) {
|
||||||
|
rewind()
|
||||||
|
} else {
|
||||||
|
fastForward()
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* not used
|
||||||
|
*/
|
||||||
|
override fun onDoubleTapEvent(e: MotionEvent?): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* on long press toggle pause/play
|
||||||
|
*/
|
||||||
|
override fun onLongPress(e: MotionEvent?) {
|
||||||
|
togglePausePlay()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
62
app/src/main/java/org/mosad/teapod/player/PlayerViewModel.kt
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
package org.mosad.teapod.player
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import org.mosad.teapod.parser.AoDParser
|
||||||
|
import org.mosad.teapod.ui.fragments.MediaFragment
|
||||||
|
import org.mosad.teapod.util.DataTypes
|
||||||
|
import org.mosad.teapod.util.Episode
|
||||||
|
import org.mosad.teapod.util.Media
|
||||||
|
|
||||||
|
class PlayerViewModel : ViewModel() {
|
||||||
|
|
||||||
|
var mediaId = 0
|
||||||
|
internal set
|
||||||
|
var episodeId = 0
|
||||||
|
internal set
|
||||||
|
|
||||||
|
var media: Media = Media(0, "", DataTypes.MediaType.OTHER)
|
||||||
|
internal set
|
||||||
|
var currentEpisode = Episode()
|
||||||
|
internal set
|
||||||
|
var nextEpisode: Episode? = null
|
||||||
|
internal set
|
||||||
|
|
||||||
|
fun loadMedia(iMediaId: Int, iEpisodeId: Int) {
|
||||||
|
mediaId = iMediaId
|
||||||
|
episodeId = iEpisodeId
|
||||||
|
|
||||||
|
runBlocking {
|
||||||
|
media = AoDParser.getMediaById(mediaId)
|
||||||
|
}
|
||||||
|
|
||||||
|
currentEpisode = media.episodes.first { it.id == episodeId }
|
||||||
|
nextEpisode = selectNextEpisode()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* update currentEpisode, episodeId, nextEpisode to new episode
|
||||||
|
* updateWatchedState for the next (now current) episode
|
||||||
|
*/
|
||||||
|
fun nextEpisode() = nextEpisode?.let { nextEp ->
|
||||||
|
currentEpisode = nextEp // set current ep to next ep
|
||||||
|
episodeId = nextEp.id
|
||||||
|
MediaFragment.instance.updateWatchedState(nextEp) // watchedCallback for next ep
|
||||||
|
|
||||||
|
nextEpisode = selectNextEpisode()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Based on the current episodeId, get the next episode. If there is no next
|
||||||
|
* episode, return null
|
||||||
|
*/
|
||||||
|
private fun selectNextEpisode(): Episode? {
|
||||||
|
val nextEpIndex = media.episodes.indexOfFirst { it.id == currentEpisode.id } + 1
|
||||||
|
return if (nextEpIndex < (media.episodes.size)) {
|
||||||
|
media.episodes[nextEpIndex]
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,22 +1,71 @@
|
|||||||
package org.mosad.teapod.preferences
|
package org.mosad.teapod.preferences
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import org.mosad.teapod.R
|
||||||
|
import org.mosad.teapod.util.DataTypes
|
||||||
|
|
||||||
object Preferences {
|
object Preferences {
|
||||||
|
|
||||||
var login = ""
|
var preferSecondary = false
|
||||||
internal set
|
internal set
|
||||||
var password = ""
|
var autoplay = true
|
||||||
|
internal set
|
||||||
|
var theme = DataTypes.Theme.LIGHT
|
||||||
internal set
|
internal set
|
||||||
|
|
||||||
|
private fun getSharedPref(context: Context): SharedPreferences {
|
||||||
fun saveCredentials(login: String, password: String) {
|
return context.getSharedPreferences(
|
||||||
this.login = login
|
context.getString(R.string.preference_file_key),
|
||||||
this.password = password
|
Context.MODE_PRIVATE
|
||||||
|
)
|
||||||
// TODO save
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun load() {
|
fun savePreferSecondary(context: Context, preferSecondary: Boolean) {
|
||||||
// TODO
|
with(getSharedPref(context).edit()) {
|
||||||
|
putBoolean(context.getString(R.string.save_key_prefer_secondary), preferSecondary)
|
||||||
|
apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.preferSecondary = preferSecondary
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun saveAutoplay(context: Context, autoplay: Boolean) {
|
||||||
|
with(getSharedPref(context).edit()) {
|
||||||
|
putBoolean(context.getString(R.string.save_key_autoplay), autoplay)
|
||||||
|
apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.autoplay = autoplay
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveTheme(context: Context, theme: DataTypes.Theme) {
|
||||||
|
with(getSharedPref(context).edit()) {
|
||||||
|
putString(context.getString(R.string.save_key_theme), theme.toString())
|
||||||
|
apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.theme = theme
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* initially load the stored values
|
||||||
|
*/
|
||||||
|
fun load(context: Context) {
|
||||||
|
val sharedPref = getSharedPref(context)
|
||||||
|
|
||||||
|
preferSecondary = sharedPref.getBoolean(
|
||||||
|
context.getString(R.string.save_key_prefer_secondary), false
|
||||||
|
)
|
||||||
|
autoplay = sharedPref.getBoolean(
|
||||||
|
context.getString(R.string.save_key_autoplay), true
|
||||||
|
)
|
||||||
|
theme = DataTypes.Theme.valueOf(
|
||||||
|
sharedPref.getString(
|
||||||
|
context.getString(R.string.save_key_theme), DataTypes.Theme.LIGHT.toString()
|
||||||
|
) ?: DataTypes.Theme.LIGHT.toString()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
@ -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)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,47 +0,0 @@
|
|||||||
package org.mosad.teapod.ui.account
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import com.afollestad.materialdialogs.MaterialDialog
|
|
||||||
import kotlinx.android.synthetic.main.fragment_account.*
|
|
||||||
import org.mosad.teapod.BuildConfig
|
|
||||||
import org.mosad.teapod.R
|
|
||||||
import org.mosad.teapod.preferences.EncryptedPreferences
|
|
||||||
import org.mosad.teapod.ui.components.LoginDialog
|
|
||||||
|
|
||||||
class AccountFragment : Fragment() {
|
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
|
||||||
return inflater.inflate(R.layout.fragment_account, container, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
|
|
||||||
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))
|
|
||||||
|
|
||||||
initActions()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initActions() {
|
|
||||||
linear_account_login.setOnClickListener {
|
|
||||||
LoginDialog(requireContext()).positiveButton {
|
|
||||||
EncryptedPreferences.saveCredentials(login, password, context)
|
|
||||||
}.show {
|
|
||||||
login = EncryptedPreferences.login
|
|
||||||
password = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
linear_about.setOnClickListener {
|
|
||||||
MaterialDialog(requireContext())
|
|
||||||
.title(R.string.info_about)
|
|
||||||
.message(R.string.info_about_dialog)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,68 @@
|
|||||||
|
package org.mosad.teapod.ui.components
|
||||||
|
|
||||||
|
import android.animation.Animator
|
||||||
|
import android.animation.AnimatorListenerAdapter
|
||||||
|
import android.animation.ObjectAnimator
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import kotlinx.android.synthetic.main.button_fast_forward.view.*
|
||||||
|
import org.mosad.teapod.R
|
||||||
|
|
||||||
|
class FastForwardButton(context: Context, attrs: AttributeSet?): FrameLayout(context, attrs) {
|
||||||
|
|
||||||
|
private val animationDuration: Long = 800
|
||||||
|
private val buttonAnimation: ObjectAnimator
|
||||||
|
private val labelAnimation: ObjectAnimator
|
||||||
|
|
||||||
|
var onAnimationEndCallback: (() -> Unit)? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
inflate(context, R.layout.button_fast_forward, this)
|
||||||
|
|
||||||
|
buttonAnimation = ObjectAnimator.ofFloat(imageButton, View.ROTATION, 0f, 50f).apply {
|
||||||
|
duration = animationDuration / 4
|
||||||
|
repeatCount = 1
|
||||||
|
repeatMode = ObjectAnimator.REVERSE
|
||||||
|
addListener(object : AnimatorListenerAdapter() {
|
||||||
|
override fun onAnimationStart(animation: Animator?) {
|
||||||
|
imageButton.isEnabled = false // disable button
|
||||||
|
imageButton.setBackgroundResource(R.drawable.ic_baseline_forward_24)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
labelAnimation = ObjectAnimator.ofFloat(textView, View.TRANSLATION_X, 35f).apply {
|
||||||
|
duration = animationDuration
|
||||||
|
addListener(object : AnimatorListenerAdapter() {
|
||||||
|
// the label animation takes longer then the button animation, reset stuff in here
|
||||||
|
override fun onAnimationEnd(animation: Animator?) {
|
||||||
|
imageButton.isEnabled = true // enable button
|
||||||
|
imageButton.setBackgroundResource(R.drawable.ic_baseline_forward_10_24)
|
||||||
|
|
||||||
|
textView.visibility = View.GONE
|
||||||
|
textView.animate().translationX(0f)
|
||||||
|
|
||||||
|
onAnimationEndCallback?.invoke()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setOnButtonClickListener(func: FastForwardButton.() -> Unit) {
|
||||||
|
imageButton.setOnClickListener {
|
||||||
|
func()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun runOnClickAnimation() {
|
||||||
|
// run button animation
|
||||||
|
buttonAnimation.start()
|
||||||
|
|
||||||
|
// run lbl animation
|
||||||
|
textView.visibility = View.VISIBLE
|
||||||
|
labelAnimation.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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)
|
||||||
|
@ -0,0 +1,67 @@
|
|||||||
|
package org.mosad.teapod.ui.components
|
||||||
|
|
||||||
|
import android.animation.Animator
|
||||||
|
import android.animation.AnimatorListenerAdapter
|
||||||
|
import android.animation.ObjectAnimator
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import kotlinx.android.synthetic.main.button_rewind.view.*
|
||||||
|
import org.mosad.teapod.R
|
||||||
|
|
||||||
|
class RewindButton(context: Context, attrs: AttributeSet): FrameLayout(context, attrs) {
|
||||||
|
|
||||||
|
private val animationDuration: Long = 800
|
||||||
|
private val buttonAnimation: ObjectAnimator
|
||||||
|
private val labelAnimation: ObjectAnimator
|
||||||
|
|
||||||
|
var onAnimationEndCallback: (() -> Unit)? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
inflate(context, R.layout.button_rewind, this)
|
||||||
|
|
||||||
|
buttonAnimation = ObjectAnimator.ofFloat(imageButton, View.ROTATION, 0f, -50f).apply {
|
||||||
|
duration = animationDuration / 4
|
||||||
|
repeatCount = 1
|
||||||
|
repeatMode = ObjectAnimator.REVERSE
|
||||||
|
addListener(object : AnimatorListenerAdapter() {
|
||||||
|
override fun onAnimationStart(animation: Animator?) {
|
||||||
|
imageButton.isEnabled = false // disable button
|
||||||
|
imageButton.setBackgroundResource(R.drawable.ic_baseline_rewind_24)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
labelAnimation = ObjectAnimator.ofFloat(textView, View.TRANSLATION_X, -35f).apply {
|
||||||
|
duration = animationDuration
|
||||||
|
addListener(object : AnimatorListenerAdapter() {
|
||||||
|
override fun onAnimationEnd(animation: Animator?) {
|
||||||
|
imageButton.isEnabled = true // enable button
|
||||||
|
imageButton.setBackgroundResource(R.drawable.ic_baseline_rewind_10_24)
|
||||||
|
|
||||||
|
textView.visibility = View.GONE
|
||||||
|
textView.animate().translationX(0f)
|
||||||
|
|
||||||
|
onAnimationEndCallback?.invoke()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setOnButtonClickListener(func: RewindButton.() -> Unit) {
|
||||||
|
imageButton.setOnClickListener {
|
||||||
|
func()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun runOnClickAnimation() {
|
||||||
|
// run button animation
|
||||||
|
buttonAnimation.start()
|
||||||
|
|
||||||
|
// run lbl animation
|
||||||
|
textView.visibility = View.VISIBLE
|
||||||
|
labelAnimation.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,127 @@
|
|||||||
|
package org.mosad.teapod.ui.fragments
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
|
import com.afollestad.materialdialogs.list.listItemsSingleChoice
|
||||||
|
import de.psdev.licensesdialog.LicensesDialog
|
||||||
|
import org.mosad.teapod.BuildConfig
|
||||||
|
import org.mosad.teapod.MainActivity
|
||||||
|
import org.mosad.teapod.R
|
||||||
|
import org.mosad.teapod.databinding.FragmentAccountBinding
|
||||||
|
import org.mosad.teapod.parser.AoDParser
|
||||||
|
import org.mosad.teapod.preferences.EncryptedPreferences
|
||||||
|
import org.mosad.teapod.preferences.Preferences
|
||||||
|
import org.mosad.teapod.ui.components.LoginDialog
|
||||||
|
import org.mosad.teapod.util.DataTypes.Theme
|
||||||
|
|
||||||
|
class AccountFragment : Fragment() {
|
||||||
|
|
||||||
|
private lateinit var binding: FragmentAccountBinding
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
|
binding = FragmentAccountBinding.inflate(inflater, container, false)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
binding.textAccountLogin.text = EncryptedPreferences.login
|
||||||
|
binding.textInfoAboutDesc.text = getString(R.string.info_about_desc, BuildConfig.VERSION_NAME, getString(R.string.build_time))
|
||||||
|
binding.textThemeSelected.text = when (Preferences.theme) {
|
||||||
|
Theme.DARK -> getString(R.string.theme_dark)
|
||||||
|
else -> getString(R.string.theme_light)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.switchSecondary.isChecked = Preferences.preferSecondary
|
||||||
|
binding.switchAutoplay.isChecked = Preferences.autoplay
|
||||||
|
|
||||||
|
initActions()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initActions() {
|
||||||
|
binding.linearAccountLogin.setOnClickListener {
|
||||||
|
showLoginDialog(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.linearTheme.setOnClickListener {
|
||||||
|
showThemeDialog()
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.linearInfo.setOnClickListener {
|
||||||
|
MaterialDialog(requireContext())
|
||||||
|
.title(R.string.info_about)
|
||||||
|
.message(R.string.info_about_dialog)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.textLicenses.setOnClickListener {
|
||||||
|
|
||||||
|
val dialogCss = when (Preferences.theme) {
|
||||||
|
Theme.DARK -> R.string.license_dialog_style_dark
|
||||||
|
else -> R.string.license_dialog_style_light
|
||||||
|
}
|
||||||
|
|
||||||
|
val themeId = when (Preferences.theme) {
|
||||||
|
Theme.DARK -> R.style.LicensesDialogTheme_Dark
|
||||||
|
else -> R.style.AppTheme_Light
|
||||||
|
}
|
||||||
|
|
||||||
|
LicensesDialog.Builder(requireContext())
|
||||||
|
.setNotices(R.raw.notices)
|
||||||
|
.setTitle(R.string.licenses)
|
||||||
|
.setIncludeOwnLicense(true)
|
||||||
|
.setThemeResourceId(themeId)
|
||||||
|
.setNoticesCssStyle(dialogCss)
|
||||||
|
.build()
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.switchSecondary.setOnClickListener {
|
||||||
|
Preferences.savePreferSecondary(requireContext(), binding.switchSecondary.isChecked)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.switchAutoplay.setOnClickListener {
|
||||||
|
Preferences.saveAutoplay(requireContext(), binding.switchAutoplay.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 = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showThemeDialog() {
|
||||||
|
val themes = listOf(
|
||||||
|
resources.getString(R.string.theme_light),
|
||||||
|
resources.getString(R.string.theme_dark)
|
||||||
|
)
|
||||||
|
|
||||||
|
MaterialDialog(requireContext()).show {
|
||||||
|
title(R.string.theme)
|
||||||
|
listItemsSingleChoice(items = themes, initialSelection = Preferences.theme.ordinal) { _, index, _ ->
|
||||||
|
when(index) {
|
||||||
|
0 -> Preferences.saveTheme(context, Theme.LIGHT)
|
||||||
|
1 -> Preferences.saveTheme(context, Theme.DARK)
|
||||||
|
else -> Preferences.saveTheme(context, Theme.LIGHT)
|
||||||
|
}
|
||||||
|
|
||||||
|
(activity as MainActivity).restart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,71 @@
|
|||||||
|
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.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.mosad.teapod.MainActivity
|
||||||
|
import org.mosad.teapod.databinding.FragmentHomeBinding
|
||||||
|
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 binding: FragmentHomeBinding
|
||||||
|
private lateinit var adapterMyList: MediaItemAdapter
|
||||||
|
private lateinit var adapterNewEpisodes: MediaItemAdapter
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
|
binding = FragmentHomeBinding.inflate(inflater, container, false)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
GlobalScope.launch {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
context?.let {
|
||||||
|
binding.recyclerMyList.addItemDecoration(MediaItemDecoration(9))
|
||||||
|
|
||||||
|
updateMyListMedia()
|
||||||
|
|
||||||
|
adapterNewEpisodes = MediaItemAdapter(AoDParser.newEpisodesList)
|
||||||
|
binding.recyclerNewEpisodes.adapter = adapterNewEpisodes
|
||||||
|
binding.recyclerNewEpisodes.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)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.recyclerMyList.adapter = adapterMyList
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initActions() {
|
||||||
|
adapterNewEpisodes.onItemClick = { mediaId, _ ->
|
||||||
|
(activity as MainActivity).showMediaFragment(mediaId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,48 @@
|
|||||||
|
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.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.mosad.teapod.MainActivity
|
||||||
|
import org.mosad.teapod.databinding.FragmentLibraryBinding
|
||||||
|
import org.mosad.teapod.parser.AoDParser
|
||||||
|
import org.mosad.teapod.util.adapter.MediaItemAdapter
|
||||||
|
import org.mosad.teapod.util.decoration.MediaItemDecoration
|
||||||
|
|
||||||
|
class LibraryFragment : Fragment() {
|
||||||
|
|
||||||
|
private lateinit var binding: FragmentLibraryBinding
|
||||||
|
private lateinit var adapter: MediaItemAdapter
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
|
binding = FragmentLibraryBinding.inflate(inflater, container, false)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
// init async
|
||||||
|
GlobalScope.launch {
|
||||||
|
// create and set the adapter, needs context
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
context?.let {
|
||||||
|
adapter = MediaItemAdapter(AoDParser.itemMediaList)
|
||||||
|
adapter.onItemClick = { mediaId, _ ->
|
||||||
|
(activity as MainActivity).showMediaFragment(mediaId)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.recyclerMediaLibrary.adapter = adapter
|
||||||
|
binding.recyclerMediaLibrary.addItemDecoration(MediaItemDecoration(9))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
184
app/src/main/java/org/mosad/teapod/ui/fragments/MediaFragment.kt
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
package org.mosad.teapod.ui.fragments
|
||||||
|
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.drawable.ColorDrawable
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import com.bumptech.glide.request.RequestOptions
|
||||||
|
import jp.wasabeef.glide.transformations.BlurTransformation
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import org.mosad.teapod.MainActivity
|
||||||
|
import org.mosad.teapod.R
|
||||||
|
import org.mosad.teapod.databinding.FragmentMediaBinding
|
||||||
|
import org.mosad.teapod.parser.AoDParser
|
||||||
|
import org.mosad.teapod.util.*
|
||||||
|
import org.mosad.teapod.util.DataTypes.MediaType
|
||||||
|
import org.mosad.teapod.util.adapter.EpisodeItemAdapter
|
||||||
|
|
||||||
|
class MediaFragment(private val mediaId: Int) : Fragment() {
|
||||||
|
|
||||||
|
private lateinit var binding: FragmentMediaBinding
|
||||||
|
private lateinit var adapterRecEpisodes: EpisodeItemAdapter
|
||||||
|
private lateinit var viewManager: RecyclerView.LayoutManager
|
||||||
|
|
||||||
|
private lateinit var media: Media
|
||||||
|
private lateinit var tmdb: TMDBResponse
|
||||||
|
private lateinit var nextEpisode: Episode
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
lateinit var instance: MediaFragment
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
instance = this
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
|
binding = FragmentMediaBinding.inflate(inflater, container, false)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
binding.frameLoading.visibility = View.VISIBLE
|
||||||
|
|
||||||
|
GlobalScope.launch(Dispatchers.Main) {
|
||||||
|
// load the streams for the selected media
|
||||||
|
media = AoDParser.getMediaById(mediaId)
|
||||||
|
tmdb = TMDBApiController().search(media.info.title, media.type)
|
||||||
|
|
||||||
|
if (this@MediaFragment.isAdded) {
|
||||||
|
updateGUI()
|
||||||
|
initActions()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* if tmdb data is present, use it, else use the aod data
|
||||||
|
*/
|
||||||
|
private fun updateGUI() = with(binding) {
|
||||||
|
// generic gui
|
||||||
|
val backdropUrl = if (tmdb.backdropUrl.isNotEmpty()) tmdb.backdropUrl else media.info.posterUrl
|
||||||
|
val posterUrl = if (tmdb.posterUrl.isNotEmpty()) tmdb.posterUrl else media.info.posterUrl
|
||||||
|
|
||||||
|
Glide.with(requireContext()).load(backdropUrl)
|
||||||
|
.apply(RequestOptions.placeholderOf(ColorDrawable(Color.DKGRAY)))
|
||||||
|
.apply(RequestOptions.bitmapTransform(BlurTransformation(20, 3)))
|
||||||
|
.into(imageBackdrop)
|
||||||
|
|
||||||
|
Glide.with(requireContext()).load(posterUrl)
|
||||||
|
.into(imagePoster)
|
||||||
|
|
||||||
|
textTitle.text = media.info.title
|
||||||
|
textYear.text = media.info.year.toString()
|
||||||
|
textAge.text = media.info.age.toString()
|
||||||
|
textOverview.text = media.info.shortDesc
|
||||||
|
if (StorageController.myList.contains(media.id)) {
|
||||||
|
Glide.with(requireContext()).load(R.drawable.ic_baseline_check_24).into(imageMyListAction)
|
||||||
|
} else {
|
||||||
|
Glide.with(requireContext()).load(R.drawable.ic_baseline_add_24).into(imageMyListAction)
|
||||||
|
}
|
||||||
|
|
||||||
|
// specific gui
|
||||||
|
if (media.type == MediaType.TVSHOW) {
|
||||||
|
adapterRecEpisodes = EpisodeItemAdapter(media.episodes)
|
||||||
|
viewManager = LinearLayoutManager(context)
|
||||||
|
recyclerEpisodes.layoutManager = viewManager
|
||||||
|
recyclerEpisodes.adapter = adapterRecEpisodes
|
||||||
|
|
||||||
|
binding.textEpisodesOrRuntime.text = getString(R.string.text_episodes_count, media.info.episodesCount)
|
||||||
|
|
||||||
|
// get next episode
|
||||||
|
nextEpisode = if (media.episodes.firstOrNull{ !it.watched } != null) {
|
||||||
|
media.episodes.first{ !it.watched }
|
||||||
|
} else {
|
||||||
|
media.episodes.first()
|
||||||
|
}
|
||||||
|
|
||||||
|
// title is the next episodes title
|
||||||
|
textTitle.text = nextEpisode.title
|
||||||
|
} else if (media.type == MediaType.MOVIE) {
|
||||||
|
recyclerEpisodes.visibility = View.GONE
|
||||||
|
|
||||||
|
if (tmdb.runtime > 0) {
|
||||||
|
textEpisodesOrRuntime.text = getString(R.string.text_runtime, tmdb.runtime)
|
||||||
|
} else {
|
||||||
|
textEpisodesOrRuntime.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
frameLoading.visibility = View.GONE // hide loading indicator
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initActions() {
|
||||||
|
binding.buttonPlay.setOnClickListener {
|
||||||
|
when (media.type) {
|
||||||
|
MediaType.MOVIE -> playStream(media.episodes.first())
|
||||||
|
MediaType.TVSHOW -> playEpisode(nextEpisode)
|
||||||
|
else -> Log.e(javaClass.name, "Wrong Type: $media.type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add or remove media from myList
|
||||||
|
binding.linearMyListAction.setOnClickListener {
|
||||||
|
if (StorageController.myList.contains(media.id)) {
|
||||||
|
StorageController.myList.remove(media.id)
|
||||||
|
Glide.with(requireContext()).load(R.drawable.ic_baseline_add_24).into(binding.imageMyListAction)
|
||||||
|
} else {
|
||||||
|
StorageController.myList.add(media.id)
|
||||||
|
Glide.with(requireContext()).load(R.drawable.ic_baseline_check_24).into(binding.imageMyListAction)
|
||||||
|
}
|
||||||
|
StorageController.saveMyList(requireContext())
|
||||||
|
|
||||||
|
// notify home fragment on change
|
||||||
|
parentFragmentManager.findFragmentByTag("HomeFragment")?.let {
|
||||||
|
(it as HomeFragment).updateMyListMedia()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// set onItemClick only in adapter is initialized
|
||||||
|
if (this::adapterRecEpisodes.isInitialized) {
|
||||||
|
adapterRecEpisodes.onImageClick = { _, position ->
|
||||||
|
playEpisode(media.episodes[position])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun playEpisode(ep: Episode) {
|
||||||
|
playStream(ep)
|
||||||
|
|
||||||
|
// update watched state
|
||||||
|
updateWatchedState(ep)
|
||||||
|
//AoDParser.sendCallback(ep.watchedCallback)
|
||||||
|
//adapterRecEpisodes.updateWatchedState(true, media.episodes.indexOf(ep))
|
||||||
|
//adapterRecEpisodes.notifyDataSetChanged()
|
||||||
|
|
||||||
|
// update nextEpisode
|
||||||
|
nextEpisode = if (media.episodes.firstOrNull{ !it.watched } != null) {
|
||||||
|
media.episodes.first{ !it.watched }
|
||||||
|
} else {
|
||||||
|
media.episodes.first()
|
||||||
|
}
|
||||||
|
binding.textTitle.text = nextEpisode.title
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun playStream(ep: Episode) {
|
||||||
|
Log.d(javaClass.name, "Starting Player with mediaId: ${media.id}")
|
||||||
|
(activity as MainActivity).startPlayer(media.id, ep.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateWatchedState(ep: Episode) {
|
||||||
|
AoDParser.sendCallback(ep.watchedCallback)
|
||||||
|
adapterRecEpisodes.updateWatchedState(true, media.episodes.indexOf(ep))
|
||||||
|
adapterRecEpisodes.notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,63 @@
|
|||||||
|
package org.mosad.teapod.ui.fragments
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.SearchView
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import org.mosad.teapod.MainActivity
|
||||||
|
import org.mosad.teapod.databinding.FragmentSearchBinding
|
||||||
|
import org.mosad.teapod.parser.AoDParser
|
||||||
|
import org.mosad.teapod.util.decoration.MediaItemDecoration
|
||||||
|
import org.mosad.teapod.util.adapter.MediaItemAdapter
|
||||||
|
|
||||||
|
class SearchFragment : Fragment() {
|
||||||
|
|
||||||
|
private lateinit var binding: FragmentSearchBinding
|
||||||
|
private var adapter : MediaItemAdapter? = null
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
|
binding = FragmentSearchBinding.inflate(inflater, container, false)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
GlobalScope.launch {
|
||||||
|
// create and set the adapter, needs context
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
context?.let {
|
||||||
|
adapter = MediaItemAdapter(AoDParser.itemMediaList)
|
||||||
|
adapter!!.onItemClick = { mediaId, _ ->
|
||||||
|
binding.searchText.clearFocus()
|
||||||
|
(activity as MainActivity).showMediaFragment(mediaId)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.recyclerMediaSearch.adapter = adapter
|
||||||
|
binding.recyclerMediaSearch.addItemDecoration(MediaItemDecoration(9))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initActions()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initActions() {
|
||||||
|
binding.searchText.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||||
|
override fun onQueryTextSubmit(query: String?): Boolean {
|
||||||
|
adapter?.filter?.filter(query)
|
||||||
|
adapter?.notifyDataSetChanged()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onQueryTextChange(newText: String?): Boolean {
|
||||||
|
adapter?.filter?.filter(newText)
|
||||||
|
adapter?.notifyDataSetChanged()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -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,54 +0,0 @@
|
|||||||
package org.mosad.teapod.ui.library
|
|
||||||
|
|
||||||
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_library.*
|
|
||||||
import kotlinx.coroutines.*
|
|
||||||
import org.mosad.teapod.MainActivity
|
|
||||||
import org.mosad.teapod.R
|
|
||||||
import org.mosad.teapod.parser.AoDParser
|
|
||||||
import org.mosad.teapod.util.CustomAdapter
|
|
||||||
import org.mosad.teapod.util.Media
|
|
||||||
|
|
||||||
class LibraryFragment : Fragment() {
|
|
||||||
|
|
||||||
private lateinit var adapter : CustomAdapter
|
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
|
||||||
return inflater.inflate(R.layout.fragment_library, container, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
|
|
||||||
GlobalScope.launch {
|
|
||||||
if (AoDParser.mediaList.isEmpty()) {
|
|
||||||
AoDParser().listAnimes()
|
|
||||||
}
|
|
||||||
|
|
||||||
// create and set the adapter, needs context
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
context?.let {
|
|
||||||
adapter = CustomAdapter(it, AoDParser.mediaList)
|
|
||||||
list_library.adapter = adapter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,68 +0,0 @@
|
|||||||
package org.mosad.teapod.ui.search
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.SearchView
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import kotlinx.android.synthetic.main.fragment_search.*
|
|
||||||
import kotlinx.coroutines.*
|
|
||||||
import org.mosad.teapod.MainActivity
|
|
||||||
import org.mosad.teapod.R
|
|
||||||
import org.mosad.teapod.parser.AoDParser
|
|
||||||
import org.mosad.teapod.util.CustomAdapter
|
|
||||||
import org.mosad.teapod.util.Media
|
|
||||||
|
|
||||||
class SearchFragment : Fragment() {
|
|
||||||
|
|
||||||
private val instance = this
|
|
||||||
private lateinit var adapter : CustomAdapter
|
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
|
||||||
return inflater.inflate(R.layout.fragment_search, container, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
|
|
||||||
GlobalScope.launch {
|
|
||||||
if (AoDParser.mediaList.isEmpty()) {
|
|
||||||
AoDParser().listAnimes()
|
|
||||||
}
|
|
||||||
|
|
||||||
// create and set the adapter, needs context
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
context?.let {
|
|
||||||
adapter = CustomAdapter(it, AoDParser.mediaList)
|
|
||||||
list_search.adapter = adapter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
initActions()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initActions() {
|
|
||||||
search_text.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
|
||||||
override fun onQueryTextSubmit(query: String?): Boolean {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onQueryTextChange(newText: String?): Boolean {
|
|
||||||
adapter.filter.filter(newText)
|
|
||||||
adapter.notifyDataSetChanged()
|
|
||||||
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,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()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -6,14 +6,80 @@ class DataTypes {
|
|||||||
MOVIE,
|
MOVIE,
|
||||||
TVSHOW
|
TVSHOW
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
data class Media(val title: String, val link: String, val type: DataTypes.MediaType, val posterLink: String, val shortDesc : String, var episodes: List<Episode> = listOf()) {
|
enum class Theme(val str: String) {
|
||||||
override fun toString(): String {
|
LIGHT("Light"),
|
||||||
return title
|
DARK("Dark")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Episode(val title: String = "", val streamUrl: String = "", val posterLink: String = "", var watched: Boolean = false)
|
/**
|
||||||
|
* this class is used to represent the item media
|
||||||
|
* it is uses in the ItemMediaAdapter (RecyclerView)
|
||||||
|
*/
|
||||||
|
data class ItemMedia(
|
||||||
|
val id: Int,
|
||||||
|
val title: String,
|
||||||
|
val posterUrl: String
|
||||||
|
)
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
44
app/src/main/java/org/mosad/teapod/util/StorageController.kt
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
package org.mosad.teapod.util
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import com.google.gson.JsonParser
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import java.io.File
|
||||||
|
import java.lang.Exception
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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() }
|
||||||
|
|
||||||
|
try {
|
||||||
|
myList.clear()
|
||||||
|
myList.addAll(JsonParser.parseString(file.readText()).asJsonArray.map { it.asInt }.distinct())
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
myList.clear()
|
||||||
|
Log.e(javaClass.name, "Parsing of My-List failed.")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveMyList(context: Context): Job {
|
||||||
|
val file = File(context.filesDir, fileNameMyList)
|
||||||
|
|
||||||
|
return GlobalScope.launch(Dispatchers.IO) {
|
||||||
|
file.writeText(Gson().toJson(myList.distinct()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,10 +1,9 @@
|
|||||||
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.*
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.net.URLEncoder
|
import java.net.URLEncoder
|
||||||
import org.mosad.teapod.util.DataTypes.MediaType
|
import org.mosad.teapod.util.DataTypes.MediaType
|
||||||
@ -14,75 +13,103 @@ 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 {
|
suspend 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).await()
|
||||||
val test = searchMovie(title)
|
MediaType.TVSHOW -> searchTVShow(searchTerm).await()
|
||||||
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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun searchTVShow(title: String) = runBlocking {
|
fun searchTVShow(title: String): Deferred<TMDBResponse> {
|
||||||
val url = URL("$searchTVUrl$preparedParamters&query=${URLEncoder.encode(title, "UTF-8")}")
|
val url = URL("$searchTVUrl$preparedParameters&query=${URLEncoder.encode(title, "UTF-8")}")
|
||||||
|
|
||||||
GlobalScope.async {
|
return 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) {
|
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()
|
||||||
}
|
}
|
||||||
}.await()
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun searchMovie(title: String) = runBlocking {
|
fun searchMovie(title: String): Deferred<TMDBResponse> {
|
||||||
val url = URL("$searchMovieUrl$preparedParamters&query=${URLEncoder.encode(title, "UTF-8")}")
|
val url = URL("$searchMovieUrl$preparedParameters&query=${URLEncoder.encode(title, "UTF-8")}")
|
||||||
|
|
||||||
GlobalScope.async {
|
return 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) {
|
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()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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()
|
}.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,65 @@
|
|||||||
|
package org.mosad.teapod.util.adapter
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
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 org.mosad.teapod.R
|
||||||
|
import org.mosad.teapod.databinding.ItemEpisodeBinding
|
||||||
|
import org.mosad.teapod.util.Episode
|
||||||
|
|
||||||
|
class EpisodeItemAdapter(private val episodes: List<Episode>) : RecyclerView.Adapter<EpisodeItemAdapter.EpisodeViewHolder>() {
|
||||||
|
|
||||||
|
var onImageClick: ((String, Int) -> Unit)? = null
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EpisodeViewHolder {
|
||||||
|
return EpisodeViewHolder(ItemEpisodeBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: EpisodeViewHolder, position: Int) {
|
||||||
|
val context = holder.binding.root.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.binding.textEpisodeTitle.text = titleText
|
||||||
|
holder.binding.textEpisodeDesc.text = ep.shortDesc
|
||||||
|
|
||||||
|
if (episodes[position].posterUrl.isNotEmpty()) {
|
||||||
|
Glide.with(context).load(ep.posterUrl)
|
||||||
|
.apply(RequestOptions.bitmapTransform(RoundedCornersTransformation(10, 0)))
|
||||||
|
.into(holder.binding.imageEpisode)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ep.watched) {
|
||||||
|
holder.binding.imageWatched.setImageDrawable(
|
||||||
|
ContextCompat.getDrawable(context, R.drawable.ic_baseline_check_circle_24)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
holder.binding.imageWatched.setImageDrawable(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int {
|
||||||
|
return episodes.size
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateWatchedState(watched: Boolean, position: Int) {
|
||||||
|
episodes[position].watched = watched
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class EpisodeViewHolder(val binding: ItemEpisodeBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||||
|
init {
|
||||||
|
binding.imageEpisode.setOnClickListener {
|
||||||
|
onImageClick?.invoke(episodes[adapterPosition].title, adapterPosition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,75 @@
|
|||||||
|
package org.mosad.teapod.util.adapter
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.Filter
|
||||||
|
import android.widget.Filterable
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import org.mosad.teapod.databinding.ItemMediaBinding
|
||||||
|
import org.mosad.teapod.util.ItemMedia
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class MediaItemAdapter(private val media: List<ItemMedia>) : RecyclerView.Adapter<MediaItemAdapter.MediaViewHolder>(), 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.MediaViewHolder {
|
||||||
|
return MediaViewHolder(ItemMediaBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: MediaItemAdapter.MediaViewHolder, position: Int) {
|
||||||
|
holder.binding.root.apply {
|
||||||
|
holder.binding.textTitle.text = filteredMedia[position].title
|
||||||
|
Glide.with(context).load(filteredMedia[position].posterUrl).into(holder.binding.imagePoster)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int {
|
||||||
|
return filteredMedia.size
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getFilter(): Filter {
|
||||||
|
return filter
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class MediaViewHolder(val binding: ItemMediaBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||||
|
init {
|
||||||
|
binding.root.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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
5
app/src/main/res/color/bottom_nav_item_tint.xml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:color="?attr/colorPrimary" android:state_checked="true"/>
|
||||||
|
<item android:color="?attr/iconAction"/>
|
||||||
|
</selector>
|
@ -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_arrow_back_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:tint="#FFFFFF"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z" />
|
||||||
|
</vector>
|
10
app/src/main/res/drawable/ic_baseline_autorenew_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,6v3l4,-4 -4,-4v3c-4.42,0 -8,3.58 -8,8 0,1.57 0.46,3.03 1.24,4.26L6.7,14.8c-0.45,-0.83 -0.7,-1.79 -0.7,-2.8 0,-3.31 2.69,-6 6,-6zM18.76,7.74L17.3,9.2c0.44,0.84 0.7,1.79 0.7,2.8 0,3.31 -2.69,6 -6,6v-3l-4,4 4,4v-3c4.42,0 8,-3.58 8,-8 0,-1.57 -0.46,-3.03 -1.24,-4.26z" />
|
||||||
|
</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>
|
15
app/src/main/res/drawable/ic_baseline_forward_10_24.xml
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M18,13c0,3.31 -2.69,6 -6,6s-6,-2.69 -6,-6s2.69,-6 6,-6v4l5,-5l-5,-5v4c-4.42,0 -8,3.58 -8,8c0,4.42 3.58,8 8,8s8,-3.58 8,-8H18z"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M10.86,15.94l0,-4.27l-0.09,0l-1.77,0.63l0,0.69l1.01,-0.31l0,3.26z"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M12.25,13.44v0.74c0,1.9 1.31,1.82 1.44,1.82c0.14,0 1.44,0.09 1.44,-1.82v-0.74c0,-1.9 -1.31,-1.82 -1.44,-1.82C13.55,11.62 12.25,11.53 12.25,13.44zM14.29,13.32v0.97c0,0.77 -0.21,1.03 -0.59,1.03c-0.38,0 -0.6,-0.26 -0.6,-1.03v-0.97c0,-0.75 0.22,-1.01 0.59,-1.01C14.07,12.3 14.29,12.57 14.29,13.32z"/>
|
||||||
|
</vector>
|
9
app/src/main/res/drawable/ic_baseline_forward_24.xml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M18,13c0,3.31 -2.69,6 -6,6s-6,-2.69 -6,-6s2.69,-6 6,-6v4l5,-5l-5,-5v4c-4.42,0 -8,3.58 -8,8c0,4.42 3.58,8 8,8c4.42,0 8,-3.58 8,-8H18z" />
|
||||||
|
</vector>
|
15
app/src/main/res/drawable/ic_baseline_rewind_10_24.xml
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M11.99,5V1l-5,5l5,5V7c3.31,0 6,2.69 6,6s-2.69,6 -6,6s-6,-2.69 -6,-6h-2c0,4.42 3.58,8 8,8s8,-3.58 8,-8S16.41,5 11.99,5z"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M10.89,16h-0.85v-3.26l-1.01,0.31v-0.69l1.77,-0.63h0.09V16z"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M15.17,14.24c0,0.32 -0.03,0.6 -0.1,0.82s-0.17,0.42 -0.29,0.57s-0.28,0.26 -0.45,0.33s-0.37,0.1 -0.59,0.1s-0.41,-0.03 -0.59,-0.1s-0.33,-0.18 -0.46,-0.33s-0.23,-0.34 -0.3,-0.57s-0.11,-0.5 -0.11,-0.82V13.5c0,-0.32 0.03,-0.6 0.1,-0.82s0.17,-0.42 0.29,-0.57s0.28,-0.26 0.45,-0.33s0.37,-0.1 0.59,-0.1s0.41,0.03 0.59,0.1c0.18,0.07 0.33,0.18 0.46,0.33s0.23,0.34 0.3,0.57s0.11,0.5 0.11,0.82V14.24zM14.32,13.38c0,-0.19 -0.01,-0.35 -0.04,-0.48s-0.07,-0.23 -0.12,-0.31s-0.11,-0.14 -0.19,-0.17s-0.16,-0.05 -0.25,-0.05s-0.18,0.02 -0.25,0.05s-0.14,0.09 -0.19,0.17s-0.09,0.18 -0.12,0.31s-0.04,0.29 -0.04,0.48v0.97c0,0.19 0.01,0.35 0.04,0.48s0.07,0.24 0.12,0.32s0.11,0.14 0.19,0.17s0.16,0.05 0.25,0.05s0.18,-0.02 0.25,-0.05s0.14,-0.09 0.19,-0.17s0.09,-0.19 0.11,-0.32s0.04,-0.29 0.04,-0.48V13.38z"/>
|
||||||
|
</vector>
|
5
app/src/main/res/drawable/ic_baseline_rewind_24.xml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||||
|
android:viewportHeight="24" android:viewportWidth="24"
|
||||||
|
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M12,5V1L7,6l5,5V7c3.31,0 6,2.69 6,6s-2.69,6 -6,6 -6,-2.69 -6,-6H4c0,4.42 3.58,8 8,8s8,-3.58 8,-8 -3.58,-8 -8,-8z"/>
|
||||||
|
</vector>
|
@ -3,7 +3,7 @@
|
|||||||
android:height="24dp"
|
android:height="24dp"
|
||||||
android:viewportWidth="24"
|
android:viewportWidth="24"
|
||||||
android:viewportHeight="24"
|
android:viewportHeight="24"
|
||||||
android:tint="?attr/colorControlNormal">
|
android:tint="?attr/iconAction">
|
||||||
<path
|
<path
|
||||||
android:fillColor="@android:color/white"
|
android:fillColor="@android:color/white"
|
||||||
android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z"/>
|
android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z"/>
|
||||||
|
5
app/src/main/res/drawable/ic_baseline_skip_next_24.xml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||||
|
android:viewportHeight="24" android:viewportWidth="24"
|
||||||
|
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M6,18l8.5,-6L6,6v12zM16,6v12h2V6h-2z"/>
|
||||||
|
</vector>
|
5
app/src/main/res/drawable/ic_baseline_style_24.xml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||||
|
android:viewportHeight="24" android:viewportWidth="24"
|
||||||
|
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M2.53,19.65l1.34,0.56v-9.03l-2.43,5.86c-0.41,1.02 0.08,2.19 1.09,2.61zM22.03,15.95L17.07,3.98c-0.31,-0.75 -1.04,-1.21 -1.81,-1.23 -0.26,0 -0.53,0.04 -0.79,0.15L7.1,5.95c-0.75,0.31 -1.21,1.03 -1.23,1.8 -0.01,0.27 0.04,0.54 0.15,0.8l4.96,11.97c0.31,0.76 1.05,1.22 1.83,1.23 0.26,0 0.52,-0.05 0.77,-0.15l7.36,-3.05c1.02,-0.42 1.51,-1.59 1.09,-2.6zM7.88,8.75c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1 1,0.45 1,1 -0.45,1 -1,1zM5.88,19.75c0,1.1 0.9,2 2,2h1.45l-3.45,-8.34v6.34z"/>
|
||||||
|
</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>
|
@ -1,10 +1,5 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||||
android:width="24dp"
|
android:viewportHeight="24" android:viewportWidth="24"
|
||||||
android:height="24dp"
|
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
android:viewportWidth="24"
|
<path android:fillColor="@android:color/white" android:pathData="M4,6L2,6v14c0,1.1 0.9,2 2,2h14v-2L4,20L4,6zM20,2L8,2c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM12,14.5v-9l6,4.5 -6,4.5z"/>
|
||||||
android:viewportHeight="24"
|
|
||||||
android:tint="?attr/colorControlNormal">
|
|
||||||
<path
|
|
||||||
android:fillColor="@android:color/white"
|
|
||||||
android:pathData="M4,6L2,6v14c0,1.1 0.9,2 2,2h14v-2L4,20L4,6zM20,2L8,2c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM12,14.5v-9l6,4.5 -6,4.5z"/>
|
|
||||||
</vector>
|
</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 |
3
app/src/main/res/drawable/ripple_background.xml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:color="?attr/colorControlHighlight" />
|
5
app/src/main/res/drawable/shape_rounded_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="?iconNoAction"/>
|
||||||
|
<corners android:radius="3dp"/>
|
||||||
|
</shape>
|
@ -9,15 +9,14 @@
|
|||||||
android:id="@+id/nav_view"
|
android:id="@+id/nav_view"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="0dp"
|
android:background="?themeSecondary"
|
||||||
android:layout_marginEnd="0dp"
|
app:itemIconTint="@color/bottom_nav_item_tint"
|
||||||
android:background="?android:attr/windowBackground"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
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"
|
||||||
|
@ -2,23 +2,91 @@
|
|||||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/player_layout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:keepScreenOn="true"
|
|
||||||
android:background="#000000"
|
android:background="#000000"
|
||||||
tools:context=".PlayerActivity">
|
android:keepScreenOn="true"
|
||||||
|
tools:context=".player.PlayerActivity">
|
||||||
|
|
||||||
<com.google.android.exoplayer2.ui.PlayerView
|
<com.google.android.exoplayer2.ui.StyledPlayerView
|
||||||
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:controller_layout_id="@layout/player_custom_control"/>-->
|
android:animateLayoutChanges="true"
|
||||||
|
android:foreground="@drawable/ripple_background"
|
||||||
|
app:controller_layout_id="@layout/player_controls"
|
||||||
|
app:fastforward_increment="10000"
|
||||||
|
app:rewind_increment="10000" />
|
||||||
|
|
||||||
<ProgressBar
|
<com.google.android.material.progressindicator.CircularProgressIndicator
|
||||||
android:id="@+id/loading"
|
android:id="@+id/loading"
|
||||||
android:layout_width="70dp"
|
android:layout_width="70dp"
|
||||||
android:layout_height="70dp"
|
android:layout_height="70dp"
|
||||||
android:layout_gravity="center" />
|
android:layout_gravity="center"
|
||||||
|
android:indeterminate="true"
|
||||||
|
app:indicatorColor="@color/exo_white"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/exo_double_tap_indicator"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:layout_weight="1" />
|
||||||
|
|
||||||
|
<org.mosad.teapod.ui.components.RewindButton
|
||||||
|
android:id="@+id/rwd_10_indicator"
|
||||||
|
android:layout_width="100dp"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:layout_weight="1" />
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:layout_width="60dp"
|
||||||
|
android:layout_height="1dp" />
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:layout_weight="1" />
|
||||||
|
|
||||||
|
<org.mosad.teapod.ui.components.FastForwardButton
|
||||||
|
android:id="@+id/ffwd_10_indicator"
|
||||||
|
android:layout_width="100dp"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:layout_weight="1" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/button_next_ep"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="bottom|end"
|
||||||
|
android:layout_marginEnd="12dp"
|
||||||
|
android:layout_marginBottom="70dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="@string/next_episode"
|
||||||
|
android:textAllCaps="false"
|
||||||
|
android:textColor="@android:color/primary_text_light"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:backgroundTint="@color/exo_white"
|
||||||
|
app:iconGravity="textStart" />
|
||||||
|
|
||||||
</FrameLayout>
|
</FrameLayout>
|
24
app/src/main/res/layout/button_fast_forward.xml
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/imageButton"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_alignParentStart="true"
|
||||||
|
android:background="@drawable/ic_baseline_forward_10_24"
|
||||||
|
android:contentDescription="@string/forward_10" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textView"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_alignParentStart="true"
|
||||||
|
android:layout_centerInParent="true"
|
||||||
|
android:layout_marginStart="42dp"
|
||||||
|
android:text="@string/fwd_10_s"
|
||||||
|
android:textColor="@color/exo_white"
|
||||||
|
android:visibility="gone" />
|
||||||
|
</RelativeLayout>
|
27
app/src/main/res/layout/button_rewind.xml
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_alignParentEnd="true">
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/imageButton"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_alignParentEnd="true"
|
||||||
|
android:background="@drawable/ic_baseline_rewind_10_24"
|
||||||
|
android:contentDescription="@string/forward_10" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textView"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_alignParentEnd="true"
|
||||||
|
android:layout_centerInParent="true"
|
||||||
|
android:layout_marginEnd="42dp"
|
||||||
|
android:text="@string/rwd_10_s"
|
||||||
|
android:textColor="@color/exo_white"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
|
||||||
|
</RelativeLayout>
|
@ -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>
|
|
@ -4,8 +4,8 @@
|
|||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:background="#f5f5f5"
|
android:background="?themePrimary"
|
||||||
tools:context=".ui.account.AccountFragment">
|
tools:context=".ui.fragments.AccountFragment">
|
||||||
|
|
||||||
<ScrollView
|
<ScrollView
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@ -21,7 +21,7 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_marginTop="12dp"
|
android:layout_marginTop="12dp"
|
||||||
android:background="#ffffff"
|
android:background="?themeSecondary"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
@ -31,7 +31,6 @@
|
|||||||
android:paddingStart="7dp"
|
android:paddingStart="7dp"
|
||||||
android:paddingEnd="7dp"
|
android:paddingEnd="7dp"
|
||||||
android:text="@string/account"
|
android:text="@string/account"
|
||||||
android:textColor="@android:color/primary_text_light"
|
|
||||||
android:textSize="16sp"
|
android:textSize="16sp"
|
||||||
android:textStyle="bold" />
|
android:textStyle="bold" />
|
||||||
|
|
||||||
@ -39,6 +38,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">
|
||||||
|
|
||||||
@ -49,10 +49,10 @@
|
|||||||
android:contentDescription="@string/account"
|
android:contentDescription="@string/account"
|
||||||
android:minWidth="48dp"
|
android:minWidth="48dp"
|
||||||
android:minHeight="48dp"
|
android:minHeight="48dp"
|
||||||
android:padding="5dp"
|
android:padding="9dp"
|
||||||
android:scaleType="fitXY"
|
android:scaleType="fitXY"
|
||||||
android:src="@drawable/ic_baseline_account_box_24"
|
android:src="@drawable/ic_baseline_account_box_24"
|
||||||
app:srcCompat="@drawable/ic_baseline_account_box_24" />
|
app:tint="?iconNoAction" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@ -65,7 +65,6 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:text="@string/account_login_ex"
|
android:text="@string/account_login_ex"
|
||||||
android:textColor="@android:color/primary_text_light"
|
|
||||||
android:textSize="16sp" />
|
android:textSize="16sp" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
@ -74,18 +73,207 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:text="@string/account_login_desc"
|
android:text="@string/account_login_desc"
|
||||||
android:textColor="@android:color/secondary_text_light" />
|
android:textColor="?textSecondary" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</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="?themeSecondary"
|
||||||
|
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: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/settings_secondary"
|
||||||
|
android:minWidth="48dp"
|
||||||
|
android:minHeight="48dp"
|
||||||
|
android:padding="9dp"
|
||||||
|
android:scaleType="fitXY"
|
||||||
|
android:src="@drawable/ic_baseline_subtitles_24"
|
||||||
|
app:tint="?iconNoAction" />
|
||||||
|
|
||||||
|
<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: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="?textSecondary" />
|
||||||
|
</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
|
||||||
|
android:id="@+id/linear_settings_autoplay"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_margin="7dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/imageView4"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:contentDescription="@string/settings_autoplay"
|
||||||
|
android:minWidth="48dp"
|
||||||
|
android:minHeight="48dp"
|
||||||
|
android:padding="9dp"
|
||||||
|
android:src="@drawable/ic_baseline_autorenew_24"
|
||||||
|
app:tint="?iconNoAction"/>
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/linearLayout2"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/switch_autoplay"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_settings_auoplay"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/settings_autoplay"
|
||||||
|
android:textSize="16sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_settings_auoplay_desc"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/settings_autoplay_desc"
|
||||||
|
android:textColor="?textSecondary" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||||
|
android:id="@+id/switch_autoplay"
|
||||||
|
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
|
||||||
|
android:id="@+id/linear_theme"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_margin="7dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/imageViewTheme"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:contentDescription="@string/account"
|
||||||
|
android:minWidth="48dp"
|
||||||
|
android:minHeight="48dp"
|
||||||
|
android:padding="9dp"
|
||||||
|
android:scaleType="fitXY"
|
||||||
|
android:src="@drawable/ic_baseline_style_24"
|
||||||
|
app:tint="?iconNoAction" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_theme"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/theme"
|
||||||
|
android:textSize="16sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_theme_selected"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/theme_light"
|
||||||
|
android:textColor="?textSecondary" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/linear_info"
|
android:id="@+id/linear_info"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_marginTop="12dp"
|
android:layout_marginTop="12dp"
|
||||||
android:background="#ffffff"
|
android:background="?themeSecondary"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
@ -95,7 +283,6 @@
|
|||||||
android:paddingStart="7dp"
|
android:paddingStart="7dp"
|
||||||
android:paddingEnd="7dp"
|
android:paddingEnd="7dp"
|
||||||
android:text="@string/info"
|
android:text="@string/info"
|
||||||
android:textColor="@android:color/primary_text_light"
|
|
||||||
android:textSize="16sp"
|
android:textSize="16sp"
|
||||||
android:textStyle="bold" />
|
android:textStyle="bold" />
|
||||||
|
|
||||||
@ -103,6 +290,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,11 +298,13 @@
|
|||||||
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="9dp"
|
||||||
android:scaleType="fitXY"
|
android:scaleType="fitXY"
|
||||||
app:srcCompat="@drawable/ic_baseline_info_24" />
|
app:srcCompat="@drawable/ic_baseline_info_24"
|
||||||
|
app:tint="?iconNoAction"/>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@ -127,7 +317,6 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:text="@string/info_about"
|
android:text="@string/info_about"
|
||||||
android:textColor="@android:color/primary_text_light"
|
|
||||||
android:textSize="16sp" />
|
android:textSize="16sp" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
@ -136,11 +325,28 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:text="@string/info_about_desc"
|
android:text="@string/info_about_desc"
|
||||||
android:textColor="@android:color/secondary_text_light" />
|
android:textColor="?textSecondary" />
|
||||||
</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:textSize="16sp" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
||||||
|
@ -2,22 +2,76 @@
|
|||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/ff_test"
|
||||||
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="?themePrimary"
|
||||||
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>
|
@ -4,16 +4,22 @@
|
|||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:background="#f5f5f5"
|
android:background="?themePrimary"
|
||||||
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>
|
@ -4,8 +4,8 @@
|
|||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:background="#f5f5f5"
|
android:background="?themePrimary"
|
||||||
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,138 @@
|
|||||||
<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"
|
||||||
|
tools:src="@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_rounded_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="?themePrimary"
|
||||||
android:textSize="16sp" />
|
android:textSize="16sp"
|
||||||
|
app:backgroundTint="?buttonBackground"
|
||||||
|
app:icon="@drawable/ic_baseline_play_arrow_24"
|
||||||
|
app:iconGravity="textStart"
|
||||||
|
app:iconTint="?themePrimary" />
|
||||||
|
|
||||||
<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="?buttonBackground"
|
||||||
|
android:contentDescription="@string/my_list" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_my_list_action"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/my_list"
|
||||||
|
android:textColor="?textSecondary"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/recycler_episodes"
|
android:id="@+id/recycler_episodes"
|
||||||
@ -74,8 +155,26 @@
|
|||||||
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>
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/frame_loading"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="?themePrimary"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
|
<com.google.android.material.progressindicator.CircularProgressIndicator
|
||||||
|
android:id="@+id/loadingIndicator"
|
||||||
|
android:layout_width="70dp"
|
||||||
|
android:layout_height="70dp"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:indeterminate="true"
|
||||||
|
app:indicatorColor="?colorPrimary"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
</FrameLayout>
|
</FrameLayout>
|
@ -4,32 +4,40 @@
|
|||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:background="#f5f5f5"
|
android:background="?themePrimary"
|
||||||
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="?themeSecondary"
|
||||||
|
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"
|
||||||
|
android:searchIcon="@drawable/ic_baseline_search_24"
|
||||||
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">
|
||||||
|
|
||||||
</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:orientation="vertical"
|
||||||
|
android:padding="3dp"
|
||||||
|
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
|
||||||
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:spanCount="2"
|
||||||
|
tools:listitem="@layout/item_media">
|
||||||
|
|
||||||
|
</androidx.recyclerview.widget.RecyclerView>
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
67
app/src/main/res/layout/item_episode.xml
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
<?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:textColor="?textPrimary"
|
||||||
|
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"
|
||||||
|
app:tint="?iconNoAction" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_episode_desc"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="3"
|
||||||
|
android:textColor="?textSecondary" />
|
||||||
|
</LinearLayout>
|
43
app/src/main/res/layout/item_media.xml
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<?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"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="195dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:backgroundTint="?themeSecondary"
|
||||||
|
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"
|
||||||
|
tools: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>
|
|
147
app/src/main/res/layout/player_controls.xml
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout 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="match_parent"
|
||||||
|
android:background="#73000000">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/exo_top_controls"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:layout_marginTop="7dp"
|
||||||
|
android:layout_marginEnd="12dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/exo_close_player"
|
||||||
|
style="@style/ExoStyledControls.Button.Center"
|
||||||
|
android:layout_width="44dp"
|
||||||
|
android:layout_height="44dp"
|
||||||
|
android:contentDescription="@string/close_player"
|
||||||
|
android:padding="10dp"
|
||||||
|
app:srcCompat="@drawable/ic_baseline_arrow_back_24" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/exo_text_title"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="44dp"
|
||||||
|
android:text="@string/text_title_ex"
|
||||||
|
android:textAlignment="center"
|
||||||
|
android:textColor="@color/exo_white"
|
||||||
|
android:textSize="16sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/exo_main_controls"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:layout_weight="1" />
|
||||||
|
|
||||||
|
<org.mosad.teapod.ui.components.RewindButton
|
||||||
|
android:id="@+id/rwd_10"
|
||||||
|
android:layout_width="100dp"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:layout_weight="1" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/exo_play_pause"
|
||||||
|
style="@style/ExoStyledControls.Button.Center.PlayPause"
|
||||||
|
android:layout_width="52dp"
|
||||||
|
android:layout_height="52dp"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:contentDescription="@string/play_pause" />
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:layout_weight="1" />
|
||||||
|
|
||||||
|
<org.mosad.teapod.ui.components.FastForwardButton
|
||||||
|
android:id="@+id/ffwd_10"
|
||||||
|
android:layout_width="100dp"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:layout_weight="1" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/exo_time_controls"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="bottom"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:layout_marginEnd="12dp"
|
||||||
|
android:layout_marginBottom="@dimen/exo_styled_progress_margin_bottom">
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/exo_progress_placeholder"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="@dimen/exo_styled_progress_layout_height"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/exo_remaining"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/exo_remaining"
|
||||||
|
style="@style/ExoStyledControls.TimeText.Position"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/exo_bottom_controls"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="42dp"
|
||||||
|
android:layout_gravity="bottom"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:layout_marginEnd="12dp"
|
||||||
|
android:layout_marginBottom="7dp">
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/button_next_ep_c"
|
||||||
|
style="@style/Widget.AppCompat.Button.Borderless"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/episode"
|
||||||
|
android:textAllCaps="false"
|
||||||
|
app:icon="@drawable/ic_baseline_skip_next_24"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/button_episodes"
|
||||||
|
style="@style/Widget.AppCompat.Button.Borderless"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="7dp"
|
||||||
|
android:text="@string/episodes"
|
||||||
|
android:textAllCaps="false"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:icon="@drawable/ic_baseline_video_library_24"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/button_next_ep_c"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
</FrameLayout>
|
@ -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" />
|
||||||
|
|
||||||
|
68
app/src/main/res/raw/notices.xml
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
<?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>
|
||||||
|
<notice>
|
||||||
|
<name>The Movie Database API</name>
|
||||||
|
<url>https://www.themoviedb.org</url>
|
||||||
|
<copyright>This product uses the TMDb API but is not endorsed or certified by TMDb</copyright>
|
||||||
|
</notice>
|
||||||
|
</notices>
|
56
app/src/main/res/values-de-rDE/strings.xml
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
<?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">Flg. %1$d %2$s</string>
|
||||||
|
<string name="component_episode_title_sub">Flg. %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: git.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>
|
||||||
|
<string name="settings_autoplay">Autoplay</string>
|
||||||
|
<string name="settings_autoplay_desc">Nächste Episode automatisch abspielen</string>
|
||||||
|
<string name="theme">Design</string>
|
||||||
|
<string name="theme_light">Hell</string>
|
||||||
|
<string name="theme_dark">Dunkel</string>
|
||||||
|
|
||||||
|
<!-- player -->
|
||||||
|
<string name="close_player">Player schließen</string>
|
||||||
|
<string name="rewind_10">10 Sekunden zurück</string>
|
||||||
|
<string name="play_pause">Abspielen/Pause</string>
|
||||||
|
<string name="forward_10">10 Sekunden vorwärts</string>
|
||||||
|
<string name="next_episode">Nächste Folge</string>
|
||||||
|
<string name="episode">Folge</string>
|
||||||
|
<string name="episodes">Folgen</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>
|
10
app/src/main/res/values/attrs.xml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<attr format="color" name="themePrimary"/>
|
||||||
|
<attr format="color" name="themeSecondary"/>
|
||||||
|
<attr format="color" name="textPrimary"/>
|
||||||
|
<attr format="color" name="textSecondary"/>
|
||||||
|
<attr format="color" name="iconAction"/>
|
||||||
|
<attr format="color" name="iconNoAction"/>
|
||||||
|
<attr format="color" name="buttonBackground"/>
|
||||||
|
</resources>
|
@ -1,6 +1,28 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<color name="colorPrimary">#6200EE</color>
|
<!-- base theme colors -->
|
||||||
<color name="colorPrimaryDark">#3700B3</color>
|
<color name="colorPrimary">#66aa00</color>
|
||||||
<color name="colorAccent">#03DAC5</color>
|
<color name="colorPrimaryLight">#99dc45</color>
|
||||||
|
<color name="colorPrimaryDark">#317a00</color>
|
||||||
|
<color name="colorAccent">#607d8b</color>
|
||||||
|
|
||||||
|
<!-- light theme colors -->
|
||||||
|
<color name="themePrimaryLight">#f7f7f7</color>
|
||||||
|
<color name="themeSecondaryLight">#ffffff</color>
|
||||||
|
<color name="textPrimaryLight">#de000000</color>
|
||||||
|
<color name="textSecondaryLight">#99000000</color>
|
||||||
|
<color name="iconActionLight">#99000000</color>
|
||||||
|
<color name="iconNoActionLight">#66000000</color>
|
||||||
|
<color name="buttonBackgroundLight">#000000</color>
|
||||||
|
|
||||||
|
<!-- dark theme colors -->
|
||||||
|
<color name="themePrimaryDark">#000000</color>
|
||||||
|
<color name="themeSecondaryDark">#202020</color>
|
||||||
|
<color name="textPrimaryDark">#deffffff</color>
|
||||||
|
<color name="textSecondaryDark">#99ffffff</color>
|
||||||
|
<color name="iconActionDark">#99ffffff</color>
|
||||||
|
<color name="iconNoActionDark">#66ffffff</color>
|
||||||
|
<color name="buttonBackgroundDark">#ffffff</color>
|
||||||
|
|
||||||
|
<color name="ic_launcher_background">#ffffff</color>
|
||||||
</resources>
|
</resources>
|
33
app/src/main/res/values/css_style.xml
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="license_dialog_style_light" translatable="false">
|
||||||
|
body {
|
||||||
|
background-color: #ffffff;
|
||||||
|
color: #000000;
|
||||||
|
font-family: sans-serif;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
pre {
|
||||||
|
background-color: #eeeeee;
|
||||||
|
padding: 1em;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
</string>
|
||||||
|
|
||||||
|
<string name="license_dialog_style_dark" translatable="false">
|
||||||
|
body {
|
||||||
|
background-color: #303030;
|
||||||
|
color: #ffffff;
|
||||||
|
font-family: sans-serif;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
pre {
|
||||||
|
background-color: #424242;
|
||||||
|
padding: 1em;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
li a {
|
||||||
|
color: #21a3df;
|
||||||
|
}
|
||||||
|
</string>
|
||||||
|
</resources>
|
@ -1,24 +1,62 @@
|
|||||||
<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">Ep. %1$d %2$s</string>
|
||||||
|
<string name="component_episode_title_sub">Ep. %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: git.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>
|
||||||
|
<string name="settings_autoplay">Autoplay</string>
|
||||||
|
<string name="settings_autoplay_desc">Play next episode automatically</string>
|
||||||
|
<string name="theme">Theme</string>
|
||||||
|
<string name="theme_light">Light</string>
|
||||||
|
<string name="theme_dark">Dark</string>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- player -->
|
||||||
|
<string name="close_player">close player</string>
|
||||||
|
<string name="rewind_10">rewind 10 seconds</string>
|
||||||
|
<string name="play_pause">play/pause</string>
|
||||||
|
<string name="forward_10">forward 10 seconds</string>
|
||||||
|
<string name="rwd_10_s" translatable="false">- 10 s</string>
|
||||||
|
<string name="fwd_10_s" translatable="false">+ 10 s</string>
|
||||||
|
<string name="next_episode">Next Episode</string>
|
||||||
|
<string name="time_min_sec" translatable="false">%1$02d:%2$02d</string>
|
||||||
|
<string name="time_hour_min_sec" translatable="false">%1$d:%2$02d:%3$02d</string>
|
||||||
|
<string name="episode">Episode</string>
|
||||||
|
<string name="episodes">Episodes</string>
|
||||||
|
|
||||||
<!-- dialogs -->
|
<!-- dialogs -->
|
||||||
<string name="save">save</string>
|
<string name="save">save</string>
|
||||||
@ -26,16 +64,22 @@
|
|||||||
|
|
||||||
<!-- 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>
|
||||||
|
<string name="save_key_autoplay" translatable="false">org.mosad.teapod.autoplay</string>
|
||||||
|
<string name="save_key_theme" translatable="false">org.mosad.teapod.theme</string>
|
||||||
|
|
||||||
<!-- intents & states -->
|
<!-- intents & states -->
|
||||||
<string name="intent_stream_url" translatable="false">intent_stream_url</string>
|
<string name="intent_media_id" translatable="false">intent_media_id</string>
|
||||||
|
<string name="intent_episode_id" translatable="false">intent_episode_id</string>
|
||||||
<string name="state_resume_window" translatable="false">state_resume_window</string>
|
<string name="state_resume_window" translatable="false">state_resume_window</string>
|
||||||
<string name="state_resume_position" translatable="false">state_resume_position</string>
|
<string name="state_resume_position" translatable="false">state_resume_position</string>
|
||||||
<string name="state_is_playing" translatable="false">state_is_playing</string>
|
<string name="state_is_playing" translatable="false">state_is_playing</string>
|
||||||
|
@ -1,18 +1,67 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<!-- Base application theme. -->
|
<!-- application themes -->
|
||||||
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
|
<style name="AppTheme" parent="Theme.MaterialComponents.Light.NoActionBar">
|
||||||
<!-- Customize your theme here. -->
|
|
||||||
<item name="colorPrimary">@color/colorPrimary</item>
|
<item name="colorPrimary">@color/colorPrimary</item>
|
||||||
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||||
<item name="colorAccent">@color/colorAccent</item>
|
<item name="colorAccent">@color/colorAccent</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="AppTheme.AppCompat.Light.NoActionBar.FullScreen" parent="@style/Theme.AppCompat.Light.NoActionBar">
|
<style name="AppTheme.Light" parent="AppTheme">
|
||||||
|
<item name="themePrimary">@color/themePrimaryLight</item>
|
||||||
|
<item name="themeSecondary">@color/themeSecondaryLight</item>
|
||||||
|
<item name="textPrimary">@color/textPrimaryLight</item>
|
||||||
|
<item name="textSecondary">@color/textSecondaryLight</item>
|
||||||
|
<item name="android:textColor">@color/textPrimaryLight</item>
|
||||||
|
<item name="android:textColorPrimary">@color/textPrimaryLight</item>
|
||||||
|
<item name="android:textColorHint">@color/textSecondaryLight</item>
|
||||||
|
<item name="iconAction">@color/iconActionLight</item>
|
||||||
|
<item name="iconNoAction">@color/iconNoActionLight</item>
|
||||||
|
<item name="buttonBackground">@color/buttonBackgroundLight</item>
|
||||||
|
<item name="md_background_color">@color/themeSecondaryLight</item>
|
||||||
|
<item name="md_color_content">@color/textSecondaryLight</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="AppTheme.Dark" parent="AppTheme">
|
||||||
|
<item name="themePrimary">@color/themePrimaryDark</item>
|
||||||
|
<item name="themeSecondary">@color/themeSecondaryDark</item>
|
||||||
|
<item name="textPrimary">@color/textPrimaryDark</item>
|
||||||
|
<item name="textSecondary">@color/textSecondaryDark</item>
|
||||||
|
<item name="android:textColor">@color/textPrimaryDark</item>
|
||||||
|
<item name="android:textColorPrimary">@color/textPrimaryDark</item>
|
||||||
|
<item name="android:textColorHint">@color/textSecondaryDark</item>
|
||||||
|
<item name="iconAction">@color/iconActionDark</item>
|
||||||
|
<item name="iconNoAction">@color/iconNoActionDark</item>
|
||||||
|
<item name="buttonBackground">@color/buttonBackgroundDark</item>
|
||||||
|
<item name="md_background_color">@color/themeSecondaryDark</item>
|
||||||
|
<item name="md_color_content">@color/textSecondaryDark</item>
|
||||||
|
|
||||||
|
<!-- without this, the unchecked single choice buttons while be black -->
|
||||||
|
<item name="md_color_widget_unchecked">@color/textSecondaryDark</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="LicensesDialogTheme.Dark" parent="Theme.AppCompat.Dialog">
|
||||||
|
<item name="android:windowBackground">@color/themeSecondaryDark</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<!-- player theme -->
|
||||||
|
<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>
|
||||||
|
<item name="android:windowTranslucentStatus">true</item>
|
||||||
|
<item name="android:windowTranslucentNavigation">true</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<!-- splash theme -->
|
||||||
|
<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>
|
@ -1,12 +1,12 @@
|
|||||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
buildscript {
|
buildscript {
|
||||||
ext.kotlin_version = "1.4.10"
|
ext.kotlin_version = "1.4.20"
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
jcenter()
|
jcenter()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:4.0.2'
|
classpath 'com.android.tools.build:gradle:4.1.1'
|
||||||
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.
|
9
fastlane/metadata/android/de-DE/changelogs/2000.txt
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
* überarbeitetes Player Interface (#10)
|
||||||
|
* Autoplay für Serien
|
||||||
|
* Gesten für Vor-/Zurückspulen(doppelt tippen), Pause/Abspielen (lang tippen)
|
||||||
|
* verbleibende Zeit und Titel
|
||||||
|
* Unterstützung für Themes (#13)
|
||||||
|
* helles/dunkles Themes
|
||||||
|
* Primärfarbe angepasst
|
||||||
|
* Fehler beim Hinzufügen/löschen in "Meine Liste" behoben (#15)
|
||||||
|
* Fehler beim Parsen von Serien behoben
|
3
fastlane/metadata/android/de-DE/changelogs/2100.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
* Ein Fehler wurde behoben, bei dem Autoplay Folgen überspringen konnte
|
||||||
|
* Der Player zeigt nun einen Button an, um zur nächsten Folge zu springen
|
||||||
|
* Die UI wurde an einigen Stellen angepasst
|
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
|