Compare commits
72 Commits
0.3.0
...
a505315781
Author | SHA1 | Date | |
---|---|---|---|
a505315781
|
|||
d76538cf28
|
|||
309a991007
|
|||
0340c83b47
|
|||
9dfd2cf70b
|
|||
26d2da923b
|
|||
c66c725ee3
|
|||
44f99295e9
|
|||
d417181b70
|
|||
9df5be003b
|
|||
cf3b1802d5
|
|||
4de97ca42e
|
|||
664959641f
|
|||
c1b0b4038c | |||
ba7d82bc2b
|
|||
e0a6485ed7
|
|||
5555269877
|
|||
3fcd1a96b2
|
|||
03e9c3dae5
|
|||
5ccf907ed8 | |||
8afbae1e1a
|
|||
164db8ebd1
|
|||
44d1825095
|
|||
1d071eafdb
|
|||
0decf317d9
|
|||
5e48e724a7
|
|||
46e3d1f1b6
|
|||
a3a89c6b64
|
|||
7ce67f57cd
|
|||
68d462eeee
|
|||
063b5405fc
|
|||
be591a961a
|
|||
8160641b8f
|
|||
86dfd69b4b
|
|||
74e8639435
|
|||
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 |
15
README.md
@ -1,9 +1,8 @@
|
||||
# teapod
|
||||
# Teapod
|
||||
|
||||
A unofficial 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.
|
||||
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://gitlab.com/IzzyOnDroid/repo/-/raw/master/assets/IzzyOnDroid.png" height="75">](https://apt.izzysoft.de/fdroid/index/apk/org.mosad.teapod)
|
||||
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" height="75">](https://f-droid.org/de/packages/org.mosad.teapod/)
|
||||
|
||||
## Features
|
||||
* Watch all animes from AoD on your Android device
|
||||
@ -18,10 +17,12 @@ Teapod is a unofficial App for Anime-on-Demand (AoD). It allows you to watch all
|
||||
[<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. Using this app may violates the ToS of AoD.
|
||||
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 parsing the website. The Parser is designed to be easy to maintain and as 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.
|
||||
|
||||
### [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.
|
||||
|
@ -10,8 +10,8 @@ android {
|
||||
applicationId "org.mosad.teapod"
|
||||
minSdkVersion 23
|
||||
targetSdkVersion 30
|
||||
versionCode 3000 //00.03.000
|
||||
versionName "0.3.0"
|
||||
versionCode 4200 //00.04.200
|
||||
versionName "0.5.0-alpha1"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
resValue "string", "build_time", buildTime()
|
||||
@ -29,6 +29,7 @@ android {
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
@ -41,35 +42,38 @@ android {
|
||||
dependencies {
|
||||
implementation fileTree(dir: "libs", include: ["*.jar"])
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.2'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.1'
|
||||
|
||||
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.2'
|
||||
implementation 'androidx.navigation:navigation-ui-ktx:2.3.2'
|
||||
implementation 'androidx.core:core-ktx:1.6.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.3.1'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.0'
|
||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5'
|
||||
implementation 'androidx.navigation:navigation-ui-ktx:2.3.5'
|
||||
implementation 'androidx.security:security-crypto:1.1.0-alpha03'
|
||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1'
|
||||
|
||||
implementation 'com.google.android.material:material:1.3.0-beta01'
|
||||
implementation 'com.google.code.gson:gson:2.8.6'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-core:2.12.2'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-hls:2.12.2'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-dash:2.12.2'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-ui:2.12.2'
|
||||
implementation 'com.google.android.material:material:1.4.0'
|
||||
implementation 'com.google.code.gson:gson:2.8.7'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-core:2.14.2'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-hls:2.14.2'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-dash:2.14.2'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-ui:2.14.2'
|
||||
implementation 'com.google.android.exoplayer:extension-mediasession:2.14.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'
|
||||
|
||||
testImplementation 'junit:junit:4.13.1'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||
|
||||
}
|
||||
|
||||
static def buildTime() {
|
||||
return new Date().format("yyyy-MM-dd", TimeZone.getTimeZone("UTC"))
|
||||
}
|
||||
}
|
||||
|
@ -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.Light">
|
||||
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=".player.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>
|
@ -40,23 +40,27 @@ object AoDParser {
|
||||
private const val baseUrl = "https://www.anime-on-demand.de"
|
||||
private const val loginPath = "/users/sign_in"
|
||||
private const val libraryPath = "/animes"
|
||||
private const val subscriptionPath = "/mypools"
|
||||
|
||||
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>() // actual media (data)
|
||||
val itemMediaList = arrayListOf<ItemMedia>() // gui media
|
||||
|
||||
// gui media
|
||||
val guiMediaList = arrayListOf<ItemMedia>()
|
||||
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)
|
||||
@ -78,7 +82,7 @@ object AoDParser {
|
||||
|
||||
val resLogin = Jsoup.connect(baseUrl + loginPath)
|
||||
.method(Connection.Method.POST)
|
||||
.timeout(60000) // login can take some time
|
||||
.timeout(60000) // login can take some time default is 60000 (60 sec)
|
||||
.data(data)
|
||||
.postDataCharset("UTF-8")
|
||||
.cookies(authCookies)
|
||||
@ -96,27 +100,20 @@ object AoDParser {
|
||||
|
||||
/**
|
||||
* initially load all media and home screen data
|
||||
* -> blocking
|
||||
*/
|
||||
fun initialLoading() = runBlocking {
|
||||
val loadHomeJob = GlobalScope.async {
|
||||
loadHome()
|
||||
suspend fun initialLoading() {
|
||||
coroutineScope {
|
||||
launch { loadHome() }
|
||||
launch { listAnimes() }
|
||||
}
|
||||
|
||||
val listJob = GlobalScope.async {
|
||||
listAnimes()
|
||||
}
|
||||
|
||||
loadHomeJob.await()
|
||||
listJob.await()
|
||||
}
|
||||
|
||||
/**
|
||||
* get a media by it's ID (int)
|
||||
* @return Media
|
||||
*/
|
||||
suspend fun getMediaById(mediaId: Int): Media {
|
||||
val media = mediaList.first { it.id == mediaId }
|
||||
suspend fun getMediaById(aodId: Int): Media {
|
||||
val media = mediaList.first { it.id == aodId }
|
||||
|
||||
if (media.episodes.isEmpty()) {
|
||||
loadStreams(media).join()
|
||||
@ -125,7 +122,27 @@ object AoDParser {
|
||||
return media
|
||||
}
|
||||
|
||||
fun markAsWatched(mediaId: Int, episodeId: Int) = GlobalScope.launch {
|
||||
/**
|
||||
* get subscription info from aod website, remove "Anime-Abo" Prefix and trim
|
||||
*/
|
||||
suspend fun getSubscriptionInfoAsync(): Deferred<String> {
|
||||
return coroutineScope {
|
||||
async(Dispatchers.IO) {
|
||||
val res = Jsoup.connect(baseUrl + subscriptionPath)
|
||||
.cookies(sessionCookies)
|
||||
.get()
|
||||
|
||||
return@async res.select("a:contains(Anime-Abo)").text()
|
||||
.removePrefix("Anime-Abo").trim()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getSubscriptionUrl(): String {
|
||||
return baseUrl + subscriptionPath
|
||||
}
|
||||
|
||||
suspend fun markAsWatched(mediaId: Int, episodeId: Int) {
|
||||
val episode = getMediaById(mediaId).getEpisodeById(episodeId)
|
||||
episode.watched = true
|
||||
sendCallback(episode.watchedCallback)
|
||||
@ -134,58 +151,70 @@ object AoDParser {
|
||||
}
|
||||
|
||||
// TODO don't use jsoup here
|
||||
fun sendCallback(callbackPath: String) = GlobalScope.launch(Dispatchers.IO) {
|
||||
val headers = mutableMapOf(
|
||||
Pair("Accept", "application/json, text/javascript, */*; q=0.01"),
|
||||
Pair("Accept-Language", "de,en-US;q=0.7,en;q=0.3"),
|
||||
Pair("Accept-Encoding", "gzip, deflate, br"),
|
||||
Pair("X-CSRF-Token", csrfToken),
|
||||
Pair("X-Requested-With", "XMLHttpRequest"),
|
||||
)
|
||||
private suspend fun sendCallback(callbackPath: String) = coroutineScope {
|
||||
launch(Dispatchers.IO) {
|
||||
val headers = mutableMapOf(
|
||||
Pair("Accept", "application/json, text/javascript, */*; q=0.01"),
|
||||
Pair("Accept-Language", "de,en-US;q=0.7,en;q=0.3"),
|
||||
Pair("Accept-Encoding", "gzip, deflate, br"),
|
||||
Pair("X-CSRF-Token", csrfToken),
|
||||
Pair("X-Requested-With", "XMLHttpRequest"),
|
||||
)
|
||||
|
||||
try {
|
||||
Jsoup.connect(baseUrl + callbackPath)
|
||||
.ignoreContentType(true)
|
||||
.cookies(sessionCookies)
|
||||
.headers(headers)
|
||||
.execute()
|
||||
} catch (ex: IOException) {
|
||||
Log.e(javaClass.name, "Callback for $callbackPath failed.", ex)
|
||||
try {
|
||||
Jsoup.connect(baseUrl + callbackPath)
|
||||
.ignoreContentType(true)
|
||||
.cookies(sessionCookies)
|
||||
.headers(headers)
|
||||
.execute()
|
||||
} catch (ex: IOException) {
|
||||
Log.e(javaClass.name, "Callback for $callbackPath failed.", ex)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* load all media from aod into itemMediaList and mediaList
|
||||
* TODO private suspend fun listAnimes() = withContext(Dispatchers.IO) should also work, maybe a bug in android studio?
|
||||
*/
|
||||
private fun listAnimes() = runBlocking {
|
||||
if (sessionCookies.isEmpty()) login()
|
||||
|
||||
withContext(Dispatchers.Default) {
|
||||
val resAnimes = Jsoup.connect(baseUrl + libraryPath)
|
||||
.cookies(sessionCookies)
|
||||
.get()
|
||||
|
||||
private suspend fun listAnimes() = withContext(Dispatchers.IO) {
|
||||
launch(Dispatchers.IO) {
|
||||
val resAnimes = Jsoup.connect(baseUrl + libraryPath).get()
|
||||
//println(resAnimes)
|
||||
|
||||
itemMediaList.clear()
|
||||
guiMediaList.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()
|
||||
val animes = resAnimes.select("div.animebox")
|
||||
|
||||
itemMediaList.add(ItemMedia(mediaId, mediaTitle, mediaImage))
|
||||
mediaList.add(Media(mediaId, mediaLink, type).apply {
|
||||
info.title = mediaTitle
|
||||
info.posterUrl = mediaImage
|
||||
guiMediaList.addAll(
|
||||
animes.map {
|
||||
ItemMedia(
|
||||
id = it.select("p.animebox-link").select("a")
|
||||
.attr("href").substringAfterLast("/").toInt(),
|
||||
title = it.select("h3.animebox-title").text(),
|
||||
posterUrl = it.select("p.animebox-image").select("img")
|
||||
.attr("src")
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
// TODO legacy
|
||||
resAnimes.select("div.animebox").forEach {
|
||||
val id = it.select("p.animebox-link").select("a").attr("href")
|
||||
.substringAfterLast("/").toInt()
|
||||
val title = it.select("h3.animebox-title").text()
|
||||
val image = it.select("p.animebox-image").select("img").attr("src")
|
||||
val link = it.select("p.animebox-link").select("a").attr("href")
|
||||
val type = when (it.select("p.animebox-link").select("a").text().lowercase(Locale.ROOT)) {
|
||||
"zur serie" -> MediaType.TVSHOW
|
||||
"zum film" -> MediaType.MOVIE
|
||||
else -> MediaType.OTHER
|
||||
}
|
||||
val mediaShortText = it.select("p.animebox-shorttext").text()
|
||||
|
||||
mediaList.add(Media(id, link, type).apply {
|
||||
info.title = title
|
||||
info.posterUrl = image
|
||||
info.shortDesc = mediaShortText
|
||||
})
|
||||
}
|
||||
@ -197,13 +226,9 @@ object AoDParser {
|
||||
/**
|
||||
* load new episodes, titles and highlights
|
||||
*/
|
||||
private fun loadHome() = runBlocking {
|
||||
if (sessionCookies.isEmpty()) login()
|
||||
|
||||
withContext(Dispatchers.Default) {
|
||||
val resHome = Jsoup.connect(baseUrl)
|
||||
.cookies(sessionCookies)
|
||||
.get()
|
||||
private suspend fun loadHome() = withContext(Dispatchers.IO) {
|
||||
launch(Dispatchers.IO) {
|
||||
val resHome = Jsoup.connect(baseUrl).get()
|
||||
|
||||
// get highlights from AoD
|
||||
highlightsList.clear()
|
||||
@ -257,6 +282,19 @@ object AoDParser {
|
||||
}
|
||||
}
|
||||
|
||||
// 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()) {
|
||||
@ -265,115 +303,129 @@ object AoDParser {
|
||||
highlightsList.add(ItemMedia(0,"", ""))
|
||||
}
|
||||
}
|
||||
|
||||
Log.i(javaClass.name, "loaded home")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO rework the media loading process, don't modify media object
|
||||
* TODO catch SocketTimeoutException from loading to show a waring dialog
|
||||
* load streams for the media path, movies have one episode
|
||||
* @param media is used as call ba reference
|
||||
*/
|
||||
private suspend fun loadStreams(media: Media) = GlobalScope.launch(Dispatchers.IO) {
|
||||
if (sessionCookies.isEmpty()) login()
|
||||
private suspend fun loadStreams(media: Media) = coroutineScope {
|
||||
launch(Dispatchers.IO) {
|
||||
if (sessionCookies.isEmpty()) login()
|
||||
|
||||
if (!loginSuccess) {
|
||||
Log.w(javaClass.name, "Login, was not successful.")
|
||||
return@launch
|
||||
}
|
||||
|
||||
// get the media page
|
||||
val res = Jsoup.connect(baseUrl + media.link)
|
||||
.cookies(sessionCookies)
|
||||
.get()
|
||||
|
||||
//println(res)
|
||||
|
||||
if (csrfToken.isEmpty()) {
|
||||
csrfToken = res.select("meta[name=csrf-token]").attr("content")
|
||||
//Log.i(javaClass.name, "New csrf token is $csrfToken")
|
||||
}
|
||||
|
||||
val besides = res.select("div.besides").first()
|
||||
val playlists = besides.select("input.streamstarter_html5").map { streamstarter ->
|
||||
parsePlaylistAsync(
|
||||
streamstarter.attr("data-playlist"),
|
||||
streamstarter.attr("data-lang")
|
||||
)
|
||||
}.awaitAll()
|
||||
|
||||
playlists.forEach { aod ->
|
||||
// TODO improve language handling
|
||||
val locale = when (aod.extLanguage) {
|
||||
"ger" -> Locale.GERMAN
|
||||
"jap" -> Locale.JAPANESE
|
||||
else -> Locale.ROOT
|
||||
if (!loginSuccess) {
|
||||
Log.w(javaClass.name, "Login, was not successful.")
|
||||
return@launch
|
||||
}
|
||||
|
||||
aod.playlist.forEach { ep ->
|
||||
try {
|
||||
if (media.hasEpisode(ep.mediaid)) {
|
||||
media.getEpisodeById(ep.mediaid).streams.add(
|
||||
Stream(ep.sources.first().file, locale)
|
||||
)
|
||||
} else {
|
||||
media.episodes.add(Episode(
|
||||
id = ep.mediaid,
|
||||
streams = mutableListOf(Stream(ep.sources.first().file, locale)),
|
||||
posterUrl = ep.image,
|
||||
title = ep.title,
|
||||
description = ep.description,
|
||||
number = getNumberFromTitle(ep.title, media.type)
|
||||
))
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
Log.w(javaClass.name, "Could not parse episode information.", ex)
|
||||
}
|
||||
// get the media page
|
||||
val res = Jsoup.connect(baseUrl + media.link)
|
||||
.cookies(sessionCookies)
|
||||
.get()
|
||||
|
||||
//println(res)
|
||||
|
||||
if (csrfToken.isEmpty()) {
|
||||
csrfToken = res.select("meta[name=csrf-token]").attr("content")
|
||||
//Log.i(javaClass.name, "New csrf token is $csrfToken")
|
||||
}
|
||||
}
|
||||
Log.i(javaClass.name, "Loaded playlists successfully")
|
||||
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
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()
|
||||
|
||||
// parse additional information for tv shows the episode title (description) is loaded from the "api"
|
||||
if (media.type == MediaType.TVSHOW) {
|
||||
res.select("div.three-box-container > div.episodebox").forEach { episodebox ->
|
||||
// make sure the episode has a streaming link
|
||||
if (episodebox.select("input.streamstarter_html5").isNotEmpty()) {
|
||||
val episodeId = episodebox.select("div.flip-front").attr("id").substringAfter("-").toInt()
|
||||
val episodeShortDesc = episodebox.select("p.episodebox-shorttext").text()
|
||||
val episodeWatched = episodebox.select("div.episodebox-icons > div").hasClass("status-icon-orange")
|
||||
val episodeWatchedCallback = episodebox.select("input.streamstarter_html5").eachAttr("data-playlist").first()
|
||||
|
||||
media.episodes.firstOrNull { it.id == episodeId }?.apply {
|
||||
shortDesc = episodeShortDesc
|
||||
watched = episodeWatched
|
||||
watchedCallback = episodeWatchedCallback
|
||||
playlists.forEach { aod ->
|
||||
aod.list.forEach { ep ->
|
||||
try {
|
||||
if (media.hasEpisode(ep.mediaid)) {
|
||||
media.getEpisodeById(ep.mediaid).streams.add(
|
||||
Stream(ep.sources.first().file, aod.language)
|
||||
)
|
||||
} else {
|
||||
media.episodes.add(Episode(
|
||||
id = ep.mediaid,
|
||||
streams = mutableListOf(Stream(ep.sources.first().file, aod.language)),
|
||||
posterUrl = ep.image,
|
||||
title = ep.title,
|
||||
description = ep.description,
|
||||
number = getNumberFromTitle(ep.title, media.type)
|
||||
))
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
Log.w(javaClass.name, "Could not parse episode information.", ex)
|
||||
}
|
||||
}
|
||||
}
|
||||
Log.i(javaClass.name, "Loaded playlists successfully")
|
||||
|
||||
// additional info from the media page
|
||||
res.select("table.vertical-table").select("tr").forEach { row ->
|
||||
when (row.select("th").text().lowercase(Locale.ROOT)) {
|
||||
"produktionsjahr" -> media.info.year = row.select("td").text().toInt()
|
||||
"fsk" -> media.info.age = row.select("td").text().toInt()
|
||||
"episodenanzahl" -> {
|
||||
media.info.episodesCount = row.select("td").text()
|
||||
.substringBefore("/")
|
||||
.filter { it.isDigit() }
|
||||
.toInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// similar titles from media page
|
||||
media.info.similar = res.select("h2:contains(Ähnliche Animes)").next().select("li").mapNotNull {
|
||||
val mediaId = it.select("a.thumbs").attr("href")
|
||||
.substringAfterLast("/").toIntOrNull()
|
||||
val mediaImage = it.select("a.thumbs > img").attr("src")
|
||||
val mediaTitle = it.select("a").text()
|
||||
|
||||
if (mediaId != null) {
|
||||
ItemMedia(mediaId, mediaTitle, mediaImage)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
// additional information for tv shows the episode title (description) is loaded from the "api"
|
||||
if (media.type == MediaType.TVSHOW) {
|
||||
res.select("div.three-box-container > div.episodebox").forEach { episodebox ->
|
||||
// make sure the episode has a streaming link
|
||||
if (episodebox.select("input.streamstarter_html5").isNotEmpty()) {
|
||||
val episodeId = episodebox.select("div.flip-front").attr("id").substringAfter("-").toInt()
|
||||
val episodeShortDesc = episodebox.select("p.episodebox-shorttext").text()
|
||||
val episodeWatched = episodebox.select("div.episodebox-icons > div").hasClass("status-icon-orange")
|
||||
val episodeWatchedCallback = episodebox.select("input.streamstarter_html5").eachAttr("data-playlist").first()
|
||||
|
||||
media.episodes.firstOrNull { it.id == episodeId }?.apply {
|
||||
shortDesc = episodeShortDesc
|
||||
watched = episodeWatched
|
||||
watchedCallback = episodeWatchedCallback
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Log.i(javaClass.name, "media loaded successfully")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* don't use Gson().fromJson() as we don't have any control over the api and it may change
|
||||
*/
|
||||
private fun parsePlaylistAsync(playlistPath: String, language: String): Deferred<AoDObject> {
|
||||
private fun parsePlaylistAsync(playlistPath: String, language: String): Deferred<AoDPlaylist> {
|
||||
if (playlistPath == "[]") {
|
||||
return CompletableDeferred(AoDObject(listOf(), language))
|
||||
return CompletableDeferred(AoDPlaylist(listOf(), Locale.ROOT))
|
||||
}
|
||||
|
||||
return GlobalScope.async(Dispatchers.IO) {
|
||||
return CoroutineScope(Dispatchers.IO).async(Dispatchers.IO) {
|
||||
val headers = mutableMapOf(
|
||||
Pair("Accept", "application/json, text/javascript, */*; q=0.01"),
|
||||
Pair("Accept-Language", "de,en-US;q=0.7,en;q=0.3"),
|
||||
@ -388,11 +440,12 @@ 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)
|
||||
|
||||
return@async AoDObject(JsonParser.parseString(res.body()).asJsonObject
|
||||
return@async AoDPlaylist(JsonParser.parseString(res.body()).asJsonObject
|
||||
.get("playlist").asJsonArray.map {
|
||||
Playlist(
|
||||
sources = it.asJsonObject.get("sources").asJsonArray.map { source ->
|
||||
@ -404,7 +457,12 @@ object AoDParser {
|
||||
mediaid = it.asJsonObject.get("mediaid").asInt
|
||||
)
|
||||
},
|
||||
language
|
||||
// TODO improve language handling (via display language etc.)
|
||||
language = when (language) {
|
||||
"ger" -> Locale.GERMAN
|
||||
"jap" -> Locale.JAPANESE
|
||||
else -> Locale.ROOT
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,9 @@ object Preferences {
|
||||
internal set
|
||||
var autoplay = true
|
||||
internal set
|
||||
var theme = DataTypes.Theme.LIGHT
|
||||
var devSettings = false
|
||||
internal set
|
||||
var theme = DataTypes.Theme.DARK
|
||||
internal set
|
||||
|
||||
private fun getSharedPref(context: Context): SharedPreferences {
|
||||
@ -39,6 +41,15 @@ object Preferences {
|
||||
this.autoplay = autoplay
|
||||
}
|
||||
|
||||
fun saveDevSettings(context: Context, devSettings: Boolean) {
|
||||
with(getSharedPref(context).edit()) {
|
||||
putBoolean(context.getString(R.string.save_key_dev_settings), devSettings)
|
||||
apply()
|
||||
}
|
||||
|
||||
this.devSettings = devSettings
|
||||
}
|
||||
|
||||
fun saveTheme(context: Context, theme: DataTypes.Theme) {
|
||||
with(getSharedPref(context).edit()) {
|
||||
putString(context.getString(R.string.save_key_theme), theme.toString())
|
||||
@ -60,10 +71,13 @@ object Preferences {
|
||||
autoplay = sharedPref.getBoolean(
|
||||
context.getString(R.string.save_key_autoplay), true
|
||||
)
|
||||
devSettings = sharedPref.getBoolean(
|
||||
context.getString(R.string.save_key_dev_settings), false
|
||||
)
|
||||
theme = DataTypes.Theme.valueOf(
|
||||
sharedPref.getString(
|
||||
context.getString(R.string.save_key_theme), DataTypes.Theme.LIGHT.toString()
|
||||
) ?: DataTypes.Theme.LIGHT.toString()
|
||||
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() {
|
@ -20,7 +20,7 @@
|
||||
*
|
||||
*/
|
||||
|
||||
package org.mosad.teapod
|
||||
package org.mosad.teapod.ui.activity.main
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
@ -29,25 +29,41 @@ import android.view.MenuItem
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.commit
|
||||
import com.google.android.material.bottomnavigation.BottomNavigationView
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.afollestad.materialdialogs.callbacks.onDismiss
|
||||
import com.google.android.material.navigation.NavigationBarView
|
||||
import kotlinx.coroutines.*
|
||||
import org.mosad.teapod.R
|
||||
import org.mosad.teapod.databinding.ActivityMainBinding
|
||||
import org.mosad.teapod.parser.AoDParser
|
||||
import org.mosad.teapod.player.PlayerActivity
|
||||
import org.mosad.teapod.preferences.EncryptedPreferences
|
||||
import org.mosad.teapod.preferences.Preferences
|
||||
import org.mosad.teapod.ui.activity.main.fragments.AccountFragment
|
||||
import org.mosad.teapod.ui.activity.main.fragments.HomeFragment
|
||||
import org.mosad.teapod.ui.activity.main.fragments.LibraryFragment
|
||||
import org.mosad.teapod.ui.activity.main.fragments.SearchFragment
|
||||
import org.mosad.teapod.ui.activity.onboarding.OnboardingActivity
|
||||
import org.mosad.teapod.ui.activity.player.PlayerActivity
|
||||
import org.mosad.teapod.ui.components.LoginDialog
|
||||
import org.mosad.teapod.ui.fragments.*
|
||||
import org.mosad.teapod.util.DataTypes
|
||||
import org.mosad.teapod.util.MetaDBController
|
||||
import org.mosad.teapod.util.StorageController
|
||||
import org.mosad.teapod.util.exitAndRemoveTask
|
||||
import java.net.SocketTimeoutException
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemSelectedListener {
|
||||
class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListener {
|
||||
|
||||
private lateinit var binding: ActivityMainBinding
|
||||
private var activeBaseFragment: Fragment = HomeFragment() // the currently active fragment, home at the start
|
||||
|
||||
companion object {
|
||||
var wasInitialized = false
|
||||
lateinit var instance: MainActivity
|
||||
}
|
||||
|
||||
init {
|
||||
instance = this
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@ -57,7 +73,7 @@ class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemS
|
||||
theme.applyStyle(getThemeResource(), true)
|
||||
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
binding.navView.setOnNavigationItemSelectedListener(this)
|
||||
binding.navView.setOnItemSelectedListener(this)
|
||||
setContentView(binding.root)
|
||||
|
||||
supportFragmentManager.commit {
|
||||
@ -111,39 +127,62 @@ class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemS
|
||||
|
||||
private fun getThemeResource(): Int {
|
||||
return when (Preferences.theme) {
|
||||
DataTypes.Theme.DARK -> R.style.AppTheme_Dark
|
||||
else -> R.style.AppTheme_Light
|
||||
DataTypes.Theme.LIGHT -> R.style.AppTheme_Light
|
||||
else -> R.style.AppTheme_Dark
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* initial loading and login are run in parallel, as initial loading doesn't require
|
||||
* any login cookies
|
||||
*/
|
||||
private fun load() {
|
||||
// running login and list in parallel does not bring any speed improvements
|
||||
val time = measureTimeMillis {
|
||||
Preferences.load(this)
|
||||
// start the initial loading
|
||||
val loadingJob = CoroutineScope(Dispatchers.IO + CoroutineName("InitialLoadingScope"))
|
||||
.async {
|
||||
launch { AoDParser.initialLoading() }
|
||||
launch { MetaDBController.list() }
|
||||
}
|
||||
|
||||
// make sure credentials are set, run's async
|
||||
// load all saved stuff here
|
||||
Preferences.load(this)
|
||||
EncryptedPreferences.readCredentials(this)
|
||||
StorageController.load(this)
|
||||
|
||||
// show onboarding
|
||||
if (EncryptedPreferences.password.isEmpty()) {
|
||||
showLoginDialog(true)
|
||||
showOnboarding()
|
||||
} else {
|
||||
// try to login in, as most sites can only bee loaded once loged in
|
||||
if (!AoDParser.login()) showLoginDialog(false)
|
||||
try {
|
||||
if (!AoDParser.login()) {
|
||||
showLoginDialog()
|
||||
}
|
||||
} catch (ex: SocketTimeoutException) {
|
||||
Log.w(javaClass.name, "Timeout during login!")
|
||||
|
||||
// show waring dialog before finishing
|
||||
MaterialDialog(this).show {
|
||||
title(R.string.dialog_timeout_head)
|
||||
message(R.string.dialog_timeout_desc)
|
||||
onDismiss { exitAndRemoveTask() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StorageController.load(this)
|
||||
AoDParser.initialLoading()
|
||||
|
||||
wasInitialized = true
|
||||
runBlocking { loadingJob.await() } // wait for initial loading to finish
|
||||
}
|
||||
Log.i(javaClass.name, "login and list in $time ms")
|
||||
Log.i(javaClass.name, "loading and login in $time ms")
|
||||
|
||||
wasInitialized = true
|
||||
}
|
||||
|
||||
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 {
|
||||
@ -153,18 +192,16 @@ class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemS
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a fragment on top of the current fragment.
|
||||
* The current fragment is replaced and the new one is added
|
||||
* to the back stack.
|
||||
* start the onboarding activity and finish the main activity
|
||||
*/
|
||||
fun showFragment(fragment: Fragment) {
|
||||
supportFragmentManager.commit {
|
||||
replace(R.id.nav_host_fragment, fragment, fragment.javaClass.simpleName)
|
||||
addToBackStack(fragment.javaClass.name)
|
||||
show(fragment)
|
||||
}
|
||||
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)
|
||||
@ -183,5 +220,4 @@ class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemS
|
||||
startActivity(restartIntent)
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -1,9 +1,12 @@
|
||||
package org.mosad.teapod.ui.fragments
|
||||
package org.mosad.teapod.ui.activity.main.fragments
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.RawRes
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
@ -11,14 +14,21 @@ import org.mosad.teapod.BuildConfig
|
||||
import org.mosad.teapod.R
|
||||
import org.mosad.teapod.databinding.FragmentAboutBinding
|
||||
import org.mosad.teapod.databinding.ItemComponentBinding
|
||||
import org.mosad.teapod.preferences.Preferences
|
||||
import org.mosad.teapod.util.DataTypes.License
|
||||
import org.mosad.teapod.util.ThirdPartyComponent
|
||||
import java.lang.StringBuilder
|
||||
import java.util.Timer
|
||||
import kotlin.concurrent.schedule
|
||||
|
||||
class AboutFragment : Fragment() {
|
||||
|
||||
private lateinit var binding: FragmentAboutBinding
|
||||
|
||||
private val teapodRepoUrl = "https://git.mosad.xyz/Seil0/teapod"
|
||||
private val devClickMax = 5
|
||||
private var devClickCount = 0
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
binding = FragmentAboutBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
@ -27,7 +37,7 @@ class AboutFragment : Fragment() {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.textVersion.text = getString(R.string.info_about_desc, BuildConfig.VERSION_NAME, getString(R.string.build_time))
|
||||
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)
|
||||
@ -44,10 +54,53 @@ class AboutFragment : Fragment() {
|
||||
|
||||
binding.linearThirdParty.addView(componentBinding.root)
|
||||
}
|
||||
|
||||
initActions()
|
||||
}
|
||||
|
||||
private fun initActions() {
|
||||
binding.imageAppIcon.setOnClickListener {
|
||||
checkDevSettings()
|
||||
}
|
||||
|
||||
binding.linearSource.setOnClickListener {
|
||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(teapodRepoUrl)))
|
||||
}
|
||||
|
||||
binding.linearLicense.setOnClickListener {
|
||||
MaterialDialog(requireContext())
|
||||
.title(text = License.GPL3.long)
|
||||
.message(text = parseLicense(R.raw.gpl_3_full))
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* check if dev settings shall be enabled
|
||||
*/
|
||||
private fun checkDevSettings() {
|
||||
// if the dev settings are already enabled show a toast
|
||||
if (Preferences.devSettings) {
|
||||
Toast.makeText(context, getString(R.string.dev_settings_already), Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
|
||||
// reset dev settings count after 5 seconds
|
||||
if (devClickCount == 0) {
|
||||
Timer("", false).schedule(5000) {
|
||||
devClickCount = 0
|
||||
}
|
||||
}
|
||||
devClickCount++
|
||||
|
||||
if (devClickCount == devClickMax) {
|
||||
Preferences.saveDevSettings(requireContext(), true)
|
||||
Toast.makeText(context, getString(R.string.dev_settings_enabled), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getThirdPartyComponents(): List<ThirdPartyComponent> {
|
||||
return listOf<ThirdPartyComponent>(
|
||||
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",
|
||||
@ -79,13 +132,10 @@ class AboutFragment : Fragment() {
|
||||
License.MIT -> parseLicense(R.raw.mit_full)
|
||||
}
|
||||
|
||||
println("showing: ${license.long}")
|
||||
|
||||
MaterialDialog(requireContext())
|
||||
.title(text = license.long)
|
||||
.message(text = licenseText)
|
||||
.show()
|
||||
|
||||
}
|
||||
|
||||
private fun parseLicense(@RawRes id: Int): String {
|
@ -1,27 +1,59 @@
|
||||
package org.mosad.teapod.ui.fragments
|
||||
package org.mosad.teapod.ui.activity.main.fragments
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.afollestad.materialdialogs.list.listItemsSingleChoice
|
||||
import kotlinx.coroutines.launch
|
||||
import org.mosad.teapod.BuildConfig
|
||||
import org.mosad.teapod.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.activity.main.MainActivity
|
||||
import org.mosad.teapod.ui.components.LoginDialog
|
||||
import org.mosad.teapod.util.DataTypes.Theme
|
||||
import org.mosad.teapod.util.StorageController
|
||||
import org.mosad.teapod.util.showFragment
|
||||
|
||||
class AccountFragment : Fragment() {
|
||||
|
||||
private lateinit var binding: FragmentAccountBinding
|
||||
|
||||
private val getUriExport = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode == Activity.RESULT_OK) {
|
||||
result.data?.data?.also { uri ->
|
||||
StorageController.exportMyList(requireContext(), uri)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val getUriImport = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode == Activity.RESULT_OK) {
|
||||
result.data?.data?.also { uri ->
|
||||
val success = StorageController.importMyList(requireContext(), uri)
|
||||
if (success == 0) {
|
||||
Toast.makeText(
|
||||
context, getString(R.string.import_data_success),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
binding = FragmentAccountBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
@ -30,6 +62,15 @@ class AccountFragment : Fragment() {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
// load subscription (async) info before anything else
|
||||
binding.textAccountSubscription.text = getString(R.string.account_subscription, getString(R.string.loading))
|
||||
lifecycleScope.launch {
|
||||
binding.textAccountSubscription.text = getString(
|
||||
R.string.account_subscription,
|
||||
AoDParser.getSubscriptionInfoAsync().await()
|
||||
)
|
||||
}
|
||||
|
||||
binding.textAccountLogin.text = EncryptedPreferences.login
|
||||
binding.textInfoAboutDesc.text = getString(R.string.info_about_desc, BuildConfig.VERSION_NAME, getString(R.string.build_time))
|
||||
binding.textThemeSelected.text = when (Preferences.theme) {
|
||||
@ -40,6 +81,8 @@ class AccountFragment : Fragment() {
|
||||
binding.switchSecondary.isChecked = Preferences.preferSecondary
|
||||
binding.switchAutoplay.isChecked = Preferences.autoplay
|
||||
|
||||
binding.linearDevSettings.isVisible = Preferences.devSettings
|
||||
|
||||
initActions()
|
||||
}
|
||||
|
||||
@ -48,12 +91,16 @@ class AccountFragment : Fragment() {
|
||||
showLoginDialog(true)
|
||||
}
|
||||
|
||||
binding.linearAccountSubscription.setOnClickListener {
|
||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(AoDParser.getSubscriptionUrl())))
|
||||
}
|
||||
|
||||
binding.linearTheme.setOnClickListener {
|
||||
showThemeDialog()
|
||||
}
|
||||
|
||||
binding.linearInfo.setOnClickListener {
|
||||
(activity as MainActivity).showFragment(AboutFragment())
|
||||
activity?.showFragment(AboutFragment())
|
||||
}
|
||||
|
||||
binding.switchSecondary.setOnClickListener {
|
||||
@ -63,6 +110,23 @@ class AccountFragment : Fragment() {
|
||||
binding.switchAutoplay.setOnClickListener {
|
||||
Preferences.saveAutoplay(requireContext(), binding.switchAutoplay.isChecked)
|
||||
}
|
||||
|
||||
binding.linearExportData.setOnClickListener {
|
||||
val i = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = "text/json"
|
||||
putExtra(Intent.EXTRA_TITLE, "my-list.json")
|
||||
}
|
||||
getUriExport.launch(i)
|
||||
}
|
||||
|
||||
binding.linearImportData.setOnClickListener {
|
||||
val i = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = "*/*"
|
||||
}
|
||||
getUriImport.launch(i)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showLoginDialog(firstTry: Boolean) {
|
||||
@ -91,11 +155,12 @@ class AccountFragment : Fragment() {
|
||||
when(index) {
|
||||
0 -> Preferences.saveTheme(context, Theme.LIGHT)
|
||||
1 -> Preferences.saveTheme(context, Theme.DARK)
|
||||
else -> Preferences.saveTheme(context, Theme.LIGHT)
|
||||
else -> Preferences.saveTheme(context, Theme.DARK)
|
||||
}
|
||||
|
||||
(activity as MainActivity).restart()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,27 +1,24 @@
|
||||
package org.mosad.teapod.ui.fragments
|
||||
package org.mosad.teapod.ui.activity.main.fragments
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.request.target.CustomTarget
|
||||
import com.bumptech.glide.request.transition.Transition
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.mosad.teapod.MainActivity
|
||||
import org.mosad.teapod.R
|
||||
import org.mosad.teapod.databinding.FragmentHomeBinding
|
||||
import org.mosad.teapod.parser.AoDParser
|
||||
import org.mosad.teapod.ui.activity.main.MainActivity
|
||||
import org.mosad.teapod.util.ItemMedia
|
||||
import org.mosad.teapod.util.StorageController
|
||||
import org.mosad.teapod.util.adapter.MediaItemAdapter
|
||||
import org.mosad.teapod.util.decoration.MediaItemDecoration
|
||||
import org.mosad.teapod.util.setDrawableTop
|
||||
import org.mosad.teapod.util.showFragment
|
||||
|
||||
class HomeFragment : Fragment() {
|
||||
|
||||
@ -30,6 +27,7 @@ class HomeFragment : Fragment() {
|
||||
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
|
||||
|
||||
@ -41,7 +39,7 @@ class HomeFragment : Fragment() {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
GlobalScope.launch(Dispatchers.Main) {
|
||||
lifecycleScope.launch {
|
||||
context?.let {
|
||||
initHighlight()
|
||||
initRecyclerViews()
|
||||
@ -59,9 +57,9 @@ class HomeFragment : Fragment() {
|
||||
.into(binding.imageHighlight)
|
||||
|
||||
if (StorageController.myList.contains(highlightMedia.id)) {
|
||||
loadIntoCompoundDrawable(R.drawable.ic_baseline_check_24, binding.textHighlightMyList)
|
||||
binding.textHighlightMyList.setDrawableTop(R.drawable.ic_baseline_check_24)
|
||||
} else {
|
||||
loadIntoCompoundDrawable(R.drawable.ic_baseline_add_24, binding.textHighlightMyList)
|
||||
binding.textHighlightMyList.setDrawableTop(R.drawable.ic_baseline_add_24)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -71,17 +69,10 @@ class HomeFragment : Fragment() {
|
||||
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)
|
||||
adapterMyList.onItemClick = { mediaId, _ ->
|
||||
(activity as MainActivity).showFragment(MediaFragment(mediaId))
|
||||
}
|
||||
adapterMyList = MediaItemAdapter(mapMyListToItemMedia())
|
||||
binding.recyclerMyList.adapter = adapterMyList
|
||||
|
||||
// new episodes
|
||||
@ -95,12 +86,16 @@ class HomeFragment : Fragment() {
|
||||
// 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 {
|
||||
lifecycleScope.launch {
|
||||
val media = AoDParser.getMediaById(highlightMedia.id)
|
||||
|
||||
Log.d(javaClass.name, "Starting Player with mediaId: ${media.id}")
|
||||
@ -111,10 +106,10 @@ class HomeFragment : Fragment() {
|
||||
binding.textHighlightMyList.setOnClickListener {
|
||||
if (StorageController.myList.contains(highlightMedia.id)) {
|
||||
StorageController.myList.remove(highlightMedia.id)
|
||||
loadIntoCompoundDrawable(R.drawable.ic_baseline_add_24, binding.textHighlightMyList)
|
||||
binding.textHighlightMyList.setDrawableTop(R.drawable.ic_baseline_add_24)
|
||||
} else {
|
||||
StorageController.myList.add(highlightMedia.id)
|
||||
loadIntoCompoundDrawable(R.drawable.ic_baseline_check_24, binding.textHighlightMyList)
|
||||
binding.textHighlightMyList.setDrawableTop(R.drawable.ic_baseline_check_24)
|
||||
}
|
||||
StorageController.saveMyList(requireContext())
|
||||
|
||||
@ -122,19 +117,27 @@ class HomeFragment : Fragment() {
|
||||
}
|
||||
|
||||
binding.textHighlightInfo.setOnClickListener {
|
||||
(activity as MainActivity).showFragment(MediaFragment(highlightMedia.id))
|
||||
activity?.showFragment(MediaFragment(highlightMedia.id))
|
||||
}
|
||||
|
||||
adapterNewEpisodes.onItemClick = { mediaId, _ ->
|
||||
(activity as MainActivity).showFragment(MediaFragment(mediaId))
|
||||
adapterMyList.onItemClick = { id, _ ->
|
||||
activity?.showFragment(MediaFragment(id))
|
||||
}
|
||||
|
||||
adapterNewSimulcasts.onItemClick = { mediaId, _ ->
|
||||
(activity as MainActivity).showFragment(MediaFragment(mediaId))
|
||||
adapterNewEpisodes.onItemClick = { id, _ ->
|
||||
activity?.showFragment(MediaFragment(id))
|
||||
}
|
||||
|
||||
adapterNewTitles.onItemClick = { mediaId, _ ->
|
||||
(activity as MainActivity).showFragment(MediaFragment(mediaId))
|
||||
adapterNewSimulcasts.onItemClick = { id, _ ->
|
||||
activity?.showFragment(MediaFragment(id))
|
||||
}
|
||||
|
||||
adapterNewTitles.onItemClick = { id, _ ->
|
||||
activity?.showFragment(MediaFragment(id))
|
||||
}
|
||||
|
||||
adapterTopTen.onItemClick = { id, _ ->
|
||||
activity?.showFragment(MediaFragment(id))
|
||||
}
|
||||
}
|
||||
|
||||
@ -145,28 +148,19 @@ class HomeFragment : Fragment() {
|
||||
* * 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.updateMediaList(mapMyListToItemMedia())
|
||||
adapterMyList.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
private fun loadIntoCompoundDrawable(drawable: Int, textView: TextView) {
|
||||
Glide.with(requireContext())
|
||||
.load(drawable)
|
||||
.into(object : CustomTarget<Drawable>(48, 48) {
|
||||
override fun onLoadCleared(drawable: Drawable?) {
|
||||
textView.setCompoundDrawablesWithIntrinsicBounds(null, drawable, null, null)
|
||||
private fun mapMyListToItemMedia(): List<ItemMedia> {
|
||||
return StorageController.myList.mapNotNull { elementId ->
|
||||
AoDParser.guiMediaList.firstOrNull { it.id == elementId }.also {
|
||||
// it the my list entry wasn't found in itemMediaList Log it
|
||||
if (it == null) {
|
||||
Log.w(javaClass.name, "The element with the id $elementId was not found.")
|
||||
}
|
||||
|
||||
override fun onResourceReady(res: Drawable, transition: Transition<in Drawable>?) {
|
||||
textView.setCompoundDrawablesWithIntrinsicBounds(null, res, null, null)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,19 +1,17 @@
|
||||
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.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.mosad.teapod.MainActivity
|
||||
import org.mosad.teapod.databinding.FragmentLibraryBinding
|
||||
import org.mosad.teapod.parser.AoDParser
|
||||
import org.mosad.teapod.util.adapter.MediaItemAdapter
|
||||
import org.mosad.teapod.util.decoration.MediaItemDecoration
|
||||
import org.mosad.teapod.util.showFragment
|
||||
|
||||
class LibraryFragment : Fragment() {
|
||||
|
||||
@ -29,18 +27,16 @@ class LibraryFragment : Fragment() {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
// init async
|
||||
GlobalScope.launch {
|
||||
lifecycleScope.launch {
|
||||
// create and set the adapter, needs context
|
||||
withContext(Dispatchers.Main) {
|
||||
context?.let {
|
||||
adapter = MediaItemAdapter(AoDParser.itemMediaList)
|
||||
adapter.onItemClick = { mediaId, _ ->
|
||||
(activity as MainActivity).showFragment(MediaFragment(mediaId))
|
||||
}
|
||||
|
||||
binding.recyclerMediaLibrary.adapter = adapter
|
||||
binding.recyclerMediaLibrary.addItemDecoration(MediaItemDecoration(9))
|
||||
context?.let {
|
||||
adapter = MediaItemAdapter(AoDParser.guiMediaList)
|
||||
adapter.onItemClick = { mediaId, _ ->
|
||||
activity?.showFragment(MediaFragment(mediaId))
|
||||
}
|
||||
|
||||
binding.recyclerMediaLibrary.adapter = adapter
|
||||
binding.recyclerMediaLibrary.addItemDecoration(MediaItemDecoration(9))
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,216 @@
|
||||
package org.mosad.teapod.ui.activity.main.fragments
|
||||
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import jp.wasabeef.glide.transformations.BlurTransformation
|
||||
import kotlinx.coroutines.launch
|
||||
import org.mosad.teapod.R
|
||||
import org.mosad.teapod.databinding.FragmentMediaBinding
|
||||
import org.mosad.teapod.ui.activity.main.MainActivity
|
||||
import org.mosad.teapod.ui.activity.main.viewmodel.MediaFragmentViewModel
|
||||
import org.mosad.teapod.util.DataTypes.MediaType
|
||||
import org.mosad.teapod.util.Episode
|
||||
import org.mosad.teapod.util.StorageController
|
||||
import org.mosad.teapod.util.tmdb.TMDBMovie
|
||||
import org.mosad.teapod.util.tmdb.TMDBApiController
|
||||
|
||||
/**
|
||||
* The media detail fragment.
|
||||
* Note: the fragment is created only once, when selecting a similar title etc.
|
||||
* therefore fragments may be not empty and model may be the old one
|
||||
*/
|
||||
class MediaFragment(private val mediaId: Int) : Fragment() {
|
||||
|
||||
private lateinit var binding: FragmentMediaBinding
|
||||
private lateinit var pagerAdapter: FragmentStateAdapter
|
||||
|
||||
private val fragments = arrayListOf<Fragment>()
|
||||
|
||||
private val model: MediaFragmentViewModel by activityViewModels()
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
binding = FragmentMediaBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
binding.frameLoading.visibility = View.VISIBLE
|
||||
|
||||
// tab layout and pager
|
||||
pagerAdapter = ScreenSlidePagerAdapter(requireActivity())
|
||||
// fix material components issue #1878, if more tabs are added increase
|
||||
binding.pagerEpisodesSimilar.offscreenPageLimit = 2
|
||||
binding.pagerEpisodesSimilar.adapter = pagerAdapter
|
||||
TabLayoutMediator(binding.tabEpisodesSimilar, binding.pagerEpisodesSimilar) { tab, position ->
|
||||
tab.text = if (model.media.type == MediaType.TVSHOW && position == 0) {
|
||||
getString(R.string.episodes)
|
||||
} else {
|
||||
getString(R.string.similar_titles)
|
||||
}
|
||||
}.attach()
|
||||
|
||||
lifecycleScope.launch {
|
||||
model.load(mediaId) // load the streams and tmdb for the selected media
|
||||
|
||||
updateGUI()
|
||||
initActions()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
// update the next ep text if there is one, since it may have changed
|
||||
if (model.nextEpisode.title.isNotEmpty()) {
|
||||
binding.textTitle.text = model.nextEpisode.title
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* if tmdb data is present, use it, else use the aod data
|
||||
*/
|
||||
private fun updateGUI() = with(model) {
|
||||
// generic gui
|
||||
val backdropUrl = tmdbResult?.backdropPath?.let { TMDBApiController.imageUrl + it }
|
||||
?: media.info.posterUrl
|
||||
val posterUrl = tmdbResult?.posterPath?.let { TMDBApiController.imageUrl + it }
|
||||
?: media.info.posterUrl
|
||||
|
||||
// load poster and backdrop
|
||||
Glide.with(requireContext()).load(posterUrl)
|
||||
.into(binding.imagePoster)
|
||||
Glide.with(requireContext()).load(backdropUrl)
|
||||
.apply(RequestOptions.placeholderOf(ColorDrawable(Color.DKGRAY)))
|
||||
.apply(RequestOptions.bitmapTransform(BlurTransformation(20, 3)))
|
||||
.into(binding.imageBackdrop)
|
||||
|
||||
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
|
||||
|
||||
// set "my list" indicator
|
||||
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) {
|
||||
val tmdbMovie = (tmdbResult as TMDBMovie?)
|
||||
|
||||
if (tmdbMovie?.runtime != null) {
|
||||
binding.textEpisodesOrRuntime.text = resources.getQuantityString(
|
||||
R.plurals.text_runtime,
|
||||
tmdbMovie.runtime,
|
||||
tmdbMovie.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, model.tmdbTVSeason?.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,12 +6,13 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.SearchView
|
||||
import androidx.fragment.app.Fragment
|
||||
import kotlinx.coroutines.*
|
||||
import org.mosad.teapod.MainActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.mosad.teapod.databinding.FragmentSearchBinding
|
||||
import org.mosad.teapod.parser.AoDParser
|
||||
import org.mosad.teapod.util.decoration.MediaItemDecoration
|
||||
import org.mosad.teapod.util.adapter.MediaItemAdapter
|
||||
import org.mosad.teapod.util.showFragment
|
||||
|
||||
class SearchFragment : Fragment() {
|
||||
|
||||
@ -26,20 +27,18 @@ class SearchFragment : Fragment() {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
GlobalScope.launch {
|
||||
lifecycleScope.launch {
|
||||
// create and set the adapter, needs context
|
||||
withContext(Dispatchers.Main) {
|
||||
context?.let {
|
||||
adapter = MediaItemAdapter(AoDParser.itemMediaList)
|
||||
adapter = MediaItemAdapter(AoDParser.guiMediaList)
|
||||
adapter!!.onItemClick = { mediaId, _ ->
|
||||
binding.searchText.clearFocus()
|
||||
(activity as MainActivity).showFragment(MediaFragment(mediaId))
|
||||
activity?.showFragment(MediaFragment(mediaId))
|
||||
}
|
||||
|
||||
binding.recyclerMediaSearch.adapter = adapter
|
||||
binding.recyclerMediaSearch.addItemDecoration(MediaItemDecoration(9))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
initActions()
|
@ -0,0 +1,112 @@
|
||||
package org.mosad.teapod.ui.activity.main.viewmodel
|
||||
|
||||
import android.app.Application
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import org.mosad.teapod.parser.AoDParser
|
||||
import org.mosad.teapod.util.*
|
||||
import org.mosad.teapod.util.DataTypes.MediaType
|
||||
import org.mosad.teapod.util.tmdb.TMDBApiController
|
||||
import org.mosad.teapod.util.tmdb.TMDBResult
|
||||
import org.mosad.teapod.util.tmdb.TMDBTVSeason
|
||||
|
||||
/**
|
||||
* handle media, next ep and tmdb
|
||||
* TODO this lives in activity, is this correct?
|
||||
*/
|
||||
class MediaFragmentViewModel(application: Application) : AndroidViewModel(application) {
|
||||
|
||||
var media = Media(-1, "", MediaType.OTHER)
|
||||
internal set
|
||||
var nextEpisode = Episode()
|
||||
internal set
|
||||
var tmdbResult: TMDBResult? = null // TODO rename
|
||||
internal set
|
||||
var tmdbTVSeason: TMDBTVSeason? =null
|
||||
internal set
|
||||
var mediaMeta: Meta? = null
|
||||
internal set
|
||||
|
||||
/**
|
||||
* set media, tmdb and nextEpisode
|
||||
* TODO run aod and tmdb load parallel
|
||||
*/
|
||||
suspend fun load(mediaId: Int) {
|
||||
val tmdbApiController = TMDBApiController()
|
||||
media = AoDParser.getMediaById(mediaId)
|
||||
|
||||
// check if metaDB knows the title
|
||||
val tmdbId: Int = if (MetaDBController.mediaList.media.contains(media.id)) {
|
||||
// load media info from metaDB
|
||||
val metaDB = MetaDBController()
|
||||
mediaMeta = when (media.type) {
|
||||
MediaType.MOVIE -> metaDB.getMovieMetadata(media.id)
|
||||
MediaType.TVSHOW -> metaDB.getTVShowMetadata(media.id)
|
||||
else -> null
|
||||
}
|
||||
|
||||
mediaMeta?.tmdbId ?: -1
|
||||
} else {
|
||||
// use tmdb search to get media info
|
||||
mediaMeta = null // set mediaMeta to null, if metaDB doesn't know the media
|
||||
tmdbApiController.search(stripTitleInfo(media.info.title), media.type)
|
||||
}
|
||||
|
||||
tmdbResult = when (media.type) {
|
||||
MediaType.MOVIE -> tmdbApiController.getMovieDetails(tmdbId)
|
||||
MediaType.TVSHOW -> tmdbApiController.getTVShowDetails(tmdbId)
|
||||
else -> null
|
||||
}
|
||||
println(tmdbResult) // TODO
|
||||
|
||||
// get season info, if metaDB knows the tv show
|
||||
tmdbTVSeason = if (media.type == MediaType.TVSHOW && mediaMeta != null) {
|
||||
val tvShowMeta = mediaMeta as TVShowMeta
|
||||
tmdbApiController.getTVSeasonDetails(tvShowMeta.tmdbId, tvShowMeta.tmdbSeasonNumber)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
if (media.type == MediaType.TVSHOW) {
|
||||
nextEpisode = media.episodes.firstOrNull{ !it.watched } ?: 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()
|
||||
}
|
||||
|
||||
// remove unneeded info from the media title before searching
|
||||
private fun stripTitleInfo(title: String): String {
|
||||
return title.replace("(Sub)", "")
|
||||
.replace(Regex("-?\\s?[0-9]+.\\s?(Staffel|Season)"), "")
|
||||
.replace(Regex("(Staffel|Season)\\s?[0-9]+"), "")
|
||||
.trim()
|
||||
}
|
||||
|
||||
/** guess Season from title
|
||||
* if the title ends with a number, that could be the season
|
||||
* if the title ends with Regex("-?\\s?[0-9]+.\\s?(Staffel|Season)") or
|
||||
* Regex("(Staffel|Season)\\s?[0-9]+"), that is the season information
|
||||
*/
|
||||
private fun guessSeasonFromTitle(title: String): Int {
|
||||
val helpTitle = title.replace("(Sub)", "").trim()
|
||||
Log.d("test", "helpTitle: $helpTitle")
|
||||
|
||||
return if (helpTitle.last().isDigit()) {
|
||||
helpTitle.last().digitToInt()
|
||||
} else {
|
||||
Regex("([0-9]+.\\s?(Staffel|Season))|((Staffel|Season)\\s?[0-9]+)")
|
||||
.find(helpTitle)
|
||||
?.value?.filter { it.isDigit() }?.toInt() ?: 1
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
package org.mosad.teapod.ui.activity.onboarding
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.*
|
||||
import org.mosad.teapod.R
|
||||
import org.mosad.teapod.databinding.FragmentOnLoginBinding
|
||||
import org.mosad.teapod.parser.AoDParser
|
||||
import org.mosad.teapod.preferences.EncryptedPreferences
|
||||
|
||||
class OnLoginFragment: Fragment() {
|
||||
|
||||
private lateinit var binding: FragmentOnLoginBinding
|
||||
private var loginJob: Job? = null
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
binding = FragmentOnLoginBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
initActions()
|
||||
}
|
||||
|
||||
private fun initActions() {
|
||||
binding.buttonLogin.setOnClickListener {
|
||||
// get login credentials from gui
|
||||
val email = binding.editTextLogin.text.toString()
|
||||
val password = binding.editTextPassword.text.toString()
|
||||
|
||||
EncryptedPreferences.saveCredentials(email, password, requireContext()) // save the credentials
|
||||
|
||||
binding.buttonLogin.isClickable = false
|
||||
loginJob = lifecycleScope.launch {
|
||||
if (AoDParser.login()) {
|
||||
// if login was successful, switch to main
|
||||
if (activity is OnboardingActivity) {
|
||||
(activity as OnboardingActivity).launchMainActivity()
|
||||
}
|
||||
} else {
|
||||
withContext(Dispatchers.Main) {
|
||||
binding.textLoginDesc.text = getString(R.string.on_login_failed)
|
||||
binding.buttonLogin.isClickable = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
package org.mosad.teapod.ui.activity.onboarding
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import org.mosad.teapod.databinding.FragmentOnWelcomeBinding
|
||||
|
||||
class OnWelcomeFragment: Fragment() {
|
||||
|
||||
private lateinit var binding: FragmentOnWelcomeBinding
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
binding = FragmentOnWelcomeBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
initActions()
|
||||
}
|
||||
|
||||
private fun initActions() {
|
||||
binding.buttonGetStarted.setOnClickListener {
|
||||
if (activity is OnboardingActivity) {
|
||||
(activity as OnboardingActivity).nextFragment()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
package org.mosad.teapod.ui.activity.onboarding
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import org.mosad.teapod.ui.activity.main.MainActivity
|
||||
import org.mosad.teapod.databinding.ActivityOnboardingBinding
|
||||
|
||||
class OnboardingActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var binding: ActivityOnboardingBinding
|
||||
private lateinit var pagerAdapter: FragmentStateAdapter
|
||||
|
||||
private val fragments = arrayOf(OnLoginFragment())
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
binding = ActivityOnboardingBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
pagerAdapter = ScreenSlidePagerAdapter(this)
|
||||
binding.viewPager.adapter = pagerAdapter
|
||||
TabLayoutMediator(binding.tabLayout, binding.viewPager) { _, _ -> }.attach()
|
||||
|
||||
// we don't use the skip button, instead we use the start button to skip the last fragment
|
||||
binding.buttonSkip.visibility = View.GONE
|
||||
|
||||
// hide tab layout if only one tab is displayed
|
||||
if (fragments.size <= 1) {
|
||||
binding.tabLayout.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
if (binding.viewPager.currentItem == 0) {
|
||||
super.onBackPressed()
|
||||
} else {
|
||||
binding.viewPager.currentItem = binding.viewPager.currentItem - 1
|
||||
}
|
||||
}
|
||||
|
||||
fun nextFragment() {
|
||||
if (binding.viewPager.currentItem < fragments.size - 1) {
|
||||
binding.viewPager.currentItem++
|
||||
} else {
|
||||
launchMainActivity()
|
||||
}
|
||||
}
|
||||
|
||||
fun btnNextClick(@Suppress("UNUSED_PARAMETER")v: View) {
|
||||
//nextFragment() // currently not used in Teapod
|
||||
}
|
||||
|
||||
fun btnSkipClick(@Suppress("UNUSED_PARAMETER")v: View) {
|
||||
//launchMainActivity() // currently not used in Teapod
|
||||
}
|
||||
|
||||
fun launchMainActivity() {
|
||||
startActivity(Intent(this, MainActivity::class.java))
|
||||
finish()
|
||||
}
|
||||
|
||||
/**
|
||||
* A simple pager adapter
|
||||
*/
|
||||
private inner class ScreenSlidePagerAdapter(fa: FragmentActivity) : FragmentStateAdapter(fa) {
|
||||
override fun getItemCount(): Int = fragments.size
|
||||
|
||||
override fun createFragment(position: Int): Fragment = fragments[position]
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -1,31 +1,38 @@
|
||||
package org.mosad.teapod.player
|
||||
package org.mosad.teapod.ui.activity.player
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorListenerAdapter
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.PictureInPictureParams
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Rect
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.*
|
||||
import android.util.Rational
|
||||
import android.view.GestureDetector
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import androidx.activity.viewModels
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.GestureDetectorCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.exoplayer2.ExoPlayer
|
||||
import com.google.android.exoplayer2.Player
|
||||
import com.google.android.exoplayer2.ui.StyledPlayerControlView
|
||||
import com.google.android.exoplayer2.util.Util
|
||||
import kotlinx.android.synthetic.main.activity_player.*
|
||||
import kotlinx.android.synthetic.main.player_controls.*
|
||||
import kotlinx.coroutines.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.*
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.concurrent.scheduleAtFixedRate
|
||||
@ -38,9 +45,7 @@ class PlayerActivity : AppCompatActivity() {
|
||||
private lateinit var gestureDetector: GestureDetectorCompat
|
||||
private lateinit var timerUpdates: TimerTask
|
||||
|
||||
private var playWhenReady = true
|
||||
private var currentWindow = 0
|
||||
private var playbackPosition: Long = 0
|
||||
private var wasInPiP = false
|
||||
private var remainingTime: Long = 0
|
||||
|
||||
private val rwdTime: Long = 10000.unaryMinus()
|
||||
@ -52,12 +57,6 @@ class PlayerActivity : AppCompatActivity() {
|
||||
setContentView(R.layout.activity_player)
|
||||
hideBars() // Initial hide the bars
|
||||
|
||||
savedInstanceState?.let {
|
||||
currentWindow = it.getInt(getString(R.string.state_resume_window))
|
||||
playbackPosition = it.getLong(getString(R.string.state_resume_position))
|
||||
playWhenReady = it.getBoolean(getString(R.string.state_is_playing))
|
||||
}
|
||||
|
||||
model.loadMedia(
|
||||
intent.getIntExtra(getString(R.string.intent_media_id), 0),
|
||||
intent.getIntExtra(getString(R.string.intent_episode_id), 0)
|
||||
@ -73,6 +72,11 @@ class PlayerActivity : AppCompatActivity() {
|
||||
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) {
|
||||
@ -83,6 +87,8 @@ class PlayerActivity : AppCompatActivity() {
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
if (isInPiPMode()) { return }
|
||||
|
||||
if (Util.SDK_INT <= 23) {
|
||||
initPlayer()
|
||||
video_view?.onResume()
|
||||
@ -91,25 +97,77 @@ class PlayerActivity : AppCompatActivity() {
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
if (Util.SDK_INT <= 23) {
|
||||
video_view?.onPause()
|
||||
releasePlayer()
|
||||
}
|
||||
|
||||
if (isInPiPMode()) { return }
|
||||
if (Util.SDK_INT <= 23) { onPauseOnStop() }
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
if (Util.SDK_INT > 23) {
|
||||
video_view?.onPause()
|
||||
releasePlayer()
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
/**
|
||||
* previous to android n, don't override
|
||||
*/
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
override fun onUserLeaveHint() {
|
||||
super.onUserLeaveHint()
|
||||
|
||||
// start pip mode, if supported
|
||||
if (packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
@Suppress("deprecation")
|
||||
enterPictureInPictureMode()
|
||||
} else {
|
||||
val width = model.player.videoFormat?.width ?: 0
|
||||
val height = model.player.videoFormat?.height ?: 0
|
||||
val contentFrame: View = video_view.findViewById(R.id.exo_content_frame)
|
||||
val contentRect = with(contentFrame) {
|
||||
val (x, y) = intArrayOf(0, 0).also(::getLocationInWindow)
|
||||
Rect(x, y, x + width, y + height)
|
||||
}
|
||||
|
||||
val params = PictureInPictureParams.Builder()
|
||||
.setAspectRatio(Rational(width, height))
|
||||
.setSourceRectHint(contentRect)
|
||||
.build()
|
||||
enterPictureInPictureMode(params)
|
||||
}
|
||||
|
||||
wasInPiP = isInPiPMode()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPictureInPictureModeChanged(
|
||||
isInPictureInPictureMode: Boolean,
|
||||
newConfig: Configuration?
|
||||
) {
|
||||
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
|
||||
|
||||
// Hide the full-screen UI (controls, etc.) while in picture-in-picture mode.
|
||||
video_view.useController = !isInPictureInPictureMode
|
||||
}
|
||||
|
||||
private fun initPlayer() {
|
||||
@ -122,8 +180,8 @@ class PlayerActivity : AppCompatActivity() {
|
||||
initTimeUpdates()
|
||||
|
||||
// if the player is ready or buffering we can simply play the file again, else do nothing
|
||||
if ((model.player.playbackState == ExoPlayer.STATE_READY || model.player.playbackState == ExoPlayer.STATE_BUFFERING)
|
||||
) {
|
||||
val playbackState = model.player.playbackState
|
||||
if ((playbackState == ExoPlayer.STATE_READY || playbackState == ExoPlayer.STATE_BUFFERING)) {
|
||||
model.player.play()
|
||||
}
|
||||
}
|
||||
@ -132,8 +190,7 @@ class PlayerActivity : AppCompatActivity() {
|
||||
* set play when ready and listeners
|
||||
*/
|
||||
private fun initExoPlayer() {
|
||||
model.player.playWhenReady = playWhenReady
|
||||
model.player.addListener(object : Player.EventListener {
|
||||
model.player.addListener(object : Player.Listener {
|
||||
override fun onPlaybackStateChanged(state: Int) {
|
||||
super.onPlaybackStateChanged(state)
|
||||
|
||||
@ -154,9 +211,9 @@ class PlayerActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
// start playing the current episode, after all needed player components have been initialized
|
||||
model.playEpisode(model.currentEpisode, true, playbackPosition)
|
||||
model.playEpisode(model.currentEpisode, true)
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@ -166,7 +223,10 @@ class PlayerActivity : AppCompatActivity() {
|
||||
// when the player controls get hidden, hide the bars too
|
||||
video_view.setControllerVisibilityListener {
|
||||
when (it) {
|
||||
View.GONE -> hideBars()
|
||||
View.GONE -> {
|
||||
hideBars()
|
||||
// TODO also hide the skip op button
|
||||
}
|
||||
View.VISIBLE -> updateControls()
|
||||
}
|
||||
}
|
||||
@ -178,10 +238,13 @@ class PlayerActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun initActions() {
|
||||
exo_close_player.setOnClickListener { this.finish() }
|
||||
exo_close_player.setOnClickListener {
|
||||
this.finish()
|
||||
}
|
||||
rwd_10.setOnButtonClickListener { rewind() }
|
||||
ffwd_10.setOnButtonClickListener { fastForward() }
|
||||
button_next_ep.setOnClickListener { playNextEpisode() }
|
||||
button_skip_op.setOnClickListener { skipOpening() }
|
||||
button_language.setOnClickListener { showLanguageSettings() }
|
||||
button_episodes.setOnClickListener { showEpisodesList() }
|
||||
button_next_ep_c.setOnClickListener { playNextEpisode() }
|
||||
@ -199,40 +262,51 @@ class PlayerActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
timerUpdates = Timer().scheduleAtFixedRate(0, 500) {
|
||||
GlobalScope.launch {
|
||||
var btnNextEpIsVisible: Boolean
|
||||
var controlsVisible: Boolean
|
||||
lifecycleScope.launch {
|
||||
val currentPosition = model.player.currentPosition
|
||||
val btnNextEpIsVisible = button_next_ep.isVisible
|
||||
val controlsVisible = controller.isVisible
|
||||
|
||||
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
|
||||
// make sure remaining time is > 0
|
||||
if (model.player.duration > 0) {
|
||||
remainingTime = model.player.duration - currentPosition
|
||||
remainingTime = if (remainingTime < 0) 0 else remainingTime
|
||||
}
|
||||
|
||||
// TODO add metaDB ending_start support
|
||||
// if remaining time < 20 sec, a next ep is set, autoplay is enabled and not in pip:
|
||||
// show next ep button
|
||||
if (remainingTime in 1..20000) {
|
||||
// if the next ep button is not visible, make it visible
|
||||
if (!btnNextEpIsVisible && model.nextEpisode != null && Preferences.autoplay) {
|
||||
withContext(Dispatchers.Main) { showButtonNextEp() }
|
||||
if (!btnNextEpIsVisible && model.nextEpisode != null && Preferences.autoplay && !isInPiPMode()) {
|
||||
showButtonNextEp()
|
||||
}
|
||||
} else if (btnNextEpIsVisible) {
|
||||
withContext(Dispatchers.Main) { hideButtonNextEp() }
|
||||
hideButtonNextEp()
|
||||
}
|
||||
|
||||
// if meta data is present and opening_start & opening_duration are valid, show skip opening
|
||||
model.currentEpisodeMeta?.let {
|
||||
if (it.openingDuration > 0 &&
|
||||
currentPosition in it.openingStart..(it.openingStart + 10000) &&
|
||||
!button_skip_op.isVisible
|
||||
) {
|
||||
showButtonSkipOp()
|
||||
} else if (button_skip_op.isVisible && currentPosition !in it.openingStart..(it.openingStart + 10000)) {
|
||||
// the button should only be visible, if currentEpisodeMeta != null
|
||||
hideButtonSkipOp()
|
||||
}
|
||||
}
|
||||
|
||||
// if controls are visible, update them
|
||||
if (controlsVisible) {
|
||||
withContext(Dispatchers.Main) { updateControls() }
|
||||
updateControls()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun releasePlayer(){
|
||||
playbackPosition = model.player.currentPosition
|
||||
currentWindow = model.player.currentWindowIndex
|
||||
playWhenReady = model.player.playWhenReady
|
||||
private fun onPauseOnStop() {
|
||||
video_view?.onPause()
|
||||
model.player.pause()
|
||||
timerUpdates.cancel()
|
||||
}
|
||||
@ -260,11 +334,19 @@ class PlayerActivity : AppCompatActivity() {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -312,25 +394,13 @@ class PlayerActivity : AppCompatActivity() {
|
||||
hideButtonNextEp()
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
private fun skipOpening() {
|
||||
// calculate the seek time
|
||||
model.currentEpisodeMeta?.let {
|
||||
val seekTime = (it.openingStart + it.openingDuration) - model.player.currentPosition
|
||||
model.seekToOffset(seekTime)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@ -338,7 +408,7 @@ class PlayerActivity : AppCompatActivity() {
|
||||
* TODO improve the show animation
|
||||
*/
|
||||
private fun showButtonNextEp() {
|
||||
button_next_ep.visibility = View.VISIBLE
|
||||
button_next_ep.isVisible = true
|
||||
button_next_ep.alpha = 0.0f
|
||||
|
||||
button_next_ep.animate()
|
||||
@ -356,7 +426,28 @@ class PlayerActivity : AppCompatActivity() {
|
||||
.setListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator?) {
|
||||
super.onAnimationEnd(animation)
|
||||
button_next_ep.visibility = View.GONE
|
||||
button_next_ep.isVisible = false
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
private fun showButtonSkipOp() {
|
||||
button_skip_op.isVisible = true
|
||||
button_skip_op.alpha = 0.0f
|
||||
|
||||
button_skip_op.animate()
|
||||
.alpha(1.0f)
|
||||
.setListener(null)
|
||||
}
|
||||
|
||||
private fun hideButtonSkipOp() {
|
||||
button_skip_op.animate()
|
||||
.alpha(0.0f)
|
||||
.setListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator?) {
|
||||
super.onAnimationEnd(animation)
|
||||
button_skip_op.isVisible = false
|
||||
}
|
||||
})
|
||||
|
||||
@ -393,7 +484,10 @@ class PlayerActivity : AppCompatActivity() {
|
||||
* on single tap hide or show the controls
|
||||
*/
|
||||
override fun onSingleTapConfirmed(e: MotionEvent?): Boolean {
|
||||
if (controller.isVisible) controller.hide() else controller.show()
|
||||
if (!isInPiPMode()) {
|
||||
if (controller.isVisible) controller.hide() else controller.show()
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
@ -1,24 +1,25 @@
|
||||
package org.mosad.teapod.player
|
||||
package org.mosad.teapod.ui.activity.player
|
||||
|
||||
import android.app.Application
|
||||
import android.net.Uri
|
||||
import android.support.v4.media.session.MediaSessionCompat
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.google.android.exoplayer2.C
|
||||
import com.google.android.exoplayer2.MediaItem
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer
|
||||
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
|
||||
import com.google.android.exoplayer2.source.MediaSource
|
||||
import com.google.android.exoplayer2.source.hls.HlsMediaSource
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory
|
||||
import com.google.android.exoplayer2.util.Util
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.mosad.teapod.R
|
||||
import org.mosad.teapod.parser.AoDParser
|
||||
import org.mosad.teapod.preferences.Preferences
|
||||
import org.mosad.teapod.ui.fragments.MediaFragment
|
||||
import org.mosad.teapod.util.DataTypes
|
||||
import org.mosad.teapod.util.Episode
|
||||
import org.mosad.teapod.util.Media
|
||||
import org.mosad.teapod.util.*
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
@ -26,14 +27,18 @@ 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.
|
||||
*
|
||||
* TODO rework don't use episodes for everything, use media instead
|
||||
* this is a major rework of the AoDParser/Player/Media architecture
|
||||
*/
|
||||
class PlayerViewModel(application: Application) : AndroidViewModel(application) {
|
||||
|
||||
val player = SimpleExoPlayer.Builder(application).build()
|
||||
val dataSourceFactory = DefaultDataSourceFactory(application, Util.getUserAgent(application, "Teapod"))
|
||||
private val dataSourceFactory = DefaultDataSourceFactory(application, Util.getUserAgent(application, "Teapod"))
|
||||
private val mediaSession = MediaSessionCompat(application, "TEAPOD_PLAYER_SESSION")
|
||||
|
||||
val currentEpisodeChangedListener = ArrayList<() -> Unit>()
|
||||
val preferredLanguage = if (Preferences.preferSecondary) Locale.JAPANESE else Locale.GERMAN
|
||||
private val preferredLanguage = if (Preferences.preferSecondary) Locale.JAPANESE else Locale.GERMAN
|
||||
|
||||
var media: Media = Media(-1, "", DataTypes.MediaType.OTHER)
|
||||
internal set
|
||||
@ -41,23 +46,46 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
|
||||
internal set
|
||||
var nextEpisode: Episode? = null
|
||||
internal set
|
||||
var mediaMeta: Meta? = null
|
||||
internal set
|
||||
var currentEpisodeMeta: EpisodeMeta? = null
|
||||
internal set
|
||||
var currentLanguage: Locale = Locale.ROOT
|
||||
internal set
|
||||
|
||||
init {
|
||||
initMediaSession()
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
|
||||
mediaSession.release()
|
||||
player.release()
|
||||
|
||||
Log.d(javaClass.name, "Released player")
|
||||
}
|
||||
|
||||
/**
|
||||
* set the media session to active
|
||||
* create a media session connector to set title and description
|
||||
*/
|
||||
private fun initMediaSession() {
|
||||
val mediaSessionConnector = MediaSessionConnector(mediaSession)
|
||||
mediaSessionConnector.setPlayer(player)
|
||||
|
||||
mediaSession.isActive = true
|
||||
}
|
||||
|
||||
fun loadMedia(mediaId: Int, episodeId: Int) {
|
||||
runBlocking {
|
||||
media = AoDParser.getMediaById(mediaId)
|
||||
mediaMeta = loadMediaMeta(media.id) // can be done blocking, since it should be cached
|
||||
}
|
||||
|
||||
currentEpisode = media.getEpisodeById(episodeId)
|
||||
nextEpisode = selectNextEpisode()
|
||||
currentEpisodeMeta = getEpisodeMetaByAoDMediaId(currentEpisode.id)
|
||||
currentLanguage = currentEpisode.getPreferredStream(preferredLanguage).language
|
||||
}
|
||||
|
||||
@ -99,6 +127,7 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
|
||||
currentLanguage = preferredStream.language // update current language, since it may have changed
|
||||
currentEpisode = episode
|
||||
nextEpisode = selectNextEpisode()
|
||||
currentEpisodeMeta = getEpisodeMetaByAoDMediaId(episode.id)
|
||||
currentEpisodeChangedListener.forEach { it() } // update player gui (title)
|
||||
|
||||
val mediaSource = HlsMediaSource.Factory(dataSourceFactory).createMediaSource(
|
||||
@ -108,10 +137,15 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
|
||||
|
||||
// if episodes has not been watched, mark as watched
|
||||
if (!episode.watched) {
|
||||
AoDParser.markAsWatched(media.id, episode.id)
|
||||
viewModelScope.launch {
|
||||
AoDParser.markAsWatched(media.id, episode.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* change the players media source and start playback
|
||||
*/
|
||||
fun playMedia(source: MediaSource, replace: Boolean = false, seekPosition: Long = 0) {
|
||||
if (replace || player.contentDuration == C.TIME_UNSET) {
|
||||
player.setMediaSource(source)
|
||||
@ -133,6 +167,23 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
|
||||
}
|
||||
}
|
||||
|
||||
fun getEpisodeMetaByAoDMediaId(aodMediaId: Int): EpisodeMeta? {
|
||||
val meta = mediaMeta
|
||||
return if (meta is TVShowMeta) {
|
||||
meta.episodes.firstOrNull { it.aodMediaId == aodMediaId }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadMediaMeta(aodId: Int): Meta? {
|
||||
return if (media.type == DataTypes.MediaType.TVSHOW) {
|
||||
MetaDBController().getTVShowMetadata(aodId)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Based on the current episodeId, get the next episode. If there is no next
|
||||
* episode, return null
|
@ -6,7 +6,7 @@ import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import org.mosad.teapod.databinding.PlayerEpisodesListBinding
|
||||
import org.mosad.teapod.player.PlayerViewModel
|
||||
import org.mosad.teapod.ui.activity.player.PlayerViewModel
|
||||
import org.mosad.teapod.util.adapter.PlayerEpisodeItemAdapter
|
||||
|
||||
class EpisodesListPlayer @JvmOverloads constructor(
|
||||
|
@ -13,7 +13,7 @@ import android.widget.TextView
|
||||
import androidx.core.view.children
|
||||
import org.mosad.teapod.R
|
||||
import org.mosad.teapod.databinding.PlayerLanguageSettingsBinding
|
||||
import org.mosad.teapod.player.PlayerViewModel
|
||||
import org.mosad.teapod.ui.activity.player.PlayerViewModel
|
||||
import java.util.*
|
||||
|
||||
class LanguageSettingsPlayer @JvmOverloads constructor(
|
||||
|
@ -1,177 +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.coroutines.*
|
||||
import org.mosad.teapod.MainActivity
|
||||
import org.mosad.teapod.R
|
||||
import org.mosad.teapod.databinding.FragmentMediaBinding
|
||||
import org.mosad.teapod.parser.AoDParser
|
||||
import org.mosad.teapod.util.*
|
||||
import org.mosad.teapod.util.DataTypes.MediaType
|
||||
import org.mosad.teapod.util.adapter.EpisodeItemAdapter
|
||||
|
||||
class MediaFragment(private val mediaId: Int) : Fragment() {
|
||||
|
||||
private lateinit var binding: FragmentMediaBinding
|
||||
private lateinit var adapterRecEpisodes: EpisodeItemAdapter
|
||||
private lateinit var viewManager: RecyclerView.LayoutManager
|
||||
|
||||
private lateinit var media: Media
|
||||
private lateinit var tmdb: TMDBResponse
|
||||
private lateinit var nextEpisode: Episode
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
binding = FragmentMediaBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
binding.frameLoading.visibility = View.VISIBLE
|
||||
|
||||
GlobalScope.launch(Dispatchers.Main) {
|
||||
// load the streams for the selected media
|
||||
media = AoDParser.getMediaById(mediaId)
|
||||
tmdb = TMDBApiController().search(media.info.title, media.type)
|
||||
|
||||
if (this@MediaFragment.isAdded) {
|
||||
updateGUI()
|
||||
initActions()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
// only notify adapter, if initialized
|
||||
if (this::adapterRecEpisodes.isInitialized) {
|
||||
// TODO find a better solution for this
|
||||
media.episodes.forEachIndexed() { index, episode ->
|
||||
adapterRecEpisodes.updateWatchedState(episode.watched, index)
|
||||
}
|
||||
adapterRecEpisodes.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* if tmdb data is present, use it, else use the aod data
|
||||
*/
|
||||
private fun updateGUI() = with(binding) {
|
||||
// generic gui
|
||||
val backdropUrl = if (tmdb.backdropUrl.isNotEmpty()) tmdb.backdropUrl else media.info.posterUrl
|
||||
val posterUrl = if (tmdb.posterUrl.isNotEmpty()) tmdb.posterUrl else media.info.posterUrl
|
||||
|
||||
Glide.with(requireContext()).load(backdropUrl)
|
||||
.apply(RequestOptions.placeholderOf(ColorDrawable(Color.DKGRAY)))
|
||||
.apply(RequestOptions.bitmapTransform(BlurTransformation(20, 3)))
|
||||
.into(imageBackdrop)
|
||||
|
||||
Glide.with(requireContext()).load(posterUrl)
|
||||
.into(imagePoster)
|
||||
|
||||
textTitle.text = media.info.title
|
||||
textYear.text = media.info.year.toString()
|
||||
textAge.text = media.info.age.toString()
|
||||
textOverview.text = media.info.shortDesc
|
||||
if (StorageController.myList.contains(media.id)) {
|
||||
Glide.with(requireContext()).load(R.drawable.ic_baseline_check_24).into(imageMyListAction)
|
||||
} else {
|
||||
Glide.with(requireContext()).load(R.drawable.ic_baseline_add_24).into(imageMyListAction)
|
||||
}
|
||||
|
||||
// specific gui
|
||||
if (media.type == MediaType.TVSHOW) {
|
||||
adapterRecEpisodes = EpisodeItemAdapter(media.episodes)
|
||||
viewManager = LinearLayoutManager(context)
|
||||
recyclerEpisodes.layoutManager = viewManager
|
||||
recyclerEpisodes.adapter = adapterRecEpisodes
|
||||
|
||||
binding.textEpisodesOrRuntime.text = getString(R.string.text_episodes_count, media.info.episodesCount)
|
||||
|
||||
// get next episode
|
||||
nextEpisode = if (media.episodes.firstOrNull{ !it.watched } != null) {
|
||||
media.episodes.first{ !it.watched }
|
||||
} else {
|
||||
media.episodes.first()
|
||||
}
|
||||
|
||||
// title is the next episodes title
|
||||
textTitle.text = nextEpisode.title
|
||||
} else if (media.type == MediaType.MOVIE) {
|
||||
recyclerEpisodes.visibility = View.GONE
|
||||
|
||||
if (tmdb.runtime > 0) {
|
||||
textEpisodesOrRuntime.text = getString(R.string.text_runtime, tmdb.runtime)
|
||||
} else {
|
||||
textEpisodesOrRuntime.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
frameLoading.visibility = View.GONE // hide loading indicator
|
||||
}
|
||||
|
||||
private fun initActions() {
|
||||
binding.buttonPlay.setOnClickListener {
|
||||
when (media.type) {
|
||||
MediaType.MOVIE -> playStream(media.episodes.first())
|
||||
MediaType.TVSHOW -> playEpisode(nextEpisode)
|
||||
else -> Log.e(javaClass.name, "Wrong Type: $media.type")
|
||||
}
|
||||
}
|
||||
|
||||
// add or remove media from myList
|
||||
binding.linearMyListAction.setOnClickListener {
|
||||
if (StorageController.myList.contains(media.id)) {
|
||||
StorageController.myList.remove(media.id)
|
||||
Glide.with(requireContext()).load(R.drawable.ic_baseline_add_24).into(binding.imageMyListAction)
|
||||
} else {
|
||||
StorageController.myList.add(media.id)
|
||||
Glide.with(requireContext()).load(R.drawable.ic_baseline_check_24).into(binding.imageMyListAction)
|
||||
}
|
||||
StorageController.saveMyList(requireContext())
|
||||
|
||||
// notify home fragment on change
|
||||
parentFragmentManager.findFragmentByTag("HomeFragment")?.let {
|
||||
(it as HomeFragment).updateMyListMedia()
|
||||
}
|
||||
}
|
||||
|
||||
// set onItemClick only in adapter is initialized
|
||||
if (this::adapterRecEpisodes.isInitialized) {
|
||||
adapterRecEpisodes.onImageClick = { _, position ->
|
||||
playEpisode(media.episodes[position])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun playEpisode(ep: Episode) {
|
||||
playStream(ep)
|
||||
|
||||
// update nextEpisode
|
||||
nextEpisode = if (media.episodes.firstOrNull{ !it.watched } != null) {
|
||||
media.episodes.first{ !it.watched }
|
||||
} else {
|
||||
media.episodes.first()
|
||||
}
|
||||
binding.textTitle.text = nextEpisode.title
|
||||
}
|
||||
|
||||
private fun playStream(ep: Episode) {
|
||||
Log.d(javaClass.name, "Starting Player with mediaId: ${media.id}")
|
||||
(activity as MainActivity).startPlayer(media.id, ep.id)
|
||||
}
|
||||
|
||||
}
|
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)
|
||||
}
|
@ -36,7 +36,7 @@ data class ThirdPartyComponent(
|
||||
* it is uses in the ItemMediaAdapter (RecyclerView)
|
||||
*/
|
||||
data class ItemMedia(
|
||||
val id: Int,
|
||||
val id: Int, // aod path id
|
||||
val title: String,
|
||||
val posterUrl: String
|
||||
)
|
||||
@ -55,6 +55,9 @@ data class Media(
|
||||
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 = "",
|
||||
@ -62,7 +65,8 @@ 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()
|
||||
)
|
||||
|
||||
/**
|
||||
@ -94,24 +98,12 @@ data class Stream(
|
||||
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
|
||||
)
|
||||
|
||||
/**
|
||||
* this class is used to represent the aod json API?
|
||||
*/
|
||||
data class AoDObject(
|
||||
val playlist: List<Playlist>,
|
||||
val extLanguage: String
|
||||
data class AoDPlaylist(
|
||||
val list: List<Playlist>,
|
||||
val language: Locale
|
||||
)
|
||||
|
||||
data class Playlist(
|
||||
|
156
app/src/main/java/org/mosad/teapod/util/MetaDBController.kt
Normal file
@ -0,0 +1,156 @@
|
||||
/**
|
||||
* Teapod
|
||||
*
|
||||
* Copyright 2020-2021 <seil0@mosad.xyz>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||
* MA 02110-1301, USA.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.mosad.teapod.util
|
||||
|
||||
import android.util.Log
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import kotlinx.coroutines.*
|
||||
import java.io.FileNotFoundException
|
||||
import java.net.URL
|
||||
|
||||
class MetaDBController {
|
||||
|
||||
companion object {
|
||||
private const val repoUrl = "https://gitlab.com/Seil0/teapodmetadb/-/raw/main/aod/"
|
||||
|
||||
var mediaList = MediaList(listOf())
|
||||
private var metaCacheList = arrayListOf<Meta>()
|
||||
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
suspend fun list() = withContext(Dispatchers.IO) {
|
||||
val url = URL("$repoUrl/list.json")
|
||||
val json = url.readText()
|
||||
|
||||
mediaList = Gson().fromJson(json, MediaList::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the meta data for a movie from MetaDB
|
||||
* @param aodId The AoD id of the media
|
||||
* @return A meta movie object, or null if not found
|
||||
*/
|
||||
suspend fun getMovieMetadata(aodId: Int): MovieMeta? {
|
||||
return metaCacheList.firstOrNull {
|
||||
it.aodId == aodId
|
||||
} as MovieMeta? ?: getMovieMetadataFromDB(aodId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the meta data for a tv show from MetaDB
|
||||
* @param aodId The AoD id of the media
|
||||
* @return A meta tv show object, or null if not found
|
||||
*/
|
||||
suspend fun getTVShowMetadata(aodId: Int): TVShowMeta? {
|
||||
return metaCacheList.firstOrNull {
|
||||
it.aodId == aodId
|
||||
} as TVShowMeta? ?: getTVShowMetadataFromDB(aodId)
|
||||
}
|
||||
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
private suspend fun getMovieMetadataFromDB(aodId: Int): MovieMeta? = withContext(Dispatchers.IO) {
|
||||
val url = URL("$repoUrl/movie/$aodId/media.json")
|
||||
return@withContext try {
|
||||
val json = url.readText()
|
||||
val meta = Gson().fromJson(json, MovieMeta::class.java)
|
||||
metaCacheList.add(meta)
|
||||
|
||||
meta
|
||||
} catch (ex: FileNotFoundException) {
|
||||
Log.w(javaClass.name, "Waring: The requested file was not found. Requested ID: $aodId", ex)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
private suspend fun getTVShowMetadataFromDB(aodId: Int): TVShowMeta? = withContext(Dispatchers.IO) {
|
||||
val url = URL("$repoUrl/tv/$aodId/media.json")
|
||||
return@withContext try {
|
||||
val json = url.readText()
|
||||
val meta = Gson().fromJson(json, TVShowMeta::class.java)
|
||||
metaCacheList.add(meta)
|
||||
|
||||
meta
|
||||
} catch (ex: FileNotFoundException) {
|
||||
Log.w(javaClass.name, "Waring: The requested file was not found. Requested ID: $aodId", ex)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// class representing the media list json object
|
||||
data class MediaList(
|
||||
val media: List<Int>
|
||||
)
|
||||
|
||||
// abstract class used for meta data objects (tv, movie)
|
||||
abstract class Meta {
|
||||
abstract val id: Int
|
||||
abstract val aodId: Int
|
||||
abstract val tmdbId: Int
|
||||
}
|
||||
|
||||
// class representing the movie json object
|
||||
data class MovieMeta(
|
||||
override val id: Int,
|
||||
@SerializedName("aod_id")
|
||||
override val aodId: Int,
|
||||
@SerializedName("tmdb_id")
|
||||
override val tmdbId: Int
|
||||
): Meta()
|
||||
|
||||
// class representing the tv show json object
|
||||
data class TVShowMeta(
|
||||
override val id: Int,
|
||||
@SerializedName("aod_id")
|
||||
override val aodId: Int,
|
||||
@SerializedName("tmdb_id")
|
||||
override val tmdbId: Int,
|
||||
@SerializedName("tmdb_season_id")
|
||||
val tmdbSeasonId: Int,
|
||||
@SerializedName("tmdb_season_number")
|
||||
val tmdbSeasonNumber: Int,
|
||||
@SerializedName("episodes")
|
||||
val episodes: List<EpisodeMeta>
|
||||
): Meta()
|
||||
|
||||
// class used in TVShowMeta, part of the tv show json object
|
||||
data class EpisodeMeta(
|
||||
val id: Int,
|
||||
@SerializedName("aod_media_id")
|
||||
val aodMediaId: Int,
|
||||
@SerializedName("tmdb_id")
|
||||
val tmdbId: Int,
|
||||
@SerializedName("tmdb_number")
|
||||
val tmdbNumber: Int,
|
||||
@SerializedName("opening_start")
|
||||
val openingStart: Long,
|
||||
@SerializedName("opening_duration")
|
||||
val openingDuration: Long,
|
||||
@SerializedName("ending_start")
|
||||
val endingStart: Long,
|
||||
@SerializedName("ending_duration")
|
||||
val endingDuration: Long
|
||||
)
|
@ -1,12 +1,14 @@
|
||||
package org.mosad.teapod.util
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.JsonParser
|
||||
import kotlinx.coroutines.*
|
||||
import java.io.File
|
||||
import java.lang.Exception
|
||||
import java.io.FileReader
|
||||
import java.io.FileWriter
|
||||
|
||||
/**
|
||||
* This controller contains the logic for permanently saved data.
|
||||
@ -19,6 +21,10 @@ object StorageController {
|
||||
val myList = ArrayList<Int>() // a list of saved mediaIds
|
||||
|
||||
fun load(context: Context) {
|
||||
loadMyList(context)
|
||||
}
|
||||
|
||||
fun loadMyList(context: Context) {
|
||||
val file = File(context.filesDir, fileNameMyList)
|
||||
|
||||
if (!file.exists()) runBlocking { saveMyList(context).join() }
|
||||
@ -30,15 +36,54 @@ object StorageController {
|
||||
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) {
|
||||
return CoroutineScope(Dispatchers.IO).launch {
|
||||
file.writeText(Gson().toJson(myList.distinct()))
|
||||
}
|
||||
}
|
||||
|
||||
fun exportMyList(context: Context, uri: Uri) {
|
||||
try {
|
||||
context.contentResolver.openFileDescriptor(uri, "w")?.use {
|
||||
FileWriter(it.fileDescriptor).use { writer ->
|
||||
writer.write(Gson().toJson(myList.distinct()))
|
||||
}
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
Log.e(javaClass.name, "Exporting my list failed.", ex)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* import my list from a (previously exported) json file
|
||||
* @param context the current context
|
||||
* @param uri the uri of the selected file
|
||||
* @return 0 if import was successfull, else 1
|
||||
*/
|
||||
fun importMyList(context: Context, uri: Uri): Int {
|
||||
try {
|
||||
val text = context.contentResolver.openFileDescriptor(uri, "r")?.use {
|
||||
FileReader(it.fileDescriptor).use { reader ->
|
||||
reader.readText()
|
||||
}
|
||||
}
|
||||
|
||||
myList.clear()
|
||||
myList.addAll(JsonParser.parseString(text).asJsonArray.map { it.asInt }.distinct())
|
||||
|
||||
// after the list has been imported also save it
|
||||
saveMyList(context)
|
||||
} catch (ex: Exception) {
|
||||
myList.clear()
|
||||
Log.e(javaClass.name, "Importing my list failed.", ex)
|
||||
return 1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
}
|
@ -1,115 +0,0 @@
|
||||
package org.mosad.teapod.util
|
||||
|
||||
import android.util.Log
|
||||
import com.google.gson.JsonObject
|
||||
import com.google.gson.JsonParser
|
||||
import kotlinx.coroutines.*
|
||||
import java.net.URL
|
||||
import java.net.URLEncoder
|
||||
import org.mosad.teapod.util.DataTypes.MediaType
|
||||
|
||||
class TMDBApiController {
|
||||
|
||||
private val apiUrl = "https://api.themoviedb.org/3"
|
||||
private val searchMovieUrl = "$apiUrl/search/movie"
|
||||
private val searchTVUrl = "$apiUrl/search/tv"
|
||||
private val getMovieUrl = "$apiUrl/movie"
|
||||
private val apiKey = "de959cf9c07a08b5ca7cb51cda9a40c2"
|
||||
private val language = "de"
|
||||
private val preparedParameters = "?api_key=$apiKey&language=$language"
|
||||
|
||||
private val imageUrl = "https://image.tmdb.org/t/p/w500"
|
||||
|
||||
suspend fun search(title: String, type: MediaType): TMDBResponse {
|
||||
val searchTerm = title.replace("(Sub)", "").trim()
|
||||
|
||||
return when (type) {
|
||||
MediaType.MOVIE -> searchMovie(searchTerm).await()
|
||||
MediaType.TVSHOW -> searchTVShow(searchTerm).await()
|
||||
else -> {
|
||||
Log.e(javaClass.name, "Wrong Type: $type")
|
||||
TMDBResponse()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun searchTVShow(title: String): Deferred<TMDBResponse> {
|
||||
val url = URL("$searchTVUrl$preparedParameters&query=${URLEncoder.encode(title, "UTF-8")}")
|
||||
|
||||
return GlobalScope.async {
|
||||
val response = JsonParser.parseString(url.readText()).asJsonObject
|
||||
//println(response)
|
||||
|
||||
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 posterPath = getStringNotNullPrefix(it, "poster_path", imageUrl)
|
||||
val backdropPath = getStringNotNullPrefix(it, "backdrop_path", imageUrl)
|
||||
|
||||
TMDBResponse(id, "", overview, posterPath, backdropPath)
|
||||
}
|
||||
} else {
|
||||
TMDBResponse()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun searchMovie(title: String): Deferred<TMDBResponse> {
|
||||
val url = URL("$searchMovieUrl$preparedParameters&query=${URLEncoder.encode(title, "UTF-8")}")
|
||||
|
||||
return GlobalScope.async {
|
||||
val response = JsonParser.parseString(url.readText()).asJsonObject
|
||||
//println(response)
|
||||
|
||||
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 posterPath = getStringNotNullPrefix(it, "poster_path", imageUrl)
|
||||
val backdropPath = getStringNotNullPrefix(it, "backdrop_path", imageUrl)
|
||||
val runtime = getMovieRuntime(id)
|
||||
|
||||
TMDBResponse(id, "", overview, posterPath, backdropPath, runtime)
|
||||
}
|
||||
} else {
|
||||
TMDBResponse()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* currently only used for runtime, need a rework
|
||||
*/
|
||||
fun getMovieRuntime(id: Int): Int = runBlocking {
|
||||
val url = URL("$getMovieUrl/$id?api_key=$apiKey&language=$language")
|
||||
|
||||
GlobalScope.async {
|
||||
val response = JsonParser.parseString(url.readText()).asJsonObject
|
||||
|
||||
return@async getStringNotNull(response,"runtime").toInt()
|
||||
}.await()
|
||||
}
|
||||
|
||||
/**
|
||||
* return memberName as string if it's not JsonNull,
|
||||
* else return an empty string
|
||||
*/
|
||||
private fun getStringNotNull(jsonObject: JsonObject, memberName: String): String {
|
||||
return getStringNotNullPrefix(jsonObject, memberName, "")
|
||||
}
|
||||
|
||||
/**
|
||||
* return memberName as string with a prefix if it's not JsonNull,
|
||||
* else return an empty string
|
||||
*/
|
||||
private fun getStringNotNullPrefix(jsonObject: JsonObject, memberName: String, prefix: String): String {
|
||||
return if (!jsonObject.get(memberName).isJsonNull) {
|
||||
prefix + jsonObject.get(memberName).asString
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
}
|
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,5 +1,7 @@
|
||||
package org.mosad.teapod.util.adapter
|
||||
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.content.ContextCompat
|
||||
@ -10,8 +12,9 @@ import jp.wasabeef.glide.transformations.RoundedCornersTransformation
|
||||
import org.mosad.teapod.R
|
||||
import org.mosad.teapod.databinding.ItemEpisodeBinding
|
||||
import org.mosad.teapod.util.Episode
|
||||
import org.mosad.teapod.util.tmdb.TMDBTVEpisode
|
||||
|
||||
class EpisodeItemAdapter(private val episodes: List<Episode>) : RecyclerView.Adapter<EpisodeItemAdapter.EpisodeViewHolder>() {
|
||||
class EpisodeItemAdapter(private val episodes: List<Episode>, private val tmdbEpisodes: List<TMDBTVEpisode>?) : RecyclerView.Adapter<EpisodeItemAdapter.EpisodeViewHolder>() {
|
||||
|
||||
var onImageClick: ((String, Int) -> Unit)? = null
|
||||
|
||||
@ -30,10 +33,17 @@ class EpisodeItemAdapter(private val episodes: List<Episode>) : RecyclerView.Ada
|
||||
}
|
||||
|
||||
holder.binding.textEpisodeTitle.text = titleText
|
||||
holder.binding.textEpisodeDesc.text = ep.shortDesc
|
||||
holder.binding.textEpisodeDesc.text = if (ep.shortDesc.isNotEmpty()) {
|
||||
ep.shortDesc
|
||||
} else if (tmdbEpisodes != null && position < tmdbEpisodes.size){
|
||||
tmdbEpisodes[position].overview
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
if (episodes[position].posterUrl.isNotEmpty()) {
|
||||
Glide.with(context).load(ep.posterUrl)
|
||||
.apply(RequestOptions.placeholderOf(ColorDrawable(Color.DKGRAY)))
|
||||
.apply(RequestOptions.bitmapTransform(RoundedCornersTransformation(10, 0)))
|
||||
.into(holder.binding.imageEpisode)
|
||||
}
|
||||
@ -52,7 +62,8 @@ 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 EpisodeViewHolder(val binding: ItemEpisodeBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||
|
@ -49,14 +49,14 @@ class MediaItemAdapter(private val initMedia: List<ItemMedia>) : RecyclerView.Ad
|
||||
|
||||
inner class MediaFilter : Filter() {
|
||||
override fun performFiltering(constraint: CharSequence?): FilterResults {
|
||||
val filterTerm = constraint.toString().toLowerCase(Locale.ROOT)
|
||||
val filterTerm = constraint.toString().lowercase(Locale.ROOT)
|
||||
val results = FilterResults()
|
||||
|
||||
val filteredList = if (filterTerm.isEmpty()) {
|
||||
initMedia
|
||||
} else {
|
||||
initMedia.filter {
|
||||
it.title.toLowerCase(Locale.ROOT).contains(filterTerm)
|
||||
it.title.lowercase(Locale.ROOT).contains(filterTerm)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,137 @@
|
||||
/**
|
||||
* Teapod
|
||||
*
|
||||
* Copyright 2020-2021 <seil0@mosad.xyz>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||
* MA 02110-1301, USA.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.mosad.teapod.util.tmdb
|
||||
|
||||
import android.util.Log
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.JsonParser
|
||||
import kotlinx.coroutines.*
|
||||
import org.mosad.teapod.util.DataTypes.MediaType
|
||||
import java.io.FileNotFoundException
|
||||
import java.net.URL
|
||||
import java.net.URLEncoder
|
||||
|
||||
/**
|
||||
* Controller for tmdb api integration.
|
||||
* Data types are in TMDBDataTypes. For the type definitions see:
|
||||
* https://developers.themoviedb.org/3/getting-started/introduction
|
||||
*
|
||||
* TODO evaluate Klaxon
|
||||
*/
|
||||
class TMDBApiController {
|
||||
|
||||
private val apiUrl = "https://api.themoviedb.org/3"
|
||||
private val searchMovieUrl = "$apiUrl/search/movie"
|
||||
private val searchTVUrl = "$apiUrl/search/tv"
|
||||
private val detailsMovieUrl = "$apiUrl/movie"
|
||||
private val detailsTVUrl = "$apiUrl/tv"
|
||||
private val apiKey = "de959cf9c07a08b5ca7cb51cda9a40c2"
|
||||
private val language = "de"
|
||||
private val preparedParameters = "?api_key=$apiKey&language=$language"
|
||||
|
||||
companion object{
|
||||
const val imageUrl = "https://image.tmdb.org/t/p/w500"
|
||||
}
|
||||
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
/**
|
||||
* Search for a media(movie or tv show) in tmdb
|
||||
* @param query The query text
|
||||
* @param type The media type (movie or tv show)
|
||||
* @return The media tmdb id, or -1 if not found
|
||||
*/
|
||||
suspend fun search(query: String, type: MediaType): Int = withContext(Dispatchers.IO) {
|
||||
val searchUrl = when (type) {
|
||||
MediaType.MOVIE -> searchMovieUrl
|
||||
MediaType.TVSHOW -> searchTVUrl
|
||||
else -> {
|
||||
Log.e(javaClass.name, "Wrong Type: $type")
|
||||
return@withContext -1
|
||||
}
|
||||
}
|
||||
|
||||
val url = URL("$searchUrl$preparedParameters&query=${URLEncoder.encode(query, "UTF-8")}")
|
||||
val response = JsonParser.parseString(url.readText()).asJsonObject
|
||||
val sortedResults = response.get("results").asJsonArray.toList().sortedBy {
|
||||
it.asJsonObject.get("title")?.asString
|
||||
}
|
||||
|
||||
return@withContext sortedResults.firstOrNull()?.asJsonObject?.get("id")?.asInt ?: -1
|
||||
}
|
||||
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
/**
|
||||
* Get details for a movie from tmdb
|
||||
* @param movieId The tmdb ID of the movie
|
||||
* @return A tmdb movie object, or null if not found
|
||||
*/
|
||||
suspend fun getMovieDetails(movieId: Int): TMDBMovie? = withContext(Dispatchers.IO) {
|
||||
val url = URL("$detailsMovieUrl/$movieId?api_key=$apiKey&language=$language")
|
||||
|
||||
return@withContext try {
|
||||
val json = url.readText()
|
||||
Gson().fromJson(json, TMDBMovie::class.java)
|
||||
} catch (ex: FileNotFoundException) {
|
||||
Log.w(javaClass.name, "Waring: The requested media was not found. Requested ID: $movieId", ex)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
/**
|
||||
* Get details for a tv show from tmdb
|
||||
* @param tvId The tmdb ID of the tv show
|
||||
* @return A tmdb tv show object, or null if not found
|
||||
*/
|
||||
suspend fun getTVShowDetails(tvId: Int): TMDBTVShow? = withContext(Dispatchers.IO) {
|
||||
val url = URL("$detailsTVUrl/$tvId?api_key=$apiKey&language=$language")
|
||||
|
||||
return@withContext try {
|
||||
val json = url.readText()
|
||||
Gson().fromJson(json, TMDBTVShow::class.java)
|
||||
} catch (ex: FileNotFoundException) {
|
||||
Log.w(javaClass.name, "Waring: The requested media was not found. Requested ID: $tvId", ex)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
/**
|
||||
* Get details for a tv show season from tmdb
|
||||
* @param tvId The tmdb ID of the tv show
|
||||
* @param seasonNumber The tmdb season number
|
||||
* @return A tmdb tv season object, or null if not found
|
||||
*/
|
||||
suspend fun getTVSeasonDetails(tvId: Int, seasonNumber: Int): TMDBTVSeason? = withContext(Dispatchers.IO) {
|
||||
val url = URL("$detailsTVUrl/$tvId/season/$seasonNumber?api_key=$apiKey&language=$language")
|
||||
|
||||
return@withContext try {
|
||||
val json = url.readText()
|
||||
Gson().fromJson(json, TMDBTVSeason::class.java)
|
||||
} catch (ex: FileNotFoundException) {
|
||||
Log.w(javaClass.name, "Waring: The requested media was not found. Requested ID: $tvId, Season: $seasonNumber", ex)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,92 @@
|
||||
/**
|
||||
* Teapod
|
||||
*
|
||||
* Copyright 2020-2021 <seil0@mosad.xyz>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||
* MA 02110-1301, USA.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.mosad.teapod.util.tmdb
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
/**
|
||||
* These data classes represent the tmdb api json objects.
|
||||
* Fields which are nullable in the tmdb api are also nullable here.
|
||||
*/
|
||||
|
||||
abstract class TMDBResult{
|
||||
abstract val id: Int
|
||||
abstract val name: String
|
||||
abstract val overview: String? // for movies tmdb return string or null
|
||||
abstract val posterPath: String?
|
||||
abstract val backdropPath: String?
|
||||
}
|
||||
|
||||
data class TMDBMovie(
|
||||
override val id: Int,
|
||||
override val name: String,
|
||||
override val overview: String?,
|
||||
@SerializedName("poster_path")
|
||||
override val posterPath: String?,
|
||||
@SerializedName("backdrop_path")
|
||||
override val backdropPath: String?,
|
||||
@SerializedName("release_date")
|
||||
val releaseDate: String,
|
||||
@SerializedName("runtime")
|
||||
val runtime: Int?,
|
||||
// TODO generes
|
||||
): TMDBResult()
|
||||
|
||||
data class TMDBTVShow(
|
||||
override val id: Int,
|
||||
override val name: String,
|
||||
override val overview: String,
|
||||
@SerializedName("poster_path")
|
||||
override val posterPath: String?,
|
||||
@SerializedName("backdrop_path")
|
||||
override val backdropPath: String?,
|
||||
@SerializedName("first_air_date")
|
||||
val firstAirDate: String,
|
||||
@SerializedName("status")
|
||||
val status: String,
|
||||
// TODO generes
|
||||
): TMDBResult()
|
||||
|
||||
data class TMDBTVSeason(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
val overview: String,
|
||||
@SerializedName("poster_path")
|
||||
val posterPath: String?,
|
||||
@SerializedName("air_date")
|
||||
val airDate: String,
|
||||
@SerializedName("episodes")
|
||||
val episodes: List<TMDBTVEpisode>,
|
||||
@SerializedName("season_number")
|
||||
val seasonNumber: Int
|
||||
)
|
||||
|
||||
data class TMDBTVEpisode(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
val overview: String,
|
||||
@SerializedName("air_date")
|
||||
val airDate: String,
|
||||
@SerializedName("episode_number")
|
||||
val episodeNumber: Int
|
||||
)
|
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
@ -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
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@drawable/dot_selected"
|
||||
android:state_selected="true"/>
|
||||
<item android:drawable="@drawable/dot_default"/>
|
||||
</selector>
|
6
app/src/main/res/drawable/ic_baseline_access_time_24.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z"/>
|
||||
<path android:fillColor="@android:color/white" android:pathData="M12.5,7H11v6l5.25,3.15 0.75,-1.23 -4.5,-2.67z"/>
|
||||
</vector>
|
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
@ -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>
|
@ -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
@ -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>
|
@ -3,17 +3,18 @@
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<group android:scaleX="0.051679686"
|
||||
android:scaleY="0.051679686"
|
||||
android:translateX="27.54"
|
||||
android:translateY="38.90954">
|
||||
<path
|
||||
android:pathData="m850.19,372.71c87.88,-11.01 119.04,-84.97 123.1,-99.87 4.06,-14.89 24.91,-80.57 11.92,-129.36 -12.99,-48.79 -34.36,-72.36 -58.62,-77.25 -24.25,-4.9 -50.59,10.51 -65,32.81 -14.41,22.3 -14.68,45.14 -14.78,55.29 -0.11,10.15 0.76,23.2 -3.37,33.29 -4.13,10.09 3.23,25.71 6.04,35.23 2.81,9.52 9.67,82.62 5.78,115.57 -3.89,32.95 -5.07,34.29 -5.07,34.29zM0.4,23.58C55.81,77.29 56.45,120.86 56.08,132.92c-0.36,12.06 4.77,130.59 11.47,150.76 4.42,13.3 12.11,50.16 41.78,74.48 25.51,20.91 58.65,31.38 58.65,31.38 0,0 36.42,78.46 78.83,108.64 31.56,22.46 39.61,23.74 46.5,35.55 6.18,10.6 93.56,62.62 275.1,47.23 127.29,-10.79 138.56,-44.3 138.56,-44.3 0,0 49.41,-21.9 101.15,-80.43 12.87,-14.56 4.41,-13.21 28.57,-17.79 24.16,-4.58 138.01,-45.58 170.66,-154.36C1039.99,175.32 1017.81,96.01 994.52,69.12 971.23,42.22 931.6,24.18 912.25,24.93c-18.47,0.71 -44.78,4.24 -80.21,46.87 -35.43,42.62 -28.94,37.4 -39.36,41.73 -6.82,2.83 -5.68,3.91 -26.75,-11.65 -20.23,-14.93 -28.9,-21.24 -43.38,-27.24 -7.96,-3.3 2.05,-5.55 2.59,-19.48 0.54,-13.93 2.4,-23.51 -17.32,-23.77 -19.72,-0.26 -408.02,0.21 -408.02,0.21 0,0 -18.8,-1.29 -7.79,24.82 4.2,9.94 -1.45,6.43 -33.27,25.85 -31.82,19.42 -55.58,34.4 -72.28,66.09 -8.43,16 -22.91,23.02 -27.97,8.05C153.44,141.43 125.2,48.96 105.17,23.22 85.56,-1.97 77.8,0.26 77.8,0.26Z"
|
||||
android:strokeLineJoin="miter"
|
||||
android:strokeWidth="0.41878"
|
||||
android:fillColor="#000000"
|
||||
android:strokeColor="#000000"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeLineCap="butt"/>
|
||||
</group>
|
||||
<group
|
||||
android:scaleX="0.051679686"
|
||||
android:scaleY="0.051679686"
|
||||
android:translateX="27.54"
|
||||
android:translateY="38.90954">
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="m850.19,372.71c87.88,-11.01 119.04,-84.97 123.1,-99.87 4.06,-14.89 24.91,-80.57 11.92,-129.36 -12.99,-48.79 -34.36,-72.36 -58.62,-77.25 -24.25,-4.9 -50.59,10.51 -65,32.81 -14.41,22.3 -14.68,45.14 -14.78,55.29 -0.11,10.15 0.76,23.2 -3.37,33.29 -4.13,10.09 3.23,25.71 6.04,35.23 2.81,9.52 9.67,82.62 5.78,115.57 -3.89,32.95 -5.07,34.29 -5.07,34.29zM0.4,23.58C55.81,77.29 56.45,120.86 56.08,132.92c-0.36,12.06 4.77,130.59 11.47,150.76 4.42,13.3 12.11,50.16 41.78,74.48 25.51,20.91 58.65,31.38 58.65,31.38 0,0 36.42,78.46 78.83,108.64 31.56,22.46 39.61,23.74 46.5,35.55 6.18,10.6 93.56,62.62 275.1,47.23 127.29,-10.79 138.56,-44.3 138.56,-44.3 0,0 49.41,-21.9 101.15,-80.43 12.87,-14.56 4.41,-13.21 28.57,-17.79 24.16,-4.58 138.01,-45.58 170.66,-154.36C1039.99,175.32 1017.81,96.01 994.52,69.12 971.23,42.22 931.6,24.18 912.25,24.93c-18.47,0.71 -44.78,4.24 -80.21,46.87 -35.43,42.62 -28.94,37.4 -39.36,41.73 -6.82,2.83 -5.68,3.91 -26.75,-11.65 -20.23,-14.93 -28.9,-21.24 -43.38,-27.24 -7.96,-3.3 2.05,-5.55 2.59,-19.48 0.54,-13.93 2.4,-23.51 -17.32,-23.77 -19.72,-0.26 -408.02,0.21 -408.02,0.21 0,0 -18.8,-1.29 -7.79,24.82 4.2,9.94 -1.45,6.43 -33.27,25.85 -31.82,19.42 -55.58,34.4 -72.28,66.09 -8.43,16 -22.91,23.02 -27.97,8.05C153.44,141.43 125.2,48.96 105.17,23.22 85.56,-1.97 77.8,0.26 77.8,0.26Z"
|
||||
android:strokeWidth="0.41878"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeLineCap="butt"
|
||||
android:strokeLineJoin="miter" />
|
||||
</group>
|
||||
</vector>
|
||||
|
10
app/src/main/res/drawable/ic_outline_download_24.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="#FFFFFF"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M18,15v3H6v-3H4v3c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2v-3H18zM17,11l-1.41,-1.41L13,12.17V4h-2v8.17L8.41,9.59L7,11l5,5L17,11z" />
|
||||
</vector>
|
10
app/src/main/res/drawable/ic_outline_info_24.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="#FFFFFF"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M11,7h2v2h-2zM11,11h2v6h-2zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8z" />
|
||||
</vector>
|
10
app/src/main/res/drawable/ic_outline_upload_24.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="#FFFFFF"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M18,15v3H6v-3H4v3c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2v-3H18zM7,9l1.41,1.41L11,7.83V16h2V7.83l2.59,2.58L17,9l-5,-5L7,9z" />
|
||||
</vector>
|
@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="?textBackground"/>
|
||||
<solid android:color="?attr/shapeTextBackground"/>
|
||||
<corners android:radius="3dp"/>
|
||||
</shape>
|
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>
|
@ -7,7 +7,7 @@
|
||||
android:layout_height="match_parent"
|
||||
android:background="#000000"
|
||||
android:keepScreenOn="true"
|
||||
tools:context=".player.PlayerActivity">
|
||||
tools:context=".ui.activity.player.PlayerActivity">
|
||||
|
||||
<com.google.android.exoplayer2.ui.StyledPlayerView
|
||||
android:id="@+id/video_view"
|
||||
@ -89,4 +89,20 @@
|
||||
app:backgroundTint="@color/exo_white"
|
||||
app:iconGravity="textStart" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_skip_op"
|
||||
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/skip_opening"
|
||||
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>
|
@ -10,7 +10,7 @@
|
||||
android:layout_height="48dp"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:background="@drawable/ic_baseline_rewind_10_24"
|
||||
android:contentDescription="@string/forward_10" />
|
||||
android:contentDescription="@string/rewind_10" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView"
|
||||
|
@ -1,10 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<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.fragments.AboutFragment">
|
||||
tools:context=".ui.activity.main.fragments.AboutFragment">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
@ -17,11 +19,11 @@
|
||||
android:orientation="vertical">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imageView5"
|
||||
android:id="@+id/image_app_icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginTop="17dp"
|
||||
android:contentDescription="@string/app_name"
|
||||
android:src="@mipmap/ic_launcher_round" />
|
||||
|
||||
@ -30,23 +32,13 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="5dp"
|
||||
android:layout_marginTop="7dp"
|
||||
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_version"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="5dp"
|
||||
android:layout_marginTop="5dp"
|
||||
android:layout_marginEnd="5dp"
|
||||
android:text="@string/info_about_desc"
|
||||
android:textAlignment="center" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_about_info"
|
||||
android:layout_width="match_parent"
|
||||
@ -54,30 +46,188 @@
|
||||
android:layout_marginStart="5dp"
|
||||
android:layout_marginTop="7dp"
|
||||
android:layout_marginEnd="5dp"
|
||||
android:text="@string/about_info"
|
||||
android:textAlignment="center" />
|
||||
android:layout_marginBottom="12dp"
|
||||
android:text="@string/about_info" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_tmdb_notice"
|
||||
<LinearLayout
|
||||
android:id="@+id/linear_version"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="7dp"
|
||||
android:layout_marginTop="7dp"
|
||||
android:layout_marginEnd="7dp"
|
||||
android:text="@string/tmdb_notice"
|
||||
android:textAlignment="center"
|
||||
android:textColor="?textSecondary" />
|
||||
android:layout_height="match_parent"
|
||||
android:foreground="?android:selectableItemBackground"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal"
|
||||
android:padding="7dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_teapod_repo"
|
||||
<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="wrap_content"
|
||||
android:layout_marginStart="7dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginEnd="7dp"
|
||||
android:autoLink="web"
|
||||
android:text="@string/teapod_repo"
|
||||
android:textAlignment="center" />
|
||||
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>
|
||||
|
||||
@ -107,5 +257,18 @@
|
||||
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>
|
@ -5,7 +5,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?themePrimary"
|
||||
tools:context=".ui.fragments.AccountFragment">
|
||||
tools:context=".ui.activity.main.fragments.AccountFragment">
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
@ -79,8 +79,52 @@
|
||||
android:text="@string/account_login_desc"
|
||||
android:textColor="?textSecondary" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/linear_account_subscription"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:foreground="?android:selectableItemBackground"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal"
|
||||
android:padding="7dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imageView6"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/account"
|
||||
android:minWidth="48dp"
|
||||
android:minHeight="48dp"
|
||||
android:padding="9dp"
|
||||
android:scaleType="fitXY"
|
||||
android:src="@drawable/ic_baseline_access_time_24"
|
||||
app:tint="?iconColor" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_account_subscription"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/account_subscription"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_account_subscription_desc"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/account_subscription_desc"
|
||||
android:textColor="?textSecondary" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
@ -176,7 +220,7 @@
|
||||
android:padding="7dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imageView4"
|
||||
android:id="@+id/image_autoplay"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/settings_autoplay"
|
||||
@ -237,7 +281,7 @@
|
||||
android:padding="7dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imageViewTheme"
|
||||
android:id="@+id/image_theme"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/account"
|
||||
@ -274,6 +318,118 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/linear_dev_settings"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:background="?themeSecondary"
|
||||
android:clipToPadding="false"
|
||||
android:elevation="5dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_dev_settings"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="7dp"
|
||||
android:paddingEnd="7dp"
|
||||
android:text="@string/dev_settings"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/linear_export_data"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:foreground="?android:selectableItemBackground"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal"
|
||||
android:padding="7dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/image_export_data"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/info"
|
||||
android:minWidth="48dp"
|
||||
android:minHeight="48dp"
|
||||
android:padding="9dp"
|
||||
android:scaleType="fitXY"
|
||||
app:srcCompat="@drawable/ic_outline_upload_24"
|
||||
app:tint="?iconColor" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_export_data"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/export_data"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_export_data_desc"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/export_data_desc"
|
||||
android:textColor="?textSecondary" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/linear_import_data"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:foreground="?android:selectableItemBackground"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal"
|
||||
android:padding="7dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/image_import_data"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/info"
|
||||
android:minWidth="48dp"
|
||||
android:minHeight="48dp"
|
||||
android:padding="9dp"
|
||||
android:scaleType="fitXY"
|
||||
app:srcCompat="@drawable/ic_outline_download_24"
|
||||
app:tint="?iconColor" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_import_data"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/import_data"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_import_data_desc"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/import_data_desc"
|
||||
android:textColor="?textSecondary" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/linear_info"
|
||||
android:layout_width="match_parent"
|
||||
@ -312,7 +468,7 @@
|
||||
android:minHeight="48dp"
|
||||
android:padding="9dp"
|
||||
android:scaleType="fitXY"
|
||||
app:srcCompat="@drawable/ic_baseline_info_24"
|
||||
app:srcCompat="@drawable/ic_outline_info_24"
|
||||
app:tint="?iconColor" />
|
||||
|
||||
<LinearLayout
|
||||
|
@ -6,7 +6,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?themePrimary"
|
||||
tools:context=".ui.fragments.HomeFragment">
|
||||
tools:context=".ui.activity.main.fragments.HomeFragment">
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
@ -97,7 +97,7 @@
|
||||
android:textColor="?textSecondary"
|
||||
android:textSize="12sp"
|
||||
app:drawableTint="?buttonBackground"
|
||||
app:drawableTopCompat="@drawable/ic_baseline_info_24" />
|
||||
app:drawableTopCompat="@drawable/ic_outline_info_24" />
|
||||
|
||||
<Space
|
||||
android:layout_width="0dp"
|
||||
@ -219,6 +219,34 @@
|
||||
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>
|
||||
|
||||
|
@ -5,7 +5,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?themePrimary"
|
||||
tools:context=".ui.fragments.LibraryFragment">
|
||||
tools:context=".ui.activity.main.fragments.LibraryFragment">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler_media_library"
|
||||
|
@ -1,164 +1,189 @@
|
||||
<?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="?themePrimary"
|
||||
tools:context=".ui.fragments.MediaFragment">
|
||||
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"
|
||||
tools:src="@drawable/ic_launcher_background" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/linear_media_info"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_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_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">
|
||||
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="?buttonBackground"
|
||||
android:contentDescription="@string/my_list" />
|
||||
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:textColor="?textSecondary"
|
||||
android:textSize="12sp" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
android:padding="2dp"
|
||||
android:text="@string/text_year_ex" />
|
||||
|
||||
<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>
|
||||
<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.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"/>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/frame_loading"
|
||||
@ -177,4 +202,4 @@
|
||||
tools:visibility="visible" />
|
||||
</FrameLayout>
|
||||
|
||||
</FrameLayout>
|
||||
</RelativeLayout>
|
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
@ -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
@ -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
@ -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>
|
@ -5,7 +5,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?themePrimary"
|
||||
tools:context=".ui.fragments.SearchFragment">
|
||||
tools:context=".ui.activity.main.fragments.SearchFragment">
|
||||
|
||||
<SearchView
|
||||
android:id="@+id/search_text"
|
||||
|
@ -86,7 +86,7 @@
|
||||
android:id="@+id/button_select"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/save"
|
||||
android:text="@string/apply"
|
||||
android:textAllCaps="false"
|
||||
android:textColor="@color/themePrimaryDark"
|
||||
android:textSize="16sp"
|
||||
|
@ -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" />
|
||||
|
||||
|
@ -1,68 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<notices>
|
||||
<notice>
|
||||
<name>AndroidX</name>
|
||||
<url>https://developer.android.com/jetpack/androidx</url>
|
||||
<copyright>Copyright The Android Open Source Project</copyright>
|
||||
<license>Apache Software License 2.0</license>
|
||||
</notice>
|
||||
<notice>
|
||||
<name>Material Components for Android</name>
|
||||
<url>https://github.com/material-components/material-components-android</url>
|
||||
<copyright>Copyright The Android Open Source Project</copyright>
|
||||
<license>Apache Software License 2.0</license>
|
||||
</notice>
|
||||
<notice>
|
||||
<name>ExoPlayer</name>
|
||||
<url>https://github.com/google/ExoPlayer</url>
|
||||
<copyright>Copyright The Android Open Source Project</copyright>
|
||||
<license>Apache Software License 2.0</license>
|
||||
</notice>
|
||||
<notice>
|
||||
<name>Gson</name>
|
||||
<url>https://github.com/google/gson</url>
|
||||
<copyright>Copyright 2008 Google Inc.</copyright>
|
||||
<license>Apache Software License 2.0</license>
|
||||
</notice>
|
||||
<notice>
|
||||
<name>Material design icons</name>
|
||||
<url>https://github.com/google/material-design-icons</url>
|
||||
<copyright>Copyright Google Inc.</copyright>
|
||||
<license>Apache Software License 2.0</license>
|
||||
</notice>
|
||||
<notice>
|
||||
<name>Material Dialogs</name>
|
||||
<url>https://github.com/afollestad/material-dialogs</url>
|
||||
<copyright>Copyright Aidan Follestad</copyright>
|
||||
<license>Apache Software License 2.0</license>
|
||||
</notice>
|
||||
<notice>
|
||||
<name>Jsoup</name>
|
||||
<url>https://jsoup.org/</url>
|
||||
<copyright>Copyright 2009 - 2020 Jonathan Hedley</copyright>
|
||||
<license>MIT License</license>
|
||||
</notice>
|
||||
<notice>
|
||||
<name>kotlinx.coroutines</name>
|
||||
<url>https://github.com/Kotlin/kotlinx.coroutines</url>
|
||||
<copyright>Copyright 2016 - 2019 JetBrains</copyright>
|
||||
<license>Apache Software License 2.0</license>
|
||||
</notice>
|
||||
<notice>
|
||||
<name>Glide</name>
|
||||
<url>https://github.com/bumptech/glide</url>
|
||||
<copyright>Copyright Google, Inc</copyright>
|
||||
<license>BSD 2-Clause License</license>
|
||||
</notice>
|
||||
<notice>
|
||||
<name>Glide Transformations</name>
|
||||
<url>https://github.com/wasabeef/glide-transformations</url>
|
||||
<copyright>Copyright 2020 Wasabeef</copyright>
|
||||
<license>Apache Software License 2.0</license>
|
||||
</notice>
|
||||
<notice>
|
||||
<name>The Movie Database API</name>
|
||||
<url>https://www.themoviedb.org</url>
|
||||
<copyright>This product uses the TMDb API but is not endorsed or certified by TMDb</copyright>
|
||||
</notice>
|
||||
</notices>
|
@ -11,20 +11,30 @@
|
||||
<string name="new_episodes">Neue Episoden</string>
|
||||
<string name="new_simulcasts">Neue Simulcasts</string>
|
||||
<string name="new_titles">Neue Titel</string>
|
||||
<string name="top_ten">Top 10</string>
|
||||
|
||||
<!-- search fragment -->
|
||||
<string name="search_hint">Suche nach Filmen und Serien</string>
|
||||
|
||||
<!-- media fragment -->
|
||||
<string name="button_play">Abspielen</string>
|
||||
<string name="text_episodes_count">%1$d Episoden</string>
|
||||
<string name="text_runtime">%1$d Minuten</string>
|
||||
<plurals name="text_episodes_count">
|
||||
<item quantity="one">%d Episode</item>
|
||||
<item quantity="other">%d Episoden</item>
|
||||
</plurals>
|
||||
<plurals name="text_runtime">
|
||||
<item quantity="one">%d Minute</item>
|
||||
<item quantity="other">%d Minuten</item>
|
||||
</plurals>
|
||||
<string name="similar_titles">Ähnliche Titel</string>
|
||||
<string name="component_episode_title">Flg. %1$d %2$s</string>
|
||||
<string name="component_episode_title_sub">Flg. %1$d %2$s (OmU)</string>
|
||||
|
||||
<!-- settings fragment -->
|
||||
<string name="account">Account</string>
|
||||
<string name="account_login_desc">Zum bearbeiten tippen</string>
|
||||
<string name="account_subscription">Abo %1$s</string>
|
||||
<string name="account_subscription_desc">Zum verlängern tippen</string>
|
||||
<string name="info">Info</string>
|
||||
<string name="info_about_desc">Version %1$s (%2$s)</string>
|
||||
<string name="settings">Einstellungen</string>
|
||||
@ -35,16 +45,23 @@
|
||||
<string name="theme">Design</string>
|
||||
<string name="theme_light">Hell</string>
|
||||
<string name="theme_dark">Dunkel</string>
|
||||
<string name="dev_settings">Entwickler Einstellungen</string>
|
||||
<string name="export_data">Daten exportieren</string>
|
||||
<string name="export_data_desc">Speichere "Meine Liste" in eine Datei</string>
|
||||
<string name="import_data">Daten importieren</string>
|
||||
<string name="import_data_desc">Lade "Meine Liste" aus einer Datei</string>
|
||||
<string name="import_data_success">"Meine Liste" erfolgreich importiert</string>
|
||||
|
||||
<!-- about fragment -->
|
||||
<string name="about_info">
|
||||
Teapod ist eine inoffizielle App für Anime on Demand.
|
||||
Sie wird unter den Bedingungen der GNU GPL 3 oder höher zur Verfügung gestellt.
|
||||
\n\n
|
||||
© 2020-2021 seil0@mosad.xyz
|
||||
</string>
|
||||
<string name="version">Version</string>
|
||||
<string name="authors">Autor</string>
|
||||
<string name="source">Quellcode</string>
|
||||
<string name="license">Lizenz</string>
|
||||
<string name="about_info">Eine inoffizielle App für Anime on Demand.</string>
|
||||
<string name="third_party_heading">Lizenzen von Drittanbietern</string>
|
||||
<string name="third_party_component_desc">© %1$s %2$s unter %3$s</string>
|
||||
<string name="dev_settings_enabled">Du bist jetzt ein Entwickler</string>
|
||||
<string name="dev_settings_already">Du bist schon ein Entwickler</string>
|
||||
|
||||
<!-- player -->
|
||||
<string name="close_player">Player schließen</string>
|
||||
@ -52,13 +69,28 @@
|
||||
<string name="play_pause">Abspielen/Pause</string>
|
||||
<string name="forward_10">10 Sekunden vorwärts</string>
|
||||
<string name="next_episode">Nächste Folge</string>
|
||||
<string name="skip_opening">Intro überspringen</string>
|
||||
<string name="language">Sprache</string>
|
||||
<string name="episodes">Folgen</string>
|
||||
<string name="episode">Folge</string>
|
||||
|
||||
<!-- Onboarding -->
|
||||
<string name="skip">Überspringen</string>
|
||||
<string name="next">Weiter</string>
|
||||
<string name="start">Fertig</string>
|
||||
<string name="on_welcome">Willkommen!\nTeapod ist eine inoffizielle App für AoD.</string>
|
||||
<string name="on_get_started">Los geht\'s</string>
|
||||
<string name="on_login_heading">Login</string>
|
||||
<string name="on_login_desc">Um Teapod verwenden zu können musst du dich mit deinem AoD Account anmelden. Deine Login-Daten werden verschlüsselt auf deinem Gerät gespeichert.</string>
|
||||
<string name="on_login_failed">Login nicht erfolgreich! Stelle sicher das deine Login-Daten korrekt sind und versuche es erneut.</string>
|
||||
|
||||
<!-- dialogs -->
|
||||
<string name="save">speichern</string>
|
||||
<string name="save">Speichern</string>
|
||||
<string name="apply">Übernehmen</string>
|
||||
<string name="cancel">@android:string/cancel</string>
|
||||
<string name="loading">Lädt…</string>
|
||||
<string name="dialog_timeout_head">Anmelden fehlgeschlagen</string>
|
||||
<string name="dialog_timeout_desc">Der Server scheint langsam zu antworten. Bitte versuche es später noch einmal.</string>
|
||||
|
||||
<!-- etc -->
|
||||
<string name="login">Login</string>
|
||||
|
@ -4,7 +4,7 @@
|
||||
<attr format="color" name="themeSecondary"/>
|
||||
<attr format="color" name="textPrimary"/>
|
||||
<attr format="color" name="textSecondary"/>
|
||||
<attr format="color" name="textBackground"/>
|
||||
<attr format="color" name="iconColor"/>
|
||||
<attr format="color" name="buttonBackground"/>
|
||||
<attr format="color" name="shapeTextBackground"/>
|
||||
</resources>
|
@ -1,6 +0,0 @@
|
||||
<resources>
|
||||
<!-- Default screen margins, per the Android Design guidelines. -->
|
||||
<dimen name="activity_horizontal_margin">16dp</dimen>
|
||||
<dimen name="activity_vertical_margin">16dp</dimen>
|
||||
<dimen name="text_margin">16dp</dimen>
|
||||
</resources>
|
@ -11,10 +11,12 @@
|
||||
<string name="new_episodes">New episodes</string>
|
||||
<string name="new_simulcasts">New simulcasts</string>
|
||||
<string name="new_titles">New titles</string>
|
||||
<string name="top_ten">Top 10</string>
|
||||
|
||||
<!-- search fragment -->
|
||||
<string name="search_hint">Search for movies and series</string>
|
||||
<string name="media_poster_desc" translatable="false">poster</string>
|
||||
<string name="media_poster_backdrop_desc" translatable="false">poster backdrop</string>
|
||||
|
||||
<!-- media fragment -->
|
||||
<string name="button_play">Play</string>
|
||||
@ -22,8 +24,16 @@
|
||||
<string name="text_overview_ex" translatable="false">Shouya Ishida starts bullying the new girl in class …</string>
|
||||
<string name="text_year_ex" translatable="false">2016</string>
|
||||
<string name="text_age_ex" translatable="false">6</string>
|
||||
<string name="text_episodes_count">%1$d episodes</string>
|
||||
<string name="text_runtime">%1$d Minutes</string>
|
||||
<string name="text_episodes_count" translatable="false">1$d episodes</string>
|
||||
<plurals name="text_episodes_count">
|
||||
<item quantity="one">%d episode</item>
|
||||
<item quantity="other">%d episodes</item>
|
||||
</plurals>
|
||||
<plurals name="text_runtime">
|
||||
<item quantity="one">%d Minute</item>
|
||||
<item quantity="other">%d Minutes</item>
|
||||
</plurals>
|
||||
<string name="similar_titles">Similar titles</string>
|
||||
<string name="component_episode_title">Ep. %1$d %2$s</string>
|
||||
<string name="component_episode_title_sub">Ep. %1$d %2$s (Sub)</string>
|
||||
<string name="component_poster_desc" translatable="false">episode poster</string>
|
||||
@ -33,6 +43,8 @@
|
||||
<string name="account">Account</string>
|
||||
<string name="account_login_ex" translatable="false">user@example.com</string>
|
||||
<string name="account_login_desc">Tap to edit</string>
|
||||
<string name="account_subscription">Subscription %1$s</string>
|
||||
<string name="account_subscription_desc">Tap to extend</string>
|
||||
<string name="info">Info</string>
|
||||
<string name="info_about" translatable="false">Teapod by @Seil0</string>
|
||||
<string name="info_about_desc">Version %1$s (%2$s)</string>
|
||||
@ -44,18 +56,29 @@
|
||||
<string name="theme">Theme</string>
|
||||
<string name="theme_light">Light</string>
|
||||
<string name="theme_dark">Dark</string>
|
||||
<string name="dev_settings">Developer Settings</string>
|
||||
<string name="export_data">export data</string>
|
||||
<string name="export_data_desc">export "My list" to a file</string>
|
||||
<string name="import_data">import data</string>
|
||||
<string name="import_data_desc">import "My list" from a file</string>
|
||||
<string name="import_data_success">imported "My list" successfully</string>
|
||||
|
||||
|
||||
<!-- about fragment -->
|
||||
<string name="about_info">
|
||||
Teapod is an unofficial app for anime on demand.
|
||||
It is published under the terms and conditions of the GNU GPL 3 or later.
|
||||
\n\n
|
||||
© 2020-2021 seil0@mosad.xyz
|
||||
</string>
|
||||
<string name="tmdb_notice" translatable="false">This product uses the TMDb API but is not endorsed or certified by TMDb.</string>
|
||||
<string name="version">Version</string>
|
||||
<string name="version_desc" translatable="false">%1$s (%2$s)</string>
|
||||
<string name="authors">Author</string>
|
||||
<string name="author_desc" translatable="false">seil0@mosad.xyz</string>
|
||||
<string name="source">Source code</string>
|
||||
<string name="teapod_repo" translatable="false">git.mosad.xyz/Seil0/teapod</string>
|
||||
<string name="license">License</string>
|
||||
<string name="license_desc" translatable="false">GNU General Public License 3</string>
|
||||
<string name="about_info">An unofficial app for anime on demand.</string>
|
||||
<string name="tmdb_notice" translatable="false">This product uses the TMDb API but is not endorsed or certified by TMDb.</string>
|
||||
<string name="third_party_heading">Third Party Licenses</string>
|
||||
<string name="third_party_component_desc">© %1$s %2$s under %3$s</string>
|
||||
<string name="dev_settings_enabled">You are now a developer</string>
|
||||
<string name="dev_settings_already">You are already a developer</string>
|
||||
|
||||
<!-- player -->
|
||||
<string name="close_player">close player</string>
|
||||
@ -65,20 +88,35 @@
|
||||
<string name="rwd_10_s" translatable="false">- 10 s</string>
|
||||
<string name="fwd_10_s" translatable="false">+ 10 s</string>
|
||||
<string name="next_episode">Next Episode</string>
|
||||
<string name="skip_opening">Skip Opening</string>
|
||||
<string name="time_min_sec" translatable="false">%1$02d:%2$02d</string>
|
||||
<string name="time_hour_min_sec" translatable="false">%1$d:%2$02d:%3$02d</string>
|
||||
<string name="language">Language</string>
|
||||
<string name="episodes">Episodes</string>
|
||||
<string name="episode">Episode</string>
|
||||
|
||||
<!-- Onboarding -->
|
||||
<string name="skip">Skip</string>
|
||||
<string name="next">Next</string>
|
||||
<string name="start">Start</string>
|
||||
<string name="on_welcome">Welcome!\nTeapod is an unofficial App for AoD.</string>
|
||||
<string name="on_get_started">Get started</string>
|
||||
<string name="on_login_heading">Login</string>
|
||||
<string name="on_login_desc">To use Teapod you need to log in with your AoD account. Your Login-Data will be stored encrypted on your device.</string>
|
||||
<string name="on_login_failed">Could not login! Make sure Username and Password are correct and try again.</string>
|
||||
|
||||
<!-- dialogs -->
|
||||
<string name="save">save</string>
|
||||
<string name="save">Save</string>
|
||||
<string name="cancel">@android:string/cancel</string>
|
||||
<string name="apply">Apply</string>
|
||||
<string name="loading">Loading…</string>
|
||||
<string name="dialog_timeout_head">Login failed</string>
|
||||
<string name="dialog_timeout_desc">Looks like the server is taking to long to respond. Please try again later.</string>
|
||||
|
||||
<!-- etc -->
|
||||
<string name="login">Login</string>
|
||||
<string name="login_desc">You need to login before you can use Teapod. The Login-Data will be stored encrypted on your device.</string>
|
||||
<string name="login_failed_desc">Could not login. Please try again.</string>
|
||||
<string name="login_failed_desc"> Please try again.</string>
|
||||
<string name="password">Password</string>
|
||||
|
||||
<!-- save keys -->
|
||||
@ -88,12 +126,10 @@
|
||||
<string name="save_key_user_password" translatable="false">org.mosad.teapod.user_password</string>
|
||||
<string name="save_key_prefer_secondary" translatable="false">org.mosad.teapod.prefer_secondary</string>
|
||||
<string name="save_key_autoplay" translatable="false">org.mosad.teapod.autoplay</string>
|
||||
<string name="save_key_dev_settings" translatable="false">org.mosad.teapod.dev.settings</string>
|
||||
<string name="save_key_theme" translatable="false">org.mosad.teapod.theme</string>
|
||||
|
||||
<!-- intents & states -->
|
||||
<string name="intent_media_id" translatable="false">intent_media_id</string>
|
||||
<string name="intent_episode_id" translatable="false">intent_episode_id</string>
|
||||
<string name="state_resume_window" translatable="false">state_resume_window</string>
|
||||
<string name="state_resume_position" translatable="false">state_resume_position</string>
|
||||
<string name="state_is_playing" translatable="false">state_is_playing</string>
|
||||
</resources>
|
@ -14,11 +14,14 @@
|
||||
<item name="android:textColor">@color/textPrimaryLight</item>
|
||||
<item name="android:textColorPrimary">@color/textPrimaryLight</item>
|
||||
<item name="android:textColorHint">@color/textSecondaryLight</item>
|
||||
<item name="textBackground">@color/textBackgroundLight</item>
|
||||
<item name="shapeTextBackground">@color/textBackgroundLight</item>
|
||||
<item name="iconColor">@color/iconColorLight</item>
|
||||
<item name="buttonBackground">@color/buttonBackgroundLight</item>
|
||||
<item name="md_background_color">@color/themeSecondaryLight</item>
|
||||
<item name="md_color_content">@color/textSecondaryLight</item>
|
||||
|
||||
<!-- without this, the unchecked single choice buttons while be white -->
|
||||
<item name="md_color_widget_unchecked">@color/textSecondaryLight</item>
|
||||
</style>
|
||||
|
||||
<style name="AppTheme.Dark" parent="AppTheme">
|
||||
@ -29,7 +32,7 @@
|
||||
<item name="android:textColor">@color/textPrimaryDark</item>
|
||||
<item name="android:textColorPrimary">@color/textPrimaryDark</item>
|
||||
<item name="android:textColorHint">@color/textSecondaryDark</item>
|
||||
<item name="textBackground">@color/textBackgroundDark</item>
|
||||
<item name="shapeTextBackground">@color/textBackgroundDark</item>
|
||||
<item name="iconColor">@color/iconColorDark</item>
|
||||
<item name="buttonBackground">@color/buttonBackgroundDark</item>
|
||||
<item name="md_background_color">@color/themeSecondaryDark</item>
|
||||
@ -41,10 +44,6 @@
|
||||
<item name="colorControlHighlight">@color/controlHighlightDark</item>
|
||||
</style>
|
||||
|
||||
<style name="LicensesDialogTheme.Dark" parent="Theme.AppCompat.Dialog">
|
||||
<item name="android:windowBackground">@color/themeSecondaryDark</item>
|
||||
</style>
|
||||
|
||||
<!-- player theme -->
|
||||
<style name="PlayerTheme" parent="Theme.MaterialComponents.Light.NoActionBar">
|
||||
<item name="android:windowNoTitle">true</item>
|
||||
|
@ -1,12 +1,12 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
buildscript {
|
||||
ext.kotlin_version = "1.4.21"
|
||||
ext.kotlin_version = "1.5.30"
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:4.1.1'
|
||||
classpath 'com.android.tools.build:gradle:7.0.1'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
@ -17,7 +17,7 @@ buildscript {
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
|
6
fastlane/metadata/android/de/changelogs/4000.txt
Normal file
@ -0,0 +1,6 @@
|
||||
* Der Player unterstützt nun den Picture in Picture Modus (#24)
|
||||
* Ähnliche Titel werden im Media Fragment angezeigt (#28)
|
||||
* Verbessertes Onboarding bei der ersten Nutzung der App (#14)
|
||||
* Top Ten Animes auf AoD zur Startseite hinzugefügt
|
||||
* Die App stürzt nicht mehr ab, wenn der Login zu lange dauert (#25)
|
||||
* Es werden nun alle Episoden angezeigt, auch einzelne (#26)
|
1
fastlane/metadata/android/de/changelogs/4100.txt
Normal file
@ -0,0 +1 @@
|
||||
* Ein Fehler beim laden einiger Serien wurde behoben (#36)
|
5
fastlane/metadata/android/de/changelogs/4200.txt
Normal file
@ -0,0 +1,5 @@
|
||||
* Entwickleroptionen
|
||||
* Export/Import für "Meine Liste"
|
||||
* Der Picture in Picture Modus hat nun Controlls (#35)
|
||||
* Teapod stürtzt nicht mehr ab, wenn ein Element aus "Meine List" nicht geladen werden konnte (#42)
|
||||
* Staffel-Informationen im Title werden bei der Suche in tmdb ignoriert (#43)
|
After Width: | Height: | Size: 401 KiB |
After Width: | Height: | Size: 419 KiB |
After Width: | Height: | Size: 393 KiB |
After Width: | Height: | Size: 261 KiB |
6
fastlane/metadata/android/en-US/changelogs/4000.txt
Normal file
@ -0,0 +1,6 @@
|
||||
* support for Picture in Picture mode was added to the player (#24)
|
||||
* show similar titles in the media fragment (#28)
|
||||
* improve the onboarding process for new users (#14)
|
||||
* add top ten animes on AoD to the home screen
|
||||
* the app doesn't crash anymore if the login times out (#25)
|
||||
* fix episodes not showing, if show has only one episode (#26)
|
1
fastlane/metadata/android/en-US/changelogs/4100.txt
Normal file
@ -0,0 +1 @@
|
||||
* fixed a issue where some tv shows could not be loaded (#36)
|
5
fastlane/metadata/android/en-US/changelogs/4200.txt
Normal file
@ -0,0 +1,5 @@
|
||||
* Developer options
|
||||
* Export/Import for "My List"
|
||||
* The Picture in Picture Modus now has Controlls (#35)
|
||||
* Teapod deosn't crash, if a element from "My List" could not be loaded (#42)
|
||||
* Season-Information in titles will be ignored, when searching in tmdb (#43)
|
@ -1,4 +1,4 @@
|
||||
Teapod is a unoffical App for Anime-on-Demand (AoD).
|
||||
Teapod is a unofficial App for Anime-on-Demand (AoD).
|
||||
|
||||
* Watch all animes from AoD on your Android device
|
||||
* Native Player based on ExoPayer
|
||||
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,5 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
2
gradlew
vendored
@ -130,7 +130,7 @@ fi
|
||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
|
||||
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
|
21
gradlew.bat
vendored
@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto init
|
||||
if "%ERRORLEVEL%" == "0" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
@ -54,7 +54,7 @@ goto fail
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto init
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
@ -64,21 +64,6 @@ echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:init
|
||||
@rem Get command-line arguments, handling Windows variants
|
||||
|
||||
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||
|
||||
:win9xME_args
|
||||
@rem Slurp the command line arguments.
|
||||
set CMD_LINE_ARGS=
|
||||
set _SKIP=2
|
||||
|
||||
:win9xME_args_slurp
|
||||
if "x%~1" == "x" goto execute
|
||||
|
||||
set CMD_LINE_ARGS=%*
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
@ -86,7 +71,7 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
|