Compare commits
110 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
e8ab11d5ff
|
|||
|
0bb433b5cb
|
|||
|
b05ecf64a6
|
|||
|
7a2f3ad265
|
|||
|
4f2bd4fd59
|
|||
|
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
|
45
README.md
45
README.md
@ -1,41 +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
|
||||
* acces all media in the library
|
||||
* search the library
|
||||
* play movies/tv shows via integrated exoplayer
|
||||
* add movies/tv shows to you list, for easier access
|
||||
|
||||
### Missing Features
|
||||
* a alternative/secondary stream is currently not supported (for dub titles the subtitle version is missing)
|
||||
* Watch all animes from AoD on your Android device
|
||||
* Native Player based on ExoPayer
|
||||
* Prefer the OmU version via the app settings
|
||||
* Save your favorite animes to "My List"
|
||||
|
||||
## Screenshots
|
||||
[<img src="https://www.mosad.xyz/images/Teapod/Teapod_Home_200px.png" width=180>](https://www.mosad.xyz/images/Teapod/Teapod_Home.png)
|
||||
[<img src="https://www.mosad.xyz/images/Teapod/Teapod_Library_200px.png" width=180>](https://www.mosad.xyz/images/Teapod/Teapod_Library.png)
|
||||
[<img src="https://www.mosad.xyz/images/Teapod/Teapod_Media_200px.png" width=180>](https://www.mosad.xyz/images/Teapod/Teapod_Media.png)
|
||||
[<img src="https://www.mosad.xyz/images/Teapod/Teapod_Search_200px.png" width=180>](https://www.mosad.xyz/images/Teapod/Teapod_Search.png)
|
||||
[<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_Library.webp" width=180>](https://www.mosad.xyz/images/Teapod/Teapod_Library.webp)
|
||||
[<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
|
||||
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.
|
||||
|
||||
### Known Issues
|
||||
If a tv show is selected, the first episode will be marked as already watched. This is due to the difficulties of parsing. The Parser is designed to be as easy to maintain and fail safe as possible.
|
||||
### Contributing
|
||||
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.
|
||||
|
||||
### Used Libraries
|
||||
* AndroidX: https://developer.android.com/jetpack/androidx
|
||||
* Material Components for Android: https://github.com/material-components/material-components-android
|
||||
* ExoPlayer: https://github.com/google/ExoPlayer
|
||||
* Gson: https://github.com/google/gson
|
||||
* Material design icons: https://github.com/google/material-design-icons
|
||||
* Material Dialogs: https://github.com/afollestad/material-dialogs
|
||||
* Jsoup: https://jsoup.org
|
||||
* kotlinx.coroutines: https://github.com/Kotlin/kotlinx.coroutines
|
||||
* Glide: https://github.com/bumptech/glide
|
||||
* Glide Transformations: https://github.com/wasabeef/glide-transformations
|
||||
### [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 [@Seil0](https://git.mosad.xyz/Seil0)
|
||||
Teapod © 2020-2021 [@Seil0](https://git.mosad.xyz/Seil0)
|
||||
|
||||
@ -4,20 +4,24 @@ apply plugin: 'kotlin-android-extensions'
|
||||
|
||||
android {
|
||||
compileSdkVersion 30
|
||||
buildToolsVersion "30.0.2"
|
||||
buildToolsVersion "30.0.3"
|
||||
|
||||
defaultConfig {
|
||||
applicationId "org.mosad.teapod"
|
||||
minSdkVersion 23
|
||||
targetSdkVersion 30
|
||||
versionCode 1000 //00.01.0000
|
||||
versionName "0.1.0"
|
||||
versionCode 4100 //00.04.100
|
||||
versionName "0.4.1"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
resValue "string", "build_time", buildTime()
|
||||
setProperty("archivesBaseName", "teapod-$versionName")
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled true
|
||||
@ -37,31 +41,30 @@ android {
|
||||
dependencies {
|
||||
implementation fileTree(dir: "libs", include: ["*.jar"])
|
||||
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.4.3'
|
||||
|
||||
implementation 'androidx.core:core-ktx:1.3.2'
|
||||
implementation 'androidx.appcompat:appcompat:1.2.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.1'
|
||||
implementation 'androidx.navigation:navigation-ui-ktx:2.3.1'
|
||||
implementation 'androidx.security:security-crypto:1.1.0-alpha02'
|
||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.4'
|
||||
implementation 'androidx.navigation:navigation-ui-ktx:2.3.4'
|
||||
implementation 'androidx.security:security-crypto:1.1.0-alpha03'
|
||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||
|
||||
implementation 'com.google.android.material:material:1.3.0-alpha03'
|
||||
implementation 'com.google.android.material:material:1.3.0'
|
||||
implementation 'com.google.code.gson:gson:2.8.6'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-core:2.12.1'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-hls:2.12.1'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-dash:2.12.1'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-ui:2.12.1'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-core:2.13.2'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-hls:2.13.2'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-dash:2.13.2'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-ui:2.13.2'
|
||||
|
||||
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 'com.afollestad.material-dialogs:core: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.1'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="org.mosad.teapod">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
@ -10,9 +11,9 @@
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme">
|
||||
android:theme="@style/AppTheme.Dark">
|
||||
<activity
|
||||
android:name=".SplashActivity"
|
||||
android:name="org.mosad.teapod.ui.activity.SplashActivity"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/SplashTheme"
|
||||
android:screenOrientation="portrait">
|
||||
@ -22,15 +23,28 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".PlayerActivity"
|
||||
android:name="org.mosad.teapod.ui.activity.onboarding.OnboardingActivity"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/PlayerTheme"
|
||||
android:configChanges="orientation|screenSize|layoutDirection" />
|
||||
android:screenOrientation="portrait"
|
||||
android:launchMode="singleTop"
|
||||
android:windowSoftInputMode="adjustPan">
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
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>
|
||||
|
||||
</manifest>
|
||||
@ -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,7 +1,7 @@
|
||||
/**
|
||||
* Teapod
|
||||
*
|
||||
* Copyright 2020 <seil0@mosad.xyz>
|
||||
* 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
|
||||
@ -31,7 +31,9 @@ import org.mosad.teapod.preferences.EncryptedPreferences
|
||||
import org.mosad.teapod.util.*
|
||||
import org.mosad.teapod.util.DataTypes.MediaType
|
||||
import java.io.IOException
|
||||
import java.lang.NumberFormatException
|
||||
import java.util.*
|
||||
import kotlin.random.Random
|
||||
|
||||
object AoDParser {
|
||||
|
||||
@ -39,19 +41,23 @@ object AoDParser {
|
||||
private const val loginPath = "/users/sign_in"
|
||||
private const val libraryPath = "/animes"
|
||||
|
||||
private const val userAgent = "Mozilla/5.0 (X11; Linux x86_64; rv:80.0) Gecko/20100101 Firefox/80.0"
|
||||
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 csrfToken: String = ""
|
||||
private var loginSuccess = false
|
||||
|
||||
private val mediaList = arrayListOf<Media>()
|
||||
val itemMediaList = arrayListOf<ItemMedia>()
|
||||
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 {
|
||||
|
||||
withContext(Dispatchers.Default) {
|
||||
withContext(Dispatchers.IO) {
|
||||
// get the authenticity token
|
||||
val resAuth = Jsoup.connect(baseUrl + loginPath)
|
||||
.header("User-Agent", userAgent)
|
||||
@ -73,6 +79,7 @@ object AoDParser {
|
||||
|
||||
val resLogin = Jsoup.connect(baseUrl + loginPath)
|
||||
.method(Connection.Method.POST)
|
||||
.timeout(60000) // login can take some time default is 60000 (60 sec)
|
||||
.data(data)
|
||||
.postDataCharset("UTF-8")
|
||||
.cookies(authCookies)
|
||||
@ -90,37 +97,36 @@ object AoDParser {
|
||||
|
||||
/**
|
||||
* initially load all media and home screen data
|
||||
* -> blocking
|
||||
*/
|
||||
fun initialLoading() = runBlocking {
|
||||
val newEPJob = GlobalScope.async {
|
||||
listNewEpisodes()
|
||||
}
|
||||
|
||||
val listJob = GlobalScope.async {
|
||||
fun initialLoading() = listOf(
|
||||
loadHome(),
|
||||
listAnimes()
|
||||
}
|
||||
|
||||
newEPJob.await()
|
||||
listJob.await()
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* get a media by it's ID (int)
|
||||
* @return Media
|
||||
*/
|
||||
fun getMediaById(mediaId: Int): Media {
|
||||
suspend fun getMediaById(mediaId: Int): Media {
|
||||
val media = mediaList.first { it.id == mediaId }
|
||||
|
||||
if (media.episodes.isEmpty()) {
|
||||
loadStreams(media)
|
||||
loadStreams(media).join()
|
||||
}
|
||||
|
||||
return media
|
||||
}
|
||||
|
||||
fun markAsWatched(mediaId: Int, episodeId: Int) = GlobalScope.launch {
|
||||
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
|
||||
fun sendCallback(callbackPath: String) = GlobalScope.launch {
|
||||
private fun sendCallback(callbackPath: String) = GlobalScope.launch(Dispatchers.IO) {
|
||||
val headers = mutableMapOf(
|
||||
Pair("Accept", "application/json, text/javascript, */*; q=0.01"),
|
||||
Pair("Accept-Language", "de,en-US;q=0.7,en;q=0.3"),
|
||||
@ -130,13 +136,11 @@ object AoDParser {
|
||||
)
|
||||
|
||||
try {
|
||||
withContext(Dispatchers.IO) {
|
||||
Jsoup.connect(baseUrl + callbackPath)
|
||||
.ignoreContentType(true)
|
||||
.cookies(sessionCookies)
|
||||
.headers(headers)
|
||||
.execute()
|
||||
}
|
||||
Jsoup.connect(baseUrl + callbackPath)
|
||||
.ignoreContentType(true)
|
||||
.cookies(sessionCookies)
|
||||
.headers(headers)
|
||||
.execute()
|
||||
} catch (ex: IOException) {
|
||||
Log.e(javaClass.name, "Callback for $callbackPath failed.", ex)
|
||||
}
|
||||
@ -146,65 +150,113 @@ object AoDParser {
|
||||
/**
|
||||
* load all media from aod into itemMediaList and mediaList
|
||||
*/
|
||||
private fun listAnimes() = runBlocking {
|
||||
if (sessionCookies.isEmpty()) login()
|
||||
private fun listAnimes() = GlobalScope.launch(Dispatchers.IO) {
|
||||
val resAnimes = Jsoup.connect(baseUrl + libraryPath).get()
|
||||
//println(resAnimes)
|
||||
|
||||
withContext(Dispatchers.Default) {
|
||||
val resAnimes = Jsoup.connect(baseUrl + libraryPath)
|
||||
.cookies(sessionCookies)
|
||||
.get()
|
||||
|
||||
//println(resAnimes)
|
||||
|
||||
itemMediaList.clear()
|
||||
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 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
|
||||
})
|
||||
itemMediaList.clear()
|
||||
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 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()
|
||||
|
||||
Log.i(javaClass.name, "Total library size is: ${mediaList.size}")
|
||||
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 all new episodes from AoD into newEpisodesList
|
||||
* load new episodes, titles and highlights
|
||||
*/
|
||||
private fun listNewEpisodes() = runBlocking {
|
||||
if (sessionCookies.isEmpty()) login()
|
||||
private fun loadHome() = GlobalScope.launch(Dispatchers.IO) {
|
||||
val resHome = Jsoup.connect(baseUrl).get()
|
||||
|
||||
withContext(Dispatchers.Default) {
|
||||
val resHome = Jsoup.connect(baseUrl)
|
||||
.cookies(sessionCookies)
|
||||
.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")
|
||||
|
||||
newEpisodesList.clear()
|
||||
resHome.select("div.jcarousel-container-new").select("li").forEach {
|
||||
if (it.select("span").hasClass("neweps")) {
|
||||
val mediaId = it.select("a.thumbs").attr("href")
|
||||
.substringAfterLast("/").toInt()
|
||||
val mediaImage = it.select("a.thumbs > img").attr("src")
|
||||
val mediaTitle = "${it.select("a").text()} - ${it.select("span.neweps").text()}"
|
||||
|
||||
newEpisodesList.add(ItemMedia(mediaId, mediaTitle, mediaImage))
|
||||
}
|
||||
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,"", ""))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -212,103 +264,98 @@ object AoDParser {
|
||||
* load streams for the media path, movies have one episode
|
||||
* @param media is used as call ba reference
|
||||
*/
|
||||
private fun loadStreams(media: Media) = runBlocking {
|
||||
private fun loadStreams(media: Media) = GlobalScope.launch(Dispatchers.IO) {
|
||||
if (sessionCookies.isEmpty()) login()
|
||||
|
||||
if (!loginSuccess) {
|
||||
Log.w(javaClass.name, "Login, was not successful.")
|
||||
return@runBlocking
|
||||
return@launch
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Default) {
|
||||
// get the media page
|
||||
val res = Jsoup.connect(baseUrl + media.link)
|
||||
.cookies(sessionCookies)
|
||||
.get()
|
||||
|
||||
// get the media page
|
||||
val res = Jsoup.connect(baseUrl + media.link)
|
||||
.cookies(sessionCookies)
|
||||
.get()
|
||||
//println(res)
|
||||
|
||||
//println(res)
|
||||
if (csrfToken.isEmpty()) {
|
||||
csrfToken = res.select("meta[name=csrf-token]").attr("content")
|
||||
//Log.i(javaClass.name, "New csrf token is $csrfToken")
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
val pl = res.select("input.streamstarter_html5").first()
|
||||
val primary = pl.attr("data-playlist")
|
||||
val secondary = pl.attr("data-otherplaylist")
|
||||
val secondaryIsOmU = secondary.contains("OmU", true)
|
||||
|
||||
// load primary and secondary playlist
|
||||
val primaryPlaylist = parsePlaylistAsync(primary)
|
||||
val secondaryPlaylist = parsePlaylistAsync(secondary)
|
||||
|
||||
primaryPlaylist.await().playlist.forEach { ep ->
|
||||
val epNumber = if (media.type == MediaType.TVSHOW) {
|
||||
ep.title.substringAfter(", Ep. ").toInt()
|
||||
} else {
|
||||
0
|
||||
}
|
||||
|
||||
media.episodes.add(
|
||||
Episode(
|
||||
id = ep.mediaid,
|
||||
priStreamUrl = ep.sources.first().file,
|
||||
posterUrl = ep.image,
|
||||
title = ep.title,
|
||||
description = ep.description,
|
||||
number = epNumber
|
||||
)
|
||||
)
|
||||
}
|
||||
Log.i(javaClass.name, "Loading primary playlist finished")
|
||||
|
||||
secondaryPlaylist.await().playlist.forEach { ep ->
|
||||
val episode = media.episodes.firstOrNull { it.id == ep.mediaid }
|
||||
|
||||
if (episode != null) {
|
||||
episode.secStreamUrl = ep.sources.first().file
|
||||
episode.secStreamOmU = secondaryIsOmU
|
||||
} else {
|
||||
val epNumber = if (media.type == MediaType.TVSHOW) {
|
||||
ep.title.substringAfter(", Ep. ").toInt()
|
||||
aod.playlist.forEach { ep ->
|
||||
try {
|
||||
if (media.hasEpisode(ep.mediaid)) {
|
||||
media.getEpisodeById(ep.mediaid).streams.add(
|
||||
Stream(ep.sources.first().file, locale)
|
||||
)
|
||||
} else {
|
||||
0
|
||||
}
|
||||
|
||||
media.episodes.add(
|
||||
Episode(
|
||||
media.episodes.add(Episode(
|
||||
id = ep.mediaid,
|
||||
secStreamUrl = ep.sources.first().file,
|
||||
secStreamOmU = secondaryIsOmU,
|
||||
streams = mutableListOf(Stream(ep.sources.first().file, locale)),
|
||||
posterUrl = ep.image,
|
||||
title = ep.title,
|
||||
description = ep.description,
|
||||
number = epNumber
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
Log.i(javaClass.name, "Loading secondary plalyist finished")
|
||||
|
||||
// parse additional info from the media page
|
||||
res.select("table.vertical-table").select("tr").forEach { row ->
|
||||
when (row.select("th").text().toLowerCase(Locale.ROOT)) {
|
||||
"produktionsjahr" -> media.info.year = row.select("td").text().toInt()
|
||||
"fsk" -> media.info.age = row.select("td").text().toInt()
|
||||
"episodenanzahl" -> {
|
||||
media.info.episodesCount = row.select("td").text()
|
||||
.substringBefore("/")
|
||||
.filter{ it.isDigit() }
|
||||
.toInt()
|
||||
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")
|
||||
|
||||
// parse additional information for tv shows
|
||||
if (media.type == MediaType.TVSHOW) {
|
||||
res.select("div.three-box-container > div.episodebox").forEach { episodebox ->
|
||||
// additional info from the media page
|
||||
res.select("table.vertical-table").select("tr").forEach { row ->
|
||||
when (row.select("th").text().toLowerCase(Locale.ROOT)) {
|
||||
"produktionsjahr" -> media.info.year = row.select("td").text().toInt()
|
||||
"fsk" -> media.info.age = row.select("td").text().toInt()
|
||||
"episodenanzahl" -> {
|
||||
media.info.episodesCount = row.select("td").text()
|
||||
.substringBefore("/")
|
||||
.filter { it.isDigit() }
|
||||
.toInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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")
|
||||
@ -321,19 +368,19 @@ object AoDParser {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
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): Deferred<AoDObject> {
|
||||
private fun parsePlaylistAsync(playlistPath: String, language: String): Deferred<AoDObject> {
|
||||
if (playlistPath == "[]") {
|
||||
return CompletableDeferred(AoDObject(listOf()))
|
||||
return CompletableDeferred(AoDObject(listOf(), language))
|
||||
}
|
||||
|
||||
return GlobalScope.async {
|
||||
return GlobalScope.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"),
|
||||
@ -348,6 +395,7 @@ object AoDParser {
|
||||
.ignoreContentType(true)
|
||||
.cookies(sessionCookies)
|
||||
.headers(headers)
|
||||
.timeout(120000) // loading the playlist can take some time
|
||||
.execute()
|
||||
|
||||
//Gson().fromJson(res.body(), AoDObject::class.java)
|
||||
@ -363,7 +411,27 @@ object AoDParser {
|
||||
description = it.asJsonObject.get("description").asString,
|
||||
mediaid = it.asJsonObject.get("mediaid").asInt
|
||||
)
|
||||
})
|
||||
},
|
||||
language
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
private fun getNumberFromTitle(title: String, type: MediaType): Int {
|
||||
return if (type == MediaType.TVSHOW) {
|
||||
try {
|
||||
title.substringAfter(", Ep. ").toInt()
|
||||
} catch (nex: NumberFormatException) {
|
||||
0
|
||||
}
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,20 +1,28 @@
|
||||
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 {
|
||||
|
||||
var preferSecondary = false
|
||||
internal set
|
||||
var autoplay = true
|
||||
internal set
|
||||
var theme = DataTypes.Theme.DARK
|
||||
internal set
|
||||
|
||||
fun savePreferSecondary(context: Context, preferSecondary: Boolean) {
|
||||
val sharedPref = context.getSharedPreferences(
|
||||
private fun getSharedPref(context: Context): SharedPreferences {
|
||||
return context.getSharedPreferences(
|
||||
context.getString(R.string.preference_file_key),
|
||||
Context.MODE_PRIVATE
|
||||
)
|
||||
}
|
||||
|
||||
with(sharedPref.edit()) {
|
||||
fun savePreferSecondary(context: Context, preferSecondary: Boolean) {
|
||||
with(getSharedPref(context).edit()) {
|
||||
putBoolean(context.getString(R.string.save_key_prefer_secondary), preferSecondary)
|
||||
apply()
|
||||
}
|
||||
@ -22,18 +30,41 @@ object Preferences {
|
||||
this.preferSecondary = preferSecondary
|
||||
}
|
||||
|
||||
fun saveAutoplay(context: Context, autoplay: Boolean) {
|
||||
with(getSharedPref(context).edit()) {
|
||||
putBoolean(context.getString(R.string.save_key_autoplay), autoplay)
|
||||
apply()
|
||||
}
|
||||
|
||||
this.autoplay = autoplay
|
||||
}
|
||||
|
||||
fun saveTheme(context: Context, theme: DataTypes.Theme) {
|
||||
with(getSharedPref(context).edit()) {
|
||||
putString(context.getString(R.string.save_key_theme), theme.toString())
|
||||
apply()
|
||||
}
|
||||
|
||||
this.theme = theme
|
||||
}
|
||||
|
||||
/**
|
||||
* initially load the stored values
|
||||
*/
|
||||
fun load(context: Context) {
|
||||
val sharedPref = context.getSharedPreferences(
|
||||
context.getString(R.string.preference_file_key),
|
||||
Context.MODE_PRIVATE
|
||||
)
|
||||
val sharedPref = getSharedPref(context)
|
||||
|
||||
preferSecondary = sharedPref.getBoolean(
|
||||
context.getString(R.string.save_key_prefer_secondary), false
|
||||
)
|
||||
autoplay = sharedPref.getBoolean(
|
||||
context.getString(R.string.save_key_autoplay), true
|
||||
)
|
||||
theme = DataTypes.Theme.valueOf(
|
||||
sharedPref.getString(
|
||||
context.getString(R.string.save_key_theme), DataTypes.Theme.DARK.toString()
|
||||
) ?: DataTypes.Theme.DARK.toString()
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
package org.mosad.teapod
|
||||
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() {
|
||||
@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Teapod
|
||||
*
|
||||
* Copyright 2020 <seil0@mosad.xyz>
|
||||
* 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
|
||||
@ -20,7 +20,7 @@
|
||||
*
|
||||
*/
|
||||
|
||||
package org.mosad.teapod
|
||||
package org.mosad.teapod.ui.activity.main
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
@ -29,29 +29,52 @@ 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.bottomnavigation.BottomNavigationView
|
||||
import kotlinx.android.synthetic.main.activity_main.*
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.joinAll
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.mosad.teapod.R
|
||||
import org.mosad.teapod.databinding.ActivityMainBinding
|
||||
import org.mosad.teapod.parser.AoDParser
|
||||
import org.mosad.teapod.ui.activity.player.PlayerActivity
|
||||
import org.mosad.teapod.preferences.EncryptedPreferences
|
||||
import org.mosad.teapod.preferences.Preferences
|
||||
import org.mosad.teapod.ui.components.LoginDialog
|
||||
import org.mosad.teapod.ui.fragments.*
|
||||
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.util.DataTypes
|
||||
import org.mosad.teapod.util.StorageController
|
||||
import org.mosad.teapod.util.TMDBApiController
|
||||
import org.mosad.teapod.util.exitAndRemoveTask
|
||||
import java.net.SocketTimeoutException
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemSelectedListener {
|
||||
|
||||
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)
|
||||
setContentView(R.layout.activity_main)
|
||||
nav_view.setOnNavigationItemSelectedListener(this)
|
||||
|
||||
load()
|
||||
if (!wasInitialized) { load() }
|
||||
theme.applyStyle(getThemeResource(), true)
|
||||
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
binding.navView.setOnNavigationItemSelectedListener(this)
|
||||
setContentView(binding.root)
|
||||
|
||||
supportFragmentManager.commit {
|
||||
replace(R.id.nav_host_fragment, activeBaseFragment, activeBaseFragment.javaClass.simpleName)
|
||||
@ -63,7 +86,7 @@ class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemS
|
||||
supportFragmentManager.popBackStack()
|
||||
} else {
|
||||
if (activeBaseFragment !is HomeFragment) {
|
||||
nav_view.selectedItemId = R.id.navigation_home
|
||||
binding.navView.selectedItemId = R.id.navigation_home
|
||||
} else {
|
||||
super.onBackPressed()
|
||||
}
|
||||
@ -102,68 +125,59 @@ class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemS
|
||||
return ret
|
||||
}
|
||||
|
||||
private fun load() {
|
||||
// running login and list in parallel does not bring any speed improvements
|
||||
val time = measureTimeMillis {
|
||||
Preferences.load(this)
|
||||
|
||||
// make sure credentials are set
|
||||
EncryptedPreferences.readCredentials(this)
|
||||
if (EncryptedPreferences.password.isEmpty()) {
|
||||
showLoginDialog(true)
|
||||
} else {
|
||||
// try to login in, as most sites can only bee loaded once loged in
|
||||
if (!AoDParser.login()) showLoginDialog(false)
|
||||
}
|
||||
|
||||
StorageController.load(this)
|
||||
AoDParser.initialLoading()
|
||||
private fun getThemeResource(): Int {
|
||||
return when (Preferences.theme) {
|
||||
DataTypes.Theme.LIGHT -> R.style.AppTheme_Light
|
||||
else -> R.style.AppTheme_Dark
|
||||
}
|
||||
Log.i(javaClass.name, "login and list in $time ms")
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the media fragment for the selected media.
|
||||
* While loading show the loading fragment.
|
||||
* The loading and media fragment are not stored in activeBaseFragment,
|
||||
* as the don't replace a fragment but are added on top of one.
|
||||
* initial loading and login are run in parallel, as initial loading doesn't require
|
||||
* any login cookies
|
||||
*/
|
||||
fun showMediaFragment(mediaId: Int) = GlobalScope.launch {
|
||||
val loadingFragment = LoadingFragment()
|
||||
supportFragmentManager.commit {
|
||||
add(R.id.nav_host_fragment, loadingFragment, "MediaFragment")
|
||||
show(loadingFragment)
|
||||
}
|
||||
private fun load() {
|
||||
val time = measureTimeMillis {
|
||||
val loadingJob = AoDParser.initialLoading() // start the initial loading
|
||||
|
||||
// load the streams for the selected media
|
||||
val media = AoDParser.getMediaById(mediaId)
|
||||
val tmdb = TMDBApiController().search(media.info.title, media.type)
|
||||
// load all saved stuff here
|
||||
Preferences.load(this)
|
||||
EncryptedPreferences.readCredentials(this)
|
||||
StorageController.load(this)
|
||||
|
||||
val mediaFragment = MediaFragment(media, tmdb)
|
||||
supportFragmentManager.commit {
|
||||
add(R.id.nav_host_fragment, mediaFragment, "MediaFragment")
|
||||
addToBackStack(null)
|
||||
show(mediaFragment)
|
||||
}
|
||||
// show onbaording
|
||||
if (EncryptedPreferences.password.isEmpty()) {
|
||||
showOnboarding()
|
||||
} else {
|
||||
try {
|
||||
if (!AoDParser.login()) {
|
||||
showLoginDialog()
|
||||
}
|
||||
} catch (ex: SocketTimeoutException) {
|
||||
Log.w(javaClass.name, "Timeout during login!")
|
||||
|
||||
supportFragmentManager.commit {
|
||||
remove(loadingFragment)
|
||||
// show waring dialog before finishing
|
||||
MaterialDialog(this).show {
|
||||
title(R.string.dialog_timeout_head)
|
||||
message(R.string.dialog_timeout_desc)
|
||||
onDismiss { exitAndRemoveTask() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
runBlocking { loadingJob.joinAll() } // wait for initial loading to finish
|
||||
}
|
||||
Log.i(javaClass.name, "loading and login in $time ms")
|
||||
|
||||
wasInitialized = true
|
||||
}
|
||||
|
||||
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 {
|
||||
private fun showLoginDialog() {
|
||||
LoginDialog(this, false).positiveButton {
|
||||
EncryptedPreferences.saveCredentials(login, password, context)
|
||||
|
||||
if (!AoDParser.login()) {
|
||||
showLoginDialog(false)
|
||||
showLoginDialog()
|
||||
Log.w(javaClass.name, "Login failed, please try again.")
|
||||
}
|
||||
}.negativeButton {
|
||||
@ -171,4 +185,34 @@ class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemS
|
||||
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,120 @@
|
||||
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 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.util.DataTypes.License
|
||||
import org.mosad.teapod.util.ThirdPartyComponent
|
||||
import java.lang.StringBuilder
|
||||
|
||||
class AboutFragment : Fragment() {
|
||||
|
||||
private val teapodRepoUrl = "https://git.mosad.xyz/Seil0/teapod"
|
||||
private lateinit var binding: FragmentAboutBinding
|
||||
|
||||
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.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()
|
||||
}
|
||||
}
|
||||
|
||||
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,102 @@
|
||||
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 com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.afollestad.materialdialogs.list.listItemsSingleChoice
|
||||
import org.mosad.teapod.BuildConfig
|
||||
import org.mosad.teapod.ui.activity.main.MainActivity
|
||||
import org.mosad.teapod.R
|
||||
import org.mosad.teapod.databinding.FragmentAccountBinding
|
||||
import org.mosad.teapod.parser.AoDParser
|
||||
import org.mosad.teapod.preferences.EncryptedPreferences
|
||||
import org.mosad.teapod.preferences.Preferences
|
||||
import org.mosad.teapod.ui.components.LoginDialog
|
||||
import org.mosad.teapod.util.DataTypes.Theme
|
||||
import org.mosad.teapod.util.showFragment
|
||||
|
||||
class AccountFragment : Fragment() {
|
||||
|
||||
private lateinit var binding: FragmentAccountBinding
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
binding = FragmentAccountBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.textAccountLogin.text = EncryptedPreferences.login
|
||||
binding.textInfoAboutDesc.text = getString(R.string.info_about_desc, BuildConfig.VERSION_NAME, getString(R.string.build_time))
|
||||
binding.textThemeSelected.text = when (Preferences.theme) {
|
||||
Theme.DARK -> getString(R.string.theme_dark)
|
||||
else -> getString(R.string.theme_light)
|
||||
}
|
||||
|
||||
binding.switchSecondary.isChecked = Preferences.preferSecondary
|
||||
binding.switchAutoplay.isChecked = Preferences.autoplay
|
||||
|
||||
initActions()
|
||||
}
|
||||
|
||||
private fun initActions() {
|
||||
binding.linearAccountLogin.setOnClickListener {
|
||||
showLoginDialog(true)
|
||||
}
|
||||
|
||||
binding.linearTheme.setOnClickListener {
|
||||
showThemeDialog()
|
||||
}
|
||||
|
||||
binding.linearInfo.setOnClickListener {
|
||||
activity?.showFragment(AboutFragment())
|
||||
}
|
||||
|
||||
binding.switchSecondary.setOnClickListener {
|
||||
Preferences.savePreferSecondary(requireContext(), binding.switchSecondary.isChecked)
|
||||
}
|
||||
|
||||
binding.switchAutoplay.setOnClickListener {
|
||||
Preferences.saveAutoplay(requireContext(), binding.switchAutoplay.isChecked)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showLoginDialog(firstTry: Boolean) {
|
||||
LoginDialog(requireContext(), firstTry).positiveButton {
|
||||
EncryptedPreferences.saveCredentials(login, password, context)
|
||||
|
||||
if (!AoDParser.login()) {
|
||||
showLoginDialog(false)
|
||||
Log.w(javaClass.name, "Login failed, please try again.")
|
||||
}
|
||||
}.show {
|
||||
login = EncryptedPreferences.login
|
||||
password = ""
|
||||
}
|
||||
}
|
||||
|
||||
private fun showThemeDialog() {
|
||||
val themes = listOf(
|
||||
resources.getString(R.string.theme_light),
|
||||
resources.getString(R.string.theme_dark)
|
||||
)
|
||||
|
||||
MaterialDialog(requireContext()).show {
|
||||
title(R.string.theme)
|
||||
listItemsSingleChoice(items = themes, initialSelection = Preferences.theme.ordinal) { _, index, _ ->
|
||||
when(index) {
|
||||
0 -> Preferences.saveTheme(context, Theme.LIGHT)
|
||||
1 -> Preferences.saveTheme(context, Theme.DARK)
|
||||
else -> Preferences.saveTheme(context, Theme.LIGHT)
|
||||
}
|
||||
|
||||
(activity as MainActivity).restart()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,167 @@
|
||||
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 com.bumptech.glide.Glide
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.mosad.teapod.R
|
||||
import org.mosad.teapod.ui.activity.main.MainActivity
|
||||
import org.mosad.teapod.databinding.FragmentHomeBinding
|
||||
import org.mosad.teapod.parser.AoDParser
|
||||
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)
|
||||
|
||||
GlobalScope.launch(Dispatchers.Main) {
|
||||
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
|
||||
val myListMedia = StorageController.myList.map { elementId ->
|
||||
AoDParser.itemMediaList.first {
|
||||
elementId == it.id
|
||||
}
|
||||
}
|
||||
adapterMyList = MediaItemAdapter(myListMedia)
|
||||
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
|
||||
GlobalScope.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() {
|
||||
val myListMedia = StorageController.myList.map { elementId ->
|
||||
AoDParser.itemMediaList.first {
|
||||
elementId == it.id
|
||||
}
|
||||
}
|
||||
|
||||
adapterMyList.updateMediaList(myListMedia)
|
||||
adapterMyList.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,24 +1,28 @@
|
||||
package org.mosad.teapod.ui.fragments
|
||||
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 kotlinx.android.synthetic.main.fragment_library.*
|
||||
import kotlinx.coroutines.*
|
||||
import org.mosad.teapod.MainActivity
|
||||
import org.mosad.teapod.R
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.mosad.teapod.databinding.FragmentLibraryBinding
|
||||
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.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? {
|
||||
return inflater.inflate(R.layout.fragment_library, container, false)
|
||||
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?) {
|
||||
@ -31,11 +35,11 @@ class LibraryFragment : Fragment() {
|
||||
context?.let {
|
||||
adapter = MediaItemAdapter(AoDParser.itemMediaList)
|
||||
adapter.onItemClick = { mediaId, _ ->
|
||||
(activity as MainActivity).showMediaFragment(mediaId)
|
||||
activity?.showFragment(MediaFragment(mediaId))
|
||||
}
|
||||
|
||||
recycler_media_library.adapter = adapter
|
||||
recycler_media_library.addItemDecoration(MediaItemDecoration(9))
|
||||
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.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.*
|
||||
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.*
|
||||
import org.mosad.teapod.util.DataTypes.MediaType
|
||||
|
||||
|
||||
/**
|
||||
* 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()
|
||||
|
||||
GlobalScope.launch(Dispatchers.Main) {
|
||||
model.load(mediaId) // load the streams and tmdb for the selected media
|
||||
|
||||
if (this@MediaFragment.isAdded) {
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
package org.mosad.teapod.ui.fragments
|
||||
package org.mosad.teapod.ui.activity.main.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
@ -6,20 +6,21 @@ 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.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? {
|
||||
return inflater.inflate(R.layout.fragment_search, container, false)
|
||||
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?) {
|
||||
@ -31,12 +32,12 @@ class SearchFragment : Fragment() {
|
||||
context?.let {
|
||||
adapter = MediaItemAdapter(AoDParser.itemMediaList)
|
||||
adapter!!.onItemClick = { mediaId, _ ->
|
||||
search_text.clearFocus()
|
||||
(activity as MainActivity).showMediaFragment(mediaId)
|
||||
binding.searchText.clearFocus()
|
||||
activity?.showFragment(MediaFragment(mediaId))
|
||||
}
|
||||
|
||||
recycler_media_search.adapter = adapter
|
||||
recycler_media_search.addItemDecoration(MediaItemDecoration(9))
|
||||
binding.recyclerMediaSearch.adapter = adapter
|
||||
binding.recyclerMediaSearch.addItemDecoration(MediaItemDecoration(9))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -45,7 +46,7 @@ class SearchFragment : Fragment() {
|
||||
}
|
||||
|
||||
private fun initActions() {
|
||||
search_text.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||
binding.searchText.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||
override fun onQueryTextSubmit(query: String?): Boolean {
|
||||
adapter?.filter?.filter(query)
|
||||
adapter?.notifyDataSetChanged()
|
||||
@ -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,54 @@
|
||||
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 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 = GlobalScope.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,473 @@
|
||||
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.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 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.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.mosad.teapod.R
|
||||
import org.mosad.teapod.preferences.Preferences
|
||||
import org.mosad.teapod.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 params = PictureInPictureParams.Builder()
|
||||
.setAspectRatio(Rational(width, height))
|
||||
.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.EventListener {
|
||||
override fun onPlaybackStateChanged(state: Int) {
|
||||
super.onPlaybackStateChanged(state)
|
||||
|
||||
loading.visibility = when (state) {
|
||||
ExoPlayer.STATE_READY -> View.GONE
|
||||
ExoPlayer.STATE_BUFFERING -> View.VISIBLE
|
||||
else -> View.GONE
|
||||
}
|
||||
|
||||
exo_play_pause.visibility = when (loading.visibility) {
|
||||
View.GONE -> View.VISIBLE
|
||||
View.VISIBLE -> View.INVISIBLE
|
||||
else -> View.VISIBLE
|
||||
}
|
||||
|
||||
if (state == ExoPlayer.STATE_ENDED && model.nextEpisode != null && Preferences.autoplay) {
|
||||
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) {
|
||||
GlobalScope.launch {
|
||||
var btnNextEpIsVisible: Boolean
|
||||
var controlsVisible: Boolean
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
if (model.player.duration > 0) {
|
||||
remainingTime = model.player.duration - model.player.currentPosition
|
||||
remainingTime = if (remainingTime < 0) 0 else remainingTime
|
||||
}
|
||||
btnNextEpIsVisible = button_next_ep.isVisible
|
||||
controlsVisible = controller.isVisible
|
||||
}
|
||||
|
||||
if (remainingTime in 1..20000) {
|
||||
// if the next ep button is not visible, make it visible. Don't show in pip mode
|
||||
if (!btnNextEpIsVisible && model.nextEpisode != null && Preferences.autoplay && !isInPiPMode()) {
|
||||
withContext(Dispatchers.Main) { showButtonNextEp() }
|
||||
}
|
||||
} else if (btnNextEpIsVisible) {
|
||||
withContext(Dispatchers.Main) { hideButtonNextEp() }
|
||||
}
|
||||
|
||||
// if controls are visible, update them
|
||||
if (controlsVisible) {
|
||||
withContext(Dispatchers.Main) { updateControls() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun 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,148 @@
|
||||
package org.mosad.teapod.ui.activity.player
|
||||
|
||||
import android.app.Application
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import com.google.android.exoplayer2.C
|
||||
import com.google.android.exoplayer2.MediaItem
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer
|
||||
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.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()
|
||||
val dataSourceFactory = DefaultDataSourceFactory(application, Util.getUserAgent(application, "Teapod"))
|
||||
|
||||
val currentEpisodeChangedListener = ArrayList<() -> Unit>()
|
||||
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
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
player.release()
|
||||
|
||||
Log.d(javaClass.name, "Released player")
|
||||
}
|
||||
|
||||
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) {
|
||||
AoDParser.markAsWatched(media.id, episode.id)
|
||||
}
|
||||
}
|
||||
|
||||
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,75 +0,0 @@
|
||||
package org.mosad.teapod.ui.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import 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.preferences.Preferences
|
||||
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))
|
||||
switch_secondary.isChecked = Preferences.preferSecondary
|
||||
|
||||
initActions()
|
||||
}
|
||||
|
||||
private fun initActions() {
|
||||
linear_account_login.setOnClickListener {
|
||||
showLoginDialog(true)
|
||||
}
|
||||
|
||||
linear_about.setOnClickListener {
|
||||
MaterialDialog(requireContext())
|
||||
.title(R.string.info_about)
|
||||
.message(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()
|
||||
}
|
||||
|
||||
switch_secondary.setOnClickListener {
|
||||
Preferences.savePreferSecondary(requireContext(), switch_secondary.isChecked)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showLoginDialog(firstTry: Boolean) {
|
||||
LoginDialog(requireContext(), firstTry).positiveButton {
|
||||
EncryptedPreferences.saveCredentials(login, password, context)
|
||||
|
||||
if (!AoDParser.login()) {
|
||||
showLoginDialog(false)
|
||||
Log.w(javaClass.name, "Login failed, please try again.")
|
||||
}
|
||||
}.show {
|
||||
login = EncryptedPreferences.login
|
||||
password = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,70 +0,0 @@
|
||||
package org.mosad.teapod.ui.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import kotlinx.android.synthetic.main.fragment_home.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.mosad.teapod.MainActivity
|
||||
import org.mosad.teapod.R
|
||||
import org.mosad.teapod.parser.AoDParser
|
||||
import org.mosad.teapod.util.StorageController
|
||||
import org.mosad.teapod.util.adapter.MediaItemAdapter
|
||||
import org.mosad.teapod.util.decoration.MediaItemDecoration
|
||||
|
||||
class HomeFragment : Fragment() {
|
||||
|
||||
private lateinit var adapterMyList: MediaItemAdapter
|
||||
private lateinit var adapterNewEpisodes: MediaItemAdapter
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.fragment_home, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
GlobalScope.launch {
|
||||
withContext(Dispatchers.Main) {
|
||||
context?.let {
|
||||
recycler_my_list.addItemDecoration(MediaItemDecoration(9))
|
||||
|
||||
updateMyListMedia()
|
||||
|
||||
adapterNewEpisodes = MediaItemAdapter(AoDParser.newEpisodesList)
|
||||
recycler_new_episodes.adapter = adapterNewEpisodes
|
||||
recycler_new_episodes.addItemDecoration(MediaItemDecoration(9))
|
||||
|
||||
initActions()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO recreating the adapter on list change is not a good solution
|
||||
fun updateMyListMedia() {
|
||||
val myListMedia = StorageController.myList.map { elementId ->
|
||||
AoDParser.itemMediaList.first {
|
||||
elementId == it.id
|
||||
}
|
||||
}
|
||||
|
||||
adapterMyList = MediaItemAdapter(myListMedia)
|
||||
adapterMyList.onItemClick = { mediaId, _ ->
|
||||
(activity as MainActivity).showMediaFragment(mediaId)
|
||||
}
|
||||
|
||||
recycler_my_list.adapter = adapterMyList
|
||||
}
|
||||
|
||||
private fun initActions() {
|
||||
adapterNewEpisodes.onItemClick = { mediaId, _ ->
|
||||
(activity as MainActivity).showMediaFragment(mediaId)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,16 +0,0 @@
|
||||
package org.mosad.teapod.ui.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import org.mosad.teapod.R
|
||||
|
||||
class LoadingFragment : Fragment() {
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.fragment_loading, container, false)
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,148 +0,0 @@
|
||||
package org.mosad.teapod.ui.fragments
|
||||
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import jp.wasabeef.glide.transformations.BlurTransformation
|
||||
import kotlinx.android.synthetic.main.fragment_media.*
|
||||
import org.mosad.teapod.MainActivity
|
||||
import org.mosad.teapod.R
|
||||
import org.mosad.teapod.parser.AoDParser
|
||||
import org.mosad.teapod.preferences.Preferences
|
||||
import org.mosad.teapod.util.DataTypes.MediaType
|
||||
import org.mosad.teapod.util.Episode
|
||||
import org.mosad.teapod.util.Media
|
||||
import org.mosad.teapod.util.StorageController
|
||||
import org.mosad.teapod.util.TMDBResponse
|
||||
import org.mosad.teapod.util.adapter.EpisodeItemAdapter
|
||||
|
||||
class MediaFragment(private val media: Media, private val tmdb: TMDBResponse) : Fragment() {
|
||||
|
||||
private lateinit var adapterRecEpisodes: EpisodeItemAdapter
|
||||
private lateinit var viewManager: RecyclerView.LayoutManager
|
||||
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.fragment_media, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
initGUI()
|
||||
initActions()
|
||||
}
|
||||
|
||||
/**
|
||||
* if tmdb data is present, use it, else use the aod data
|
||||
*/
|
||||
private fun initGUI() {
|
||||
// generic gui
|
||||
val backdropUrl = if (tmdb.backdropUrl.isNotEmpty()) tmdb.backdropUrl else media.info.posterUrl
|
||||
val posterUrl = if (tmdb.posterUrl.isNotEmpty()) tmdb.posterUrl else media.info.posterUrl
|
||||
|
||||
Glide.with(requireContext()).load(backdropUrl)
|
||||
.apply(RequestOptions.placeholderOf(ColorDrawable(Color.DKGRAY)))
|
||||
.apply(RequestOptions.bitmapTransform(BlurTransformation(20, 3)))
|
||||
.into(image_backdrop)
|
||||
|
||||
Glide.with(requireContext()).load(posterUrl)
|
||||
.into(image_poster)
|
||||
|
||||
text_title.text = media.info.title
|
||||
text_year.text = media.info.year.toString()
|
||||
text_age.text = media.info.age.toString()
|
||||
text_overview.text = media.info.shortDesc
|
||||
if (StorageController.myList.contains(media.id)) {
|
||||
Glide.with(requireContext()).load(R.drawable.ic_baseline_check_24).into(image_my_list_action)
|
||||
} else {
|
||||
Glide.with(requireContext()).load(R.drawable.ic_baseline_add_24).into(image_my_list_action)
|
||||
}
|
||||
|
||||
// specific gui
|
||||
if (media.type == MediaType.TVSHOW) {
|
||||
adapterRecEpisodes = EpisodeItemAdapter(media.episodes)
|
||||
viewManager = LinearLayoutManager(context)
|
||||
recycler_episodes.layoutManager = viewManager
|
||||
recycler_episodes.adapter = adapterRecEpisodes
|
||||
|
||||
text_episodes_or_runtime.text = getString(R.string.text_episodes_count, media.info.episodesCount)
|
||||
} else if (media.type == MediaType.MOVIE) {
|
||||
recycler_episodes.visibility = View.GONE
|
||||
|
||||
if (tmdb.runtime > 0) {
|
||||
text_episodes_or_runtime.text = getString(R.string.text_runtime, tmdb.runtime)
|
||||
} else {
|
||||
text_episodes_or_runtime.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initActions() {
|
||||
button_play.setOnClickListener {
|
||||
when (media.type) {
|
||||
MediaType.MOVIE -> playStream(media.episodes.first())
|
||||
MediaType.TVSHOW -> playStream(media.episodes.first())
|
||||
else -> Log.e(javaClass.name, "Wrong Type: $media.type")
|
||||
}
|
||||
}
|
||||
|
||||
// add or remove media from myList
|
||||
linear_my_list_action.setOnClickListener {
|
||||
if (StorageController.myList.contains(media.id)) {
|
||||
StorageController.myList.remove(media.id)
|
||||
Glide.with(requireContext()).load(R.drawable.ic_baseline_add_24).into(image_my_list_action)
|
||||
} else {
|
||||
StorageController.myList.add(media.id)
|
||||
Glide.with(requireContext()).load(R.drawable.ic_baseline_check_24).into(image_my_list_action)
|
||||
}
|
||||
StorageController.saveMyList(requireContext())
|
||||
|
||||
// notify home fragment on change
|
||||
parentFragmentManager.findFragmentByTag("HomeFragment")?.let {
|
||||
(it as HomeFragment).updateMyListMedia()
|
||||
}
|
||||
}
|
||||
|
||||
// set onItemClick only in adapter is initialized
|
||||
if (this::adapterRecEpisodes.isInitialized) {
|
||||
adapterRecEpisodes.onImageClick = { _, position ->
|
||||
playStream(media.episodes[position])
|
||||
|
||||
// update watched state
|
||||
AoDParser.sendCallback(media.episodes[position].watchedCallback)
|
||||
adapterRecEpisodes.updateWatchedState(true, position)
|
||||
adapterRecEpisodes.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Play the media's stream
|
||||
* If prefer secondary or primary is empty and secondary is present (secStreamOmU),
|
||||
* use the secondary stream. Else, if the primary stream is set use the primary stream.
|
||||
*/
|
||||
private fun playStream(ep: Episode) {
|
||||
val streamUrl = if ((Preferences.preferSecondary || ep.priStreamUrl.isEmpty()) && ep.secStreamOmU) {
|
||||
ep.secStreamUrl
|
||||
} else if (ep.priStreamUrl.isNotEmpty()) {
|
||||
ep.priStreamUrl
|
||||
} else {
|
||||
Log.e(javaClass.name, "No stream url set.")
|
||||
""
|
||||
}
|
||||
|
||||
Log.d(javaClass.name, "Playing stream: $streamUrl")
|
||||
(activity as MainActivity).startPlayer(streamUrl)
|
||||
}
|
||||
|
||||
}
|
||||
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,13 +1,36 @@
|
||||
package org.mosad.teapod.util
|
||||
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
class DataTypes {
|
||||
enum class MediaType {
|
||||
OTHER,
|
||||
MOVIE,
|
||||
TVSHOW
|
||||
}
|
||||
|
||||
enum class Theme(val str: String) {
|
||||
LIGHT("Light"),
|
||||
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)
|
||||
@ -18,7 +41,6 @@ data class ItemMedia(
|
||||
val posterUrl: String
|
||||
)
|
||||
|
||||
|
||||
/**
|
||||
* TODO the episodes workflow could use a clean up/rework
|
||||
*/
|
||||
@ -27,9 +49,15 @@ data class Media(
|
||||
val link: String,
|
||||
val type: DataTypes.MediaType,
|
||||
val info: Info = Info(),
|
||||
var episodes: ArrayList<Episode> = arrayListOf()
|
||||
)
|
||||
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(
|
||||
var title: String = "",
|
||||
var posterUrl: String = "",
|
||||
@ -37,36 +65,58 @@ data class Info(
|
||||
var description: String = "",
|
||||
var year: Int = 0,
|
||||
var age: Int = 0,
|
||||
var episodesCount: Int = 0
|
||||
var episodesCount: Int = 0,
|
||||
var similar: List<ItemMedia> = listOf()
|
||||
)
|
||||
|
||||
/**
|
||||
* if secStreamOmU == true, then a secondary stream is present
|
||||
* number = episode number (0..n)
|
||||
*/
|
||||
data class Episode(
|
||||
val id: Int = 0,
|
||||
var title: String = "",
|
||||
var priStreamUrl: String = "",
|
||||
var secStreamUrl: String = "",
|
||||
var secStreamOmU: Boolean = false,
|
||||
var posterUrl: String = "",
|
||||
var description: String = "",
|
||||
val id: Int = -1,
|
||||
val streams: MutableList<Stream> = mutableListOf(),
|
||||
val title: String = "",
|
||||
val posterUrl: String = "",
|
||||
val description: String = "",
|
||||
var shortDesc: String = "",
|
||||
var number: Int = 0,
|
||||
val number: Int = 0,
|
||||
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(
|
||||
val id: Int = 0,
|
||||
val title: String = "",
|
||||
val overview: String = "",
|
||||
val posterUrl: String = "",
|
||||
val backdropUrl: String = "",
|
||||
var runtime: Int = 0
|
||||
val runtime: Int = 0
|
||||
)
|
||||
|
||||
data class AoDObject(val playlist: List<Playlist>)
|
||||
/**
|
||||
* 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>,
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
package org.mosad.teapod.util
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.JsonParser
|
||||
import kotlinx.coroutines.*
|
||||
import java.io.File
|
||||
import java.lang.Exception
|
||||
|
||||
/**
|
||||
* This controller contains the logic for permanently saved data.
|
||||
@ -21,17 +23,21 @@ object StorageController {
|
||||
|
||||
if (!file.exists()) runBlocking { saveMyList(context).join() }
|
||||
|
||||
myList.clear()
|
||||
myList.addAll(
|
||||
GsonBuilder().create().fromJson(file.readText(), ArrayList<Int>().javaClass)
|
||||
)
|
||||
try {
|
||||
myList.clear()
|
||||
myList.addAll(JsonParser.parseString(file.readText()).asJsonArray.map { it.asInt }.distinct())
|
||||
} catch (ex: Exception) {
|
||||
myList.clear()
|
||||
Log.e(javaClass.name, "Parsing of My-List failed.")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun saveMyList(context: Context): Job {
|
||||
val file = File(context.filesDir, fileNameMyList)
|
||||
|
||||
return GlobalScope.launch(Dispatchers.IO) {
|
||||
file.writeText(Gson().toJson(myList))
|
||||
file.writeText(Gson().toJson(myList.distinct()))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -3,9 +3,7 @@ package org.mosad.teapod.util
|
||||
import android.util.Log
|
||||
import com.google.gson.JsonObject
|
||||
import com.google.gson.JsonParser
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.*
|
||||
import java.net.URL
|
||||
import java.net.URLEncoder
|
||||
import org.mosad.teapod.util.DataTypes.MediaType
|
||||
@ -22,12 +20,12 @@ class TMDBApiController {
|
||||
|
||||
private val imageUrl = "https://image.tmdb.org/t/p/w500"
|
||||
|
||||
fun search(title: String, type: MediaType): TMDBResponse {
|
||||
suspend fun search(title: String, type: MediaType): TMDBResponse {
|
||||
val searchTerm = title.replace("(Sub)", "").trim()
|
||||
|
||||
return when (type) {
|
||||
MediaType.MOVIE -> searchMovie(searchTerm)
|
||||
MediaType.TVSHOW -> searchTVShow(searchTerm)
|
||||
MediaType.MOVIE -> searchMovie(searchTerm).await()
|
||||
MediaType.TVSHOW -> searchTVShow(searchTerm).await()
|
||||
else -> {
|
||||
Log.e(javaClass.name, "Wrong Type: $type")
|
||||
TMDBResponse()
|
||||
@ -36,17 +34,17 @@ class TMDBApiController {
|
||||
|
||||
}
|
||||
|
||||
fun searchTVShow(title: String) = runBlocking {
|
||||
fun searchTVShow(title: String): Deferred<TMDBResponse> {
|
||||
val url = URL("$searchTVUrl$preparedParameters&query=${URLEncoder.encode(title, "UTF-8")}")
|
||||
|
||||
GlobalScope.async {
|
||||
return GlobalScope.async {
|
||||
val response = JsonParser.parseString(url.readText()).asJsonObject
|
||||
//println(response)
|
||||
|
||||
return@async if (response.get("total_results").asInt > 0) {
|
||||
if (response.get("total_results").asInt > 0) {
|
||||
response.get("results").asJsonArray.first().asJsonObject.let {
|
||||
val id = getStringNotNull(it,"id").toInt()
|
||||
val overview = getStringNotNull(it,"overview")
|
||||
val id = getStringNotNull(it, "id").toInt()
|
||||
val overview = getStringNotNull(it, "overview")
|
||||
val posterPath = getStringNotNullPrefix(it, "poster_path", imageUrl)
|
||||
val backdropPath = getStringNotNullPrefix(it, "backdrop_path", imageUrl)
|
||||
|
||||
@ -55,18 +53,17 @@ class TMDBApiController {
|
||||
} else {
|
||||
TMDBResponse()
|
||||
}
|
||||
}.await()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
fun searchMovie(title: String) = runBlocking {
|
||||
fun searchMovie(title: String): Deferred<TMDBResponse> {
|
||||
val url = URL("$searchMovieUrl$preparedParameters&query=${URLEncoder.encode(title, "UTF-8")}")
|
||||
|
||||
GlobalScope.async {
|
||||
return GlobalScope.async {
|
||||
val response = JsonParser.parseString(url.readText()).asJsonObject
|
||||
//println(response)
|
||||
|
||||
return@async if (response.get("total_results").asInt > 0) {
|
||||
if (response.get("total_results").asInt > 0) {
|
||||
response.get("results").asJsonArray.first().asJsonObject.let {
|
||||
val id = getStringNotNull(it,"id").toInt()
|
||||
val overview = getStringNotNull(it,"overview")
|
||||
@ -79,9 +76,7 @@ class TMDBApiController {
|
||||
} else {
|
||||
TMDBResponse()
|
||||
}
|
||||
|
||||
|
||||
}.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)
|
||||
}
|
||||
@ -1,53 +1,52 @@
|
||||
package org.mosad.teapod.util.adapter
|
||||
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import jp.wasabeef.glide.transformations.RoundedCornersTransformation
|
||||
import kotlinx.android.synthetic.main.item_episode.view.*
|
||||
import org.mosad.teapod.R
|
||||
import org.mosad.teapod.databinding.ItemEpisodeBinding
|
||||
import org.mosad.teapod.util.Episode
|
||||
|
||||
class EpisodeItemAdapter(private val episodes: List<Episode>) : RecyclerView.Adapter<EpisodeItemAdapter.MyViewHolder>() {
|
||||
class EpisodeItemAdapter(private val episodes: List<Episode>) : RecyclerView.Adapter<EpisodeItemAdapter.EpisodeViewHolder>() {
|
||||
|
||||
var onItemClick: ((String, Int) -> Unit)? = null
|
||||
var onImageClick: ((String, Int) -> Unit)? = null
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
|
||||
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_episode, parent, false)
|
||||
|
||||
return MyViewHolder(view)
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EpisodeViewHolder {
|
||||
return EpisodeViewHolder(ItemEpisodeBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
|
||||
val context = holder.view.context
|
||||
override fun onBindViewHolder(holder: EpisodeViewHolder, position: Int) {
|
||||
val context = holder.binding.root.context
|
||||
val ep = episodes[position]
|
||||
|
||||
val titleText = if (ep.priStreamUrl.isEmpty() && ep.secStreamOmU) {
|
||||
context.getString(R.string.component_episode_title_sub, ep.number, ep.description)
|
||||
} else {
|
||||
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.view.text_episode_title.text = titleText
|
||||
holder.view.text_episode_desc.text = ep.shortDesc
|
||||
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.view.image_episode)
|
||||
.into(holder.binding.imageEpisode)
|
||||
}
|
||||
|
||||
if (ep.watched) {
|
||||
holder.view.image_watched.setImageDrawable(
|
||||
holder.binding.imageWatched.setImageDrawable(
|
||||
ContextCompat.getDrawable(context, R.drawable.ic_baseline_check_circle_24)
|
||||
)
|
||||
} else {
|
||||
holder.view.image_watched.setImageDrawable(null)
|
||||
holder.binding.imageWatched.setImageDrawable(null)
|
||||
}
|
||||
}
|
||||
|
||||
@ -56,16 +55,13 @@ class EpisodeItemAdapter(private val episodes: List<Episode>) : RecyclerView.Ada
|
||||
}
|
||||
|
||||
fun updateWatchedState(watched: Boolean, position: Int) {
|
||||
episodes[position].watched = watched
|
||||
// use getOrNull as there could be a index out of bound when running this in onResume()
|
||||
episodes.getOrNull(position)?.watched = watched
|
||||
}
|
||||
|
||||
inner class MyViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
|
||||
inner class EpisodeViewHolder(val binding: ItemEpisodeBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||
init {
|
||||
view.setOnClickListener {
|
||||
onItemClick?.invoke(episodes[adapterPosition].title, adapterPosition)
|
||||
}
|
||||
|
||||
view.image_episode.setOnClickListener {
|
||||
binding.imageEpisode.setOnClickListener {
|
||||
onImageClick?.invoke(episodes[adapterPosition].title, adapterPosition)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,33 +1,29 @@
|
||||
package org.mosad.teapod.util.adapter
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Filter
|
||||
import android.widget.Filterable
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bumptech.glide.Glide
|
||||
import kotlinx.android.synthetic.main.item_media.view.*
|
||||
import org.mosad.teapod.R
|
||||
import org.mosad.teapod.databinding.ItemMediaBinding
|
||||
import org.mosad.teapod.util.ItemMedia
|
||||
import java.util.*
|
||||
|
||||
class MediaItemAdapter(private val media: List<ItemMedia>) : RecyclerView.Adapter<MediaItemAdapter.ViewHolder>(), Filterable {
|
||||
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 = media.map { it.copy() }
|
||||
private var filteredMedia = initMedia.map { it.copy() }
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MediaItemAdapter.ViewHolder {
|
||||
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_media, parent, false)
|
||||
|
||||
return ViewHolder(view)
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MediaItemAdapter.MediaViewHolder {
|
||||
return MediaViewHolder(ItemMediaBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: MediaItemAdapter.ViewHolder, position: Int) {
|
||||
holder.view.apply {
|
||||
text_title.text = filteredMedia[position].title
|
||||
Glide.with(context).load(filteredMedia[position].posterUrl).into(image_poster)
|
||||
override fun 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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -39,9 +35,13 @@ class MediaItemAdapter(private val media: List<ItemMedia>) : RecyclerView.Adapte
|
||||
return filter
|
||||
}
|
||||
|
||||
inner class ViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
|
||||
fun updateMediaList(mediaList: List<ItemMedia>) {
|
||||
filteredMedia = mediaList
|
||||
}
|
||||
|
||||
inner class MediaViewHolder(val binding: ItemMediaBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||
init {
|
||||
view.setOnClickListener {
|
||||
binding.root.setOnClickListener {
|
||||
onItemClick?.invoke(filteredMedia[adapterPosition].id, adapterPosition)
|
||||
}
|
||||
}
|
||||
@ -53,9 +53,9 @@ class MediaItemAdapter(private val media: List<ItemMedia>) : RecyclerView.Adapte
|
||||
val results = FilterResults()
|
||||
|
||||
val filteredList = if (filterTerm.isEmpty()) {
|
||||
media
|
||||
initMedia
|
||||
} else {
|
||||
media.filter {
|
||||
initMedia.filter {
|
||||
it.title.toLowerCase(Locale.ROOT).contains(filterTerm)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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>
|
||||
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>
|
||||
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>
|
||||
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>
|
||||
@ -1,10 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-6h2v6zM13,9h-2L11,7h2v2z"/>
|
||||
</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:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
android:tint="?attr/iconColor">
|
||||
<path
|
||||
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"/>
|
||||
|
||||
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>
|
||||
@ -1,10 +1,5 @@
|
||||
<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="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 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="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>
|
||||
|
||||
5
app/src/main/res/drawable/ic_outline_info_24.xml
Normal file
5
app/src/main/res/drawable/ic_outline_info_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="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>
|
||||
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"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="#B0B0B0"/>
|
||||
<solid android:color="?textBackground"/>
|
||||
<corners android:radius="3dp"/>
|
||||
</shape>
|
||||
@ -9,9 +9,8 @@
|
||||
android:id="@+id/nav_view"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="0dp"
|
||||
android:layout_marginEnd="0dp"
|
||||
android:background="?android:attr/windowBackground"
|
||||
android:background="?themeSecondary"
|
||||
app:itemIconTint="@color/bottom_nav_item_tint"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="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,27 +2,91 @@
|
||||
<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:id="@+id/player_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:keepScreenOn="true"
|
||||
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:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
android:animateLayoutChanges="true"
|
||||
android:foreground="@drawable/ripple_background"
|
||||
app:controller_layout_id="@layout/player_controls"
|
||||
app:fastforward_increment="10000"
|
||||
app:rewind_increment="10000" />
|
||||
<!-- app:controller_layout_id="@layout/player_custom_control"/>-->
|
||||
|
||||
<com.google.android.material.progressindicator.ProgressIndicator
|
||||
<com.google.android.material.progressindicator.CircularProgressIndicator
|
||||
android:id="@+id/loading"
|
||||
style="@style/Widget.MaterialComponents.ProgressIndicator.Circular.Indeterminate"
|
||||
android:layout_width="70dp"
|
||||
android:layout_height="70dp"
|
||||
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>
|
||||
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"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#f5f5f5"
|
||||
tools:context=".ui.fragments.AccountFragment">
|
||||
android:background="?themePrimary"
|
||||
tools:context=".ui.activity.main.fragments.AccountFragment">
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
@ -14,14 +14,17 @@
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
android:clipToPadding="false"
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="12dp">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/linear_account"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:background="#ffffff"
|
||||
android:background="?themeSecondary"
|
||||
android:elevation="5dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
@ -31,7 +34,6 @@
|
||||
android:paddingStart="7dp"
|
||||
android:paddingEnd="7dp"
|
||||
android:text="@string/account"
|
||||
android:textColor="@android:color/primary_text_light"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
@ -39,9 +41,10 @@
|
||||
android:id="@+id/linear_account_login"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="7dp"
|
||||
android:foreground="?android:selectableItemBackground"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal">
|
||||
android:orientation="horizontal"
|
||||
android:padding="7dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imageView"
|
||||
@ -50,10 +53,10 @@
|
||||
android:contentDescription="@string/account"
|
||||
android:minWidth="48dp"
|
||||
android:minHeight="48dp"
|
||||
android:padding="5dp"
|
||||
android:padding="9dp"
|
||||
android:scaleType="fitXY"
|
||||
android:src="@drawable/ic_baseline_account_box_24"
|
||||
app:srcCompat="@drawable/ic_baseline_account_box_24" />
|
||||
app:tint="?iconColor" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
@ -66,7 +69,6 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/account_login_ex"
|
||||
android:textColor="@android:color/primary_text_light"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<TextView
|
||||
@ -75,7 +77,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/account_login_desc"
|
||||
android:textColor="@android:color/secondary_text_light" />
|
||||
android:textColor="?textSecondary" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
@ -84,9 +86,10 @@
|
||||
<LinearLayout
|
||||
android:id="@+id/linear_settings"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:background="#ffffff"
|
||||
android:background="?themeSecondary"
|
||||
android:elevation="5dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
@ -96,7 +99,6 @@
|
||||
android:paddingStart="7dp"
|
||||
android:paddingEnd="7dp"
|
||||
android:text="@string/settings"
|
||||
android:textColor="@android:color/primary_text_light"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
@ -104,20 +106,21 @@
|
||||
android:id="@+id/linear_settings_secondary"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="7dp"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal">
|
||||
android:orientation="horizontal"
|
||||
android:padding="7dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imageView3"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/account"
|
||||
android:contentDescription="@string/settings_secondary"
|
||||
android:minWidth="48dp"
|
||||
android:minHeight="48dp"
|
||||
android:padding="5dp"
|
||||
android:padding="9dp"
|
||||
android:scaleType="fitXY"
|
||||
android:src="@drawable/ic_baseline_subtitles_24" />
|
||||
android:src="@drawable/ic_baseline_subtitles_24"
|
||||
app:tint="?iconColor" />
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
@ -139,7 +142,6 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/settings_secondary"
|
||||
android:textColor="@android:color/primary_text_light"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<TextView
|
||||
@ -149,7 +151,7 @@
|
||||
android:layout_weight="1"
|
||||
android:maxLines="2"
|
||||
android:text="@string/settings_secondary_desc"
|
||||
android:textColor="@android:color/secondary_text_light" />
|
||||
android:textColor="?textSecondary" />
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
@ -164,14 +166,122 @@
|
||||
|
||||
|
||||
</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/imageView4"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/settings_autoplay"
|
||||
android:minWidth="48dp"
|
||||
android:minHeight="48dp"
|
||||
android:padding="9dp"
|
||||
android:src="@drawable/ic_baseline_autorenew_24"
|
||||
app:tint="?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/imageViewTheme"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/account"
|
||||
android:minWidth="48dp"
|
||||
android:minHeight="48dp"
|
||||
android:padding="9dp"
|
||||
android:scaleType="fitXY"
|
||||
android:src="@drawable/ic_baseline_style_24"
|
||||
app:tint="?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
|
||||
android:id="@+id/linear_info"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:background="#ffffff"
|
||||
android:background="?themeSecondary"
|
||||
android:clipToPadding="false"
|
||||
android:elevation="5dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
@ -181,7 +291,6 @@
|
||||
android:paddingStart="7dp"
|
||||
android:paddingEnd="7dp"
|
||||
android:text="@string/info"
|
||||
android:textColor="@android:color/primary_text_light"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
@ -189,9 +298,10 @@
|
||||
android:id="@+id/linear_about"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="7dp"
|
||||
android:foreground="?android:selectableItemBackground"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal">
|
||||
android:orientation="horizontal"
|
||||
android:padding="7dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imageView2"
|
||||
@ -200,9 +310,10 @@
|
||||
android:contentDescription="@string/info"
|
||||
android:minWidth="48dp"
|
||||
android:minHeight="48dp"
|
||||
android:padding="5dp"
|
||||
android:padding="9dp"
|
||||
android:scaleType="fitXY"
|
||||
app:srcCompat="@drawable/ic_baseline_info_24" />
|
||||
app:srcCompat="@drawable/ic_outline_info_24"
|
||||
app:tint="?iconColor" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
@ -215,7 +326,6 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/info_about"
|
||||
android:textColor="@android:color/primary_text_light"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<TextView
|
||||
@ -224,27 +334,11 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/info_about_desc"
|
||||
android:textColor="@android:color/secondary_text_light" />
|
||||
android:textColor="?textSecondary" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:id="@+id/divider"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="?android:attr/listDivider" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_licenses"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="7dp"
|
||||
android:paddingStart="48dp"
|
||||
android:paddingEnd="48dp"
|
||||
android:text="@string/licenses"
|
||||
android:textColor="@android:color/primary_text_light"
|
||||
android:textSize="16sp" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@ -2,10 +2,11 @@
|
||||
<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:id="@+id/ff_test"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#f5f5f5"
|
||||
tools:context=".ui.fragments.HomeFragment">
|
||||
android:background="?themePrimary"
|
||||
tools:context=".ui.activity.main.fragments.HomeFragment">
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
@ -14,9 +15,100 @@
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical" >
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/linear_highlight"
|
||||
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"
|
||||
@ -44,6 +136,7 @@
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/linear_new_episodes"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
@ -70,6 +163,90 @@
|
||||
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>
|
||||
|
||||
|
||||
@ -4,8 +4,8 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#f5f5f5"
|
||||
tools:context=".ui.fragments.LibraryFragment">
|
||||
android:background="?themePrimary"
|
||||
tools:context=".ui.activity.main.fragments.LibraryFragment">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler_media_library"
|
||||
|
||||
@ -1,17 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#f5f5f5"
|
||||
android:clickable="true"
|
||||
android:focusable="true">
|
||||
|
||||
<com.google.android.material.progressindicator.ProgressIndicator
|
||||
android:id="@+id/progressBar2"
|
||||
style="@style/Widget.MaterialComponents.ProgressIndicator.Circular.Indeterminate"
|
||||
android:layout_width="70dp"
|
||||
android:layout_height="70dp"
|
||||
android:layout_gravity="center"
|
||||
tools:visibility="visible" />
|
||||
</FrameLayout>
|
||||
@ -1,159 +1,205 @@
|
||||
<?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:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#f5f5f5"
|
||||
tools:context=".ui.fragments.MediaFragment">
|
||||
android:background="?themePrimary"
|
||||
tools:context=".ui.activity.main.fragments.MediaFragment">
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fillViewport="true">
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/app_layout"
|
||||
android:layout_width="match_parent"
|
||||
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_height="wrap_content">
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
app:layout_scrollFlags="scroll">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/image_backdrop"
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:adjustViewBounds="false"
|
||||
android:maxHeight="231dp"
|
||||
android:minHeight="220dp"
|
||||
android:scaleType="centerCrop" />
|
||||
|
||||
<com.google.android.material.imageview.ShapeableImageView
|
||||
android:id="@+id/image_poster"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="200dp"
|
||||
android:layout_gravity="center"
|
||||
app:shapeAppearance="@style/ShapeAppearance.Teapod.RoundedPoster"
|
||||
app:srcCompat="@drawable/ic_launcher_background" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/linear_media_info"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_year"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="2dp"
|
||||
android:text="@string/text_year_ex" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_age"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="7dp"
|
||||
android:background="@drawable/shape_rounden_corner"
|
||||
android:paddingStart="3dp"
|
||||
android:paddingTop="2dp"
|
||||
android:paddingEnd="3dp"
|
||||
android:paddingBottom="2dp"
|
||||
android:text="@string/text_age_ex" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_episodes_or_runtime"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="7dp"
|
||||
android:padding="2dp"
|
||||
android:text="@string/text_episodes_count" />
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_play"
|
||||
android: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="@android:color/primary_text_dark"
|
||||
android:textSize="16sp"
|
||||
app:backgroundTint="#4A4141"
|
||||
app:icon="@drawable/ic_baseline_play_arrow_24"
|
||||
app:iconGravity="textStart" />
|
||||
|
||||
<TextView
|
||||
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
|
||||
android:id="@+id/text_overview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginTop="7dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:text="@string/text_overview_ex" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/linear_actions"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginTop="7dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/linear_my_list_action"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical">
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/image_my_list_action"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:src="@drawable/ic_baseline_add_24"
|
||||
app:tint="#4A4141" />
|
||||
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_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_my_list_action"
|
||||
android:id="@+id/text_year"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/my_list" />
|
||||
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_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
|
||||
android:id="@+id/text_overview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginTop="7dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:text="@string/text_overview_ex" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/linear_actions"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginTop="7dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/linear_my_list_action"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/image_my_list_action"
|
||||
android:layout_width="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_marginStart="7dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginEnd="7dp"
|
||||
android:background="@android:color/transparent"
|
||||
app:tabGravity="start"
|
||||
app:tabMode="scrollable"
|
||||
app:tabSelectedTextColor="?textPrimary"
|
||||
app:tabTextColor="?textSecondary" />
|
||||
|
||||
</LinearLayout>
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler_episodes"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="7dp"
|
||||
android:layout_marginTop="17dp"
|
||||
android:layout_marginEnd="7dp"
|
||||
tools:layout_editor_absoluteY="298dp"
|
||||
tools:listitem="@layout/item_episode" />
|
||||
</LinearLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
android:id="@+id/pager_episodes_similar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_anchor="@id/app_layout"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||
android:layout_gravity="bottom"/>
|
||||
|
||||
</FrameLayout>
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/frame_loading"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?themePrimary"
|
||||
android:visibility="gone">
|
||||
|
||||
<com.google.android.material.progressindicator.CircularProgressIndicator
|
||||
android:id="@+id/loadingIndicator"
|
||||
android:layout_width="70dp"
|
||||
android:layout_height="70dp"
|
||||
android:layout_gravity="center"
|
||||
android:indeterminate="true"
|
||||
app:indicatorColor="?colorPrimary"
|
||||
tools:visibility="visible" />
|
||||
</FrameLayout>
|
||||
|
||||
</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,18 +4,19 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#f5f5f5"
|
||||
tools:context=".ui.fragments.SearchFragment">
|
||||
android:background="?themePrimary"
|
||||
tools:context=".ui.activity.main.fragments.SearchFragment">
|
||||
|
||||
<SearchView
|
||||
android:id="@+id/search_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:background="#FFFFFF"
|
||||
android:background="?themeSecondary"
|
||||
android:elevation="8dp"
|
||||
android:iconifiedByDefault="false"
|
||||
android:paddingBottom="5dp"
|
||||
android:queryHint="@string/search_hint"
|
||||
android:searchIcon="@drawable/ic_baseline_search_24"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
@ -27,13 +28,13 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:clipToPadding="false"
|
||||
android:padding="3dp"
|
||||
android:orientation="vertical"
|
||||
android:padding="3dp"
|
||||
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/search_text"
|
||||
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
|
||||
app:spanCount="2"
|
||||
tools:listitem="@layout/item_media">
|
||||
|
||||
|
||||
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>
|
||||
@ -44,6 +44,7 @@
|
||||
android:layout_marginStart="7dp"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/component_episode_title"
|
||||
android:textColor="?textPrimary"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<ImageView
|
||||
@ -52,13 +53,15 @@
|
||||
android:layout_height="30dp"
|
||||
android:layout_margin="2dp"
|
||||
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>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_episode_desc"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:maxLines="2"
|
||||
android:ellipsize="end"/>
|
||||
android:ellipsize="end"
|
||||
android:maxLines="3"
|
||||
android:textColor="?textSecondary" />
|
||||
</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>
|
||||
@ -1,9 +1,10 @@
|
||||
<?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="#FFFFFF"
|
||||
android:backgroundTint="?themeSecondary"
|
||||
android:visibility="visible"
|
||||
app:cardCornerRadius="7dp"
|
||||
app:cardElevation="4dp">
|
||||
@ -23,7 +24,7 @@
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@color/md_disabled_text_dark_theme" />
|
||||
tools:srcCompat="@color/md_disabled_text_dark_theme" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_title"
|
||||
|
||||
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/save"
|
||||
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>
|
||||
@ -7,25 +7,25 @@
|
||||
|
||||
<fragment
|
||||
android:id="@+id/navigation_home"
|
||||
android:name="org.mosad.teapod.ui.fragments.HomeFragment"
|
||||
android:name="org.mosad.teapod.ui.activity.main.fragments.HomeFragment"
|
||||
android:label="@string/title_home"
|
||||
tools:layout="@layout/fragment_home" />
|
||||
|
||||
<fragment
|
||||
android:id="@+id/navigation_library"
|
||||
android:name="org.mosad.teapod.ui.fragments.LibraryFragment"
|
||||
android:name="org.mosad.teapod.ui.activity.main.fragments.LibraryFragment"
|
||||
android:label="@string/title_library"
|
||||
tools:layout="@layout/fragment_library" />
|
||||
|
||||
<fragment
|
||||
android:id="@+id/navigation_search"
|
||||
android:name="org.mosad.teapod.ui.fragments.SearchFragment"
|
||||
android:name="org.mosad.teapod.ui.activity.main.fragments.SearchFragment"
|
||||
android:label="@string/title_search"
|
||||
tools:layout="@layout/fragment_search" />
|
||||
|
||||
<fragment
|
||||
android:id="@+id/navigation_account"
|
||||
android:name="org.mosad.teapod.ui.fragments.AccountFragment"
|
||||
android:name="org.mosad.teapod.ui.activity.main.fragments.AccountFragment"
|
||||
android:label="@string/title_account"
|
||||
tools:layout="@layout/fragment_account" />
|
||||
|
||||
|
||||
235
app/src/main/res/raw/al_20_full.txt
Normal file
235
app/src/main/res/raw/al_20_full.txt
Normal file
@ -0,0 +1,235 @@
|
||||
Apache License
|
||||
|
||||
Version 2.0, January 2004
|
||||
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
|
||||
1. Definitions.
|
||||
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
13
app/src/main/res/raw/bsd_2_full.txt
Normal file
13
app/src/main/res/raw/bsd_2_full.txt
Normal file
@ -0,0 +1,13 @@
|
||||
Copyright <YEAR> <COPYRIGHT HOLDER>
|
||||
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
750
app/src/main/res/raw/gpl_3_full.txt
Normal file
750
app/src/main/res/raw/gpl_3_full.txt
Normal file
@ -0,0 +1,750 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
|
||||
|
||||
Version 3, 29 June 2007
|
||||
|
||||
|
||||
Copyright © 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim copies of this license
|
||||
document, but changing it is not allowed.
|
||||
|
||||
|
||||
Preamble
|
||||
|
||||
|
||||
The GNU General Public License is a free, copyleft license for software and
|
||||
other kinds of works.
|
||||
|
||||
|
||||
The licenses for most software and other practical works are designed to take
|
||||
away your freedom to share and change the works. By contrast, the GNU General
|
||||
Public License is intended to guarantee your freedom to share and change all
|
||||
versions of a program--to make sure it remains free software for all its users.
|
||||
We, the Free Software Foundation, use the GNU General Public License for most
|
||||
of our software; it applies also to any other work released this way by its
|
||||
authors. You can apply it to your programs, too.
|
||||
|
||||
|
||||
When we speak of free software, we are referring to freedom, not price. Our
|
||||
General Public Licenses are designed to make sure that you have the freedom
|
||||
to distribute copies of free software (and charge for them if you wish), that
|
||||
you receive source code or can get it if you want it, that you can change
|
||||
the software or use pieces of it in new free programs, and that you know you
|
||||
can do these things.
|
||||
|
||||
|
||||
To protect your rights, we need to prevent others from denying you these rights
|
||||
or asking you to surrender the rights. Therefore, you have certain responsibilities
|
||||
if you distribute copies of the software, or if you modify it: responsibilities
|
||||
to respect the freedom of others.
|
||||
|
||||
|
||||
For example, if you distribute copies of such a program, whether gratis or
|
||||
for a fee, you must pass on to the recipients the same freedoms that you received.
|
||||
You must make sure that they, too, receive or can get the source code. And
|
||||
you must show them these terms so they know their rights.
|
||||
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps: (1) assert
|
||||
copyright on the software, and (2) offer you this License giving you legal
|
||||
permission to copy, distribute and/or modify it.
|
||||
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains that
|
||||
there is no warranty for this free software. For both users' and authors'
|
||||
sake, the GPL requires that modified versions be marked as changed, so that
|
||||
their problems will not be attributed erroneously to authors of previous versions.
|
||||
|
||||
|
||||
Some devices are designed to deny users access to install or run modified
|
||||
versions of the software inside them, although the manufacturer can do so.
|
||||
This is fundamentally incompatible with the aim of protecting users' freedom
|
||||
to change the software. The systematic pattern of such abuse occurs in the
|
||||
area of products for individuals to use, which is precisely where it is most
|
||||
unacceptable. Therefore, we have designed this version of the GPL to prohibit
|
||||
the practice for those products. If such problems arise substantially in other
|
||||
domains, we stand ready to extend this provision to those domains in future
|
||||
versions of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
|
||||
Finally, every program is threatened constantly by software patents. States
|
||||
should not allow patents to restrict development and use of software on general-purpose
|
||||
computers, but in those that do, we wish to avoid the special danger that
|
||||
patents applied to a free program could make it effectively proprietary. To
|
||||
prevent this, the GPL assures that patents cannot be used to render the program
|
||||
non-free.
|
||||
|
||||
|
||||
The precise terms and conditions for copying, distribution and modification
|
||||
follow.
|
||||
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
|
||||
0. Definitions.
|
||||
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of works,
|
||||
such as semiconductor masks.
|
||||
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this License.
|
||||
Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals
|
||||
or organizations.
|
||||
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work in
|
||||
a fashion requiring copyright permission, other than the making of an exact
|
||||
copy. The resulting work is called a "modified version" of the earlier work
|
||||
or a work "based on" the earlier work.
|
||||
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based on the
|
||||
Program.
|
||||
|
||||
|
||||
To "propagate" a work means to do anything with it that, without permission,
|
||||
would make you directly or secondarily liable for infringement under applicable
|
||||
copyright law, except executing it on a computer or modifying a private copy.
|
||||
Propagation includes copying, distribution (with or without modification),
|
||||
making available to the public, and in some countries other activities as
|
||||
well.
|
||||
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other parties
|
||||
to make or receive copies. Mere interaction with a user through a computer
|
||||
network, with no transfer of a copy, is not conveying.
|
||||
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices" to the
|
||||
extent that it includes a convenient and prominently visible feature that
|
||||
(1) displays an appropriate copyright notice, and (2) tells the user that
|
||||
there is no warranty for the work (except to the extent that warranties are
|
||||
provided), that licensees may convey the work under this License, and how
|
||||
to view a copy of this License. If the interface presents a list of user commands
|
||||
or options, such as a menu, a prominent item in the list meets this criterion.
|
||||
|
||||
|
||||
1. Source Code.
|
||||
|
||||
|
||||
The "source code" for a work means the preferred form of the work for making
|
||||
modifications to it. "Object code" means any non-source form of a work.
|
||||
|
||||
|
||||
A "Standard Interface" means an interface that either is an official standard
|
||||
defined by a recognized standards body, or, in the case of interfaces specified
|
||||
for a particular programming language, one that is widely used among developers
|
||||
working in that language.
|
||||
|
||||
|
||||
The "System Libraries" of an executable work include anything, other than
|
||||
the work as a whole, that (a) is included in the normal form of packaging
|
||||
a Major Component, but which is not part of that Major Component, and (b)
|
||||
serves only to enable use of the work with that Major Component, or to implement
|
||||
a Standard Interface for which an implementation is available to the public
|
||||
in source code form. A "Major Component", in this context, means a major essential
|
||||
component (kernel, window system, and so on) of the specific operating system
|
||||