Compare commits
168 Commits
0.1-alpha2
...
0.4.2
Author | SHA1 | Date | |
---|---|---|---|
19552d3950 | |||
4de97ca42e
|
|||
664959641f
|
|||
c1b0b4038c | |||
ba7d82bc2b
|
|||
e0a6485ed7
|
|||
5555269877
|
|||
3fcd1a96b2
|
|||
03e9c3dae5
|
|||
5ccf907ed8 | |||
8afbae1e1a
|
|||
164db8ebd1
|
|||
44d1825095
|
|||
1d071eafdb
|
|||
0decf317d9
|
|||
5e48e724a7
|
|||
46e3d1f1b6
|
|||
a3a89c6b64
|
|||
7ce67f57cd
|
|||
68d462eeee
|
|||
063b5405fc
|
|||
be591a961a
|
|||
8160641b8f
|
|||
86dfd69b4b
|
|||
74e8639435
|
|||
49e0b1ec29 | |||
e8ab11d5ff
|
|||
0bb433b5cb
|
|||
b05ecf64a6
|
|||
7a2f3ad265
|
|||
4f2bd4fd59
|
|||
af66d968cc | |||
06770559ee
|
|||
1a9de4124d
|
|||
6cc59a72fc
|
|||
a07f291098
|
|||
fad64ad385
|
|||
9d3e9c5019
|
|||
542164be9f | |||
09191f6732
|
|||
9d698a974d
|
|||
e762745705
|
|||
f342d1a3f4
|
|||
b02fadaa89
|
|||
f4760d1ba3 | |||
5bb51c9054 | |||
1e9e02c879 | |||
67c1e2bfdc
|
|||
70aafb1a14
|
|||
373f5c56c9
|
|||
4c5d6e6e24
|
|||
c6874d0e54
|
|||
a740ccfee1
|
|||
8a22554846
|
|||
3f45d769d2
|
|||
7dc120ccfe | |||
7a95304ee1
|
|||
8c0f4965e7 | |||
8e8db386a0
|
|||
86e07ba2cf
|
|||
e5037cf9ac
|
|||
a0111d45cf | |||
0efad7e2b7
|
|||
b12daa9d39 | |||
e4ac01605f
|
|||
75ecac6144
|
|||
1efc108bd7
|
|||
31197a5d44
|
|||
489ef02a35
|
|||
9705a752fb
|
|||
7a5f90cb82
|
|||
800c2a144c
|
|||
6bec0512ba
|
|||
b3ce43c614
|
|||
7845770067
|
|||
94da8c6cee
|
|||
8a43567737
|
|||
8f60a30d61
|
|||
8fc2d69eb8 | |||
2a0bccaf5a | |||
9a45d4453c
|
|||
c648acdff0
|
|||
00699aaec7
|
|||
bba642e9e3
|
|||
f4518056db | |||
1edcf29c07 | |||
04893060e4
|
|||
6fc7bb2c1e
|
|||
ab180ddd89
|
|||
98636d326e
|
|||
b73822c945 | |||
6775a4da2e
|
|||
a390bc9686
|
|||
82bf34e4cb
|
|||
e34e5b2bbd
|
|||
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
|
41
README.md
41
README.md
@ -1,27 +1,30 @@
|
|||||||
# teapod
|
# Teapod
|
||||||
|
|
||||||
A unoffical App for Anime-on-Demand.
|
Teapod is a unofficial App for Anime on Demand (AoD). It allows you to watch all your favourite animes from AoD on your android device. To use Teapod you need to have a subscription to AoD.
|
||||||
|
|
||||||
|
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" height="75">](https://f-droid.org/de/packages/org.mosad.teapod/)
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
* acces all media in the library
|
* Watch all animes from AoD on your Android device
|
||||||
* search the library
|
* Native Player based on ExoPayer
|
||||||
* play movies/tv shows via integrated exoplayer
|
* Prefer the OmU version via the app settings
|
||||||
|
* Save your favorite animes to "My List"
|
||||||
|
|
||||||
## 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.webp" width=180>](https://www.mosad.xyz/images/Teapod/Teapod_Home.webp)
|
||||||
[<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.webp" width=180>](https://www.mosad.xyz/images/Teapod/Teapod_Library.webp)
|
||||||
[<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.webp" width=180>](https://www.mosad.xyz/images/Teapod/Teapod_Media.webp)
|
||||||
|
[<img src="https://www.mosad.xyz/images/Teapod/Teapod_Search.webp" width=180>](https://www.mosad.xyz/images/Teapod/Teapod_Search.webp)
|
||||||
|
|
||||||
## License
|
### License
|
||||||
This App is licensed under the terms and conditions of GPL 3. This Project is not associated with Anime-on-Demand in any way.
|
Teapod is licensed under the terms and conditions of GPL 3. This Project is not associated with Anime on Demand in any way. But they allow open source apps for their service.
|
||||||
|
|
||||||
### Used Libraries
|
### Contributing
|
||||||
* gson: https://github.com/google/gson
|
Currentl you need to have an AoD account to contrtibut to Teapod. Contributing without on is kind of impossible.If you want to contribute to Teapod and need an account on this gitea instance, please write me an email.
|
||||||
* exoplayer: https://github.com/google/ExoPlayer
|
|
||||||
* jsoup: https://jsoup.org/
|
|
||||||
* material-dialogs: https://github.com/afollestad/material-dialogs
|
|
||||||
* kotlin.coroutines: https://github.com/Kotlin/kotlinx.coroutines
|
|
||||||
* Material design icons: https://github.com/google/material-design-icons
|
|
||||||
* androidx libraries
|
|
||||||
|
|
||||||
Teapod © 2020 [@Seil0](https://git.mosad.xyz/Seil0)
|
### [FAQ](https://git.mosad.xyz/Seil0/teapod/wiki#hilfe)
|
||||||
|
|
||||||
|
#### 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-2021 [@Seil0](https://git.mosad.xyz/Seil0)
|
||||||
|
@ -4,23 +4,28 @@ apply plugin: 'kotlin-android-extensions'
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 30
|
compileSdkVersion 30
|
||||||
buildToolsVersion "30.0.2"
|
buildToolsVersion "30.0.3"
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "org.mosad.teapod"
|
applicationId "org.mosad.teapod"
|
||||||
minSdkVersion 23
|
minSdkVersion 23
|
||||||
targetSdkVersion 30
|
targetSdkVersion 30
|
||||||
versionCode 1
|
versionCode 4200 //00.04.200
|
||||||
versionName "0.1-alpha2"
|
versionName "0.4.2"
|
||||||
|
|
||||||
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'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -36,33 +41,35 @@ android {
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation fileTree(dir: "libs", include: ["*.jar"])
|
implementation fileTree(dir: "libs", include: ["*.jar"])
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'
|
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0'
|
||||||
|
|
||||||
implementation 'androidx.core:core-ktx:1.3.2'
|
implementation 'androidx.core:core-ktx:1.6.0'
|
||||||
implementation 'androidx.appcompat:appcompat:1.2.0'
|
implementation 'androidx.appcompat:appcompat:1.3.0'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.2'
|
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
||||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.0'
|
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5'
|
||||||
implementation 'androidx.navigation:navigation-ui-ktx:2.3.0'
|
implementation 'androidx.navigation:navigation-ui-ktx:2.3.5'
|
||||||
implementation 'androidx.security:security-crypto:1.1.0-alpha02'
|
implementation 'androidx.security:security-crypto:1.1.0-alpha03'
|
||||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||||
|
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
|
||||||
|
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1'
|
||||||
|
|
||||||
implementation 'com.google.android.material:material:1.2.1'
|
implementation 'com.google.android.material:material:1.4.0'
|
||||||
implementation 'com.google.code.gson:gson:2.8.6'
|
implementation 'com.google.code.gson:gson:2.8.7'
|
||||||
implementation 'com.google.android.exoplayer:exoplayer-core:2.12.0'
|
implementation 'com.google.android.exoplayer:exoplayer-core:2.14.1'
|
||||||
implementation 'com.google.android.exoplayer:exoplayer-hls:2.12.0'
|
implementation 'com.google.android.exoplayer:exoplayer-hls:2.14.1'
|
||||||
implementation 'com.google.android.exoplayer:exoplayer-dash:2.12.0'
|
implementation 'com.google.android.exoplayer:exoplayer-dash:2.14.1'
|
||||||
implementation 'com.google.android.exoplayer:exoplayer-ui:2.12.0'
|
implementation 'com.google.android.exoplayer:exoplayer-ui:2.14.1'
|
||||||
|
implementation 'com.google.android.exoplayer:extension-mediasession:2.14.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.12.0'
|
||||||
implementation 'jp.wasabeef:glide-transformations:4.3.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 'de.psdev.licensesdialog:licensesdialog:2.1.0'
|
|
||||||
|
|
||||||
testImplementation 'junit:junit:4.12'
|
testImplementation 'junit:junit:4.13.2'
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
|
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
10
app/proguard-rules.pro
vendored
10
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
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
package="org.mosad.teapod">
|
package="org.mosad.teapod">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
@ -10,22 +11,40 @@
|
|||||||
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.Dark">
|
||||||
<activity
|
<activity
|
||||||
android:name=".PlayerActivity"
|
android:name="org.mosad.teapod.ui.activity.SplashActivity"
|
||||||
android:label="@string/app_name"
|
|
||||||
android:configChanges="orientation|screenSize|layoutDirection"
|
|
||||||
android:theme="@style/AppTheme.MaterialComponents.Light.NoActionBar.FullScreen" />
|
|
||||||
<activity
|
|
||||||
android:name=".MainActivity"
|
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
|
android:theme="@style/SplashTheme"
|
||||||
android:screenOrientation="portrait">
|
android:screenOrientation="portrait">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name="org.mosad.teapod.ui.activity.onboarding.OnboardingActivity"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:screenOrientation="portrait"
|
||||||
|
android:launchMode="singleTop"
|
||||||
|
android:windowSoftInputMode="adjustPan">
|
||||||
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name="org.mosad.teapod.ui.activity.main.MainActivity"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:screenOrientation="portrait">
|
||||||
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name="org.mosad.teapod.ui.activity.player.PlayerActivity"
|
||||||
|
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|layoutDirection"
|
||||||
|
android:autoRemoveFromRecents="true"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:launchMode="singleTask"
|
||||||
|
android:parentActivityName="org.mosad.teapod.ui.activity.main.MainActivity"
|
||||||
|
android:supportsPictureInPicture="true"
|
||||||
|
android:taskAffinity=".player.PlayerActivity"
|
||||||
|
android:theme="@style/PlayerTheme"
|
||||||
|
tools:targetApi="n" />
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
BIN
app/src/main/ic_launcher-playstore.png
Normal file
BIN
app/src/main/ic_launcher-playstore.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
@ -1,147 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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
|
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.util.Log
|
|
||||||
import android.view.MenuItem
|
|
||||||
import com.google.android.material.bottomnavigation.BottomNavigationView
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.fragment.app.commit
|
|
||||||
import kotlinx.android.synthetic.main.activity_main.*
|
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.mosad.teapod.parser.AoDParser
|
|
||||||
import org.mosad.teapod.preferences.EncryptedPreferences
|
|
||||||
import org.mosad.teapod.ui.MediaFragment
|
|
||||||
import org.mosad.teapod.ui.account.AccountFragment
|
|
||||||
import org.mosad.teapod.ui.components.LoginDialog
|
|
||||||
import org.mosad.teapod.ui.home.HomeFragment
|
|
||||||
import org.mosad.teapod.ui.library.LibraryFragment
|
|
||||||
import org.mosad.teapod.ui.search.SearchFragment
|
|
||||||
import org.mosad.teapod.util.Media
|
|
||||||
import org.mosad.teapod.util.TMDBApiController
|
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemSelectedListener {
|
|
||||||
|
|
||||||
private var activeFragment: Fragment = HomeFragment() // the currently active fragment, home at the start
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
setContentView(R.layout.activity_main)
|
|
||||||
val navView: BottomNavigationView = findViewById(R.id.nav_view)
|
|
||||||
navView.setOnNavigationItemSelectedListener(this)
|
|
||||||
|
|
||||||
load()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBackPressed() {
|
|
||||||
if (supportFragmentManager.backStackEntryCount > 0) {
|
|
||||||
supportFragmentManager.popBackStack()
|
|
||||||
} else {
|
|
||||||
if (activeFragment !is HomeFragment) {
|
|
||||||
nav_view.selectedItemId = R.id.navigation_home
|
|
||||||
} else {
|
|
||||||
super.onBackPressed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onNavigationItemSelected(item: MenuItem): Boolean {
|
|
||||||
val ret = when (item.itemId) {
|
|
||||||
R.id.navigation_home -> {
|
|
||||||
activeFragment = HomeFragment()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.navigation_library -> {
|
|
||||||
activeFragment = LibraryFragment()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.navigation_search -> {
|
|
||||||
activeFragment = SearchFragment()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.navigation_account -> {
|
|
||||||
activeFragment = AccountFragment()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
|
|
||||||
supportFragmentManager.commit {
|
|
||||||
replace(R.id.nav_host_fragment, activeFragment)
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun load() {
|
|
||||||
EncryptedPreferences.readCredentials(this)
|
|
||||||
|
|
||||||
// make sure credentials are set and valid
|
|
||||||
if (EncryptedPreferences.password.isEmpty()) {
|
|
||||||
showLoginDialog(true)
|
|
||||||
} else if (!AoDParser().login()) {
|
|
||||||
showLoginDialog(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO show loading fragment
|
|
||||||
*/
|
|
||||||
fun showDetailFragment(media: Media) = GlobalScope.launch {
|
|
||||||
media.episodes = AoDParser().loadStreams(media) // load the streams for the selected media
|
|
||||||
|
|
||||||
val tmdb = TMDBApiController().search(media.title, media.type)
|
|
||||||
|
|
||||||
val mediaFragment = MediaFragment(media, tmdb)
|
|
||||||
supportFragmentManager.commit {
|
|
||||||
add(R.id.nav_host_fragment, mediaFragment, "MediaFragment")
|
|
||||||
addToBackStack(null)
|
|
||||||
show(mediaFragment)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun startPlayer(streamUrl: String) {
|
|
||||||
val intent = Intent(this, PlayerActivity::class.java).apply {
|
|
||||||
putExtra(getString(R.string.intent_stream_url), streamUrl)
|
|
||||||
}
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showLoginDialog(firstTry: Boolean) {
|
|
||||||
LoginDialog(this, firstTry).positiveButton {
|
|
||||||
EncryptedPreferences.saveCredentials(login, password, context)
|
|
||||||
|
|
||||||
if (!AoDParser().login()) {
|
|
||||||
showLoginDialog(false)
|
|
||||||
Log.w(javaClass.name, "Login failed, please try again.")
|
|
||||||
}
|
|
||||||
}.negativeButton {
|
|
||||||
Log.i(javaClass.name, "Login canceled, exiting.")
|
|
||||||
finish()
|
|
||||||
}.show()
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,3 +1,25 @@
|
|||||||
|
/**
|
||||||
|
* Teapod
|
||||||
|
*
|
||||||
|
* Copyright 2020-2021 <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,30 +28,37 @@ 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.lang.NumberFormatException
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.random.Random
|
||||||
|
|
||||||
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"
|
||||||
|
private const val subscriptionPath = "/mypools"
|
||||||
|
|
||||||
companion object {
|
private const val userAgent = "Mozilla/5.0 (X11; Linux x86_64; rv:84.0) Gecko/20100101 Firefox/84.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 val mediaList = arrayListOf<Media>() // actual media (data)
|
||||||
|
val itemMediaList = arrayListOf<ItemMedia>() // gui media
|
||||||
|
val highlightsList = arrayListOf<ItemMedia>()
|
||||||
|
val newEpisodesList = arrayListOf<ItemMedia>()
|
||||||
|
val newSimulcastsList = arrayListOf<ItemMedia>()
|
||||||
|
val newTitlesList = arrayListOf<ItemMedia>()
|
||||||
|
val topTenList = arrayListOf<ItemMedia>()
|
||||||
|
|
||||||
fun login(): Boolean = runBlocking {
|
fun login(): Boolean = runBlocking {
|
||||||
|
|
||||||
val userAgent = "Mozilla/5.0 (X11; Linux x86_64; rv:80.0) Gecko/20100101 Firefox/80.0"
|
withContext(Dispatchers.IO) {
|
||||||
|
|
||||||
withContext(Dispatchers.Default) {
|
|
||||||
// get the authenticity token
|
// get the authenticity token
|
||||||
val resAuth = Jsoup.connect(baseUrl + loginPath)
|
val resAuth = Jsoup.connect(baseUrl + loginPath)
|
||||||
.header("User-Agent", userAgent)
|
.header("User-Agent", userAgent)
|
||||||
@ -38,8 +67,8 @@ class AoDParser {
|
|||||||
val authenticityToken = resAuth.parse().select("meta[name=csrf-token]").attr("content")
|
val authenticityToken = resAuth.parse().select("meta[name=csrf-token]").attr("content")
|
||||||
val authCookies = resAuth.cookies()
|
val authCookies = resAuth.cookies()
|
||||||
|
|
||||||
Log.i(javaClass.name, "Received authenticity token: $authenticityToken")
|
//Log.d(javaClass.name, "Received authenticity token: $authenticityToken")
|
||||||
Log.i(javaClass.name, "Received authenticity cookies: $authCookies")
|
//Log.d(javaClass.name, "Received authenticity cookies: $authCookies")
|
||||||
|
|
||||||
val data = mapOf(
|
val data = mapOf(
|
||||||
Pair("user[login]", EncryptedPreferences.login),
|
Pair("user[login]", EncryptedPreferences.login),
|
||||||
@ -51,15 +80,16 @@ 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 default is 60000 (60 sec)
|
||||||
.data(data)
|
.data(data)
|
||||||
.postDataCharset("UTF-8")
|
.postDataCharset("UTF-8")
|
||||||
.cookies(authCookies)
|
.cookies(authCookies)
|
||||||
.execute()
|
.execute()
|
||||||
|
|
||||||
//println(resLogin.body())
|
//println(resLogin.body())
|
||||||
|
|
||||||
sessionCookies = resLogin.cookies()
|
sessionCookies = resLogin.cookies()
|
||||||
loginSuccess = resLogin.body().contains("Hallo, du bist jetzt angemeldet.")
|
loginSuccess = resLogin.body().contains("Hallo, du bist jetzt angemeldet.")
|
||||||
|
|
||||||
Log.i(javaClass.name, "Status: ${resLogin.statusCode()} (${resLogin.statusMessage()}), login successful: $loginSuccess")
|
Log.i(javaClass.name, "Status: ${resLogin.statusCode()} (${resLogin.statusMessage()}), login successful: $loginSuccess")
|
||||||
|
|
||||||
loginSuccess
|
loginSuccess
|
||||||
@ -67,108 +97,60 @@ class AoDParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* list all animes from the website
|
* initially load all media and home screen data
|
||||||
*/
|
*/
|
||||||
fun listAnimes(): ArrayList<Media> = runBlocking {
|
suspend fun initialLoading() {
|
||||||
if (sessionCookies.isEmpty()) login()
|
coroutineScope {
|
||||||
|
launch { loadHome() }
|
||||||
withContext(Dispatchers.Default) {
|
launch { listAnimes() }
|
||||||
val resAnimes = Jsoup.connect(baseUrl + libraryPath)
|
|
||||||
.cookies(sessionCookies)
|
|
||||||
.get()
|
|
||||||
|
|
||||||
//println(resAnimes)
|
|
||||||
|
|
||||||
mediaList.clear()
|
|
||||||
resAnimes.select("div.animebox").forEach {
|
|
||||||
val type = if (it.select("p.animebox-link").select("a").text().toLowerCase(Locale.ROOT) == "zur serie") {
|
|
||||||
MediaType.TVSHOW
|
|
||||||
} else {
|
|
||||||
MediaType.MOVIE
|
|
||||||
}
|
|
||||||
|
|
||||||
val media = Media(
|
|
||||||
it.select("h3.animebox-title").text(),
|
|
||||||
it.select("p.animebox-link").select("a").attr("href"),
|
|
||||||
type
|
|
||||||
)
|
|
||||||
media.info.posterLink = it.select("p.animebox-image").select("img").attr("src")
|
|
||||||
media.info.shortDesc = it.select("p.animebox-shorttext").text()
|
|
||||||
|
|
||||||
mediaList.add(media)
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.i(javaClass.name, "Total library size is: ${mediaList.size}")
|
|
||||||
|
|
||||||
return@withContext mediaList
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* load streams for the media path
|
* get a media by it's ID (int)
|
||||||
|
* @return Media
|
||||||
*/
|
*/
|
||||||
fun loadStreams(media: Media): List<Episode> = runBlocking {
|
suspend fun getMediaById(mediaId: Int): Media {
|
||||||
if (sessionCookies.isEmpty()) login()
|
val media = mediaList.first { it.id == mediaId }
|
||||||
|
|
||||||
if (!loginSuccess) {
|
if (media.episodes.isEmpty()) {
|
||||||
Log.w(javaClass.name, "Login, was not successful.")
|
loadStreams(media).join()
|
||||||
return@runBlocking listOf()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
withContext(Dispatchers.Default) {
|
return media
|
||||||
|
}
|
||||||
|
|
||||||
val res = Jsoup.connect(baseUrl + media.link)
|
/**
|
||||||
.cookies(sessionCookies)
|
* get subscription info from aod website, remove "Anime-Abo" Prefix and trim
|
||||||
.get()
|
*/
|
||||||
|
suspend fun getSubscriptionInfoAsync(): Deferred<String> {
|
||||||
|
return coroutineScope {
|
||||||
|
async(Dispatchers.IO) {
|
||||||
|
val res = Jsoup.connect(baseUrl + subscriptionPath)
|
||||||
|
.cookies(sessionCookies)
|
||||||
|
.get()
|
||||||
|
|
||||||
//println(res)
|
return@async res.select("a:contains(Anime-Abo)").text()
|
||||||
|
.removePrefix("Anime-Abo").trim()
|
||||||
// parse additional info from the media page
|
|
||||||
res.select("table.vertical-table").select("tr").forEach {
|
|
||||||
when (it.select("th").text().toLowerCase(Locale.ROOT)) {
|
|
||||||
"produktionsjahr" -> media.info.year = it.select("td").text().toInt()
|
|
||||||
"fsk" -> media.info.age = it.select("td").text().toInt()
|
|
||||||
"episodenanzahl" -> media.info.episodesCount = it.select("td").text().toInt()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO tv show specific for each episode (div.episodebox)
|
|
||||||
* * watchedCallback
|
|
||||||
*/
|
|
||||||
val episodes = if (media.type == MediaType.TVSHOW) {
|
|
||||||
res.select("div.three-box-container > div.episodebox").map { episodebox ->
|
|
||||||
val episodeId = episodebox.select("div.flip-front").attr("id").substringAfter("-").toInt()
|
|
||||||
val episodeWatched = episodebox.select("div.episodebox-icons > div").hasClass("status-icon-orange")
|
|
||||||
val episodeShortDesc = episodebox.select("p.episodebox-shorttext").text()
|
|
||||||
|
|
||||||
Episode(id = episodeId, watched = episodeWatched, shortDesc = episodeShortDesc)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
listOf(Episode())
|
|
||||||
}
|
|
||||||
|
|
||||||
// has attr data-lag (ger or jap)
|
|
||||||
val playlists = res.select("input.streamstarter_html5").eachAttr("data-playlist")
|
|
||||||
val csrfToken = res.select("meta[name=csrf-token]").attr("content")
|
|
||||||
|
|
||||||
//println("first entry: ${playlists.first()}")
|
|
||||||
//println("csrf token is: $csrfToken")
|
|
||||||
|
|
||||||
return@withContext if (playlists.size > 0) {
|
|
||||||
loadStreamInfo(playlists.first(), csrfToken, media.type, episodes)
|
|
||||||
} else {
|
|
||||||
listOf()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
fun getSubscriptionUrl(): String {
|
||||||
* load the playlist path and parse it, read the stream info from json
|
return baseUrl + subscriptionPath
|
||||||
* @param episodes is used as call ba reference, additionally it is passed a return value
|
}
|
||||||
*/
|
|
||||||
private fun loadStreamInfo(playlistPath: String, csrfToken: String, type: MediaType, episodes: List<Episode>): List<Episode> = runBlocking {
|
suspend fun markAsWatched(mediaId: Int, episodeId: Int) {
|
||||||
withContext(Dispatchers.Default) {
|
val episode = getMediaById(mediaId).getEpisodeById(episodeId)
|
||||||
|
episode.watched = true
|
||||||
|
sendCallback(episode.watchedCallback)
|
||||||
|
|
||||||
|
Log.d(javaClass.name, "Marked episode ${episode.id} as watched")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO don't use jsoup here
|
||||||
|
private suspend fun sendCallback(callbackPath: String) = coroutineScope {
|
||||||
|
launch(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"),
|
||||||
@ -177,56 +159,314 @@ class AoDParser {
|
|||||||
Pair("X-Requested-With", "XMLHttpRequest"),
|
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
|
||||||
|
* TODO private suspend fun listAnimes() = withContext(Dispatchers.IO) should also work, maybe a bug in android studio?
|
||||||
|
*/
|
||||||
|
private suspend fun listAnimes() = withContext(Dispatchers.IO) {
|
||||||
|
launch(Dispatchers.IO) {
|
||||||
|
val resAnimes = Jsoup.connect(baseUrl + libraryPath).get()
|
||||||
|
//println(resAnimes)
|
||||||
|
|
||||||
|
itemMediaList.clear()
|
||||||
|
mediaList.clear()
|
||||||
|
resAnimes.select("div.animebox").forEach {
|
||||||
|
val type = if (it.select("p.animebox-link").select("a").text().lowercase(Locale.ROOT) == "zur serie") {
|
||||||
|
MediaType.TVSHOW
|
||||||
|
} else {
|
||||||
|
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()
|
||||||
|
|
||||||
|
itemMediaList.add(ItemMedia(mediaId, mediaTitle, mediaImage))
|
||||||
|
mediaList.add(Media(mediaId, mediaLink, type).apply {
|
||||||
|
info.title = mediaTitle
|
||||||
|
info.posterUrl = mediaImage
|
||||||
|
info.shortDesc = mediaShortText
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.i(javaClass.name, "Total library size is: ${mediaList.size}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* load new episodes, titles and highlights
|
||||||
|
*/
|
||||||
|
private suspend fun loadHome() = withContext(Dispatchers.IO) {
|
||||||
|
launch(Dispatchers.IO) {
|
||||||
|
val resHome = Jsoup.connect(baseUrl).get()
|
||||||
|
|
||||||
|
// get highlights from AoD
|
||||||
|
highlightsList.clear()
|
||||||
|
resHome.select("#aod-highlights").select("div.news-item").forEach {
|
||||||
|
val mediaId = it.select("div.news-item-text").select("a.serienlink")
|
||||||
|
.attr("href").substringAfterLast("/").toIntOrNull()
|
||||||
|
val mediaTitle = it.select("div.news-title").select("h2").text()
|
||||||
|
val mediaImage = it.select("img").attr("src")
|
||||||
|
|
||||||
|
if (mediaId != null) {
|
||||||
|
highlightsList.add(ItemMedia(mediaId, mediaTitle, mediaImage))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// get all new episodes from AoD
|
||||||
|
newEpisodesList.clear()
|
||||||
|
resHome.select("h2:contains(Neue Episoden)").next().select("li").forEach {
|
||||||
|
val mediaId = it.select("a.thumbs").attr("href")
|
||||||
|
.substringAfterLast("/").toIntOrNull()
|
||||||
|
val mediaImage = it.select("a.thumbs > img").attr("src")
|
||||||
|
val mediaTitle = "${it.select("a").text()} - ${it.select("span.neweps").text()}"
|
||||||
|
|
||||||
|
if (mediaId != null) {
|
||||||
|
newEpisodesList.add(ItemMedia(mediaId, mediaTitle, mediaImage))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// get new simulcasts from AoD
|
||||||
|
newSimulcastsList.clear()
|
||||||
|
resHome.select("h2:contains(Neue Simulcasts)").next().select("li").forEach {
|
||||||
|
val mediaId = it.select("a.thumbs").attr("href")
|
||||||
|
.substringAfterLast("/").toIntOrNull()
|
||||||
|
val mediaImage = it.select("a.thumbs > img").attr("src")
|
||||||
|
val mediaTitle = it.select("a").text()
|
||||||
|
|
||||||
|
if (mediaId != null) {
|
||||||
|
newSimulcastsList.add(ItemMedia(mediaId, mediaTitle, mediaImage))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// get new titles from AoD
|
||||||
|
newTitlesList.clear()
|
||||||
|
resHome.select("h2:contains(Neue Anime-Titel)").next().select("li").forEach {
|
||||||
|
val mediaId = it.select("a.thumbs").attr("href")
|
||||||
|
.substringAfterLast("/").toIntOrNull()
|
||||||
|
val mediaImage = it.select("a.thumbs > img").attr("src")
|
||||||
|
val mediaTitle = it.select("a").text()
|
||||||
|
|
||||||
|
if (mediaId != null) {
|
||||||
|
newTitlesList.add(ItemMedia(mediaId, mediaTitle, mediaImage))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// get top ten from AoD
|
||||||
|
topTenList.clear()
|
||||||
|
resHome.select("h2:contains(Anime Top 10)").next().select("li").forEach {
|
||||||
|
val mediaId = it.select("a.thumbs").attr("href")
|
||||||
|
.substringAfterLast("/").toIntOrNull()
|
||||||
|
val mediaImage = it.select("a.thumbs > img").attr("src")
|
||||||
|
val mediaTitle = it.select("a").text()
|
||||||
|
|
||||||
|
if (mediaId != null) {
|
||||||
|
topTenList.add(ItemMedia(mediaId, mediaTitle, mediaImage))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if highlights is empty, add a random new title
|
||||||
|
if (highlightsList.isEmpty()) {
|
||||||
|
if (newTitlesList.isNotEmpty()) {
|
||||||
|
highlightsList.add(newTitlesList[Random.nextInt(0, newTitlesList.size)])
|
||||||
|
} else {
|
||||||
|
highlightsList.add(ItemMedia(0,"", ""))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.i(javaClass.name, "loaded home")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO rework the media loading process, don't modify media object
|
||||||
|
* TODO catch SocketTimeoutException from loading to show a waring dialog
|
||||||
|
* load streams for the media path, movies have one episode
|
||||||
|
* @param media is used as call ba reference
|
||||||
|
*/
|
||||||
|
private suspend fun loadStreams(media: Media) = coroutineScope {
|
||||||
|
launch(Dispatchers.IO) {
|
||||||
|
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 besides = res.select("div.besides").first()
|
||||||
|
val playlists = besides.select("input.streamstarter_html5").map { streamstarter ->
|
||||||
|
parsePlaylistAsync(
|
||||||
|
streamstarter.attr("data-playlist"),
|
||||||
|
streamstarter.attr("data-lang")
|
||||||
|
)
|
||||||
|
}.awaitAll()
|
||||||
|
|
||||||
|
playlists.forEach { aod ->
|
||||||
|
// TODO improve language handling
|
||||||
|
val locale = when (aod.extLanguage) {
|
||||||
|
"ger" -> Locale.GERMAN
|
||||||
|
"jap" -> Locale.JAPANESE
|
||||||
|
else -> Locale.ROOT
|
||||||
|
}
|
||||||
|
|
||||||
|
aod.playlist.forEach { ep ->
|
||||||
|
try {
|
||||||
|
if (media.hasEpisode(ep.mediaid)) {
|
||||||
|
media.getEpisodeById(ep.mediaid).streams.add(
|
||||||
|
Stream(ep.sources.first().file, locale)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
media.episodes.add(Episode(
|
||||||
|
id = ep.mediaid,
|
||||||
|
streams = mutableListOf(Stream(ep.sources.first().file, locale)),
|
||||||
|
posterUrl = ep.image,
|
||||||
|
title = ep.title,
|
||||||
|
description = ep.description,
|
||||||
|
number = getNumberFromTitle(ep.title, media.type)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
Log.w(javaClass.name, "Could not parse episode information.", ex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Log.i(javaClass.name, "Loaded playlists successfully")
|
||||||
|
|
||||||
|
// additional info from the media page
|
||||||
|
res.select("table.vertical-table").select("tr").forEach { row ->
|
||||||
|
when (row.select("th").text().lowercase(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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// similar titles from media page
|
||||||
|
media.info.similar = res.select("h2:contains(Ähnliche Animes)").next().select("li").mapNotNull {
|
||||||
|
val mediaId = it.select("a.thumbs").attr("href")
|
||||||
|
.substringAfterLast("/").toIntOrNull()
|
||||||
|
val mediaImage = it.select("a.thumbs > img").attr("src")
|
||||||
|
val mediaTitle = it.select("a").text()
|
||||||
|
|
||||||
|
if (mediaId != null) {
|
||||||
|
ItemMedia(mediaId, mediaTitle, mediaImage)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Log.i(javaClass.name, "media loaded successfully")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* don't use Gson().fromJson() as we don't have any control over the api and it may change
|
||||||
|
*/
|
||||||
|
private fun parsePlaylistAsync(playlistPath: String, language: String): Deferred<AoDObject> {
|
||||||
|
if (playlistPath == "[]") {
|
||||||
|
return CompletableDeferred(AoDObject(listOf(), language))
|
||||||
|
}
|
||||||
|
|
||||||
|
return CoroutineScope(Dispatchers.IO).async(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"),
|
||||||
|
)
|
||||||
|
|
||||||
|
//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)
|
||||||
|
.timeout(120000) // loading the playlist can take some time
|
||||||
.execute()
|
.execute()
|
||||||
|
|
||||||
//println(res.body())
|
//Gson().fromJson(res.body(), AoDObject::class.java)
|
||||||
|
|
||||||
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)
|
||||||
|
},
|
||||||
|
image = it.asJsonObject.get("image").asString,
|
||||||
|
title = it.asJsonObject.get("title").asString,
|
||||||
|
description = it.asJsonObject.get("description").asString,
|
||||||
|
mediaid = it.asJsonObject.get("mediaid").asInt
|
||||||
|
)
|
||||||
|
},
|
||||||
|
language
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
movie.first().asJsonObject.get("sources").asJsonArray.toList().forEach {
|
/**
|
||||||
episodes.first().streamUrl = it.asJsonObject.get("file").asString
|
* get the episode number from the title
|
||||||
}
|
* @param title the episode title, containing a number after "Ep."
|
||||||
}
|
* @param type the media type, if not TVSHOW, return 0
|
||||||
|
* @return the episode number, on NumberFormatException return 0
|
||||||
MediaType.TVSHOW -> {
|
*/
|
||||||
val episodesJson = JsonParser.parseString(res.body()).asJsonObject
|
private fun getNumberFromTitle(title: String, type: MediaType): Int {
|
||||||
.get("playlist").asJsonArray
|
return if (type == MediaType.TVSHOW) {
|
||||||
|
try {
|
||||||
|
title.substringAfter(", Ep. ").toInt()
|
||||||
episodesJson.forEach { jsonElement ->
|
} catch (nex: NumberFormatException) {
|
||||||
val episodeId = jsonElement.asJsonObject.get("mediaid")
|
0
|
||||||
val episodeStream = jsonElement.asJsonObject.get("sources").asJsonArray
|
|
||||||
.first().asJsonObject
|
|
||||||
.get("file").asString
|
|
||||||
val episodeTitle = jsonElement.asJsonObject.get("title").asString
|
|
||||||
val episodePoster = jsonElement.asJsonObject.get("image").asString
|
|
||||||
val episodeDescription = jsonElement.asJsonObject.get("description").asString
|
|
||||||
val episodeNumber = episodeTitle.substringAfter(", Ep. ").toInt()
|
|
||||||
|
|
||||||
episodes.first { it.id == episodeId.asInt }.apply {
|
|
||||||
this.title = episodeTitle
|
|
||||||
this.posterLink = episodePoster
|
|
||||||
this.streamUrl = episodeStream
|
|
||||||
this.description = episodeDescription
|
|
||||||
this.number = episodeNumber
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
Log.e(javaClass.name, "Wrong Type, please report this issue.")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
return@withContext episodes
|
0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,22 +1,85 @@
|
|||||||
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 devSettings = false
|
||||||
|
internal set
|
||||||
|
var theme = DataTypes.Theme.DARK
|
||||||
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 saveDevSettings(context: Context, devSettings: Boolean) {
|
||||||
|
with(getSharedPref(context).edit()) {
|
||||||
|
putBoolean(context.getString(R.string.save_key_dev_settings), devSettings)
|
||||||
|
apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.devSettings = devSettings
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
devSettings = sharedPref.getBoolean(
|
||||||
|
context.getString(R.string.save_key_dev_settings), false
|
||||||
|
)
|
||||||
|
theme = DataTypes.Theme.valueOf(
|
||||||
|
sharedPref.getString(
|
||||||
|
context.getString(R.string.save_key_theme), DataTypes.Theme.DARK.toString()
|
||||||
|
) ?: DataTypes.Theme.DARK.toString()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
@ -1,103 +0,0 @@
|
|||||||
package org.mosad.teapod.ui
|
|
||||||
|
|
||||||
import android.graphics.Color
|
|
||||||
import android.graphics.drawable.ColorDrawable
|
|
||||||
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 com.bumptech.glide.request.RequestOptions
|
|
||||||
import jp.wasabeef.glide.transformations.BlurTransformation
|
|
||||||
import kotlinx.android.synthetic.main.fragment_media.*
|
|
||||||
import org.mosad.teapod.MainActivity
|
|
||||||
import org.mosad.teapod.R
|
|
||||||
import org.mosad.teapod.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)
|
|
||||||
|
|
||||||
initGUI()
|
|
||||||
initActions()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* if tmdb data is present, use it, else use the aod data
|
|
||||||
*/
|
|
||||||
private fun initGUI() {
|
|
||||||
// generic gui
|
|
||||||
val backdropUrl = if (tmdb.backdropUrl.isNotEmpty()) tmdb.backdropUrl else media.info.posterLink
|
|
||||||
val posterUrl = if (tmdb.posterUrl.isNotEmpty()) tmdb.posterUrl else media.info.posterLink
|
|
||||||
|
|
||||||
Glide.with(requireContext()).load(backdropUrl)
|
|
||||||
.apply(RequestOptions.placeholderOf(ColorDrawable(Color.DKGRAY)))
|
|
||||||
.apply(RequestOptions.bitmapTransform(BlurTransformation(25, 3)))
|
|
||||||
.into(image_backdrop)
|
|
||||||
|
|
||||||
Glide.with(requireContext()).load(posterUrl)
|
|
||||||
.into(image_poster)
|
|
||||||
|
|
||||||
text_title.text = media.title
|
|
||||||
text_year.text = media.info.year.toString()
|
|
||||||
text_age.text = media.info.age.toString()
|
|
||||||
text_overview.text = media.info.shortDesc //if (tmdb.overview.isNotEmpty()) tmdb.overview else media.shortDesc
|
|
||||||
|
|
||||||
// specific gui
|
|
||||||
if (media.type == MediaType.TVSHOW) {
|
|
||||||
adapterRecEpisodes = EpisodesAdapter(media.episodes, requireContext())
|
|
||||||
viewManager = LinearLayoutManager(context)
|
|
||||||
recycler_episodes.layoutManager = viewManager
|
|
||||||
recycler_episodes.adapter = adapterRecEpisodes
|
|
||||||
|
|
||||||
text_episodes_or_runtime.text = getString(R.string.text_episodes_count, media.info.episodesCount)
|
|
||||||
} else if (media.type == MediaType.MOVIE) {
|
|
||||||
recycler_episodes.visibility = View.GONE
|
|
||||||
|
|
||||||
if (tmdb.runtime > 0) {
|
|
||||||
text_episodes_or_runtime.text = getString(R.string.text_runtime, tmdb.runtime)
|
|
||||||
} else {
|
|
||||||
text_episodes_or_runtime.visibility = View.GONE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initActions() {
|
|
||||||
button_play.setOnClickListener {
|
|
||||||
when (media.type) {
|
|
||||||
MediaType.MOVIE -> playStream(media.episodes.first().streamUrl)
|
|
||||||
MediaType.TVSHOW -> playStream(media.episodes.first().streamUrl)
|
|
||||||
else -> Log.e(javaClass.name, "Wrong Type: $media.type")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// set onItemClick only in adapter is initialized
|
|
||||||
if (this::adapterRecEpisodes.isInitialized) {
|
|
||||||
adapterRecEpisodes.onItemClick = { _, position ->
|
|
||||||
playStream(media.episodes[position].streamUrl)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun playStream(url: String) {
|
|
||||||
val mainActivity = activity as MainActivity
|
|
||||||
mainActivity.startPlayer(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,69 +0,0 @@
|
|||||||
package org.mosad.teapod.ui.account
|
|
||||||
|
|
||||||
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.google.android.material.dialog.MaterialAlertDialogBuilder
|
|
||||||
import de.psdev.licensesdialog.LicensesDialog
|
|
||||||
import kotlinx.android.synthetic.main.fragment_account.*
|
|
||||||
import org.mosad.teapod.BuildConfig
|
|
||||||
import org.mosad.teapod.R
|
|
||||||
import org.mosad.teapod.parser.AoDParser
|
|
||||||
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 {
|
|
||||||
showLoginDialog(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
linear_about.setOnClickListener {
|
|
||||||
MaterialAlertDialogBuilder(requireContext())
|
|
||||||
.setTitle(R.string.info_about)
|
|
||||||
.setMessage(R.string.info_about_dialog)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
text_licenses.setOnClickListener {
|
|
||||||
LicensesDialog.Builder(requireContext())
|
|
||||||
.setNotices(R.raw.notices)
|
|
||||||
.setTitle(R.string.licenses)
|
|
||||||
.setIncludeOwnLicense(true)
|
|
||||||
.setThemeResourceId(R.style.AppTheme)
|
|
||||||
.build()
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showLoginDialog(firstTry: Boolean) {
|
|
||||||
LoginDialog(requireContext(), firstTry).positiveButton {
|
|
||||||
EncryptedPreferences.saveCredentials(login, password, context)
|
|
||||||
|
|
||||||
if (!AoDParser().login()) {
|
|
||||||
showLoginDialog(false)
|
|
||||||
Log.w(javaClass.name, "Login failed, please try again.")
|
|
||||||
}
|
|
||||||
}.show {
|
|
||||||
login = EncryptedPreferences.login
|
|
||||||
password = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,18 @@
|
|||||||
|
package org.mosad.teapod.ui.activity
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import org.mosad.teapod.ui.activity.main.MainActivity
|
||||||
|
|
||||||
|
|
||||||
|
class SplashActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
val intent = Intent(this, MainActivity::class.java)
|
||||||
|
startActivity(intent)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,218 @@
|
|||||||
|
/**
|
||||||
|
* Teapod
|
||||||
|
*
|
||||||
|
* Copyright 2020-2021 <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.ui.activity.main
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.MenuItem
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.commit
|
||||||
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
|
import com.afollestad.materialdialogs.callbacks.onDismiss
|
||||||
|
import com.google.android.material.navigation.NavigationBarView
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import org.mosad.teapod.R
|
||||||
|
import org.mosad.teapod.databinding.ActivityMainBinding
|
||||||
|
import org.mosad.teapod.parser.AoDParser
|
||||||
|
import org.mosad.teapod.preferences.EncryptedPreferences
|
||||||
|
import org.mosad.teapod.preferences.Preferences
|
||||||
|
import org.mosad.teapod.ui.activity.main.fragments.AccountFragment
|
||||||
|
import org.mosad.teapod.ui.activity.main.fragments.HomeFragment
|
||||||
|
import org.mosad.teapod.ui.activity.main.fragments.LibraryFragment
|
||||||
|
import org.mosad.teapod.ui.activity.main.fragments.SearchFragment
|
||||||
|
import org.mosad.teapod.ui.activity.onboarding.OnboardingActivity
|
||||||
|
import org.mosad.teapod.ui.activity.player.PlayerActivity
|
||||||
|
import org.mosad.teapod.ui.components.LoginDialog
|
||||||
|
import org.mosad.teapod.util.DataTypes
|
||||||
|
import org.mosad.teapod.util.StorageController
|
||||||
|
import org.mosad.teapod.util.exitAndRemoveTask
|
||||||
|
import java.net.SocketTimeoutException
|
||||||
|
import kotlin.system.measureTimeMillis
|
||||||
|
|
||||||
|
class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListener {
|
||||||
|
|
||||||
|
private lateinit var binding: ActivityMainBinding
|
||||||
|
private var activeBaseFragment: Fragment = HomeFragment() // the currently active fragment, home at the start
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
var wasInitialized = false
|
||||||
|
lateinit var instance: MainActivity
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
instance = this
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
if (!wasInitialized) { load() }
|
||||||
|
theme.applyStyle(getThemeResource(), true)
|
||||||
|
|
||||||
|
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||||
|
binding.navView.setOnItemSelectedListener(this)
|
||||||
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
supportFragmentManager.commit {
|
||||||
|
replace(R.id.nav_host_fragment, activeBaseFragment, activeBaseFragment.javaClass.simpleName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBackPressed() {
|
||||||
|
if (supportFragmentManager.backStackEntryCount > 0) {
|
||||||
|
supportFragmentManager.popBackStack()
|
||||||
|
} else {
|
||||||
|
if (activeBaseFragment !is HomeFragment) {
|
||||||
|
binding.navView.selectedItemId = R.id.navigation_home
|
||||||
|
} else {
|
||||||
|
super.onBackPressed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNavigationItemSelected(item: MenuItem): Boolean {
|
||||||
|
if (supportFragmentManager.backStackEntryCount > 0) {
|
||||||
|
supportFragmentManager.popBackStack()
|
||||||
|
}
|
||||||
|
|
||||||
|
val ret = when (item.itemId) {
|
||||||
|
R.id.navigation_home -> {
|
||||||
|
activeBaseFragment = HomeFragment()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
R.id.navigation_library -> {
|
||||||
|
activeBaseFragment = LibraryFragment()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
R.id.navigation_search -> {
|
||||||
|
activeBaseFragment = SearchFragment()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
R.id.navigation_account -> {
|
||||||
|
activeBaseFragment = AccountFragment()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
|
||||||
|
supportFragmentManager.commit {
|
||||||
|
replace(R.id.nav_host_fragment, activeBaseFragment, activeBaseFragment.javaClass.simpleName)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getThemeResource(): Int {
|
||||||
|
return when (Preferences.theme) {
|
||||||
|
DataTypes.Theme.LIGHT -> R.style.AppTheme_Light
|
||||||
|
else -> R.style.AppTheme_Dark
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* initial loading and login are run in parallel, as initial loading doesn't require
|
||||||
|
* any login cookies
|
||||||
|
*/
|
||||||
|
private fun load() {
|
||||||
|
val time = measureTimeMillis {
|
||||||
|
val loadingJob = CoroutineScope(Dispatchers.IO + CoroutineName("InitialLoadingScope"))
|
||||||
|
.async { AoDParser.initialLoading() } // start the initial loading
|
||||||
|
|
||||||
|
// load all saved stuff here
|
||||||
|
Preferences.load(this)
|
||||||
|
EncryptedPreferences.readCredentials(this)
|
||||||
|
StorageController.load(this)
|
||||||
|
|
||||||
|
// show onboarding
|
||||||
|
if (EncryptedPreferences.password.isEmpty()) {
|
||||||
|
showOnboarding()
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
if (!AoDParser.login()) {
|
||||||
|
showLoginDialog()
|
||||||
|
}
|
||||||
|
} catch (ex: SocketTimeoutException) {
|
||||||
|
Log.w(javaClass.name, "Timeout during login!")
|
||||||
|
|
||||||
|
// show waring dialog before finishing
|
||||||
|
MaterialDialog(this).show {
|
||||||
|
title(R.string.dialog_timeout_head)
|
||||||
|
message(R.string.dialog_timeout_desc)
|
||||||
|
onDismiss { exitAndRemoveTask() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
runBlocking { loadingJob.await() } // wait for initial loading to finish
|
||||||
|
}
|
||||||
|
Log.i(javaClass.name, "loading and login in $time ms")
|
||||||
|
|
||||||
|
wasInitialized = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showLoginDialog() {
|
||||||
|
LoginDialog(this, false).positiveButton {
|
||||||
|
EncryptedPreferences.saveCredentials(login, password, context)
|
||||||
|
|
||||||
|
if (!AoDParser.login()) {
|
||||||
|
showLoginDialog()
|
||||||
|
Log.w(javaClass.name, "Login failed, please try again.")
|
||||||
|
}
|
||||||
|
}.negativeButton {
|
||||||
|
Log.i(javaClass.name, "Login canceled, exiting.")
|
||||||
|
finish()
|
||||||
|
}.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* start the onboarding activity and finish the main activity
|
||||||
|
*/
|
||||||
|
private fun showOnboarding() {
|
||||||
|
startActivity(Intent(this, OnboardingActivity::class.java))
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* start the player as new activity
|
||||||
|
*/
|
||||||
|
fun startPlayer(mediaId: Int, episodeId: Int) {
|
||||||
|
val intent = Intent(this, PlayerActivity::class.java).apply {
|
||||||
|
putExtra(getString(R.string.intent_media_id), mediaId)
|
||||||
|
putExtra(getString(R.string.intent_episode_id), episodeId)
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,155 @@
|
|||||||
|
package org.mosad.teapod.ui.activity.main.fragments
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.annotation.RawRes
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
|
import org.mosad.teapod.BuildConfig
|
||||||
|
import org.mosad.teapod.R
|
||||||
|
import org.mosad.teapod.databinding.FragmentAboutBinding
|
||||||
|
import org.mosad.teapod.databinding.ItemComponentBinding
|
||||||
|
import org.mosad.teapod.preferences.Preferences
|
||||||
|
import org.mosad.teapod.util.DataTypes.License
|
||||||
|
import org.mosad.teapod.util.ThirdPartyComponent
|
||||||
|
import java.lang.StringBuilder
|
||||||
|
import java.util.Timer
|
||||||
|
import kotlin.concurrent.schedule
|
||||||
|
|
||||||
|
class AboutFragment : Fragment() {
|
||||||
|
|
||||||
|
private lateinit var binding: FragmentAboutBinding
|
||||||
|
|
||||||
|
private val teapodRepoUrl = "https://git.mosad.xyz/Seil0/teapod"
|
||||||
|
private val devClickMax = 5
|
||||||
|
private var devClickCount = 0
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
|
binding = FragmentAboutBinding.inflate(inflater, container, false)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
binding.textVersionDesc.text = getString(R.string.version_desc, BuildConfig.VERSION_NAME, getString(R.string.build_time))
|
||||||
|
|
||||||
|
getThirdPartyComponents().forEach { thirdParty ->
|
||||||
|
val componentBinding = ItemComponentBinding.inflate(layoutInflater) //(R.layout.item_component, container, false)
|
||||||
|
componentBinding.textComponentTitle.text = thirdParty.name
|
||||||
|
componentBinding.textComponentDesc.text = getString(
|
||||||
|
R.string.third_party_component_desc,
|
||||||
|
thirdParty.year,
|
||||||
|
thirdParty.copyrightOwner,
|
||||||
|
thirdParty.license.short
|
||||||
|
)
|
||||||
|
componentBinding.linearComponent.setOnClickListener {
|
||||||
|
showLicense(thirdParty.license)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.linearThirdParty.addView(componentBinding.root)
|
||||||
|
}
|
||||||
|
|
||||||
|
initActions()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initActions() {
|
||||||
|
binding.imageAppIcon.setOnClickListener {
|
||||||
|
checkDevSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.linearSource.setOnClickListener {
|
||||||
|
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(teapodRepoUrl)))
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.linearLicense.setOnClickListener {
|
||||||
|
MaterialDialog(requireContext())
|
||||||
|
.title(text = License.GPL3.long)
|
||||||
|
.message(text = parseLicense(R.raw.gpl_3_full))
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* check if dev settings shall be enabled
|
||||||
|
*/
|
||||||
|
private fun checkDevSettings() {
|
||||||
|
// if the dev settings are already enabled show a toast
|
||||||
|
if (Preferences.devSettings) {
|
||||||
|
Toast.makeText(context, getString(R.string.dev_settings_already), Toast.LENGTH_SHORT).show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// reset dev settings count after 5 seconds
|
||||||
|
if (devClickCount == 0) {
|
||||||
|
Timer("", false).schedule(5000) {
|
||||||
|
devClickCount = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
devClickCount++
|
||||||
|
|
||||||
|
if (devClickCount == devClickMax) {
|
||||||
|
Preferences.saveDevSettings(requireContext(), true)
|
||||||
|
Toast.makeText(context, getString(R.string.dev_settings_enabled), Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getThirdPartyComponents(): List<ThirdPartyComponent> {
|
||||||
|
return listOf(
|
||||||
|
ThirdPartyComponent("AndroidX", "", "The Android Open Source Project",
|
||||||
|
"https://developer.android.com/jetpack/androidx", License.APACHE2),
|
||||||
|
ThirdPartyComponent("Material Components for Android", "2020", "The Android Open Source Project",
|
||||||
|
"https://github.com/material-components/material-components-android", License.APACHE2),
|
||||||
|
ThirdPartyComponent("ExoPlayer", "2014 - 2020", "The Android Open Source Project",
|
||||||
|
"https://github.com/google/ExoPlayer", License.APACHE2),
|
||||||
|
ThirdPartyComponent("Gson", "2008", "Google Inc.",
|
||||||
|
"https://github.com/google/gson", License.APACHE2),
|
||||||
|
ThirdPartyComponent("Material design icons", "2020", "Google Inc.",
|
||||||
|
"https://github.com/google/material-design-icons", License.APACHE2),
|
||||||
|
ThirdPartyComponent("Material Dialogs", "", "Aidan Follestad",
|
||||||
|
"https://github.com/afollestad/material-dialogs", License.APACHE2),
|
||||||
|
ThirdPartyComponent("Jsoup", "2009 - 2020", "Jonathan Hedley",
|
||||||
|
"https://jsoup.org/", License.MIT),
|
||||||
|
ThirdPartyComponent("kotlinx.coroutines", "2016 - 2019", "JetBrains",
|
||||||
|
"https://github.com/Kotlin/kotlinx.coroutines", License.APACHE2),
|
||||||
|
ThirdPartyComponent("Glide", "2014", "Google Inc.",
|
||||||
|
"https://github.com/bumptech/glide", License.BSD2),
|
||||||
|
ThirdPartyComponent("Glide Transformations", "2020", "Wasabeef",
|
||||||
|
"https://github.com/wasabeef/glide-transformations", License.APACHE2)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showLicense(license: License) {
|
||||||
|
val licenseText = when(license) {
|
||||||
|
License.APACHE2 -> parseLicense(R.raw.al_20_full)
|
||||||
|
License.BSD2 -> parseLicense(R.raw.bsd_2_full)
|
||||||
|
License.GPL3 -> parseLicense(R.raw.gpl_3_full)
|
||||||
|
License.MIT -> parseLicense(R.raw.mit_full)
|
||||||
|
}
|
||||||
|
|
||||||
|
MaterialDialog(requireContext())
|
||||||
|
.title(text = license.long)
|
||||||
|
.message(text = licenseText)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseLicense(@RawRes id: Int): String {
|
||||||
|
val sb = StringBuilder()
|
||||||
|
|
||||||
|
resources.openRawResource(id).bufferedReader().forEachLine {
|
||||||
|
if (it.isEmpty()) {
|
||||||
|
sb.appendLine(" ")
|
||||||
|
} else {
|
||||||
|
sb.append(it.trim() + " ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,166 @@
|
|||||||
|
package org.mosad.teapod.ui.activity.main.fragments
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
|
import com.afollestad.materialdialogs.list.listItemsSingleChoice
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.mosad.teapod.BuildConfig
|
||||||
|
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.activity.main.MainActivity
|
||||||
|
import org.mosad.teapod.ui.components.LoginDialog
|
||||||
|
import org.mosad.teapod.util.DataTypes.Theme
|
||||||
|
import org.mosad.teapod.util.StorageController
|
||||||
|
import org.mosad.teapod.util.showFragment
|
||||||
|
|
||||||
|
class AccountFragment : Fragment() {
|
||||||
|
|
||||||
|
private lateinit var binding: FragmentAccountBinding
|
||||||
|
|
||||||
|
private val getUriExport = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
|
if (result.resultCode == Activity.RESULT_OK) {
|
||||||
|
result.data?.data?.also { uri ->
|
||||||
|
StorageController.exportMyList(requireContext(), uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val getUriImport = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
|
if (result.resultCode == Activity.RESULT_OK) {
|
||||||
|
result.data?.data?.also { uri ->
|
||||||
|
val success = StorageController.importMyList(requireContext(), uri)
|
||||||
|
if (success == 0) {
|
||||||
|
Toast.makeText(
|
||||||
|
context, getString(R.string.import_data_success),
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
// load subscription (async) info before anything else
|
||||||
|
binding.textAccountSubscription.text = getString(R.string.account_subscription, getString(R.string.loading))
|
||||||
|
lifecycleScope.launch {
|
||||||
|
binding.textAccountSubscription.text = getString(
|
||||||
|
R.string.account_subscription,
|
||||||
|
AoDParser.getSubscriptionInfoAsync().await()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
binding.linearDevSettings.isVisible = Preferences.devSettings
|
||||||
|
|
||||||
|
initActions()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initActions() {
|
||||||
|
binding.linearAccountLogin.setOnClickListener {
|
||||||
|
showLoginDialog(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.linearAccountSubscription.setOnClickListener {
|
||||||
|
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(AoDParser.getSubscriptionUrl())))
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.linearTheme.setOnClickListener {
|
||||||
|
showThemeDialog()
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.linearInfo.setOnClickListener {
|
||||||
|
activity?.showFragment(AboutFragment())
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.switchSecondary.setOnClickListener {
|
||||||
|
Preferences.savePreferSecondary(requireContext(), binding.switchSecondary.isChecked)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.switchAutoplay.setOnClickListener {
|
||||||
|
Preferences.saveAutoplay(requireContext(), binding.switchAutoplay.isChecked)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.linearExportData.setOnClickListener {
|
||||||
|
val i = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||||
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
|
type = "text/json"
|
||||||
|
putExtra(Intent.EXTRA_TITLE, "my-list.json")
|
||||||
|
}
|
||||||
|
getUriExport.launch(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.linearImportData.setOnClickListener {
|
||||||
|
val i = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
||||||
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
|
type = "*/*"
|
||||||
|
}
|
||||||
|
getUriImport.launch(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.DARK)
|
||||||
|
}
|
||||||
|
|
||||||
|
(activity as MainActivity).restart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,166 @@
|
|||||||
|
package org.mosad.teapod.ui.activity.main.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 androidx.lifecycle.lifecycleScope
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.mosad.teapod.R
|
||||||
|
import org.mosad.teapod.databinding.FragmentHomeBinding
|
||||||
|
import org.mosad.teapod.parser.AoDParser
|
||||||
|
import org.mosad.teapod.ui.activity.main.MainActivity
|
||||||
|
import org.mosad.teapod.util.ItemMedia
|
||||||
|
import org.mosad.teapod.util.StorageController
|
||||||
|
import org.mosad.teapod.util.adapter.MediaItemAdapter
|
||||||
|
import org.mosad.teapod.util.decoration.MediaItemDecoration
|
||||||
|
import org.mosad.teapod.util.setDrawableTop
|
||||||
|
import org.mosad.teapod.util.showFragment
|
||||||
|
|
||||||
|
class HomeFragment : Fragment() {
|
||||||
|
|
||||||
|
private lateinit var binding: FragmentHomeBinding
|
||||||
|
private lateinit var adapterMyList: MediaItemAdapter
|
||||||
|
private lateinit var adapterNewEpisodes: MediaItemAdapter
|
||||||
|
private lateinit var adapterNewSimulcasts: MediaItemAdapter
|
||||||
|
private lateinit var adapterNewTitles: MediaItemAdapter
|
||||||
|
private lateinit var adapterTopTen: MediaItemAdapter
|
||||||
|
|
||||||
|
private lateinit var highlightMedia: ItemMedia
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
context?.let {
|
||||||
|
initHighlight()
|
||||||
|
initRecyclerViews()
|
||||||
|
initActions()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initHighlight() {
|
||||||
|
if (AoDParser.highlightsList.isNotEmpty()) {
|
||||||
|
highlightMedia = AoDParser.highlightsList[0]
|
||||||
|
|
||||||
|
binding.textHighlightTitle.text = highlightMedia.title
|
||||||
|
Glide.with(requireContext()).load(highlightMedia.posterUrl)
|
||||||
|
.into(binding.imageHighlight)
|
||||||
|
|
||||||
|
if (StorageController.myList.contains(highlightMedia.id)) {
|
||||||
|
binding.textHighlightMyList.setDrawableTop(R.drawable.ic_baseline_check_24)
|
||||||
|
} else {
|
||||||
|
binding.textHighlightMyList.setDrawableTop(R.drawable.ic_baseline_add_24)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initRecyclerViews() {
|
||||||
|
binding.recyclerMyList.addItemDecoration(MediaItemDecoration(9))
|
||||||
|
binding.recyclerNewEpisodes.addItemDecoration(MediaItemDecoration(9))
|
||||||
|
binding.recyclerNewSimulcasts.addItemDecoration(MediaItemDecoration(9))
|
||||||
|
binding.recyclerNewTitles.addItemDecoration(MediaItemDecoration(9))
|
||||||
|
binding.recyclerTopTen.addItemDecoration(MediaItemDecoration(9))
|
||||||
|
|
||||||
|
// my list
|
||||||
|
adapterMyList = MediaItemAdapter(mapMyListToItemMedia())
|
||||||
|
binding.recyclerMyList.adapter = adapterMyList
|
||||||
|
|
||||||
|
// new episodes
|
||||||
|
adapterNewEpisodes = MediaItemAdapter(AoDParser.newEpisodesList)
|
||||||
|
binding.recyclerNewEpisodes.adapter = adapterNewEpisodes
|
||||||
|
|
||||||
|
// new simulcasts
|
||||||
|
adapterNewSimulcasts = MediaItemAdapter(AoDParser.newSimulcastsList)
|
||||||
|
binding.recyclerNewSimulcasts.adapter = adapterNewSimulcasts
|
||||||
|
|
||||||
|
// new titles
|
||||||
|
adapterNewTitles = MediaItemAdapter(AoDParser.newTitlesList)
|
||||||
|
binding.recyclerNewTitles.adapter = adapterNewTitles
|
||||||
|
|
||||||
|
// top ten
|
||||||
|
adapterTopTen = MediaItemAdapter(AoDParser.topTenList)
|
||||||
|
binding.recyclerTopTen.adapter = adapterTopTen
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initActions() {
|
||||||
|
binding.buttonPlayHighlight.setOnClickListener {
|
||||||
|
// TODO get next episode
|
||||||
|
lifecycleScope.launch {
|
||||||
|
val media = AoDParser.getMediaById(highlightMedia.id)
|
||||||
|
|
||||||
|
Log.d(javaClass.name, "Starting Player with mediaId: ${media.id}")
|
||||||
|
(activity as MainActivity).startPlayer(media.id, media.episodes.first().id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.textHighlightMyList.setOnClickListener {
|
||||||
|
if (StorageController.myList.contains(highlightMedia.id)) {
|
||||||
|
StorageController.myList.remove(highlightMedia.id)
|
||||||
|
binding.textHighlightMyList.setDrawableTop(R.drawable.ic_baseline_add_24)
|
||||||
|
} else {
|
||||||
|
StorageController.myList.add(highlightMedia.id)
|
||||||
|
binding.textHighlightMyList.setDrawableTop(R.drawable.ic_baseline_check_24)
|
||||||
|
}
|
||||||
|
StorageController.saveMyList(requireContext())
|
||||||
|
|
||||||
|
updateMyListMedia() // update my list, since it has changed
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.textHighlightInfo.setOnClickListener {
|
||||||
|
activity?.showFragment(MediaFragment(highlightMedia.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
adapterMyList.onItemClick = { mediaId, _ ->
|
||||||
|
activity?.showFragment(MediaFragment(mediaId))
|
||||||
|
}
|
||||||
|
|
||||||
|
adapterNewEpisodes.onItemClick = { mediaId, _ ->
|
||||||
|
activity?.showFragment(MediaFragment(mediaId))
|
||||||
|
}
|
||||||
|
|
||||||
|
adapterNewSimulcasts.onItemClick = { mediaId, _ ->
|
||||||
|
activity?.showFragment(MediaFragment(mediaId))
|
||||||
|
}
|
||||||
|
|
||||||
|
adapterNewTitles.onItemClick = { mediaId, _ ->
|
||||||
|
activity?.showFragment(MediaFragment(mediaId))
|
||||||
|
}
|
||||||
|
|
||||||
|
adapterTopTen.onItemClick = { mediaId, _ ->
|
||||||
|
activity?.showFragment(MediaFragment(mediaId))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* update my media list
|
||||||
|
* TODO
|
||||||
|
* * auto call when StorageController.myList is changed
|
||||||
|
* * only update actual change and not all data (performance)
|
||||||
|
*/
|
||||||
|
fun updateMyListMedia() {
|
||||||
|
adapterMyList.updateMediaList(mapMyListToItemMedia())
|
||||||
|
adapterMyList.notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mapMyListToItemMedia(): List<ItemMedia> {
|
||||||
|
return StorageController.myList.mapNotNull { elementId ->
|
||||||
|
AoDParser.itemMediaList.firstOrNull { it.id == elementId }.also {
|
||||||
|
// it the my list entry wasn't found in itemMediaList Log it
|
||||||
|
if (it == null) {
|
||||||
|
Log.w(javaClass.name, "The element with the id $elementId was not found.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,44 @@
|
|||||||
|
package org.mosad.teapod.ui.activity.main.fragments
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
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
|
||||||
|
import org.mosad.teapod.util.showFragment
|
||||||
|
|
||||||
|
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
|
||||||
|
lifecycleScope.launch {
|
||||||
|
// create and set the adapter, needs context
|
||||||
|
context?.let {
|
||||||
|
adapter = MediaItemAdapter(AoDParser.itemMediaList)
|
||||||
|
adapter.onItemClick = { mediaId, _ ->
|
||||||
|
activity?.showFragment(MediaFragment(mediaId))
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.recyclerMediaLibrary.adapter = adapter
|
||||||
|
binding.recyclerMediaLibrary.addItemDecoration(MediaItemDecoration(9))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,210 @@
|
|||||||
|
package org.mosad.teapod.ui.activity.main.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.fragment.app.FragmentActivity
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import com.bumptech.glide.request.RequestOptions
|
||||||
|
import com.google.android.material.appbar.AppBarLayout
|
||||||
|
import com.google.android.material.tabs.TabLayoutMediator
|
||||||
|
import jp.wasabeef.glide.transformations.BlurTransformation
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.mosad.teapod.R
|
||||||
|
import org.mosad.teapod.databinding.FragmentMediaBinding
|
||||||
|
import org.mosad.teapod.ui.activity.main.MainActivity
|
||||||
|
import org.mosad.teapod.ui.activity.main.viewmodel.MediaFragmentViewModel
|
||||||
|
import org.mosad.teapod.util.DataTypes.MediaType
|
||||||
|
import org.mosad.teapod.util.Episode
|
||||||
|
import org.mosad.teapod.util.StorageController
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The media detail fragment.
|
||||||
|
* Note: the fragment is created only once, when selecting a similar title etc.
|
||||||
|
* therefore fragments may be not empty and model may be the old one
|
||||||
|
*/
|
||||||
|
class MediaFragment(private val mediaId: Int) : Fragment() {
|
||||||
|
|
||||||
|
private lateinit var binding: FragmentMediaBinding
|
||||||
|
private lateinit var pagerAdapter: FragmentStateAdapter
|
||||||
|
|
||||||
|
private val fragments = arrayListOf<Fragment>()
|
||||||
|
|
||||||
|
private val model: MediaFragmentViewModel by activityViewModels()
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
// tab layout and pager
|
||||||
|
pagerAdapter = ScreenSlidePagerAdapter(requireActivity())
|
||||||
|
// fix material components issue #1878, if more tabs are added increase
|
||||||
|
binding.pagerEpisodesSimilar.offscreenPageLimit = 2
|
||||||
|
binding.pagerEpisodesSimilar.adapter = pagerAdapter
|
||||||
|
TabLayoutMediator(binding.tabEpisodesSimilar, binding.pagerEpisodesSimilar) { tab, position ->
|
||||||
|
tab.text = if (model.media.type == MediaType.TVSHOW && position == 0) {
|
||||||
|
getString(R.string.episodes)
|
||||||
|
} else {
|
||||||
|
getString(R.string.similar_titles)
|
||||||
|
}
|
||||||
|
}.attach()
|
||||||
|
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
model.load(mediaId) // load the streams and tmdb for the selected media
|
||||||
|
|
||||||
|
updateGUI()
|
||||||
|
initActions()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
|
||||||
|
// update the next ep text if there is one, since it may have changed
|
||||||
|
if (model.nextEpisode.title.isNotEmpty()) {
|
||||||
|
binding.textTitle.text = model.nextEpisode.title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* if tmdb data is present, use it, else use the aod data
|
||||||
|
*/
|
||||||
|
private fun updateGUI() = with(model) {
|
||||||
|
// 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(binding.imageBackdrop)
|
||||||
|
|
||||||
|
Glide.with(requireContext()).load(posterUrl)
|
||||||
|
.into(binding.imagePoster)
|
||||||
|
|
||||||
|
binding.textTitle.text = media.info.title
|
||||||
|
binding.textYear.text = media.info.year.toString()
|
||||||
|
binding.textAge.text = media.info.age.toString()
|
||||||
|
binding.textOverview.text = media.info.shortDesc
|
||||||
|
if (StorageController.myList.contains(media.id)) {
|
||||||
|
Glide.with(requireContext()).load(R.drawable.ic_baseline_check_24).into(binding.imageMyListAction)
|
||||||
|
} else {
|
||||||
|
Glide.with(requireContext()).load(R.drawable.ic_baseline_add_24).into(binding.imageMyListAction)
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear fragments, since it lives in onCreate scope (don't do this in onPause/onStop -> FragmentManager transaction)
|
||||||
|
fragments.clear()
|
||||||
|
pagerAdapter.notifyDataSetChanged()
|
||||||
|
|
||||||
|
// specific gui
|
||||||
|
if (media.type == MediaType.TVSHOW) {
|
||||||
|
// 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
|
||||||
|
binding.textTitle.text = nextEpisode.title
|
||||||
|
|
||||||
|
// episodes count
|
||||||
|
binding.textEpisodesOrRuntime.text = resources.getQuantityString(
|
||||||
|
R.plurals.text_episodes_count,
|
||||||
|
media.info.episodesCount,
|
||||||
|
media.info.episodesCount
|
||||||
|
)
|
||||||
|
|
||||||
|
// episodes
|
||||||
|
fragments.add(MediaFragmentEpisodes())
|
||||||
|
pagerAdapter.notifyDataSetChanged()
|
||||||
|
} else if (media.type == MediaType.MOVIE) {
|
||||||
|
|
||||||
|
if (tmdb.runtime > 0) {
|
||||||
|
binding.textEpisodesOrRuntime.text = resources.getQuantityString(
|
||||||
|
R.plurals.text_runtime,
|
||||||
|
tmdb.runtime,
|
||||||
|
tmdb.runtime
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
binding.textEpisodesOrRuntime.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if has similar titles
|
||||||
|
if (media.info.similar.isNotEmpty()) {
|
||||||
|
fragments.add(MediaFragmentSimilar())
|
||||||
|
pagerAdapter.notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
// disable scrolling on appbar, if no tabs where added
|
||||||
|
if(fragments.isEmpty()) {
|
||||||
|
val params = binding.linearMedia.layoutParams as AppBarLayout.LayoutParams
|
||||||
|
params.scrollFlags = 0 // clear all scroll flags
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.frameLoading.visibility = View.GONE // hide loading indicator
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initActions() = with(model) {
|
||||||
|
binding.buttonPlay.setOnClickListener {
|
||||||
|
when (media.type) {
|
||||||
|
MediaType.MOVIE -> playEpisode(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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* play the current episode
|
||||||
|
* TODO this is also used in MediaFragmentEpisode, we should only have on implementation
|
||||||
|
*/
|
||||||
|
private fun playEpisode(ep: Episode) {
|
||||||
|
(activity as MainActivity).startPlayer(model.media.id, ep.id)
|
||||||
|
Log.d(javaClass.name, "Started Player with episodeId: ${ep.id}")
|
||||||
|
|
||||||
|
model.updateNextEpisode(ep) // set the correct next episode
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple pager adapter
|
||||||
|
*/
|
||||||
|
private inner class ScreenSlidePagerAdapter(fa: FragmentActivity) : FragmentStateAdapter(fa) {
|
||||||
|
override fun getItemCount(): Int = fragments.size
|
||||||
|
|
||||||
|
override fun createFragment(position: Int): Fragment = fragments[position]
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,61 @@
|
|||||||
|
package org.mosad.teapod.ui.activity.main.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 androidx.fragment.app.activityViewModels
|
||||||
|
import org.mosad.teapod.ui.activity.main.MainActivity
|
||||||
|
import org.mosad.teapod.ui.activity.main.viewmodel.MediaFragmentViewModel
|
||||||
|
import org.mosad.teapod.databinding.FragmentMediaEpisodesBinding
|
||||||
|
import org.mosad.teapod.util.Episode
|
||||||
|
import org.mosad.teapod.util.adapter.EpisodeItemAdapter
|
||||||
|
|
||||||
|
class MediaFragmentEpisodes : Fragment() {
|
||||||
|
|
||||||
|
private lateinit var binding: FragmentMediaEpisodesBinding
|
||||||
|
private lateinit var adapterRecEpisodes: EpisodeItemAdapter
|
||||||
|
|
||||||
|
private val model: MediaFragmentViewModel by activityViewModels()
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
|
binding = FragmentMediaEpisodesBinding.inflate(inflater, container, false)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
adapterRecEpisodes = EpisodeItemAdapter(model.media.episodes)
|
||||||
|
binding.recyclerEpisodes.adapter = adapterRecEpisodes
|
||||||
|
|
||||||
|
// set onItemClick only in adapter is initialized
|
||||||
|
if (this::adapterRecEpisodes.isInitialized) {
|
||||||
|
adapterRecEpisodes.onImageClick = { _, position ->
|
||||||
|
playEpisode(model.media.episodes[position])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
|
||||||
|
// if adapterRecEpisodes is initialized, update the watched state for the episodes
|
||||||
|
if (this::adapterRecEpisodes.isInitialized) {
|
||||||
|
model.media.episodes.forEachIndexed { index, episode ->
|
||||||
|
adapterRecEpisodes.updateWatchedState(episode.watched, index)
|
||||||
|
}
|
||||||
|
adapterRecEpisodes.notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun playEpisode(ep: Episode) {
|
||||||
|
(activity as MainActivity).startPlayer(model.media.id, ep.id)
|
||||||
|
Log.d(javaClass.name, "Started Player with episodeId: ${ep.id}")
|
||||||
|
|
||||||
|
model.updateNextEpisode(ep) // set the correct next episode
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
package org.mosad.teapod.ui.activity.main.fragments
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import org.mosad.teapod.databinding.FragmentMediaSimilarBinding
|
||||||
|
import org.mosad.teapod.ui.activity.main.viewmodel.MediaFragmentViewModel
|
||||||
|
import org.mosad.teapod.util.adapter.MediaItemAdapter
|
||||||
|
import org.mosad.teapod.util.decoration.MediaItemDecoration
|
||||||
|
import org.mosad.teapod.util.showFragment
|
||||||
|
|
||||||
|
class MediaFragmentSimilar : Fragment() {
|
||||||
|
|
||||||
|
private lateinit var binding: FragmentMediaSimilarBinding
|
||||||
|
private val model: MediaFragmentViewModel by activityViewModels()
|
||||||
|
|
||||||
|
private lateinit var adapterSimilar: MediaItemAdapter
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
|
binding = FragmentMediaSimilarBinding.inflate(inflater, container, false)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
adapterSimilar = MediaItemAdapter(model.media.info.similar)
|
||||||
|
binding.recyclerMediaSimilar.adapter = adapterSimilar
|
||||||
|
binding.recyclerMediaSimilar.addItemDecoration(MediaItemDecoration(9))
|
||||||
|
|
||||||
|
// set onItemClick only in adapter is initialized
|
||||||
|
if (this::adapterSimilar.isInitialized) {
|
||||||
|
adapterSimilar.onItemClick = { mediaId, _ ->
|
||||||
|
activity?.showFragment(MediaFragment(mediaId))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,62 @@
|
|||||||
|
package org.mosad.teapod.ui.activity.main.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 androidx.lifecycle.lifecycleScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
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
|
||||||
|
import org.mosad.teapod.util.showFragment
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
// create and set the adapter, needs context
|
||||||
|
context?.let {
|
||||||
|
adapter = MediaItemAdapter(AoDParser.itemMediaList)
|
||||||
|
adapter!!.onItemClick = { mediaId, _ ->
|
||||||
|
binding.searchText.clearFocus()
|
||||||
|
activity?.showFragment(MediaFragment(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
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,48 @@
|
|||||||
|
package org.mosad.teapod.ui.activity.main.viewmodel
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import androidx.lifecycle.AndroidViewModel
|
||||||
|
import org.mosad.teapod.parser.AoDParser
|
||||||
|
import org.mosad.teapod.util.*
|
||||||
|
import org.mosad.teapod.util.DataTypes.MediaType
|
||||||
|
|
||||||
|
/**
|
||||||
|
* handle media, next ep and tmdb
|
||||||
|
*/
|
||||||
|
class MediaFragmentViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
|
|
||||||
|
var media = Media(-1, "", MediaType.OTHER)
|
||||||
|
internal set
|
||||||
|
var nextEpisode = Episode()
|
||||||
|
internal set
|
||||||
|
var tmdb = TMDBResponse()
|
||||||
|
internal set
|
||||||
|
|
||||||
|
/**
|
||||||
|
* set media, tmdb and nextEpisode
|
||||||
|
*/
|
||||||
|
suspend fun load(mediaId: Int) {
|
||||||
|
media = AoDParser.getMediaById(mediaId)
|
||||||
|
tmdb = TMDBApiController().search(media.info.title, media.type)
|
||||||
|
|
||||||
|
if (media.type == MediaType.TVSHOW) {
|
||||||
|
nextEpisode = if (media.episodes.firstOrNull{ !it.watched } != null) {
|
||||||
|
media.episodes.first{ !it.watched }
|
||||||
|
} else {
|
||||||
|
media.episodes.first()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get the next episode based on episode number (the true next episode)
|
||||||
|
* if no matching is found, use first episode
|
||||||
|
*/
|
||||||
|
fun updateNextEpisode(currentEp: Episode) {
|
||||||
|
if (media.type == MediaType.MOVIE) return // return if movie
|
||||||
|
|
||||||
|
nextEpisode = media.episodes.firstOrNull{ it.number > currentEp.number }
|
||||||
|
?: media.episodes.first()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,55 @@
|
|||||||
|
package org.mosad.teapod.ui.activity.onboarding
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import org.mosad.teapod.R
|
||||||
|
import org.mosad.teapod.databinding.FragmentOnLoginBinding
|
||||||
|
import org.mosad.teapod.parser.AoDParser
|
||||||
|
import org.mosad.teapod.preferences.EncryptedPreferences
|
||||||
|
|
||||||
|
class OnLoginFragment: Fragment() {
|
||||||
|
|
||||||
|
private lateinit var binding: FragmentOnLoginBinding
|
||||||
|
private var loginJob: Job? = null
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
|
binding = FragmentOnLoginBinding.inflate(inflater, container, false)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
initActions()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initActions() {
|
||||||
|
binding.buttonLogin.setOnClickListener {
|
||||||
|
// get login credentials from gui
|
||||||
|
val email = binding.editTextLogin.text.toString()
|
||||||
|
val password = binding.editTextPassword.text.toString()
|
||||||
|
|
||||||
|
EncryptedPreferences.saveCredentials(email, password, requireContext()) // save the credentials
|
||||||
|
|
||||||
|
binding.buttonLogin.isClickable = false
|
||||||
|
loginJob = lifecycleScope.launch {
|
||||||
|
if (AoDParser.login()) {
|
||||||
|
// if login was successful, switch to main
|
||||||
|
if (activity is OnboardingActivity) {
|
||||||
|
(activity as OnboardingActivity).launchMainActivity()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
binding.textLoginDesc.text = getString(R.string.on_login_failed)
|
||||||
|
binding.buttonLogin.isClickable = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
package org.mosad.teapod.ui.activity.onboarding
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import org.mosad.teapod.databinding.FragmentOnWelcomeBinding
|
||||||
|
|
||||||
|
class OnWelcomeFragment: Fragment() {
|
||||||
|
|
||||||
|
private lateinit var binding: FragmentOnWelcomeBinding
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
|
binding = FragmentOnWelcomeBinding.inflate(inflater, container, false)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
initActions()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initActions() {
|
||||||
|
binding.buttonGetStarted.setOnClickListener {
|
||||||
|
if (activity is OnboardingActivity) {
|
||||||
|
(activity as OnboardingActivity).nextFragment()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,79 @@
|
|||||||
|
package org.mosad.teapod.ui.activity.onboarding
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||||
|
import com.google.android.material.tabs.TabLayoutMediator
|
||||||
|
import org.mosad.teapod.ui.activity.main.MainActivity
|
||||||
|
import org.mosad.teapod.databinding.ActivityOnboardingBinding
|
||||||
|
|
||||||
|
class OnboardingActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
private lateinit var binding: ActivityOnboardingBinding
|
||||||
|
private lateinit var pagerAdapter: FragmentStateAdapter
|
||||||
|
|
||||||
|
private val fragments = arrayOf(OnLoginFragment())
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
binding = ActivityOnboardingBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
pagerAdapter = ScreenSlidePagerAdapter(this)
|
||||||
|
binding.viewPager.adapter = pagerAdapter
|
||||||
|
TabLayoutMediator(binding.tabLayout, binding.viewPager) { _, _ -> }.attach()
|
||||||
|
|
||||||
|
// we don't use the skip button, instead we use the start button to skip the last fragment
|
||||||
|
binding.buttonSkip.visibility = View.GONE
|
||||||
|
|
||||||
|
// hide tab layout if only one tab is displayed
|
||||||
|
if (fragments.size <= 1) {
|
||||||
|
binding.tabLayout.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBackPressed() {
|
||||||
|
if (binding.viewPager.currentItem == 0) {
|
||||||
|
super.onBackPressed()
|
||||||
|
} else {
|
||||||
|
binding.viewPager.currentItem = binding.viewPager.currentItem - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun nextFragment() {
|
||||||
|
if (binding.viewPager.currentItem < fragments.size - 1) {
|
||||||
|
binding.viewPager.currentItem++
|
||||||
|
} else {
|
||||||
|
launchMainActivity()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun btnNextClick(@Suppress("UNUSED_PARAMETER")v: View) {
|
||||||
|
//nextFragment() // currently not used in Teapod
|
||||||
|
}
|
||||||
|
|
||||||
|
fun btnSkipClick(@Suppress("UNUSED_PARAMETER")v: View) {
|
||||||
|
//launchMainActivity() // currently not used in Teapod
|
||||||
|
}
|
||||||
|
|
||||||
|
fun launchMainActivity() {
|
||||||
|
startActivity(Intent(this, MainActivity::class.java))
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple pager adapter
|
||||||
|
*/
|
||||||
|
private inner class ScreenSlidePagerAdapter(fa: FragmentActivity) : FragmentStateAdapter(fa) {
|
||||||
|
override fun getItemCount(): Int = fragments.size
|
||||||
|
|
||||||
|
override fun createFragment(position: Int): Fragment = fragments[position]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,475 @@
|
|||||||
|
package org.mosad.teapod.ui.activity.player
|
||||||
|
|
||||||
|
import android.animation.Animator
|
||||||
|
import android.animation.AnimatorListenerAdapter
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.PictureInPictureParams
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import android.graphics.Rect
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import android.util.Rational
|
||||||
|
import android.view.GestureDetector
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import android.view.View
|
||||||
|
import androidx.activity.viewModels
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.view.GestureDetectorCompat
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.google.android.exoplayer2.ExoPlayer
|
||||||
|
import com.google.android.exoplayer2.Player
|
||||||
|
import com.google.android.exoplayer2.ui.StyledPlayerControlView
|
||||||
|
import com.google.android.exoplayer2.util.Util
|
||||||
|
import kotlinx.android.synthetic.main.activity_player.*
|
||||||
|
import kotlinx.android.synthetic.main.player_controls.*
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.mosad.teapod.R
|
||||||
|
import org.mosad.teapod.preferences.Preferences
|
||||||
|
import org.mosad.teapod.ui.components.EpisodesListPlayer
|
||||||
|
import org.mosad.teapod.ui.components.LanguageSettingsPlayer
|
||||||
|
import org.mosad.teapod.util.DataTypes
|
||||||
|
import org.mosad.teapod.util.hideBars
|
||||||
|
import org.mosad.teapod.util.isInPiPMode
|
||||||
|
import org.mosad.teapod.util.navToLauncherTask
|
||||||
|
import java.util.*
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import kotlin.concurrent.scheduleAtFixedRate
|
||||||
|
|
||||||
|
class PlayerActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
private val model: PlayerViewModel by viewModels()
|
||||||
|
|
||||||
|
private lateinit var controller: StyledPlayerControlView
|
||||||
|
private lateinit var gestureDetector: GestureDetectorCompat
|
||||||
|
private lateinit var timerUpdates: TimerTask
|
||||||
|
|
||||||
|
private var wasInPiP = false
|
||||||
|
private var remainingTime: Long = 0
|
||||||
|
|
||||||
|
private val rwdTime: Long = 10000.unaryMinus()
|
||||||
|
private val fwdTime: Long = 10000
|
||||||
|
private val defaultShowTimeoutMs = 5000
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(R.layout.activity_player)
|
||||||
|
hideBars() // Initial hide the bars
|
||||||
|
|
||||||
|
model.loadMedia(
|
||||||
|
intent.getIntExtra(getString(R.string.intent_media_id), 0),
|
||||||
|
intent.getIntExtra(getString(R.string.intent_episode_id), 0)
|
||||||
|
)
|
||||||
|
model.currentEpisodeChangedListener.add { onMediaChanged() }
|
||||||
|
gestureDetector = GestureDetectorCompat(this, PlayerGestureListener())
|
||||||
|
|
||||||
|
controller = video_view.findViewById(R.id.exo_controller)
|
||||||
|
controller.isAnimationEnabled = false // disable controls (time-bar) animation
|
||||||
|
|
||||||
|
initExoPlayer() // call in onCreate, exoplayer lives in view model
|
||||||
|
initGUI()
|
||||||
|
initActions()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* once minimum is android 7.0 this can be simplified
|
||||||
|
* only onStart and onStop should be needed then
|
||||||
|
* see: https://developer.android.com/guide/topics/ui/picture-in-picture#continuing_playback
|
||||||
|
*/
|
||||||
|
override fun onStart() {
|
||||||
|
super.onStart()
|
||||||
|
if (Util.SDK_INT > 23) {
|
||||||
|
initPlayer()
|
||||||
|
video_view?.onResume()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
if (isInPiPMode()) { return }
|
||||||
|
|
||||||
|
if (Util.SDK_INT <= 23) {
|
||||||
|
initPlayer()
|
||||||
|
video_view?.onResume()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
super.onPause()
|
||||||
|
|
||||||
|
if (isInPiPMode()) { return }
|
||||||
|
if (Util.SDK_INT <= 23) { onPauseOnStop() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStop() {
|
||||||
|
super.onStop()
|
||||||
|
|
||||||
|
if (Util.SDK_INT > 23) { onPauseOnStop() }
|
||||||
|
// if the player was in pip, it's on a different task
|
||||||
|
if (wasInPiP) { navToLauncherTask() }
|
||||||
|
// if the player is in pip, remove the task, else we'll get a zombie
|
||||||
|
if (isInPiPMode()) { finishAndRemoveTask() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* used, when the player is in pip and the user selects a new media
|
||||||
|
*/
|
||||||
|
override fun onNewIntent(intent: Intent?) {
|
||||||
|
super.onNewIntent(intent)
|
||||||
|
|
||||||
|
// when the intent changed, lead the new media and play it
|
||||||
|
intent?.let {
|
||||||
|
model.loadMedia(
|
||||||
|
it.getIntExtra(getString(R.string.intent_media_id), 0),
|
||||||
|
it.getIntExtra(getString(R.string.intent_episode_id), 0)
|
||||||
|
)
|
||||||
|
model.playEpisode(model.currentEpisode, replace = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* previous to android n, don't override
|
||||||
|
*/
|
||||||
|
@RequiresApi(Build.VERSION_CODES.N)
|
||||||
|
override fun onUserLeaveHint() {
|
||||||
|
super.onUserLeaveHint()
|
||||||
|
|
||||||
|
// start pip mode, if supported
|
||||||
|
if (packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)) {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||||
|
@Suppress("deprecation")
|
||||||
|
enterPictureInPictureMode()
|
||||||
|
} else {
|
||||||
|
val width = model.player.videoFormat?.width ?: 0
|
||||||
|
val height = model.player.videoFormat?.height ?: 0
|
||||||
|
val contentFrame: View = video_view.findViewById(R.id.exo_content_frame)
|
||||||
|
val contentRect = with(contentFrame) {
|
||||||
|
val (x, y) = intArrayOf(0, 0).also(::getLocationInWindow)
|
||||||
|
Rect(x, y, x + width, y + height)
|
||||||
|
}
|
||||||
|
|
||||||
|
val params = PictureInPictureParams.Builder()
|
||||||
|
.setAspectRatio(Rational(width, height))
|
||||||
|
.setSourceRectHint(contentRect)
|
||||||
|
.build()
|
||||||
|
enterPictureInPictureMode(params)
|
||||||
|
}
|
||||||
|
|
||||||
|
wasInPiP = isInPiPMode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPictureInPictureModeChanged(
|
||||||
|
isInPictureInPictureMode: Boolean,
|
||||||
|
newConfig: Configuration?
|
||||||
|
) {
|
||||||
|
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
|
||||||
|
|
||||||
|
// Hide the full-screen UI (controls, etc.) while in picture-in-picture mode.
|
||||||
|
video_view.useController = !isInPictureInPictureMode
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initPlayer() {
|
||||||
|
if (model.media.id < 0) {
|
||||||
|
Log.e(javaClass.name, "No media was set.")
|
||||||
|
this.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
initVideoView()
|
||||||
|
initTimeUpdates()
|
||||||
|
|
||||||
|
// if the player is ready or buffering we can simply play the file again, else do nothing
|
||||||
|
val playbackState = model.player.playbackState
|
||||||
|
if ((playbackState == ExoPlayer.STATE_READY || playbackState == ExoPlayer.STATE_BUFFERING)) {
|
||||||
|
model.player.play()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* set play when ready and listeners
|
||||||
|
*/
|
||||||
|
private fun initExoPlayer() {
|
||||||
|
model.player.addListener(object : Player.Listener {
|
||||||
|
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) {
|
||||||
|
playNextEpisode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// start playing the current episode, after all needed player components have been initialized
|
||||||
|
model.playEpisode(model.currentEpisode, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
|
private fun initVideoView() {
|
||||||
|
video_view.player = model.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_language.setOnClickListener { showLanguageSettings() }
|
||||||
|
button_episodes.setOnClickListener { showEpisodesList() }
|
||||||
|
button_next_ep_c.setOnClickListener { playNextEpisode() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initGUI() {
|
||||||
|
if (model.media.type == DataTypes.MediaType.MOVIE) {
|
||||||
|
button_episodes.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initTimeUpdates() {
|
||||||
|
if (this::timerUpdates.isInitialized) {
|
||||||
|
timerUpdates.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
timerUpdates = Timer().scheduleAtFixedRate(0, 500) {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
val btnNextEpIsVisible = button_next_ep.isVisible
|
||||||
|
val controlsVisible = controller.isVisible
|
||||||
|
|
||||||
|
if (model.player.duration > 0) {
|
||||||
|
remainingTime = model.player.duration - model.player.currentPosition
|
||||||
|
remainingTime = if (remainingTime < 0) 0 else remainingTime
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remainingTime in 1..20000) {
|
||||||
|
// if the next ep button is not visible, make it visible. Don't show in pip mode
|
||||||
|
if (!btnNextEpIsVisible && model.nextEpisode != null && Preferences.autoplay && !isInPiPMode()) {
|
||||||
|
showButtonNextEp()
|
||||||
|
}
|
||||||
|
} else if (btnNextEpIsVisible) {
|
||||||
|
hideButtonNextEp()
|
||||||
|
}
|
||||||
|
|
||||||
|
// if controls are visible, update them
|
||||||
|
if (controlsVisible) {
|
||||||
|
updateControls()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onPauseOnStop() {
|
||||||
|
video_view?.onPause()
|
||||||
|
model.player.pause()
|
||||||
|
timerUpdates.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* update title text and next ep button visibility, set ignoreNextStateEnded
|
||||||
|
*/
|
||||||
|
private fun onMediaChanged() {
|
||||||
|
exo_text_title.text = model.getMediaTitle()
|
||||||
|
|
||||||
|
// hide the next ep button, if there is none
|
||||||
|
button_next_ep_c.visibility = if (model.nextEpisode == null) {
|
||||||
|
View.GONE
|
||||||
|
} else {
|
||||||
|
View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
// hide the episodes button, if the media type changed
|
||||||
|
button_episodes.visibility = if (model.media.type == DataTypes.MediaType.MOVIE) {
|
||||||
|
View.GONE
|
||||||
|
} else {
|
||||||
|
View.VISIBLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO set position of rewind/fast forward indicators programmatically
|
||||||
|
*/
|
||||||
|
|
||||||
|
private fun rewind() {
|
||||||
|
model.seekToOffset(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() {
|
||||||
|
model.seekToOffset(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 playNextEpisode() {
|
||||||
|
model.playNextEpisode()
|
||||||
|
hideButtonNextEp()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showEpisodesList() {
|
||||||
|
val episodesList = EpisodesListPlayer(this, model = model).apply {
|
||||||
|
onViewRemovedAction = { model.player.play() }
|
||||||
|
}
|
||||||
|
player_layout.addView(episodesList)
|
||||||
|
pauseAndHideControls()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showLanguageSettings() {
|
||||||
|
val languageSettings = LanguageSettingsPlayer(this, model = model).apply {
|
||||||
|
onViewRemovedAction = { model.player.play() }
|
||||||
|
}
|
||||||
|
player_layout.addView(languageSettings)
|
||||||
|
pauseAndHideControls()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* pause playback and hide controls
|
||||||
|
*/
|
||||||
|
private fun pauseAndHideControls() {
|
||||||
|
model.player.pause() // showTimeoutMs is set to 0 when calling pause, but why
|
||||||
|
controller.showTimeoutMs = defaultShowTimeoutMs // fix showTimeoutMs set to 0
|
||||||
|
controller.hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class PlayerGestureListener : GestureDetector.SimpleOnGestureListener() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* on single tap hide or show the controls
|
||||||
|
*/
|
||||||
|
override fun onSingleTapConfirmed(e: MotionEvent?): Boolean {
|
||||||
|
if (!isInPiPMode()) {
|
||||||
|
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?) {
|
||||||
|
model.togglePausePlay()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,175 @@
|
|||||||
|
package org.mosad.teapod.ui.activity.player
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.net.Uri
|
||||||
|
import android.support.v4.media.session.MediaSessionCompat
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.lifecycle.AndroidViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.google.android.exoplayer2.C
|
||||||
|
import com.google.android.exoplayer2.MediaItem
|
||||||
|
import com.google.android.exoplayer2.SimpleExoPlayer
|
||||||
|
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
|
||||||
|
import com.google.android.exoplayer2.source.MediaSource
|
||||||
|
import com.google.android.exoplayer2.source.hls.HlsMediaSource
|
||||||
|
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory
|
||||||
|
import com.google.android.exoplayer2.util.Util
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import org.mosad.teapod.R
|
||||||
|
import org.mosad.teapod.parser.AoDParser
|
||||||
|
import org.mosad.teapod.preferences.Preferences
|
||||||
|
import org.mosad.teapod.util.DataTypes
|
||||||
|
import org.mosad.teapod.util.Episode
|
||||||
|
import org.mosad.teapod.util.Media
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.collections.ArrayList
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PlayerViewModel handles all stuff related to media/episodes.
|
||||||
|
* When currentEpisode is changed the player will start playing it (not initial media),
|
||||||
|
* the next episode will be update and the callback is handled.
|
||||||
|
*/
|
||||||
|
class PlayerViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
|
|
||||||
|
val player = SimpleExoPlayer.Builder(application).build()
|
||||||
|
private val dataSourceFactory = DefaultDataSourceFactory(application, Util.getUserAgent(application, "Teapod"))
|
||||||
|
private val mediaSession = MediaSessionCompat(application, "TEAPOD_PLAYER_SESSION")
|
||||||
|
|
||||||
|
val currentEpisodeChangedListener = ArrayList<() -> Unit>()
|
||||||
|
private val preferredLanguage = if (Preferences.preferSecondary) Locale.JAPANESE else Locale.GERMAN
|
||||||
|
|
||||||
|
var media: Media = Media(-1, "", DataTypes.MediaType.OTHER)
|
||||||
|
internal set
|
||||||
|
var currentEpisode = Episode()
|
||||||
|
internal set
|
||||||
|
var nextEpisode: Episode? = null
|
||||||
|
internal set
|
||||||
|
var currentLanguage: Locale = Locale.ROOT
|
||||||
|
internal set
|
||||||
|
|
||||||
|
init {
|
||||||
|
initMediaSession()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
super.onCleared()
|
||||||
|
|
||||||
|
mediaSession.release()
|
||||||
|
player.release()
|
||||||
|
|
||||||
|
Log.d(javaClass.name, "Released player")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* set the media session to active
|
||||||
|
* create a media session connector to set title and description
|
||||||
|
*/
|
||||||
|
private fun initMediaSession() {
|
||||||
|
val mediaSessionConnector = MediaSessionConnector(mediaSession)
|
||||||
|
mediaSessionConnector.setPlayer(player)
|
||||||
|
|
||||||
|
mediaSession.isActive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadMedia(mediaId: Int, episodeId: Int) {
|
||||||
|
runBlocking {
|
||||||
|
media = AoDParser.getMediaById(mediaId)
|
||||||
|
}
|
||||||
|
|
||||||
|
currentEpisode = media.getEpisodeById(episodeId)
|
||||||
|
nextEpisode = selectNextEpisode()
|
||||||
|
currentLanguage = currentEpisode.getPreferredStream(preferredLanguage).language
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setLanguage(language: Locale) {
|
||||||
|
currentLanguage = language
|
||||||
|
|
||||||
|
val seekTime = player.currentPosition
|
||||||
|
val mediaSource = HlsMediaSource.Factory(dataSourceFactory).createMediaSource(
|
||||||
|
MediaItem.fromUri(Uri.parse(currentEpisode.getPreferredStream(language).url))
|
||||||
|
)
|
||||||
|
playMedia(mediaSource, true, seekTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
// player actions
|
||||||
|
|
||||||
|
fun seekToOffset(offset: Long) {
|
||||||
|
player.seekTo(player.currentPosition + offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun togglePausePlay() {
|
||||||
|
if (player.isPlaying) player.pause() else player.play()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* play the next episode, if nextEpisode is not null
|
||||||
|
*/
|
||||||
|
fun playNextEpisode() = nextEpisode?.let { it ->
|
||||||
|
playEpisode(it, replace = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* set currentEpisode to the param episode and start playing it
|
||||||
|
* update nextEpisode to reflect the change
|
||||||
|
*
|
||||||
|
* updateWatchedState for the next (now current) episode
|
||||||
|
*/
|
||||||
|
fun playEpisode(episode: Episode, replace: Boolean = false, seekPosition: Long = 0) {
|
||||||
|
val preferredStream = episode.getPreferredStream(currentLanguage)
|
||||||
|
currentLanguage = preferredStream.language // update current language, since it may have changed
|
||||||
|
currentEpisode = episode
|
||||||
|
nextEpisode = selectNextEpisode()
|
||||||
|
currentEpisodeChangedListener.forEach { it() } // update player gui (title)
|
||||||
|
|
||||||
|
val mediaSource = HlsMediaSource.Factory(dataSourceFactory).createMediaSource(
|
||||||
|
MediaItem.fromUri(Uri.parse(preferredStream.url))
|
||||||
|
)
|
||||||
|
playMedia(mediaSource, replace, seekPosition)
|
||||||
|
|
||||||
|
// if episodes has not been watched, mark as watched
|
||||||
|
if (!episode.watched) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
AoDParser.markAsWatched(media.id, episode.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* change the players media source and start playback
|
||||||
|
*/
|
||||||
|
fun playMedia(source: MediaSource, replace: Boolean = false, seekPosition: Long = 0) {
|
||||||
|
if (replace || player.contentDuration == C.TIME_UNSET) {
|
||||||
|
player.setMediaSource(source)
|
||||||
|
player.prepare()
|
||||||
|
if (seekPosition > 0) player.seekTo(seekPosition)
|
||||||
|
player.playWhenReady = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getMediaTitle(): String {
|
||||||
|
return if (media.type == DataTypes.MediaType.TVSHOW) {
|
||||||
|
getApplication<Application>().getString(
|
||||||
|
R.string.component_episode_title,
|
||||||
|
currentEpisode.number,
|
||||||
|
currentEpisode.description
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
currentEpisode.title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,44 @@
|
|||||||
|
package org.mosad.teapod.ui.components
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import org.mosad.teapod.databinding.PlayerEpisodesListBinding
|
||||||
|
import org.mosad.teapod.ui.activity.player.PlayerViewModel
|
||||||
|
import org.mosad.teapod.util.adapter.PlayerEpisodeItemAdapter
|
||||||
|
|
||||||
|
class EpisodesListPlayer @JvmOverloads constructor(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
defStyleAttr: Int = 0,
|
||||||
|
model: PlayerViewModel? = null
|
||||||
|
) : LinearLayout(context, attrs, defStyleAttr) {
|
||||||
|
|
||||||
|
private val binding = PlayerEpisodesListBinding.inflate(LayoutInflater.from(context), this, true)
|
||||||
|
private lateinit var adapterRecEpisodes: PlayerEpisodeItemAdapter
|
||||||
|
|
||||||
|
var onViewRemovedAction: (() -> Unit)? = null // TODO find a better solution for this
|
||||||
|
|
||||||
|
init {
|
||||||
|
binding.buttonCloseEpisodesList.setOnClickListener {
|
||||||
|
(this.parent as ViewGroup).removeView(this)
|
||||||
|
onViewRemovedAction?.invoke()
|
||||||
|
}
|
||||||
|
|
||||||
|
model?.let {
|
||||||
|
adapterRecEpisodes = PlayerEpisodeItemAdapter(model.media.episodes)
|
||||||
|
|
||||||
|
adapterRecEpisodes.onImageClick = { _, position ->
|
||||||
|
(this.parent as ViewGroup).removeView(this)
|
||||||
|
model.playEpisode(model.media.episodes[position], replace = true)
|
||||||
|
}
|
||||||
|
adapterRecEpisodes.currentSelected = model.currentEpisode.number - 1
|
||||||
|
|
||||||
|
binding.recyclerEpisodesPlayer.adapter = adapterRecEpisodes
|
||||||
|
binding.recyclerEpisodesPlayer.scrollToPosition(model.currentEpisode.number - 1) // number != index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,103 @@
|
|||||||
|
package org.mosad.teapod.ui.components
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.Typeface
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.util.TypedValue
|
||||||
|
import android.view.Gravity
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.core.view.children
|
||||||
|
import org.mosad.teapod.R
|
||||||
|
import org.mosad.teapod.databinding.PlayerLanguageSettingsBinding
|
||||||
|
import org.mosad.teapod.ui.activity.player.PlayerViewModel
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class LanguageSettingsPlayer @JvmOverloads constructor(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
defStyleAttr: Int = 0,
|
||||||
|
model: PlayerViewModel? = null
|
||||||
|
) : LinearLayout(context, attrs, defStyleAttr) {
|
||||||
|
|
||||||
|
private val binding = PlayerLanguageSettingsBinding.inflate(LayoutInflater.from(context), this, true)
|
||||||
|
var onViewRemovedAction: (() -> Unit)? = null // TODO find a better solution for this
|
||||||
|
|
||||||
|
private var currentLanguage = model?.currentLanguage ?: Locale.ROOT
|
||||||
|
|
||||||
|
init {
|
||||||
|
model?.let {
|
||||||
|
model.currentEpisode.streams.forEach { stream ->
|
||||||
|
addLanguage(stream.language.displayName, stream.language == currentLanguage) {
|
||||||
|
currentLanguage = stream.language
|
||||||
|
updateSelectedLanguage(it as TextView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.buttonCloseLanguageSettings.setOnClickListener { close() }
|
||||||
|
binding.buttonCancel.setOnClickListener { close() }
|
||||||
|
binding.buttonSelect.setOnClickListener {
|
||||||
|
model?.setLanguage(currentLanguage)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addLanguage(str: String, isSelected: Boolean, onClick: OnClickListener) {
|
||||||
|
val text = TextView(context).apply {
|
||||||
|
height = 96
|
||||||
|
gravity = Gravity.CENTER_VERTICAL
|
||||||
|
text = str
|
||||||
|
setTextSize(TypedValue.COMPLEX_UNIT_SP, 16f)
|
||||||
|
|
||||||
|
if (isSelected) {
|
||||||
|
setTextColor(context.resources.getColor(R.color.exo_white, context.theme))
|
||||||
|
setTypeface(null, Typeface.BOLD)
|
||||||
|
setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_baseline_check_24, 0, 0, 0)
|
||||||
|
compoundDrawablesRelative.getOrNull(0)?.setTint(Color.WHITE)
|
||||||
|
compoundDrawablePadding = 12
|
||||||
|
} else {
|
||||||
|
setTextColor(context.resources.getColor(R.color.textPrimaryDark, context.theme))
|
||||||
|
setPadding(75, 0, 0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
setOnClickListener(onClick)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.linearLanguages.addView(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateSelectedLanguage(selected: TextView) {
|
||||||
|
// rest all tf to not selected style
|
||||||
|
binding.linearLanguages.children.forEach { child ->
|
||||||
|
if (child is TextView) {
|
||||||
|
child.apply {
|
||||||
|
setTextColor(context.resources.getColor(R.color.textPrimaryDark, context.theme))
|
||||||
|
setTypeface(null, Typeface.NORMAL)
|
||||||
|
setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
|
||||||
|
setPadding(75, 0, 0, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// set selected to selected style
|
||||||
|
selected.apply {
|
||||||
|
setTextColor(context.resources.getColor(R.color.exo_white, context.theme))
|
||||||
|
setTypeface(null, Typeface.BOLD)
|
||||||
|
setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_baseline_check_24, 0, 0, 0)
|
||||||
|
setPadding(0, 0, 0, 0)
|
||||||
|
compoundDrawablesRelative.getOrNull(0)?.setTint(Color.WHITE)
|
||||||
|
compoundDrawablePadding = 12
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun close() {
|
||||||
|
(this.parent as ViewGroup).removeView(this)
|
||||||
|
onViewRemovedAction?.invoke()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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,71 +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 var adapter : CustomAdapter? = null
|
|
||||||
|
|
||||||
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 {
|
|
||||||
adapter?.filter?.filter(query)
|
|
||||||
adapter?.notifyDataSetChanged()
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onQueryTextChange(newText: String?): Boolean {
|
|
||||||
adapter?.filter?.filter(newText)
|
|
||||||
adapter?.notifyDataSetChanged()
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
list_search.setOnItemClickListener { _, _, position, _ ->
|
|
||||||
search_text.clearFocus() // remove focus from the SearchView
|
|
||||||
|
|
||||||
runBlocking {
|
|
||||||
val media = adapter?.getItem(position) as Media
|
|
||||||
println("selected item is: ${media.title}")
|
|
||||||
|
|
||||||
(activity as MainActivity).showDetailFragment(media).join()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
82
app/src/main/java/org/mosad/teapod/util/ActivityUtils.kt
Normal file
82
app/src/main/java/org/mosad/teapod/util/ActivityUtils.kt
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
package org.mosad.teapod.util
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.app.ActivityManager
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import android.view.View
|
||||||
|
import android.view.WindowInsets
|
||||||
|
import android.view.WindowInsetsController
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import androidx.fragment.app.commit
|
||||||
|
import org.mosad.teapod.R
|
||||||
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show a fragment on top of the current fragment.
|
||||||
|
* The current fragment is replaced and the new one is added
|
||||||
|
* to the back stack.
|
||||||
|
*/
|
||||||
|
fun FragmentActivity.showFragment(fragment: Fragment) {
|
||||||
|
supportFragmentManager.commit {
|
||||||
|
replace(R.id.nav_host_fragment, fragment, fragment.javaClass.simpleName)
|
||||||
|
addToBackStack(fragment.javaClass.name)
|
||||||
|
show(fragment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* hide the status and navigation bar
|
||||||
|
*/
|
||||||
|
fun Activity.hideBars() {
|
||||||
|
window.apply {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
setDecorFitsSystemWindows(false)
|
||||||
|
insetsController?.apply {
|
||||||
|
hide(WindowInsets.Type.statusBars() or WindowInsets.Type.navigationBars())
|
||||||
|
systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_BARS_BY_SWIPE
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
@Suppress("deprecation")
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Activity.isInPiPMode(): Boolean {
|
||||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
|
isInPictureInPictureMode
|
||||||
|
} else {
|
||||||
|
false // pip mode not supported
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bring up launcher task to front
|
||||||
|
*/
|
||||||
|
fun Activity.navToLauncherTask() {
|
||||||
|
val activityManager = (getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager)
|
||||||
|
activityManager.appTasks.forEach { task ->
|
||||||
|
val baseIntent = task.taskInfo.baseIntent
|
||||||
|
val categories = baseIntent.categories
|
||||||
|
if (categories != null && categories.contains(Intent.CATEGORY_LAUNCHER)) {
|
||||||
|
task.moveToFront()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* exit and remove the app from tasks
|
||||||
|
*/
|
||||||
|
fun Activity.exitAndRemoveTask() {
|
||||||
|
finishAndRemoveTask()
|
||||||
|
exitProcess(0)
|
||||||
|
}
|
@ -1,71 +0,0 @@
|
|||||||
package org.mosad.teapod.util
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.*
|
|
||||||
import com.bumptech.glide.Glide
|
|
||||||
import org.mosad.teapod.R
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class CustomAdapter(val context: Context, private val originalMedia: ArrayList<Media>) : BaseAdapter(), Filterable {
|
|
||||||
|
|
||||||
private var filteredMedia = originalMedia.map { it.copy() }
|
|
||||||
private val customFilter = CustomFilter()
|
|
||||||
|
|
||||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
|
||||||
val view = convertView ?: LayoutInflater.from(context).inflate(R.layout.linear_media, parent, false)
|
|
||||||
|
|
||||||
val textTitle = view.findViewById<TextView>(R.id.text_title)
|
|
||||||
val imagePoster = view.findViewById<ImageView>(R.id.image_poster)
|
|
||||||
|
|
||||||
textTitle.text = filteredMedia[position].title
|
|
||||||
Glide.with(context).load(filteredMedia[position].info.posterLink).into(imagePoster)
|
|
||||||
|
|
||||||
return view
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getFilter(): Filter {
|
|
||||||
return customFilter
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getCount(): Int {
|
|
||||||
return filteredMedia.size
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItem(position: Int): Any {
|
|
||||||
return filteredMedia[position]
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemId(position: Int): Long {
|
|
||||||
return position.toLong()
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class CustomFilter : Filter() {
|
|
||||||
override fun performFiltering(constraint: CharSequence?): FilterResults {
|
|
||||||
val filterTerm = constraint.toString().toLowerCase(Locale.ROOT)
|
|
||||||
val results = FilterResults()
|
|
||||||
|
|
||||||
val filteredList = if (filterTerm.isEmpty()) {
|
|
||||||
originalMedia
|
|
||||||
} else {
|
|
||||||
originalMedia.filter {
|
|
||||||
it.title.toLowerCase(Locale.ROOT).contains(filterTerm)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
results.values = filteredList
|
|
||||||
results.count = filteredList.size
|
|
||||||
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun publishResults(constraint: CharSequence?, results: FilterResults?) {
|
|
||||||
filteredMedia = results?.values as ArrayList<Media>
|
|
||||||
notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,44 +1,131 @@
|
|||||||
package org.mosad.teapod.util
|
package org.mosad.teapod.util
|
||||||
|
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.collections.ArrayList
|
||||||
|
|
||||||
class DataTypes {
|
class DataTypes {
|
||||||
enum class MediaType {
|
enum class MediaType {
|
||||||
OTHER,
|
OTHER,
|
||||||
MOVIE,
|
MOVIE,
|
||||||
TVSHOW
|
TVSHOW
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
data class Media(val title: String, val link: String, val type: DataTypes.MediaType, val info : Info = Info(), var episodes: List<Episode> = listOf()) {
|
enum class Theme(val str: String) {
|
||||||
override fun toString(): String {
|
LIGHT("Light"),
|
||||||
return title
|
DARK("Dark")
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class License(val short: String, val long: String) {
|
||||||
|
APACHE2("AL 2.0", "Apache License Version 2.0"),
|
||||||
|
MIT("MIT", "MIT License"),
|
||||||
|
GPL3("GPL 3", "GNU General Public License Version 3"),
|
||||||
|
BSD2("BSD 2", "BSD 2-Clause License")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class ThirdPartyComponent(
|
||||||
|
val name: String,
|
||||||
|
val year: String,
|
||||||
|
val copyrightOwner: String,
|
||||||
|
val link: String,
|
||||||
|
val license: DataTypes.License
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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(),
|
||||||
|
val episodes: ArrayList<Episode> = arrayListOf()
|
||||||
|
) {
|
||||||
|
fun hasEpisode(id: Int) = episodes.any { it.id == id }
|
||||||
|
fun getEpisodeById(id: Int) = episodes.first { it.id == id }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* uses var, since the values are written in different steps
|
||||||
|
*/
|
||||||
data class Info(
|
data class Info(
|
||||||
var posterLink: String = "",
|
var title: String = "",
|
||||||
|
var posterUrl: String = "",
|
||||||
var shortDesc: String = "",
|
var shortDesc: String = "",
|
||||||
var description: String = "",
|
var description: String = "",
|
||||||
var year: Int = 0,
|
var year: Int = 0,
|
||||||
var age: Int = 0,
|
var age: Int = 0,
|
||||||
var episodesCount: Int = 0
|
var episodesCount: Int = 0,
|
||||||
|
var similar: List<ItemMedia> = listOf()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* number = episode number (0..n)
|
||||||
|
*/
|
||||||
data class Episode(
|
data class Episode(
|
||||||
val id: Int = 0,
|
val id: Int = -1,
|
||||||
var title: String = "",
|
val streams: MutableList<Stream> = mutableListOf(),
|
||||||
var streamUrl: String = "",
|
val title: String = "",
|
||||||
var posterLink: String = "",
|
val posterUrl: String = "",
|
||||||
var description: String = "",
|
val description: String = "",
|
||||||
var shortDesc: String = "",
|
var shortDesc: String = "",
|
||||||
var number: Int = 0,
|
val number: Int = 0,
|
||||||
var watched: Boolean = false
|
var watched: Boolean = false,
|
||||||
|
var watchedCallback: String = ""
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* get the preferred stream
|
||||||
|
* @return the preferred stream, if not present use the first stream
|
||||||
|
*/
|
||||||
|
fun getPreferredStream(language: Locale) =
|
||||||
|
streams.firstOrNull { it.language == language } ?: streams.first()
|
||||||
|
|
||||||
|
fun hasDub() = streams.any { it.language == Locale.GERMAN }
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Stream(
|
||||||
|
val url: String,
|
||||||
|
val language : Locale
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* this class is used for tmdb responses
|
||||||
|
*/
|
||||||
data class TMDBResponse(
|
data class TMDBResponse(
|
||||||
val id: Int = 0,
|
val id: Int = 0,
|
||||||
val title: String = "",
|
val title: String = "",
|
||||||
val overview: String = "",
|
val overview: String = "",
|
||||||
val posterUrl: String = "",
|
val posterUrl: String = "",
|
||||||
val backdropUrl: String = "",
|
val backdropUrl: String = "",
|
||||||
var runtime: Int = 0
|
val runtime: Int = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* this class is used to represent the aod json API?
|
||||||
|
*/
|
||||||
|
data class AoDObject(
|
||||||
|
val playlist: List<Playlist>,
|
||||||
|
val extLanguage: String
|
||||||
|
)
|
||||||
|
|
||||||
|
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,50 +0,0 @@
|
|||||||
package org.mosad.teapod.util
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import com.bumptech.glide.Glide
|
|
||||||
import kotlinx.android.synthetic.main.component_episode.view.*
|
|
||||||
import org.mosad.teapod.R
|
|
||||||
|
|
||||||
class EpisodesAdapter(private val episodes: List<Episode>, private val context: Context) : RecyclerView.Adapter<EpisodesAdapter.MyViewHolder>() {
|
|
||||||
|
|
||||||
var onItemClick: ((String, Int) -> Unit)? = null
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
|
|
||||||
val view = LayoutInflater.from(parent.context).inflate(R.layout.component_episode, parent, false)
|
|
||||||
|
|
||||||
return MyViewHolder(view)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
|
|
||||||
holder.view.text_episode_title.text = context.getString(
|
|
||||||
R.string.component_episode_title,
|
|
||||||
episodes[position].number,
|
|
||||||
episodes[position].description
|
|
||||||
)
|
|
||||||
holder.view.text_episode_desc.text = episodes[position].shortDesc
|
|
||||||
|
|
||||||
if (episodes[position].posterLink.isNotEmpty()) {
|
|
||||||
Glide.with(context).load(episodes[position].posterLink).into(holder.view.image_episode)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!episodes[position].watched) {
|
|
||||||
holder.view.image_watched.setImageDrawable(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemCount(): Int {
|
|
||||||
return episodes.size
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class MyViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
|
|
||||||
init {
|
|
||||||
view.setOnClickListener {
|
|
||||||
onItemClick?.invoke(episodes[adapterPosition].title, adapterPosition)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
89
app/src/main/java/org/mosad/teapod/util/StorageController.kt
Normal file
89
app/src/main/java/org/mosad/teapod/util/StorageController.kt
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
package org.mosad.teapod.util
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import android.util.Log
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import com.google.gson.JsonParser
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileReader
|
||||||
|
import java.io.FileWriter
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) {
|
||||||
|
loadMyList(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadMyList(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 CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
file.writeText(Gson().toJson(myList.distinct()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun exportMyList(context: Context, uri: Uri) {
|
||||||
|
try {
|
||||||
|
context.contentResolver.openFileDescriptor(uri, "w")?.use {
|
||||||
|
FileWriter(it.fileDescriptor).use { writer ->
|
||||||
|
writer.write(Gson().toJson(myList.distinct()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
Log.e(javaClass.name, "Exporting my list failed.", ex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* import my list from a (previously exported) json file
|
||||||
|
* @param context the current context
|
||||||
|
* @param uri the uri of the selected file
|
||||||
|
* @return 0 if import was successfull, else 1
|
||||||
|
*/
|
||||||
|
fun importMyList(context: Context, uri: Uri): Int {
|
||||||
|
try {
|
||||||
|
val text = context.contentResolver.openFileDescriptor(uri, "r")?.use {
|
||||||
|
FileReader(it.fileDescriptor).use { reader ->
|
||||||
|
reader.readText()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
myList.clear()
|
||||||
|
myList.addAll(JsonParser.parseString(text).asJsonArray.map { it.asInt }.distinct())
|
||||||
|
|
||||||
|
// after the list has been imported also save it
|
||||||
|
saveMyList(context)
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
myList.clear()
|
||||||
|
Log.e(javaClass.name, "Importing my list failed.", ex)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -3,12 +3,10 @@ package org.mosad.teapod.util
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.google.gson.JsonObject
|
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 org.mosad.teapod.util.DataTypes.MediaType
|
||||||
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
|
|
||||||
|
|
||||||
class TMDBApiController {
|
class TMDBApiController {
|
||||||
|
|
||||||
@ -22,8 +20,12 @@ class TMDBApiController {
|
|||||||
|
|
||||||
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()
|
// remove unneeded text from the media title before searching
|
||||||
|
val searchTerm = title.replace("(Sub)", "")
|
||||||
|
.replace(Regex("-?\\s?[0-9]+.\\s?(Staffel|Season)"), "")
|
||||||
|
.replace(Regex("(Staffel|Season)\\s?[0-9]+"), "")
|
||||||
|
.trim()
|
||||||
|
|
||||||
return when (type) {
|
return when (type) {
|
||||||
MediaType.MOVIE -> searchMovie(searchTerm)
|
MediaType.MOVIE -> searchMovie(searchTerm)
|
||||||
@ -36,70 +38,64 @@ class TMDBApiController {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun searchTVShow(title: String) = runBlocking {
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
|
private suspend fun searchTVShow(title: String): TMDBResponse = withContext(Dispatchers.IO) {
|
||||||
val url = URL("$searchTVUrl$preparedParameters&query=${URLEncoder.encode(title, "UTF-8")}")
|
val url = URL("$searchTVUrl$preparedParameters&query=${URLEncoder.encode(title, "UTF-8")}")
|
||||||
|
val response = JsonParser.parseString(url.readText()).asJsonObject
|
||||||
|
// println(response)
|
||||||
|
|
||||||
GlobalScope.async {
|
val sortedResults = response.get("results").asJsonArray.toList().sortedBy {
|
||||||
val response = JsonParser.parseString(url.readText()).asJsonObject
|
getStringNotNull(it.asJsonObject, "name")
|
||||||
//println(response)
|
}
|
||||||
|
|
||||||
return@async if (response.get("total_results").asInt > 0) {
|
return@withContext if (sortedResults.isNotEmpty()) {
|
||||||
response.get("results").asJsonArray.first().asJsonObject.let {
|
sortedResults.first().asJsonObject.let {
|
||||||
val id = getStringNotNull(it,"id").toInt()
|
val id = getStringNotNull(it, "id").toInt()
|
||||||
val overview = getStringNotNull(it,"overview")
|
val overview = getStringNotNull(it, "overview")
|
||||||
val posterPath = getStringNotNullPrefix(it, "poster_path", imageUrl)
|
val posterPath = getStringNotNullPrefix(it, "poster_path", imageUrl)
|
||||||
val backdropPath = getStringNotNullPrefix(it, "backdrop_path", imageUrl)
|
val backdropPath = getStringNotNullPrefix(it, "backdrop_path", imageUrl)
|
||||||
|
|
||||||
TMDBResponse(id, "", overview, posterPath, backdropPath)
|
TMDBResponse(id, "", overview, posterPath, backdropPath)
|
||||||
}
|
|
||||||
} else {
|
|
||||||
TMDBResponse()
|
|
||||||
}
|
}
|
||||||
}.await()
|
} else {
|
||||||
|
TMDBResponse()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun searchMovie(title: String) = runBlocking {
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
|
private suspend fun searchMovie(title: String): TMDBResponse = withContext(Dispatchers.IO) {
|
||||||
val url = URL("$searchMovieUrl$preparedParameters&query=${URLEncoder.encode(title, "UTF-8")}")
|
val url = URL("$searchMovieUrl$preparedParameters&query=${URLEncoder.encode(title, "UTF-8")}")
|
||||||
|
val response = JsonParser.parseString(url.readText()).asJsonObject
|
||||||
|
// println(response)
|
||||||
|
|
||||||
GlobalScope.async {
|
val sortedResults = response.get("results").asJsonArray.toList().sortedBy {
|
||||||
val response = JsonParser.parseString(url.readText()).asJsonObject
|
getStringNotNull(it.asJsonObject, "title")
|
||||||
//println(response)
|
}
|
||||||
|
|
||||||
return@async if (response.get("total_results").asInt > 0) {
|
return@withContext if (sortedResults.isNotEmpty()) {
|
||||||
response.get("results").asJsonArray.first().asJsonObject.let {
|
sortedResults.first().asJsonObject.let {
|
||||||
val id = getStringNotNull(it,"id").toInt()
|
val id = getStringNotNull(it,"id").toInt()
|
||||||
val overview = getStringNotNull(it,"overview")
|
val overview = getStringNotNull(it,"overview")
|
||||||
val posterPath = getStringNotNullPrefix(it, "poster_path", imageUrl)
|
val posterPath = getStringNotNullPrefix(it, "poster_path", imageUrl)
|
||||||
val backdropPath = getStringNotNullPrefix(it, "backdrop_path", imageUrl)
|
val backdropPath = getStringNotNullPrefix(it, "backdrop_path", imageUrl)
|
||||||
val runtime = getMovieRuntime(id)
|
val runtime = getMovieRuntime(id)
|
||||||
|
|
||||||
TMDBResponse(id, "", overview, posterPath, backdropPath, runtime)
|
TMDBResponse(id, "", overview, posterPath, backdropPath, runtime)
|
||||||
}
|
|
||||||
} else {
|
|
||||||
TMDBResponse()
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
TMDBResponse()
|
||||||
}.await()
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* currently only used for runtime, need a rework
|
* currently only used for runtime, need a rework
|
||||||
*/
|
*/
|
||||||
fun getMovieRuntime(id: Int): Int = runBlocking {
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
|
suspend fun getMovieRuntime(id: Int): Int = withContext(Dispatchers.IO) {
|
||||||
val url = URL("$getMovieUrl/$id?api_key=$apiKey&language=$language")
|
val url = URL("$getMovieUrl/$id?api_key=$apiKey&language=$language")
|
||||||
|
|
||||||
GlobalScope.async {
|
val response = JsonParser.parseString(url.readText()).asJsonObject
|
||||||
val response = JsonParser.parseString(url.readText()).asJsonObject
|
return@withContext getStringNotNull(response,"runtime").toInt()
|
||||||
//println(response)
|
|
||||||
|
|
||||||
val runtime = getStringNotNull(response,"runtime").toInt()
|
|
||||||
println(runtime)
|
|
||||||
|
|
||||||
|
|
||||||
return@async runtime
|
|
||||||
}.await()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
7
app/src/main/java/org/mosad/teapod/util/Utils.kt
Normal file
7
app/src/main/java/org/mosad/teapod/util/Utils.kt
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
package org.mosad.teapod.util
|
||||||
|
|
||||||
|
import android.widget.TextView
|
||||||
|
|
||||||
|
fun TextView.setDrawableTop(drawable: Int) {
|
||||||
|
this.setCompoundDrawablesWithIntrinsicBounds(0, drawable, 0, 0)
|
||||||
|
}
|
@ -0,0 +1,69 @@
|
|||||||
|
package org.mosad.teapod.util.adapter
|
||||||
|
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.drawable.ColorDrawable
|
||||||
|
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.hasDub()) {
|
||||||
|
context.getString(R.string.component_episode_title, ep.number, ep.description)
|
||||||
|
} else {
|
||||||
|
context.getString(R.string.component_episode_title_sub, 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.placeholderOf(ColorDrawable(Color.DKGRAY)))
|
||||||
|
.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) {
|
||||||
|
// use getOrNull as there could be a index out of bound when running this in onResume()
|
||||||
|
episodes.getOrNull(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,79 @@
|
|||||||
|
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 initMedia: List<ItemMedia>) : RecyclerView.Adapter<MediaItemAdapter.MediaViewHolder>(), Filterable {
|
||||||
|
|
||||||
|
var onItemClick: ((Int, Int) -> Unit)? = null
|
||||||
|
private val filter = MediaFilter()
|
||||||
|
private var filteredMedia = initMedia.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
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateMediaList(mediaList: List<ItemMedia>) {
|
||||||
|
filteredMedia = mediaList
|
||||||
|
}
|
||||||
|
|
||||||
|
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().lowercase(Locale.ROOT)
|
||||||
|
val results = FilterResults()
|
||||||
|
|
||||||
|
val filteredList = if (filterTerm.isEmpty()) {
|
||||||
|
initMedia
|
||||||
|
} else {
|
||||||
|
initMedia.filter {
|
||||||
|
it.title.lowercase(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,64 @@
|
|||||||
|
package org.mosad.teapod.util.adapter
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
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.ItemEpisodePlayerBinding
|
||||||
|
import org.mosad.teapod.util.Episode
|
||||||
|
|
||||||
|
class PlayerEpisodeItemAdapter(private val episodes: List<Episode>) : RecyclerView.Adapter<PlayerEpisodeItemAdapter.EpisodeViewHolder>() {
|
||||||
|
|
||||||
|
var onImageClick: ((String, Int) -> Unit)? = null
|
||||||
|
var currentSelected: Int = -1 // -1, since position should never be < 0
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EpisodeViewHolder {
|
||||||
|
return EpisodeViewHolder(ItemEpisodePlayerBinding.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.hasDub()) {
|
||||||
|
context.getString(R.string.component_episode_title, ep.number, ep.description)
|
||||||
|
} else {
|
||||||
|
context.getString(R.string.component_episode_title_sub, ep.number, ep.description)
|
||||||
|
}
|
||||||
|
|
||||||
|
holder.binding.textEpisodeTitle2.text = titleText
|
||||||
|
holder.binding.textEpisodeDesc2.text = ep.shortDesc
|
||||||
|
|
||||||
|
if (episodes[position].posterUrl.isNotEmpty()) {
|
||||||
|
Glide.with(context).load(ep.posterUrl)
|
||||||
|
.apply(RequestOptions.bitmapTransform(RoundedCornersTransformation(10, 0)))
|
||||||
|
.into(holder.binding.imageEpisode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// hide the play icon, if it's the current episode
|
||||||
|
holder.binding.imageEpisodePlay.visibility = if (currentSelected == position) {
|
||||||
|
View.GONE
|
||||||
|
} else {
|
||||||
|
View.VISIBLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int {
|
||||||
|
return episodes.size
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class EpisodeViewHolder(val binding: ItemEpisodePlayerBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||||
|
init {
|
||||||
|
binding.imageEpisode.setOnClickListener {
|
||||||
|
// don't execute, if it's the current episode
|
||||||
|
if (currentSelected != adapterPosition) {
|
||||||
|
onImageClick?.invoke(episodes[adapterPosition].title, adapterPosition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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
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/iconColor"/>
|
||||||
|
</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
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>
|
12
app/src/main/res/drawable/dot_default.xml
Normal file
12
app/src/main/res/drawable/dot_default.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>
|
||||||
|
<shape
|
||||||
|
android:innerRadius="0dp"
|
||||||
|
android:shape="ring"
|
||||||
|
android:thickness="4dp"
|
||||||
|
android:useLevel="false">
|
||||||
|
<solid android:color="?iconColor"/>
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
</layer-list>
|
12
app/src/main/res/drawable/dot_selected.xml
Normal file
12
app/src/main/res/drawable/dot_selected.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>
|
||||||
|
<shape
|
||||||
|
android:innerRadius="0dp"
|
||||||
|
android:shape="ring"
|
||||||
|
android:thickness="4dp"
|
||||||
|
android:useLevel="false">
|
||||||
|
<solid android:color="@color/colorAccent" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
</layer-list>
|
6
app/src/main/res/drawable/dot_tab_selector.xml
Normal file
6
app/src/main/res/drawable/dot_tab_selector.xml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="@drawable/dot_selected"
|
||||||
|
android:state_selected="true"/>
|
||||||
|
<item android:drawable="@drawable/dot_default"/>
|
||||||
|
</selector>
|
6
app/src/main/res/drawable/ic_baseline_access_time_24.xml
Normal file
6
app/src/main/res/drawable/ic_baseline_access_time_24.xml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<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="M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z"/>
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M12.5,7H11v6l5.25,3.15 0.75,-1.23 -4.5,-2.67z"/>
|
||||||
|
</vector>
|
@ -6,5 +6,5 @@
|
|||||||
android:tint="?attr/colorControlNormal">
|
android:tint="?attr/colorControlNormal">
|
||||||
<path
|
<path
|
||||||
android:fillColor="@android:color/white"
|
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,2zM13,17h-2v-6h2v6zM13,9h-2L11,7h2v2z"/>
|
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
|
||||||
</vector>
|
</vector>
|
10
app/src/main/res/drawable/ic_baseline_arrow_back_24.xml
Normal file
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
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
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>
|
5
app/src/main/res/drawable/ic_baseline_code_24.xml
Normal file
5
app/src/main/res/drawable/ic_baseline_code_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="M9.4,16.6L4.8,12l4.6,-4.6L8,6l-6,6 6,6 1.4,-1.4zM14.6,16.6l4.6,-4.6 -4.6,-4.6L16,6l6,6 -6,6 -1.4,-1.4z"/>
|
||||||
|
</vector>
|
5
app/src/main/res/drawable/ic_baseline_description_24.xml
Normal file
5
app/src/main/res/drawable/ic_baseline_description_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="M14,2L6,2c-1.1,0 -1.99,0.9 -1.99,2L4,20c0,1.1 0.89,2 1.99,2L18,22c1.1,0 2,-0.9 2,-2L20,8l-6,-6zM16,18L8,18v-2h8v2zM16,14L8,14v-2h8v2zM13,9L13,3.5L18.5,9L13,9z"/>
|
||||||
|
</vector>
|
15
app/src/main/res/drawable/ic_baseline_forward_10_24.xml
Normal file
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
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>
|
5
app/src/main/res/drawable/ic_baseline_people_24.xml
Normal file
5
app/src/main/res/drawable/ic_baseline_people_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="M16,11c1.66,0 2.99,-1.34 2.99,-3S17.66,5 16,5c-1.66,0 -3,1.34 -3,3s1.34,3 3,3zM8,11c1.66,0 2.99,-1.34 2.99,-3S9.66,5 8,5C6.34,5 5,6.34 5,8s1.34,3 3,3zM8,13c-2.33,0 -7,1.17 -7,3.5L1,19h14v-2.5c0,-2.33 -4.67,-3.5 -7,-3.5zM16,13c-0.29,0 -0.62,0.02 -0.97,0.05 1.16,0.84 1.97,1.97 1.97,3.45L17,19h6v-2.5c0,-2.33 -4.67,-3.5 -7,-3.5z"/>
|
||||||
|
</vector>
|
15
app/src/main/res/drawable/ic_baseline_rewind_10_24.xml
Normal file
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
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/iconColor">
|
||||||
<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
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
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
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>
|
||||||
|
20
app/src/main/res/drawable/ic_launcher_foreground.xml
Normal file
20
app/src/main/res/drawable/ic_launcher_foreground.xml
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<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:fillColor="#000000"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
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:strokeWidth="0.41878"
|
||||||
|
android:strokeColor="#000000"
|
||||||
|
android:strokeLineCap="butt"
|
||||||
|
android:strokeLineJoin="miter" />
|
||||||
|
</group>
|
||||||
|
</vector>
|
10
app/src/main/res/drawable/ic_outline_download_24.xml
Normal file
10
app/src/main/res/drawable/ic_outline_download_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="M18,15v3H6v-3H4v3c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2v-3H18zM17,11l-1.41,-1.41L13,12.17V4h-2v8.17L8.41,9.59L7,11l5,5L17,11z" />
|
||||||
|
</vector>
|
10
app/src/main/res/drawable/ic_outline_info_24.xml
Normal file
10
app/src/main/res/drawable/ic_outline_info_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="M11,7h2v2h-2zM11,11h2v6h-2zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8z" />
|
||||||
|
</vector>
|
10
app/src/main/res/drawable/ic_outline_upload_24.xml
Normal file
10
app/src/main/res/drawable/ic_outline_upload_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="M18,15v3H6v-3H4v3c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2v-3H18zM7,9l1.41,1.41L11,7.83V16h2V7.83l2.59,2.58L17,9l-5,-5L7,9z" />
|
||||||
|
</vector>
|
BIN
app/src/main/res/drawable/ic_splash_logo.png
Normal file
BIN
app/src/main/res/drawable/ic_splash_logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
3
app/src/main/res/drawable/ripple_background.xml
Normal file
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" />
|
@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<solid android:color="#B0B0B0"/>
|
<solid android:color="?textBackground"/>
|
||||||
<corners android:radius="3dp"/>
|
<corners android:radius="3dp"/>
|
||||||
</shape>
|
</shape>
|
@ -9,9 +9,8 @@
|
|||||||
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"
|
||||||
|
50
app/src/main/res/layout/activity_onboarding.xml
Normal file
50
app/src/main/res/layout/activity_onboarding.xml
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<RelativeLayout 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="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<androidx.viewpager2.widget.ViewPager2
|
||||||
|
android:id="@+id/viewPager"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
</androidx.viewpager2.widget.ViewPager2>
|
||||||
|
|
||||||
|
<com.google.android.material.tabs.TabLayout
|
||||||
|
android:id="@+id/tab_layout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_alignParentBottom="true"
|
||||||
|
android:layout_gravity="bottom"
|
||||||
|
android:layout_marginBottom="0dp"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
app:tabBackground="@drawable/dot_tab_selector"
|
||||||
|
app:tabGravity="center"
|
||||||
|
app:tabIndicatorHeight="0dp"
|
||||||
|
app:tabPaddingStart="6dp"
|
||||||
|
app:tabPaddingEnd="6dp"/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/button_next"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_alignParentEnd="true"
|
||||||
|
android:layout_alignParentBottom="true"
|
||||||
|
android:background="@null"
|
||||||
|
android:onClick="btnNextClick"
|
||||||
|
android:text="@string/next"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/button_skip"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_alignParentStart="true"
|
||||||
|
android:layout_alignParentBottom="true"
|
||||||
|
android:background="@null"
|
||||||
|
android:onClick="btnSkipClick"
|
||||||
|
android:text="@string/skip"
|
||||||
|
tools:visibility="gone" />
|
||||||
|
|
||||||
|
</RelativeLayout>
|
@ -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=".ui.activity.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
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
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/rewind_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>
|
274
app/src/main/res/layout/fragment_about.xml
Normal file
274
app/src/main/res/layout/fragment_about.xml
Normal file
@ -0,0 +1,274 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.core.widget.NestedScrollView
|
||||||
|
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="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="?themePrimary"
|
||||||
|
tools:context=".ui.activity.main.fragments.AboutFragment">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/image_app_icon"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:layout_marginTop="17dp"
|
||||||
|
android:contentDescription="@string/app_name"
|
||||||
|
android:src="@mipmap/ic_launcher_round" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_app_name"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="5dp"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
android:layout_marginEnd="5dp"
|
||||||
|
android:text="@string/app_name"
|
||||||
|
android:textAlignment="center"
|
||||||
|
android:textSize="20sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_about_info"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="5dp"
|
||||||
|
android:layout_marginTop="7dp"
|
||||||
|
android:layout_marginEnd="5dp"
|
||||||
|
android:layout_marginBottom="12dp"
|
||||||
|
android:text="@string/about_info" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/linear_version"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:foreground="?android:selectableItemBackground"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:padding="7dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/image_version"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:contentDescription="@string/version"
|
||||||
|
android:minWidth="48dp"
|
||||||
|
android:minHeight="48dp"
|
||||||
|
android:padding="9dp"
|
||||||
|
android:scaleType="fitXY"
|
||||||
|
android:src="@drawable/ic_outline_info_24"
|
||||||
|
app:tint="?iconColor" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_version"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/version"
|
||||||
|
android:textSize="16sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_version_desc"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/version_desc"
|
||||||
|
android:textColor="?textSecondary" />
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/linear_authors"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:foreground="?android:selectableItemBackground"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:padding="7dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/image_authors"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:contentDescription="@string/authors"
|
||||||
|
android:minWidth="48dp"
|
||||||
|
android:minHeight="48dp"
|
||||||
|
android:padding="9dp"
|
||||||
|
android:scaleType="fitXY"
|
||||||
|
android:src="@drawable/ic_baseline_people_24"
|
||||||
|
app:tint="?iconColor" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_authors"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/authors"
|
||||||
|
android:textSize="16sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_authors_desc"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/author_desc"
|
||||||
|
android:textColor="?textSecondary" />
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/linear_source"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:foreground="?android:selectableItemBackground"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:padding="7dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/image_source"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:contentDescription="@string/source"
|
||||||
|
android:minWidth="48dp"
|
||||||
|
android:minHeight="48dp"
|
||||||
|
android:padding="9dp"
|
||||||
|
android:scaleType="fitXY"
|
||||||
|
android:src="@drawable/ic_baseline_code_24"
|
||||||
|
app:tint="?iconColor" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_source"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/source"
|
||||||
|
android:textSize="16sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_source_desc"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/teapod_repo"
|
||||||
|
android:textColor="?textSecondary" />
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/linear_license"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:foreground="?android:selectableItemBackground"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:padding="7dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/image_license"
|
||||||
|
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_description_24"
|
||||||
|
app:tint="?iconColor" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_license"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/license"
|
||||||
|
android:textSize="16sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_license_desc"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/license_desc"
|
||||||
|
android:textColor="?textSecondary" />
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingBottom="5dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_third_party"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="5dp"
|
||||||
|
android:layout_marginTop="17dp"
|
||||||
|
android:layout_marginEnd="5dp"
|
||||||
|
android:text="@string/third_party_heading"
|
||||||
|
android:textSize="17sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/linear_third_party"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginTop="7dp"
|
||||||
|
android:orientation="vertical" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_tmdb_notice"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="7dp"
|
||||||
|
android:layout_marginTop="7dp"
|
||||||
|
android:layout_marginEnd="7dp"
|
||||||
|
android:paddingBottom="5dp"
|
||||||
|
android:text="@string/tmdb_notice"
|
||||||
|
android:textAlignment="center"
|
||||||
|
android:textColor="?textSecondary" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</androidx.core.widget.NestedScrollView>
|
@ -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.activity.main.fragments.AccountFragment">
|
||||||
|
|
||||||
<ScrollView
|
<ScrollView
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@ -14,14 +14,17 @@
|
|||||||
<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:clipToPadding="false"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingBottom="12dp">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/linear_account"
|
android:id="@+id/linear_account"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="12dp"
|
android:layout_marginTop="12dp"
|
||||||
android:background="#ffffff"
|
android:background="?themeSecondary"
|
||||||
|
android:elevation="5dp"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
@ -31,7 +34,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,9 +41,10 @@
|
|||||||
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:foreground="?android:selectableItemBackground"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:orientation="horizontal">
|
android:orientation="horizontal"
|
||||||
|
android:padding="7dp">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/imageView"
|
android:id="@+id/imageView"
|
||||||
@ -50,10 +53,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="?iconColor" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@ -66,7 +69,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
|
||||||
@ -75,18 +77,367 @@
|
|||||||
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
|
||||||
|
android:id="@+id/linear_account_subscription"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:foreground="?android:selectableItemBackground"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:padding="7dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/imageView6"
|
||||||
|
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_access_time_24"
|
||||||
|
app:tint="?iconColor" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_account_subscription"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/account_subscription"
|
||||||
|
android:textSize="16sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_account_subscription_desc"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/account_subscription_desc"
|
||||||
|
android:textColor="?textSecondary" />
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/linear_settings"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
android:background="?themeSecondary"
|
||||||
|
android:elevation="5dp"
|
||||||
|
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:gravity="center"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:padding="7dp">
|
||||||
|
|
||||||
|
<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="?iconColor" />
|
||||||
|
|
||||||
|
<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:gravity="center"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:padding="7dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/image_autoplay"
|
||||||
|
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="?iconColor" />
|
||||||
|
|
||||||
|
<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:foreground="?android:selectableItemBackground"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:padding="7dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/image_theme"
|
||||||
|
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="?iconColor" />
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/linear_dev_settings"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
android:background="?themeSecondary"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:elevation="5dp"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_dev_settings"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingStart="7dp"
|
||||||
|
android:paddingEnd="7dp"
|
||||||
|
android:text="@string/dev_settings"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/linear_export_data"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:foreground="?android:selectableItemBackground"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:padding="7dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/image_export_data"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:contentDescription="@string/info"
|
||||||
|
android:minWidth="48dp"
|
||||||
|
android:minHeight="48dp"
|
||||||
|
android:padding="9dp"
|
||||||
|
android:scaleType="fitXY"
|
||||||
|
app:srcCompat="@drawable/ic_outline_upload_24"
|
||||||
|
app:tint="?iconColor" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_export_data"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/export_data"
|
||||||
|
android:textSize="16sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_export_data_desc"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/export_data_desc"
|
||||||
|
android:textColor="?textSecondary" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/linear_import_data"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:foreground="?android:selectableItemBackground"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:padding="7dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/image_import_data"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:contentDescription="@string/info"
|
||||||
|
android:minWidth="48dp"
|
||||||
|
android:minHeight="48dp"
|
||||||
|
android:padding="9dp"
|
||||||
|
android:scaleType="fitXY"
|
||||||
|
app:srcCompat="@drawable/ic_outline_download_24"
|
||||||
|
app:tint="?iconColor" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_import_data"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/import_data"
|
||||||
|
android:textSize="16sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_import_data_desc"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/import_data_desc"
|
||||||
|
android:textColor="?textSecondary" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</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="wrap_content"
|
||||||
android:layout_marginTop="12dp"
|
android:layout_marginTop="12dp"
|
||||||
android:background="#ffffff"
|
android:background="?themeSecondary"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:elevation="5dp"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
@ -96,7 +447,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" />
|
||||||
|
|
||||||
@ -104,9 +454,10 @@
|
|||||||
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:foreground="?android:selectableItemBackground"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:orientation="horizontal">
|
android:orientation="horizontal"
|
||||||
|
android:padding="7dp">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/imageView2"
|
android:id="@+id/imageView2"
|
||||||
@ -115,9 +466,10 @@
|
|||||||
android:contentDescription="@string/info"
|
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_outline_info_24"
|
||||||
|
app:tint="?iconColor" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@ -130,7 +482,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
|
||||||
@ -139,28 +490,13 @@
|
|||||||
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="Licenses"
|
|
||||||
android:textColor="@android:color/primary_text_light"
|
|
||||||
android:textSize="16sp" />
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
||||||
|
@ -2,22 +2,252 @@
|
|||||||
<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.activity.main.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:id="@+id/linear_highlight"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingBottom="7dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/image_highlight"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:contentDescription="@string/highlight_media"
|
||||||
|
app:layout_constraintDimensionRatio="H,16:9"
|
||||||
|
tools:src="@drawable/ic_launcher_background" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_highlight_title"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="7dp"
|
||||||
|
android:text="@string/text_title_ex"
|
||||||
|
android:textAlignment="center"
|
||||||
|
android:textSize="16sp" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginTop="7dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:layout_weight="1" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_highlight_my_list"
|
||||||
|
android:layout_width="64dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="@string/my_list"
|
||||||
|
android:textColor="?textSecondary"
|
||||||
|
android:textSize="12sp"
|
||||||
|
app:drawableTint="?buttonBackground"
|
||||||
|
app:drawableTopCompat="@drawable/ic_baseline_add_24" />
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:layout_weight="1" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/button_play_highlight"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="@string/button_play"
|
||||||
|
android:textAllCaps="false"
|
||||||
|
android:textColor="?themePrimary"
|
||||||
|
android:textSize="16sp"
|
||||||
|
app:backgroundTint="?buttonBackground"
|
||||||
|
app:icon="@drawable/ic_baseline_play_arrow_24"
|
||||||
|
app:iconGravity="textStart"
|
||||||
|
app:iconTint="?themePrimary" />
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:layout_weight="1" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_highlight_info"
|
||||||
|
android:layout_width="64dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="@string/info"
|
||||||
|
android:textColor="?textSecondary"
|
||||||
|
android:textSize="12sp"
|
||||||
|
app:drawableTint="?buttonBackground"
|
||||||
|
app:drawableTopCompat="@drawable/ic_outline_info_24" />
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:layout_weight="1" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/linear_my_list"
|
||||||
|
android:layout_width="match_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:id="@+id/linear_new_episodes"
|
||||||
|
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
|
||||||
|
android:id="@+id/linear_new_simulcasts"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingBottom="7dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_new_simulcasts"
|
||||||
|
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_simulcasts"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/recycler_new_simulcasts"
|
||||||
|
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:id="@+id/linear_new_titles"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingBottom="7dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_new_titles"
|
||||||
|
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_titles"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/recycler_new_titles"
|
||||||
|
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:id="@+id/linear_top_ten"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingBottom="7dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_top_ten"
|
||||||
|
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/top_ten"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/recycler_top_ten"
|
||||||
|
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.activity.main.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>
|
@ -1,127 +1,205 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<RelativeLayout 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: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.activity.main.fragments.MediaFragment">
|
||||||
|
|
||||||
<androidx.core.widget.NestedScrollView
|
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent">
|
||||||
android:fillViewport="true">
|
|
||||||
|
|
||||||
<LinearLayout
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
|
android:id="@+id/app_layout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical">
|
android:background="?themePrimary">
|
||||||
|
|
||||||
<FrameLayout
|
<LinearLayout
|
||||||
|
android:id="@+id/linear_media"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
app:layout_scrollFlags="scroll">
|
||||||
|
|
||||||
<ImageView
|
<RelativeLayout
|
||||||
android:id="@+id/image_backdrop"
|
android:layout_width="match_parent"
|
||||||
|
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:contentDescription="@string/media_poster_backdrop_desc"
|
||||||
|
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_centerInParent="true"
|
||||||
|
app:shapeAppearance="@style/ShapeAppearance.Teapod.RoundedPoster"
|
||||||
|
tools:src="@drawable/ic_launcher_background" />
|
||||||
|
|
||||||
|
</RelativeLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/linear_media_info"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:adjustViewBounds="false"
|
android:layout_marginTop="10dp"
|
||||||
android:maxHeight="231dp"
|
android:gravity="center"
|
||||||
android:minHeight="220dp"
|
android:orientation="horizontal">
|
||||||
android:scaleType="centerCrop" />
|
|
||||||
|
|
||||||
<ImageView
|
<TextView
|
||||||
android:id="@+id/image_poster"
|
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:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="7dp"
|
||||||
|
android:layout_marginTop="6dp"
|
||||||
|
android:layout_marginEnd="7dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="@string/button_play"
|
||||||
|
android:textAllCaps="false"
|
||||||
|
android:textColor="?themePrimary"
|
||||||
|
android:textSize="16sp"
|
||||||
|
app:backgroundTint="?buttonBackground"
|
||||||
|
app:icon="@drawable/ic_baseline_play_arrow_24"
|
||||||
|
app:iconGravity="textStart"
|
||||||
|
app:iconTint="?themePrimary" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_title"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
android:minHeight="200dp"
|
android:layout_marginStart="7dp"
|
||||||
android:src="@drawable/ic_launcher_background" />
|
android:layout_marginTop="12dp"
|
||||||
|
android:layout_marginEnd="7dp"
|
||||||
</FrameLayout>
|
android:text="@string/text_title_ex"
|
||||||
|
android:textStyle="bold" />
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/linear_media_info"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="10dp"
|
|
||||||
android:gravity="center"
|
|
||||||
android:orientation="horizontal">
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/text_year"
|
android:id="@+id/text_overview"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:padding="2dp"
|
android:layout_gravity="center"
|
||||||
android:text="@string/text_year_ex" />
|
android:layout_marginStart="12dp"
|
||||||
|
android:layout_marginTop="7dp"
|
||||||
|
android:layout_marginEnd="12dp"
|
||||||
|
android:text="@string/text_overview_ex" />
|
||||||
|
|
||||||
<TextView
|
<LinearLayout
|
||||||
android:id="@+id/text_age"
|
android:id="@+id/linear_actions"
|
||||||
android:layout_width="wrap_content"
|
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="36dp"
|
||||||
|
android:layout_height="36dp"
|
||||||
|
android:contentDescription="@string/my_list"
|
||||||
|
android:padding="5dp"
|
||||||
|
android:src="@drawable/ic_baseline_add_24"
|
||||||
|
app:tint="?buttonBackground" />
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<com.google.android.material.tabs.TabLayout
|
||||||
|
android:id="@+id/tab_episodes_similar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="7dp"
|
android:layout_marginStart="7dp"
|
||||||
android:background="@drawable/shape_rounden_corner"
|
android:layout_marginTop="12dp"
|
||||||
android:paddingStart="3dp"
|
android:layout_marginEnd="7dp"
|
||||||
android:paddingTop="2dp"
|
android:background="@android:color/transparent"
|
||||||
android:paddingEnd="3dp"
|
app:tabGravity="start"
|
||||||
android:paddingBottom="2dp"
|
app:tabMode="scrollable"
|
||||||
android:text="@string/text_age_ex" />
|
app:tabSelectedTextColor="?textPrimary"
|
||||||
|
app:tabTextColor="?textSecondary" />
|
||||||
|
|
||||||
<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>
|
</LinearLayout>
|
||||||
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<androidx.viewpager2.widget.ViewPager2
|
||||||
android:id="@+id/button_play"
|
android:id="@+id/pager_episodes_similar"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="match_parent"
|
||||||
android:layout_marginStart="7dp"
|
app:layout_anchor="@id/app_layout"
|
||||||
android:layout_marginTop="6dp"
|
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||||
android:layout_marginEnd="7dp"
|
android:layout_gravity="bottom"/>
|
||||||
android:gravity="center"
|
|
||||||
android:text="@string/button_play"
|
|
||||||
android:textAllCaps="false"
|
|
||||||
android:textColor="@android:color/primary_text_dark"
|
|
||||||
android:textSize="16sp"
|
|
||||||
app:backgroundTint="#4A4141"
|
|
||||||
app:icon="@drawable/ic_baseline_play_arrow_24"
|
|
||||||
app:iconGravity="textStart" />
|
|
||||||
|
|
||||||
<TextView
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
android:id="@+id/text_title"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
android:layout_marginStart="7dp"
|
|
||||||
android:layout_marginTop="12dp"
|
|
||||||
android:layout_marginEnd="7dp"
|
|
||||||
android:text="@string/text_title_ex"
|
|
||||||
android:textStyle="bold" />
|
|
||||||
|
|
||||||
<TextView
|
<FrameLayout
|
||||||
android:id="@+id/text_overview"
|
android:id="@+id/frame_loading"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="match_parent"
|
||||||
android:layout_gravity="center"
|
android:background="?themePrimary"
|
||||||
android:layout_marginStart="12dp"
|
android:visibility="gone">
|
||||||
android:layout_marginTop="7dp"
|
|
||||||
android:layout_marginEnd="12dp"
|
|
||||||
android:text="@string/text_overview_ex" />
|
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<com.google.android.material.progressindicator.CircularProgressIndicator
|
||||||
android:id="@+id/recycler_episodes"
|
android:id="@+id/loadingIndicator"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="70dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="70dp"
|
||||||
android:layout_marginStart="7dp"
|
android:layout_gravity="center"
|
||||||
android:layout_marginTop="17dp"
|
android:indeterminate="true"
|
||||||
android:layout_marginEnd="7dp"
|
app:indicatorColor="?colorPrimary"
|
||||||
tools:layout_editor_absoluteY="298dp" />
|
tools:visibility="visible" />
|
||||||
</LinearLayout>
|
</FrameLayout>
|
||||||
</androidx.core.widget.NestedScrollView>
|
|
||||||
|
|
||||||
</FrameLayout>
|
</RelativeLayout>
|
19
app/src/main/res/layout/fragment_media_episodes.xml
Normal file
19
app/src/main/res/layout/fragment_media_episodes.xml
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<?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"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/recycler_episodes"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:paddingStart="7dp"
|
||||||
|
android:paddingEnd="7dp"
|
||||||
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||||
|
tools:layout_editor_absoluteY="298dp"
|
||||||
|
tools:listitem="@layout/item_episode" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
22
app/src/main/res/layout/fragment_media_similar.xml
Normal file
22
app/src/main/res/layout/fragment_media_similar.xml
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<?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"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/recycler_media_similar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingStart="3dp"
|
||||||
|
android:paddingTop="6dp"
|
||||||
|
android:paddingEnd="3dp"
|
||||||
|
android:paddingBottom="3dp"
|
||||||
|
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
|
||||||
|
app:spanCount="2"
|
||||||
|
tools:listitem="@layout/item_media" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
90
app/src/main/res/layout/fragment_on_login.xml
Normal file
90
app/src/main/res/layout/fragment_on_login.xml
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout 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="?themePrimary">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/image_login"
|
||||||
|
android:layout_width="128dp"
|
||||||
|
android:layout_height="128dp"
|
||||||
|
android:contentDescription="@string/app_name"
|
||||||
|
android:scaleType="fitCenter"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintHorizontal_bias="0.5"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:srcCompat="@drawable/ic_launcher_foreground"
|
||||||
|
app:tint="?buttonBackground" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/linear_login"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:gravity="center_horizontal"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="10dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/image_login">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_login_heading"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/on_login_heading"
|
||||||
|
android:textAlignment="center"
|
||||||
|
android:textSize="26sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_login_desc"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="7dp"
|
||||||
|
android:text="@string/on_login_desc"
|
||||||
|
android:textAlignment="center"
|
||||||
|
android:textSize="18sp" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/edit_text_login"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="7dp"
|
||||||
|
android:ems="10"
|
||||||
|
android:hint="@string/login"
|
||||||
|
android:importantForAutofill="no"
|
||||||
|
android:inputType="textEmailAddress" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/edit_text_password"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="7dp"
|
||||||
|
android:ems="10"
|
||||||
|
android:hint="@string/password"
|
||||||
|
android:importantForAutofill="no"
|
||||||
|
android:inputType="textPassword" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/button_login"
|
||||||
|
style="@style/Widget.AppCompat.Button.Colored"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:layout_marginStart="7dp"
|
||||||
|
android:layout_marginTop="7dp"
|
||||||
|
android:layout_marginEnd="7dp"
|
||||||
|
android:text="@string/login"
|
||||||
|
android:textAllCaps="false"
|
||||||
|
android:textColor="#FFFFFFFF"
|
||||||
|
android:textStyle="bold"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
74
app/src/main/res/layout/fragment_on_welcome.xml
Normal file
74
app/src/main/res/layout/fragment_on_welcome.xml
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
<?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:orientation="vertical"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="?themePrimary">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/image_logo"
|
||||||
|
android:layout_width="128dp"
|
||||||
|
android:layout_height="128dp"
|
||||||
|
android:contentDescription="@string/app_name"
|
||||||
|
android:scaleType="fitCenter"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintHorizontal_bias="0.5"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:srcCompat="@drawable/ic_launcher_foreground"
|
||||||
|
app:tint="?buttonBackground" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/linearLayout3"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="10dp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/image_logo">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_app_name"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/app_name"
|
||||||
|
android:textAlignment="center"
|
||||||
|
android:textSize="26sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_welcome"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="7dp"
|
||||||
|
android:text="@string/on_welcome"
|
||||||
|
android:textAlignment="center"
|
||||||
|
android:textSize="18sp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/button_get_started"
|
||||||
|
style="@style/Widget.AppCompat.Button.Colored"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:layout_marginBottom="40dp"
|
||||||
|
android:text="@string/on_get_started"
|
||||||
|
android:textAllCaps="false"
|
||||||
|
android:textColor="#FFFFFFFF"
|
||||||
|
android:textStyle="bold"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
@ -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.activity.main.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>
|
25
app/src/main/res/layout/item_component.xml
Normal file
25
app/src/main/res/layout/item_component.xml
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:id="@+id/linear_component"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?android:selectableItemBackground"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingStart="12dp"
|
||||||
|
android:paddingTop="7dp"
|
||||||
|
android:paddingEnd="12dp"
|
||||||
|
android:paddingBottom="7dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_component_title"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/app_name"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_component_desc"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/third_party_component_desc" />
|
||||||
|
</LinearLayout>
|
@ -14,12 +14,28 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="horizontal">
|
android:orientation="horizontal">
|
||||||
|
|
||||||
<ImageView
|
<FrameLayout
|
||||||
android:id="@+id/image_episode"
|
android:layout_width="wrap_content"
|
||||||
android:layout_width="128dp"
|
android:layout_height="wrap_content">
|
||||||
android:layout_height="72dp"
|
|
||||||
android:contentDescription="@string/component_poster_desc"
|
<com.google.android.material.imageview.ShapeableImageView
|
||||||
app:srcCompat="@drawable/ic_baseline_account_box_24" />
|
android:id="@+id/image_episode"
|
||||||
|
android:layout_width="128dp"
|
||||||
|
android:layout_height="72dp"
|
||||||
|
android:contentDescription="@string/component_poster_desc"
|
||||||
|
app:shapeAppearance="@style/ShapeAppearance.Teapod.RoundedPoster"
|
||||||
|
app:srcCompat="@color/md_disabled_text_dark_theme" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/image_episode_play"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:background="@drawable/bg_circle__black_transparent_24dp"
|
||||||
|
android:contentDescription="@string/button_play"
|
||||||
|
app:srcCompat="@drawable/ic_baseline_play_arrow_24"
|
||||||
|
app:tint="#FFFFFF" />
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/text_episode_title"
|
android:id="@+id/text_episode_title"
|
||||||
@ -28,6 +44,7 @@
|
|||||||
android:layout_marginStart="7dp"
|
android:layout_marginStart="7dp"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:text="@string/component_episode_title"
|
android:text="@string/component_episode_title"
|
||||||
|
android:textColor="?textPrimary"
|
||||||
android:textSize="16sp" />
|
android:textSize="16sp" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
@ -36,13 +53,15 @@
|
|||||||
android:layout_height="30dp"
|
android:layout_height="30dp"
|
||||||
android:layout_margin="2dp"
|
android:layout_margin="2dp"
|
||||||
android:contentDescription="@string/component_watched_desc"
|
android:contentDescription="@string/component_watched_desc"
|
||||||
app:srcCompat="@drawable/ic_baseline_check_circle_24" />
|
app:srcCompat="@drawable/ic_baseline_check_circle_24"
|
||||||
|
app:tint="?iconColor" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/text_episode_desc"
|
android:id="@+id/text_episode_desc"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:maxLines="2"
|
android:ellipsize="end"
|
||||||
android:ellipsize="end"/>
|
android:maxLines="3"
|
||||||
|
android:textColor="?textSecondary" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
57
app/src/main/res/layout/item_episode_player.xml
Normal file
57
app/src/main/res/layout/item_episode_player.xml
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
<?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="wrap_content"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="7dp">
|
||||||
|
|
||||||
|
<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="192dp"
|
||||||
|
android:layout_height="108dp"
|
||||||
|
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_title2"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="7dp"
|
||||||
|
android:text="@string/component_episode_title"
|
||||||
|
android:textColor="@color/textPrimaryDark"
|
||||||
|
android:textSize="16sp" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/divider"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:layout_marginTop="5dp"
|
||||||
|
android:layout_marginBottom="5dp"
|
||||||
|
android:background="@color/textSecondaryDark" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_episode_desc2"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginTop="5dp"
|
||||||
|
android:text="@string/text_overview_ex"
|
||||||
|
android:textColor="@color/textPrimaryDark"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
43
app/src/main/res/layout/item_media.xml
Normal file
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>
|
|
162
app/src/main/res/layout/player_controls.xml
Normal file
162
app/src/main/res/layout/player_controls.xml
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
<?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"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
android:scaleType="fitXY"
|
||||||
|
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"
|
||||||
|
android:layout_marginBottom="2dp"
|
||||||
|
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"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="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_language"
|
||||||
|
style="@style/Widget.AppCompat.Button.Borderless"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="7dp"
|
||||||
|
android:text="@string/language"
|
||||||
|
android:textAllCaps="false"
|
||||||
|
app:icon="@drawable/ic_baseline_subtitles_24"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/button_episodes"
|
||||||
|
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"
|
||||||
|
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" />
|
||||||
|
|
||||||
|
<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" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
</FrameLayout>
|
46
app/src/main/res/layout/player_episodes_list.xml
Normal file
46
app/src/main/res/layout/player_episodes_list.xml
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout 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="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="#73000000"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/linearLayout3"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:layout_marginTop="7dp"
|
||||||
|
android:layout_marginEnd="12dp"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/button_close_episodes_list"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
android:scaleType="fitXY"
|
||||||
|
android:layout_width="44dp"
|
||||||
|
android:layout_height="44dp"
|
||||||
|
android:contentDescription="@string/close_player"
|
||||||
|
android:padding="10dp"
|
||||||
|
app:srcCompat="@drawable/ic_baseline_arrow_back_24" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/recycler_episodes_player"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/linearLayout3"
|
||||||
|
tools:listitem="@layout/item_episode_player" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
99
app/src/main/res/layout/player_language_settings.xml
Normal file
99
app/src/main/res/layout/player_language_settings.xml
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout 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"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/linear_top"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:layout_marginTop="7dp"
|
||||||
|
android:layout_marginEnd="12dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/button_close_language_settings"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
android:scaleType="fitXY"
|
||||||
|
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_language"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="44dp"
|
||||||
|
android:text="@string/language"
|
||||||
|
android:textAlignment="center"
|
||||||
|
android:textColor="@color/exo_white"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/linear_languages"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_marginStart="56dp"
|
||||||
|
android:layout_marginEnd="56dp"
|
||||||
|
android:orientation="vertical"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/linear_bottom"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/linear_top" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/linear_bottom"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:layout_marginEnd="12dp"
|
||||||
|
android:layout_marginBottom="7dp"
|
||||||
|
android:gravity="end"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent">
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/button_cancel"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="7dp"
|
||||||
|
android:text="@string/cancel"
|
||||||
|
android:textAllCaps="false"
|
||||||
|
android:textColor="@color/exo_white"
|
||||||
|
android:textSize="16sp"
|
||||||
|
app:backgroundTint="@color/buttonBackgroundLight"
|
||||||
|
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_select"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/apply"
|
||||||
|
android:textAllCaps="false"
|
||||||
|
android:textColor="@color/themePrimaryDark"
|
||||||
|
android:textSize="16sp"
|
||||||
|
app:backgroundTint="@color/buttonBackgroundDark"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -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>
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user