Merge pull request 'version 0.4.2' (#44) from develop into master
Reviewed-on: #44
This commit is contained in:
commit
19552d3950
@ -10,8 +10,8 @@ android {
|
|||||||
applicationId "org.mosad.teapod"
|
applicationId "org.mosad.teapod"
|
||||||
minSdkVersion 23
|
minSdkVersion 23
|
||||||
targetSdkVersion 30
|
targetSdkVersion 30
|
||||||
versionCode 4100 //00.04.100
|
versionCode 4200 //00.04.200
|
||||||
versionName "0.4.1"
|
versionName "0.4.2"
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
resValue "string", "build_time", buildTime()
|
resValue "string", "build_time", buildTime()
|
||||||
@ -41,22 +41,25 @@ android {
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation fileTree(dir: "libs", include: ["*.jar"])
|
implementation fileTree(dir: "libs", include: ["*.jar"])
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.3'
|
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0'
|
||||||
|
|
||||||
implementation 'androidx.core:core-ktx:1.3.2'
|
implementation 'androidx.core:core-ktx:1.6.0'
|
||||||
implementation 'androidx.appcompat:appcompat:1.2.0'
|
implementation 'androidx.appcompat:appcompat:1.3.0'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
||||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.4'
|
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5'
|
||||||
implementation 'androidx.navigation:navigation-ui-ktx:2.3.4'
|
implementation 'androidx.navigation:navigation-ui-ktx:2.3.5'
|
||||||
implementation 'androidx.security:security-crypto:1.1.0-alpha03'
|
implementation 'androidx.security:security-crypto:1.1.0-alpha03'
|
||||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||||
|
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
|
||||||
|
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1'
|
||||||
|
|
||||||
implementation 'com.google.android.material:material:1.3.0'
|
implementation 'com.google.android.material:material:1.4.0'
|
||||||
implementation 'com.google.code.gson:gson:2.8.6'
|
implementation 'com.google.code.gson:gson:2.8.7'
|
||||||
implementation 'com.google.android.exoplayer:exoplayer-core:2.13.2'
|
implementation 'com.google.android.exoplayer:exoplayer-core:2.14.1'
|
||||||
implementation 'com.google.android.exoplayer:exoplayer-hls:2.13.2'
|
implementation 'com.google.android.exoplayer:exoplayer-hls:2.14.1'
|
||||||
implementation 'com.google.android.exoplayer:exoplayer-dash:2.13.2'
|
implementation 'com.google.android.exoplayer:exoplayer-dash:2.14.1'
|
||||||
implementation 'com.google.android.exoplayer:exoplayer-ui:2.13.2'
|
implementation 'com.google.android.exoplayer:exoplayer-ui:2.14.1'
|
||||||
|
implementation 'com.google.android.exoplayer:extension-mediasession:2.14.1'
|
||||||
|
|
||||||
implementation 'org.jsoup:jsoup:1.13.1'
|
implementation 'org.jsoup:jsoup:1.13.1'
|
||||||
implementation 'com.github.bumptech.glide:glide:4.12.0'
|
implementation 'com.github.bumptech.glide:glide:4.12.0'
|
||||||
@ -64,12 +67,12 @@ dependencies {
|
|||||||
implementation 'com.afollestad.material-dialogs:core:3.3.0'
|
implementation 'com.afollestad.material-dialogs:core:3.3.0'
|
||||||
implementation 'com.afollestad.material-dialogs:bottomsheets:3.3.0'
|
implementation 'com.afollestad.material-dialogs:bottomsheets:3.3.0'
|
||||||
|
|
||||||
testImplementation 'junit:junit:4.13.1'
|
testImplementation 'junit:junit:4.13.2'
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
|
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static def buildTime() {
|
static def buildTime() {
|
||||||
return new Date().format("yyyy-MM-dd", TimeZone.getTimeZone("UTC"))
|
return new Date().format("yyyy-MM-dd", TimeZone.getTimeZone("UTC"))
|
||||||
}
|
}
|
||||||
|
@ -40,6 +40,7 @@ object AoDParser {
|
|||||||
private const val baseUrl = "https://www.anime-on-demand.de"
|
private const val baseUrl = "https://www.anime-on-demand.de"
|
||||||
private const val loginPath = "/users/sign_in"
|
private const val loginPath = "/users/sign_in"
|
||||||
private const val libraryPath = "/animes"
|
private const val libraryPath = "/animes"
|
||||||
|
private const val subscriptionPath = "/mypools"
|
||||||
|
|
||||||
private const val userAgent = "Mozilla/5.0 (X11; Linux x86_64; rv:84.0) Gecko/20100101 Firefox/84.0"
|
private const val userAgent = "Mozilla/5.0 (X11; Linux x86_64; rv:84.0) Gecko/20100101 Firefox/84.0"
|
||||||
|
|
||||||
@ -98,10 +99,12 @@ object AoDParser {
|
|||||||
/**
|
/**
|
||||||
* initially load all media and home screen data
|
* initially load all media and home screen data
|
||||||
*/
|
*/
|
||||||
fun initialLoading() = listOf(
|
suspend fun initialLoading() {
|
||||||
loadHome(),
|
coroutineScope {
|
||||||
listAnimes()
|
launch { loadHome() }
|
||||||
)
|
launch { listAnimes() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* get a media by it's ID (int)
|
* get a media by it's ID (int)
|
||||||
@ -117,7 +120,27 @@ object AoDParser {
|
|||||||
return media
|
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)
|
val episode = getMediaById(mediaId).getEpisodeById(episodeId)
|
||||||
episode.watched = true
|
episode.watched = true
|
||||||
sendCallback(episode.watchedCallback)
|
sendCallback(episode.watchedCallback)
|
||||||
@ -126,250 +149,262 @@ object AoDParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO don't use jsoup here
|
// TODO don't use jsoup here
|
||||||
private fun sendCallback(callbackPath: String) = GlobalScope.launch(Dispatchers.IO) {
|
private suspend fun sendCallback(callbackPath: String) = coroutineScope {
|
||||||
val headers = mutableMapOf(
|
launch(Dispatchers.IO) {
|
||||||
Pair("Accept", "application/json, text/javascript, */*; q=0.01"),
|
val headers = mutableMapOf(
|
||||||
Pair("Accept-Language", "de,en-US;q=0.7,en;q=0.3"),
|
Pair("Accept", "application/json, text/javascript, */*; q=0.01"),
|
||||||
Pair("Accept-Encoding", "gzip, deflate, br"),
|
Pair("Accept-Language", "de,en-US;q=0.7,en;q=0.3"),
|
||||||
Pair("X-CSRF-Token", csrfToken),
|
Pair("Accept-Encoding", "gzip, deflate, br"),
|
||||||
Pair("X-Requested-With", "XMLHttpRequest"),
|
Pair("X-CSRF-Token", csrfToken),
|
||||||
)
|
Pair("X-Requested-With", "XMLHttpRequest"),
|
||||||
|
)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Jsoup.connect(baseUrl + callbackPath)
|
Jsoup.connect(baseUrl + callbackPath)
|
||||||
.ignoreContentType(true)
|
.ignoreContentType(true)
|
||||||
.cookies(sessionCookies)
|
.cookies(sessionCookies)
|
||||||
.headers(headers)
|
.headers(headers)
|
||||||
.execute()
|
.execute()
|
||||||
} catch (ex: IOException) {
|
} catch (ex: IOException) {
|
||||||
Log.e(javaClass.name, "Callback for $callbackPath failed.", ex)
|
Log.e(javaClass.name, "Callback for $callbackPath failed.", ex)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* load all media from aod into itemMediaList and mediaList
|
* 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() = GlobalScope.launch(Dispatchers.IO) {
|
private suspend fun listAnimes() = withContext(Dispatchers.IO) {
|
||||||
val resAnimes = Jsoup.connect(baseUrl + libraryPath).get()
|
launch(Dispatchers.IO) {
|
||||||
//println(resAnimes)
|
val resAnimes = Jsoup.connect(baseUrl + libraryPath).get()
|
||||||
|
//println(resAnimes)
|
||||||
|
|
||||||
itemMediaList.clear()
|
itemMediaList.clear()
|
||||||
mediaList.clear()
|
mediaList.clear()
|
||||||
resAnimes.select("div.animebox").forEach {
|
resAnimes.select("div.animebox").forEach {
|
||||||
val type = if (it.select("p.animebox-link").select("a").text().toLowerCase(Locale.ROOT) == "zur serie") {
|
val type = if (it.select("p.animebox-link").select("a").text().lowercase(Locale.ROOT) == "zur serie") {
|
||||||
MediaType.TVSHOW
|
MediaType.TVSHOW
|
||||||
} else {
|
} else {
|
||||||
MediaType.MOVIE
|
MediaType.MOVIE
|
||||||
|
}
|
||||||
|
val mediaTitle = it.select("h3.animebox-title").text()
|
||||||
|
val mediaLink = it.select("p.animebox-link").select("a").attr("href")
|
||||||
|
val mediaImage = it.select("p.animebox-image").select("img").attr("src")
|
||||||
|
val mediaShortText = it.select("p.animebox-shorttext").text()
|
||||||
|
val mediaId = mediaLink.substringAfterLast("/").toInt()
|
||||||
|
|
||||||
|
itemMediaList.add(ItemMedia(mediaId, mediaTitle, mediaImage))
|
||||||
|
mediaList.add(Media(mediaId, mediaLink, type).apply {
|
||||||
|
info.title = mediaTitle
|
||||||
|
info.posterUrl = mediaImage
|
||||||
|
info.shortDesc = mediaShortText
|
||||||
|
})
|
||||||
}
|
}
|
||||||
val mediaTitle = it.select("h3.animebox-title").text()
|
|
||||||
val mediaLink = it.select("p.animebox-link").select("a").attr("href")
|
|
||||||
val mediaImage = it.select("p.animebox-image").select("img").attr("src")
|
|
||||||
val mediaShortText = it.select("p.animebox-shorttext").text()
|
|
||||||
val mediaId = mediaLink.substringAfterLast("/").toInt()
|
|
||||||
|
|
||||||
itemMediaList.add(ItemMedia(mediaId, mediaTitle, mediaImage))
|
Log.i(javaClass.name, "Total library size is: ${mediaList.size}")
|
||||||
mediaList.add(Media(mediaId, mediaLink, type).apply {
|
|
||||||
info.title = mediaTitle
|
|
||||||
info.posterUrl = mediaImage
|
|
||||||
info.shortDesc = mediaShortText
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.i(javaClass.name, "Total library size is: ${mediaList.size}")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* load new episodes, titles and highlights
|
* load new episodes, titles and highlights
|
||||||
*/
|
*/
|
||||||
private fun loadHome() = GlobalScope.launch(Dispatchers.IO) {
|
private suspend fun loadHome() = withContext(Dispatchers.IO) {
|
||||||
val resHome = Jsoup.connect(baseUrl).get()
|
launch(Dispatchers.IO) {
|
||||||
|
val resHome = Jsoup.connect(baseUrl).get()
|
||||||
|
|
||||||
// get highlights from AoD
|
// get highlights from AoD
|
||||||
highlightsList.clear()
|
highlightsList.clear()
|
||||||
resHome.select("#aod-highlights").select("div.news-item").forEach {
|
resHome.select("#aod-highlights").select("div.news-item").forEach {
|
||||||
val mediaId = it.select("div.news-item-text").select("a.serienlink")
|
val mediaId = it.select("div.news-item-text").select("a.serienlink")
|
||||||
.attr("href").substringAfterLast("/").toIntOrNull()
|
.attr("href").substringAfterLast("/").toIntOrNull()
|
||||||
val mediaTitle = it.select("div.news-title").select("h2").text()
|
val mediaTitle = it.select("div.news-title").select("h2").text()
|
||||||
val mediaImage = it.select("img").attr("src")
|
val mediaImage = it.select("img").attr("src")
|
||||||
|
|
||||||
if (mediaId != null) {
|
if (mediaId != null) {
|
||||||
highlightsList.add(ItemMedia(mediaId, mediaTitle, mediaImage))
|
highlightsList.add(ItemMedia(mediaId, mediaTitle, mediaImage))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// get all new episodes from AoD
|
// get all new episodes from AoD
|
||||||
newEpisodesList.clear()
|
newEpisodesList.clear()
|
||||||
resHome.select("h2:contains(Neue Episoden)").next().select("li").forEach {
|
resHome.select("h2:contains(Neue Episoden)").next().select("li").forEach {
|
||||||
val mediaId = it.select("a.thumbs").attr("href")
|
val mediaId = it.select("a.thumbs").attr("href")
|
||||||
.substringAfterLast("/").toIntOrNull()
|
.substringAfterLast("/").toIntOrNull()
|
||||||
val mediaImage = it.select("a.thumbs > img").attr("src")
|
val mediaImage = it.select("a.thumbs > img").attr("src")
|
||||||
val mediaTitle = "${it.select("a").text()} - ${it.select("span.neweps").text()}"
|
val mediaTitle = "${it.select("a").text()} - ${it.select("span.neweps").text()}"
|
||||||
|
|
||||||
if (mediaId != null) {
|
if (mediaId != null) {
|
||||||
newEpisodesList.add(ItemMedia(mediaId, mediaTitle, mediaImage))
|
newEpisodesList.add(ItemMedia(mediaId, mediaTitle, mediaImage))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// get new simulcasts from AoD
|
// get new simulcasts from AoD
|
||||||
newSimulcastsList.clear()
|
newSimulcastsList.clear()
|
||||||
resHome.select("h2:contains(Neue Simulcasts)").next().select("li").forEach {
|
resHome.select("h2:contains(Neue Simulcasts)").next().select("li").forEach {
|
||||||
val mediaId = it.select("a.thumbs").attr("href")
|
val mediaId = it.select("a.thumbs").attr("href")
|
||||||
.substringAfterLast("/").toIntOrNull()
|
.substringAfterLast("/").toIntOrNull()
|
||||||
val mediaImage = it.select("a.thumbs > img").attr("src")
|
val mediaImage = it.select("a.thumbs > img").attr("src")
|
||||||
val mediaTitle = it.select("a").text()
|
val mediaTitle = it.select("a").text()
|
||||||
|
|
||||||
if (mediaId != null) {
|
if (mediaId != null) {
|
||||||
newSimulcastsList.add(ItemMedia(mediaId, mediaTitle, mediaImage))
|
newSimulcastsList.add(ItemMedia(mediaId, mediaTitle, mediaImage))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// get new titles from AoD
|
// get new titles from AoD
|
||||||
newTitlesList.clear()
|
newTitlesList.clear()
|
||||||
resHome.select("h2:contains(Neue Anime-Titel)").next().select("li").forEach {
|
resHome.select("h2:contains(Neue Anime-Titel)").next().select("li").forEach {
|
||||||
val mediaId = it.select("a.thumbs").attr("href")
|
val mediaId = it.select("a.thumbs").attr("href")
|
||||||
.substringAfterLast("/").toIntOrNull()
|
.substringAfterLast("/").toIntOrNull()
|
||||||
val mediaImage = it.select("a.thumbs > img").attr("src")
|
val mediaImage = it.select("a.thumbs > img").attr("src")
|
||||||
val mediaTitle = it.select("a").text()
|
val mediaTitle = it.select("a").text()
|
||||||
|
|
||||||
if (mediaId != null) {
|
if (mediaId != null) {
|
||||||
newTitlesList.add(ItemMedia(mediaId, mediaTitle, mediaImage))
|
newTitlesList.add(ItemMedia(mediaId, mediaTitle, mediaImage))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// get top ten from AoD
|
// get top ten from AoD
|
||||||
topTenList.clear()
|
topTenList.clear()
|
||||||
resHome.select("h2:contains(Anime Top 10)").next().select("li").forEach {
|
resHome.select("h2:contains(Anime Top 10)").next().select("li").forEach {
|
||||||
val mediaId = it.select("a.thumbs").attr("href")
|
val mediaId = it.select("a.thumbs").attr("href")
|
||||||
.substringAfterLast("/").toIntOrNull()
|
.substringAfterLast("/").toIntOrNull()
|
||||||
val mediaImage = it.select("a.thumbs > img").attr("src")
|
val mediaImage = it.select("a.thumbs > img").attr("src")
|
||||||
val mediaTitle = it.select("a").text()
|
val mediaTitle = it.select("a").text()
|
||||||
|
|
||||||
if (mediaId != null) {
|
if (mediaId != null) {
|
||||||
topTenList.add(ItemMedia(mediaId, mediaTitle, mediaImage))
|
topTenList.add(ItemMedia(mediaId, mediaTitle, mediaImage))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// if highlights is empty, add a random new title
|
// if highlights is empty, add a random new title
|
||||||
if (highlightsList.isEmpty()) {
|
if (highlightsList.isEmpty()) {
|
||||||
if (newTitlesList.isNotEmpty()) {
|
if (newTitlesList.isNotEmpty()) {
|
||||||
highlightsList.add(newTitlesList[Random.nextInt(0, newTitlesList.size)])
|
highlightsList.add(newTitlesList[Random.nextInt(0, newTitlesList.size)])
|
||||||
} else {
|
} else {
|
||||||
highlightsList.add(ItemMedia(0,"", ""))
|
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
|
* load streams for the media path, movies have one episode
|
||||||
* @param media is used as call ba reference
|
* @param media is used as call ba reference
|
||||||
*/
|
*/
|
||||||
private fun loadStreams(media: Media) = GlobalScope.launch(Dispatchers.IO) {
|
private suspend fun loadStreams(media: Media) = coroutineScope {
|
||||||
if (sessionCookies.isEmpty()) login()
|
launch(Dispatchers.IO) {
|
||||||
|
if (sessionCookies.isEmpty()) login()
|
||||||
|
|
||||||
if (!loginSuccess) {
|
if (!loginSuccess) {
|
||||||
Log.w(javaClass.name, "Login, was not successful.")
|
Log.w(javaClass.name, "Login, was not successful.")
|
||||||
return@launch
|
return@launch
|
||||||
}
|
|
||||||
|
|
||||||
// get the media page
|
|
||||||
val res = Jsoup.connect(baseUrl + media.link)
|
|
||||||
.cookies(sessionCookies)
|
|
||||||
.get()
|
|
||||||
|
|
||||||
//println(res)
|
|
||||||
|
|
||||||
if (csrfToken.isEmpty()) {
|
|
||||||
csrfToken = res.select("meta[name=csrf-token]").attr("content")
|
|
||||||
//Log.i(javaClass.name, "New csrf token is $csrfToken")
|
|
||||||
}
|
|
||||||
|
|
||||||
val besides = res.select("div.besides").first()
|
|
||||||
val playlists = besides.select("input.streamstarter_html5").map { streamstarter ->
|
|
||||||
parsePlaylistAsync(
|
|
||||||
streamstarter.attr("data-playlist"),
|
|
||||||
streamstarter.attr("data-lang")
|
|
||||||
)
|
|
||||||
}.awaitAll()
|
|
||||||
|
|
||||||
playlists.forEach { aod ->
|
|
||||||
// TODO improve language handling
|
|
||||||
val locale = when (aod.extLanguage) {
|
|
||||||
"ger" -> Locale.GERMAN
|
|
||||||
"jap" -> Locale.JAPANESE
|
|
||||||
else -> Locale.ROOT
|
|
||||||
}
|
}
|
||||||
|
|
||||||
aod.playlist.forEach { ep ->
|
// get the media page
|
||||||
try {
|
val res = Jsoup.connect(baseUrl + media.link)
|
||||||
if (media.hasEpisode(ep.mediaid)) {
|
.cookies(sessionCookies)
|
||||||
media.getEpisodeById(ep.mediaid).streams.add(
|
.get()
|
||||||
Stream(ep.sources.first().file, locale)
|
|
||||||
)
|
//println(res)
|
||||||
} else {
|
|
||||||
media.episodes.add(Episode(
|
if (csrfToken.isEmpty()) {
|
||||||
id = ep.mediaid,
|
csrfToken = res.select("meta[name=csrf-token]").attr("content")
|
||||||
streams = mutableListOf(Stream(ep.sources.first().file, locale)),
|
//Log.i(javaClass.name, "New csrf token is $csrfToken")
|
||||||
posterUrl = ep.image,
|
}
|
||||||
title = ep.title,
|
|
||||||
description = ep.description,
|
val besides = res.select("div.besides").first()
|
||||||
number = getNumberFromTitle(ep.title, media.type)
|
val playlists = besides.select("input.streamstarter_html5").map { streamstarter ->
|
||||||
))
|
parsePlaylistAsync(
|
||||||
}
|
streamstarter.attr("data-playlist"),
|
||||||
} catch (ex: Exception) {
|
streamstarter.attr("data-lang")
|
||||||
Log.w(javaClass.name, "Could not parse episode information.", ex)
|
)
|
||||||
|
}.awaitAll()
|
||||||
|
|
||||||
|
playlists.forEach { aod ->
|
||||||
|
// TODO improve language handling
|
||||||
|
val locale = when (aod.extLanguage) {
|
||||||
|
"ger" -> Locale.GERMAN
|
||||||
|
"jap" -> Locale.JAPANESE
|
||||||
|
else -> Locale.ROOT
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
Log.i(javaClass.name, "Loaded playlists successfully")
|
|
||||||
|
|
||||||
// additional info from the media page
|
aod.playlist.forEach { ep ->
|
||||||
res.select("table.vertical-table").select("tr").forEach { row ->
|
try {
|
||||||
when (row.select("th").text().toLowerCase(Locale.ROOT)) {
|
if (media.hasEpisode(ep.mediaid)) {
|
||||||
"produktionsjahr" -> media.info.year = row.select("td").text().toInt()
|
media.getEpisodeById(ep.mediaid).streams.add(
|
||||||
"fsk" -> media.info.age = row.select("td").text().toInt()
|
Stream(ep.sources.first().file, locale)
|
||||||
"episodenanzahl" -> {
|
)
|
||||||
media.info.episodesCount = row.select("td").text()
|
} else {
|
||||||
.substringBefore("/")
|
media.episodes.add(Episode(
|
||||||
.filter { it.isDigit() }
|
id = ep.mediaid,
|
||||||
.toInt()
|
streams = mutableListOf(Stream(ep.sources.first().file, locale)),
|
||||||
}
|
posterUrl = ep.image,
|
||||||
}
|
title = ep.title,
|
||||||
}
|
description = ep.description,
|
||||||
|
number = getNumberFromTitle(ep.title, media.type)
|
||||||
// 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")
|
} catch (ex: Exception) {
|
||||||
.substringAfterLast("/").toIntOrNull()
|
Log.w(javaClass.name, "Could not parse episode information.", ex)
|
||||||
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, "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")
|
||||||
}
|
}
|
||||||
Log.i(javaClass.name, "media loaded successfully")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -380,7 +415,7 @@ object AoDParser {
|
|||||||
return CompletableDeferred(AoDObject(listOf(), language))
|
return CompletableDeferred(AoDObject(listOf(), language))
|
||||||
}
|
}
|
||||||
|
|
||||||
return GlobalScope.async(Dispatchers.IO) {
|
return CoroutineScope(Dispatchers.IO).async(Dispatchers.IO) {
|
||||||
val headers = mutableMapOf(
|
val headers = mutableMapOf(
|
||||||
Pair("Accept", "application/json, text/javascript, */*; q=0.01"),
|
Pair("Accept", "application/json, text/javascript, */*; q=0.01"),
|
||||||
Pair("Accept-Language", "de,en-US;q=0.7,en;q=0.3"),
|
Pair("Accept-Language", "de,en-US;q=0.7,en;q=0.3"),
|
||||||
|
@ -11,6 +11,8 @@ object Preferences {
|
|||||||
internal set
|
internal set
|
||||||
var autoplay = true
|
var autoplay = true
|
||||||
internal set
|
internal set
|
||||||
|
var devSettings = false
|
||||||
|
internal set
|
||||||
var theme = DataTypes.Theme.DARK
|
var theme = DataTypes.Theme.DARK
|
||||||
internal set
|
internal set
|
||||||
|
|
||||||
@ -39,6 +41,15 @@ object Preferences {
|
|||||||
this.autoplay = autoplay
|
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) {
|
fun saveTheme(context: Context, theme: DataTypes.Theme) {
|
||||||
with(getSharedPref(context).edit()) {
|
with(getSharedPref(context).edit()) {
|
||||||
putString(context.getString(R.string.save_key_theme), theme.toString())
|
putString(context.getString(R.string.save_key_theme), theme.toString())
|
||||||
@ -60,6 +71,9 @@ object Preferences {
|
|||||||
autoplay = sharedPref.getBoolean(
|
autoplay = sharedPref.getBoolean(
|
||||||
context.getString(R.string.save_key_autoplay), true
|
context.getString(R.string.save_key_autoplay), true
|
||||||
)
|
)
|
||||||
|
devSettings = sharedPref.getBoolean(
|
||||||
|
context.getString(R.string.save_key_dev_settings), false
|
||||||
|
)
|
||||||
theme = DataTypes.Theme.valueOf(
|
theme = DataTypes.Theme.valueOf(
|
||||||
sharedPref.getString(
|
sharedPref.getString(
|
||||||
context.getString(R.string.save_key_theme), DataTypes.Theme.DARK.toString()
|
context.getString(R.string.save_key_theme), DataTypes.Theme.DARK.toString()
|
||||||
|
@ -31,28 +31,27 @@ import androidx.fragment.app.Fragment
|
|||||||
import androidx.fragment.app.commit
|
import androidx.fragment.app.commit
|
||||||
import com.afollestad.materialdialogs.MaterialDialog
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
import com.afollestad.materialdialogs.callbacks.onDismiss
|
import com.afollestad.materialdialogs.callbacks.onDismiss
|
||||||
import com.google.android.material.bottomnavigation.BottomNavigationView
|
import com.google.android.material.navigation.NavigationBarView
|
||||||
import kotlinx.coroutines.joinAll
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import org.mosad.teapod.R
|
import org.mosad.teapod.R
|
||||||
import org.mosad.teapod.databinding.ActivityMainBinding
|
import org.mosad.teapod.databinding.ActivityMainBinding
|
||||||
import org.mosad.teapod.parser.AoDParser
|
import org.mosad.teapod.parser.AoDParser
|
||||||
import org.mosad.teapod.ui.activity.player.PlayerActivity
|
|
||||||
import org.mosad.teapod.preferences.EncryptedPreferences
|
import org.mosad.teapod.preferences.EncryptedPreferences
|
||||||
import org.mosad.teapod.preferences.Preferences
|
import org.mosad.teapod.preferences.Preferences
|
||||||
import org.mosad.teapod.ui.components.LoginDialog
|
|
||||||
import org.mosad.teapod.ui.activity.main.fragments.AccountFragment
|
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.HomeFragment
|
||||||
import org.mosad.teapod.ui.activity.main.fragments.LibraryFragment
|
import org.mosad.teapod.ui.activity.main.fragments.LibraryFragment
|
||||||
import org.mosad.teapod.ui.activity.main.fragments.SearchFragment
|
import org.mosad.teapod.ui.activity.main.fragments.SearchFragment
|
||||||
import org.mosad.teapod.ui.activity.onboarding.OnboardingActivity
|
import org.mosad.teapod.ui.activity.onboarding.OnboardingActivity
|
||||||
|
import org.mosad.teapod.ui.activity.player.PlayerActivity
|
||||||
|
import org.mosad.teapod.ui.components.LoginDialog
|
||||||
import org.mosad.teapod.util.DataTypes
|
import org.mosad.teapod.util.DataTypes
|
||||||
import org.mosad.teapod.util.StorageController
|
import org.mosad.teapod.util.StorageController
|
||||||
import org.mosad.teapod.util.exitAndRemoveTask
|
import org.mosad.teapod.util.exitAndRemoveTask
|
||||||
import java.net.SocketTimeoutException
|
import java.net.SocketTimeoutException
|
||||||
import kotlin.system.measureTimeMillis
|
import kotlin.system.measureTimeMillis
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemSelectedListener {
|
class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListener {
|
||||||
|
|
||||||
private lateinit var binding: ActivityMainBinding
|
private lateinit var binding: ActivityMainBinding
|
||||||
private var activeBaseFragment: Fragment = HomeFragment() // the currently active fragment, home at the start
|
private var activeBaseFragment: Fragment = HomeFragment() // the currently active fragment, home at the start
|
||||||
@ -73,7 +72,7 @@ class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemS
|
|||||||
theme.applyStyle(getThemeResource(), true)
|
theme.applyStyle(getThemeResource(), true)
|
||||||
|
|
||||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||||
binding.navView.setOnNavigationItemSelectedListener(this)
|
binding.navView.setOnItemSelectedListener(this)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
|
||||||
supportFragmentManager.commit {
|
supportFragmentManager.commit {
|
||||||
@ -138,14 +137,15 @@ class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemS
|
|||||||
*/
|
*/
|
||||||
private fun load() {
|
private fun load() {
|
||||||
val time = measureTimeMillis {
|
val time = measureTimeMillis {
|
||||||
val loadingJob = AoDParser.initialLoading() // start the initial loading
|
val loadingJob = CoroutineScope(Dispatchers.IO + CoroutineName("InitialLoadingScope"))
|
||||||
|
.async { AoDParser.initialLoading() } // start the initial loading
|
||||||
|
|
||||||
// load all saved stuff here
|
// load all saved stuff here
|
||||||
Preferences.load(this)
|
Preferences.load(this)
|
||||||
EncryptedPreferences.readCredentials(this)
|
EncryptedPreferences.readCredentials(this)
|
||||||
StorageController.load(this)
|
StorageController.load(this)
|
||||||
|
|
||||||
// show onbaording
|
// show onboarding
|
||||||
if (EncryptedPreferences.password.isEmpty()) {
|
if (EncryptedPreferences.password.isEmpty()) {
|
||||||
showOnboarding()
|
showOnboarding()
|
||||||
} else {
|
} else {
|
||||||
@ -165,7 +165,7 @@ class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemS
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
runBlocking { loadingJob.joinAll() } // wait for initial loading to finish
|
runBlocking { loadingJob.await() } // wait for initial loading to finish
|
||||||
}
|
}
|
||||||
Log.i(javaClass.name, "loading and login in $time ms")
|
Log.i(javaClass.name, "loading and login in $time ms")
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ import android.os.Bundle
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.annotation.RawRes
|
import androidx.annotation.RawRes
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import com.afollestad.materialdialogs.MaterialDialog
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
@ -13,15 +14,21 @@ import org.mosad.teapod.BuildConfig
|
|||||||
import org.mosad.teapod.R
|
import org.mosad.teapod.R
|
||||||
import org.mosad.teapod.databinding.FragmentAboutBinding
|
import org.mosad.teapod.databinding.FragmentAboutBinding
|
||||||
import org.mosad.teapod.databinding.ItemComponentBinding
|
import org.mosad.teapod.databinding.ItemComponentBinding
|
||||||
|
import org.mosad.teapod.preferences.Preferences
|
||||||
import org.mosad.teapod.util.DataTypes.License
|
import org.mosad.teapod.util.DataTypes.License
|
||||||
import org.mosad.teapod.util.ThirdPartyComponent
|
import org.mosad.teapod.util.ThirdPartyComponent
|
||||||
import java.lang.StringBuilder
|
import java.lang.StringBuilder
|
||||||
|
import java.util.Timer
|
||||||
|
import kotlin.concurrent.schedule
|
||||||
|
|
||||||
class AboutFragment : Fragment() {
|
class AboutFragment : Fragment() {
|
||||||
|
|
||||||
private val teapodRepoUrl = "https://git.mosad.xyz/Seil0/teapod"
|
|
||||||
private lateinit var binding: FragmentAboutBinding
|
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 {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
binding = FragmentAboutBinding.inflate(inflater, container, false)
|
binding = FragmentAboutBinding.inflate(inflater, container, false)
|
||||||
return binding.root
|
return binding.root
|
||||||
@ -52,6 +59,10 @@ class AboutFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun initActions() {
|
private fun initActions() {
|
||||||
|
binding.imageAppIcon.setOnClickListener {
|
||||||
|
checkDevSettings()
|
||||||
|
}
|
||||||
|
|
||||||
binding.linearSource.setOnClickListener {
|
binding.linearSource.setOnClickListener {
|
||||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(teapodRepoUrl)))
|
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(teapodRepoUrl)))
|
||||||
}
|
}
|
||||||
@ -64,6 +75,30 @@ class AboutFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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> {
|
private fun getThirdPartyComponents(): List<ThirdPartyComponent> {
|
||||||
return listOf(
|
return listOf(
|
||||||
ThirdPartyComponent("AndroidX", "", "The Android Open Source Project",
|
ThirdPartyComponent("AndroidX", "", "The Android Open Source Project",
|
||||||
|
@ -1,28 +1,59 @@
|
|||||||
package org.mosad.teapod.ui.activity.main.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.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.afollestad.materialdialogs.MaterialDialog
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
import com.afollestad.materialdialogs.list.listItemsSingleChoice
|
import com.afollestad.materialdialogs.list.listItemsSingleChoice
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import org.mosad.teapod.BuildConfig
|
import org.mosad.teapod.BuildConfig
|
||||||
import org.mosad.teapod.ui.activity.main.MainActivity
|
|
||||||
import org.mosad.teapod.R
|
import org.mosad.teapod.R
|
||||||
import org.mosad.teapod.databinding.FragmentAccountBinding
|
import org.mosad.teapod.databinding.FragmentAccountBinding
|
||||||
import org.mosad.teapod.parser.AoDParser
|
import org.mosad.teapod.parser.AoDParser
|
||||||
import org.mosad.teapod.preferences.EncryptedPreferences
|
import org.mosad.teapod.preferences.EncryptedPreferences
|
||||||
import org.mosad.teapod.preferences.Preferences
|
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.ui.components.LoginDialog
|
||||||
import org.mosad.teapod.util.DataTypes.Theme
|
import org.mosad.teapod.util.DataTypes.Theme
|
||||||
|
import org.mosad.teapod.util.StorageController
|
||||||
import org.mosad.teapod.util.showFragment
|
import org.mosad.teapod.util.showFragment
|
||||||
|
|
||||||
class AccountFragment : Fragment() {
|
class AccountFragment : Fragment() {
|
||||||
|
|
||||||
private lateinit var binding: FragmentAccountBinding
|
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 {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
binding = FragmentAccountBinding.inflate(inflater, container, false)
|
binding = FragmentAccountBinding.inflate(inflater, container, false)
|
||||||
return binding.root
|
return binding.root
|
||||||
@ -31,6 +62,15 @@ class AccountFragment : Fragment() {
|
|||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
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.textAccountLogin.text = EncryptedPreferences.login
|
||||||
binding.textInfoAboutDesc.text = getString(R.string.info_about_desc, BuildConfig.VERSION_NAME, getString(R.string.build_time))
|
binding.textInfoAboutDesc.text = getString(R.string.info_about_desc, BuildConfig.VERSION_NAME, getString(R.string.build_time))
|
||||||
binding.textThemeSelected.text = when (Preferences.theme) {
|
binding.textThemeSelected.text = when (Preferences.theme) {
|
||||||
@ -41,6 +81,8 @@ class AccountFragment : Fragment() {
|
|||||||
binding.switchSecondary.isChecked = Preferences.preferSecondary
|
binding.switchSecondary.isChecked = Preferences.preferSecondary
|
||||||
binding.switchAutoplay.isChecked = Preferences.autoplay
|
binding.switchAutoplay.isChecked = Preferences.autoplay
|
||||||
|
|
||||||
|
binding.linearDevSettings.isVisible = Preferences.devSettings
|
||||||
|
|
||||||
initActions()
|
initActions()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,6 +91,10 @@ class AccountFragment : Fragment() {
|
|||||||
showLoginDialog(true)
|
showLoginDialog(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
binding.linearAccountSubscription.setOnClickListener {
|
||||||
|
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(AoDParser.getSubscriptionUrl())))
|
||||||
|
}
|
||||||
|
|
||||||
binding.linearTheme.setOnClickListener {
|
binding.linearTheme.setOnClickListener {
|
||||||
showThemeDialog()
|
showThemeDialog()
|
||||||
}
|
}
|
||||||
@ -64,6 +110,23 @@ class AccountFragment : Fragment() {
|
|||||||
binding.switchAutoplay.setOnClickListener {
|
binding.switchAutoplay.setOnClickListener {
|
||||||
Preferences.saveAutoplay(requireContext(), binding.switchAutoplay.isChecked)
|
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) {
|
private fun showLoginDialog(firstTry: Boolean) {
|
||||||
@ -92,11 +155,12 @@ class AccountFragment : Fragment() {
|
|||||||
when(index) {
|
when(index) {
|
||||||
0 -> Preferences.saveTheme(context, Theme.LIGHT)
|
0 -> Preferences.saveTheme(context, Theme.LIGHT)
|
||||||
1 -> Preferences.saveTheme(context, Theme.DARK)
|
1 -> Preferences.saveTheme(context, Theme.DARK)
|
||||||
else -> Preferences.saveTheme(context, Theme.LIGHT)
|
else -> Preferences.saveTheme(context, Theme.DARK)
|
||||||
}
|
}
|
||||||
|
|
||||||
(activity as MainActivity).restart()
|
(activity as MainActivity).restart()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -6,14 +6,13 @@ import android.view.LayoutInflater
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.mosad.teapod.R
|
import org.mosad.teapod.R
|
||||||
import org.mosad.teapod.ui.activity.main.MainActivity
|
|
||||||
import org.mosad.teapod.databinding.FragmentHomeBinding
|
import org.mosad.teapod.databinding.FragmentHomeBinding
|
||||||
import org.mosad.teapod.parser.AoDParser
|
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.ItemMedia
|
||||||
import org.mosad.teapod.util.StorageController
|
import org.mosad.teapod.util.StorageController
|
||||||
import org.mosad.teapod.util.adapter.MediaItemAdapter
|
import org.mosad.teapod.util.adapter.MediaItemAdapter
|
||||||
@ -40,7 +39,7 @@ class HomeFragment : Fragment() {
|
|||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
GlobalScope.launch(Dispatchers.Main) {
|
lifecycleScope.launch {
|
||||||
context?.let {
|
context?.let {
|
||||||
initHighlight()
|
initHighlight()
|
||||||
initRecyclerViews()
|
initRecyclerViews()
|
||||||
@ -73,12 +72,7 @@ class HomeFragment : Fragment() {
|
|||||||
binding.recyclerTopTen.addItemDecoration(MediaItemDecoration(9))
|
binding.recyclerTopTen.addItemDecoration(MediaItemDecoration(9))
|
||||||
|
|
||||||
// my list
|
// my list
|
||||||
val myListMedia = StorageController.myList.map { elementId ->
|
adapterMyList = MediaItemAdapter(mapMyListToItemMedia())
|
||||||
AoDParser.itemMediaList.first {
|
|
||||||
elementId == it.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
adapterMyList = MediaItemAdapter(myListMedia)
|
|
||||||
binding.recyclerMyList.adapter = adapterMyList
|
binding.recyclerMyList.adapter = adapterMyList
|
||||||
|
|
||||||
// new episodes
|
// new episodes
|
||||||
@ -101,7 +95,7 @@ class HomeFragment : Fragment() {
|
|||||||
private fun initActions() {
|
private fun initActions() {
|
||||||
binding.buttonPlayHighlight.setOnClickListener {
|
binding.buttonPlayHighlight.setOnClickListener {
|
||||||
// TODO get next episode
|
// TODO get next episode
|
||||||
GlobalScope.launch {
|
lifecycleScope.launch {
|
||||||
val media = AoDParser.getMediaById(highlightMedia.id)
|
val media = AoDParser.getMediaById(highlightMedia.id)
|
||||||
|
|
||||||
Log.d(javaClass.name, "Starting Player with mediaId: ${media.id}")
|
Log.d(javaClass.name, "Starting Player with mediaId: ${media.id}")
|
||||||
@ -154,14 +148,19 @@ class HomeFragment : Fragment() {
|
|||||||
* * only update actual change and not all data (performance)
|
* * only update actual change and not all data (performance)
|
||||||
*/
|
*/
|
||||||
fun updateMyListMedia() {
|
fun updateMyListMedia() {
|
||||||
val myListMedia = StorageController.myList.map { elementId ->
|
adapterMyList.updateMediaList(mapMyListToItemMedia())
|
||||||
AoDParser.itemMediaList.first {
|
|
||||||
elementId == it.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
adapterMyList.updateMediaList(myListMedia)
|
|
||||||
adapterMyList.notifyDataSetChanged()
|
adapterMyList.notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun mapMyListToItemMedia(): List<ItemMedia> {
|
||||||
|
return StorageController.myList.mapNotNull { elementId ->
|
||||||
|
AoDParser.itemMediaList.firstOrNull { it.id == elementId }.also {
|
||||||
|
// it the my list entry wasn't found in itemMediaList Log it
|
||||||
|
if (it == null) {
|
||||||
|
Log.w(javaClass.name, "The element with the id $elementId was not found.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -5,10 +5,8 @@ import android.view.LayoutInflater
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import kotlinx.coroutines.Dispatchers
|
import androidx.lifecycle.lifecycleScope
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.mosad.teapod.databinding.FragmentLibraryBinding
|
import org.mosad.teapod.databinding.FragmentLibraryBinding
|
||||||
import org.mosad.teapod.parser.AoDParser
|
import org.mosad.teapod.parser.AoDParser
|
||||||
import org.mosad.teapod.util.adapter.MediaItemAdapter
|
import org.mosad.teapod.util.adapter.MediaItemAdapter
|
||||||
@ -29,18 +27,16 @@ class LibraryFragment : Fragment() {
|
|||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
// init async
|
// init async
|
||||||
GlobalScope.launch {
|
lifecycleScope.launch {
|
||||||
// create and set the adapter, needs context
|
// create and set the adapter, needs context
|
||||||
withContext(Dispatchers.Main) {
|
context?.let {
|
||||||
context?.let {
|
adapter = MediaItemAdapter(AoDParser.itemMediaList)
|
||||||
adapter = MediaItemAdapter(AoDParser.itemMediaList)
|
adapter.onItemClick = { mediaId, _ ->
|
||||||
adapter.onItemClick = { mediaId, _ ->
|
activity?.showFragment(MediaFragment(mediaId))
|
||||||
activity?.showFragment(MediaFragment(mediaId))
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.recyclerMediaLibrary.adapter = adapter
|
|
||||||
binding.recyclerMediaLibrary.addItemDecoration(MediaItemDecoration(9))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
binding.recyclerMediaLibrary.adapter = adapter
|
||||||
|
binding.recyclerMediaLibrary.addItemDecoration(MediaItemDecoration(9))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -10,20 +10,21 @@ import android.view.ViewGroup
|
|||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.bumptech.glide.request.RequestOptions
|
import com.bumptech.glide.request.RequestOptions
|
||||||
import com.google.android.material.appbar.AppBarLayout
|
import com.google.android.material.appbar.AppBarLayout
|
||||||
import com.google.android.material.tabs.TabLayoutMediator
|
import com.google.android.material.tabs.TabLayoutMediator
|
||||||
import jp.wasabeef.glide.transformations.BlurTransformation
|
import jp.wasabeef.glide.transformations.BlurTransformation
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.launch
|
||||||
import org.mosad.teapod.R
|
import org.mosad.teapod.R
|
||||||
import org.mosad.teapod.databinding.FragmentMediaBinding
|
import org.mosad.teapod.databinding.FragmentMediaBinding
|
||||||
import org.mosad.teapod.ui.activity.main.MainActivity
|
import org.mosad.teapod.ui.activity.main.MainActivity
|
||||||
import org.mosad.teapod.ui.activity.main.viewmodel.MediaFragmentViewModel
|
import org.mosad.teapod.ui.activity.main.viewmodel.MediaFragmentViewModel
|
||||||
import org.mosad.teapod.util.*
|
|
||||||
import org.mosad.teapod.util.DataTypes.MediaType
|
import org.mosad.teapod.util.DataTypes.MediaType
|
||||||
|
import org.mosad.teapod.util.Episode
|
||||||
|
import org.mosad.teapod.util.StorageController
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The media detail fragment.
|
* The media detail fragment.
|
||||||
@ -61,13 +62,12 @@ class MediaFragment(private val mediaId: Int) : Fragment() {
|
|||||||
}
|
}
|
||||||
}.attach()
|
}.attach()
|
||||||
|
|
||||||
GlobalScope.launch(Dispatchers.Main) {
|
|
||||||
|
lifecycleScope.launch {
|
||||||
model.load(mediaId) // load the streams and tmdb for the selected media
|
model.load(mediaId) // load the streams and tmdb for the selected media
|
||||||
|
|
||||||
if (this@MediaFragment.isAdded) {
|
updateGUI()
|
||||||
updateGUI()
|
initActions()
|
||||||
initActions()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,7 +6,8 @@ import android.view.View
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.SearchView
|
import android.widget.SearchView
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import kotlinx.coroutines.*
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import org.mosad.teapod.databinding.FragmentSearchBinding
|
import org.mosad.teapod.databinding.FragmentSearchBinding
|
||||||
import org.mosad.teapod.parser.AoDParser
|
import org.mosad.teapod.parser.AoDParser
|
||||||
import org.mosad.teapod.util.decoration.MediaItemDecoration
|
import org.mosad.teapod.util.decoration.MediaItemDecoration
|
||||||
@ -26,9 +27,8 @@ class SearchFragment : Fragment() {
|
|||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
GlobalScope.launch {
|
lifecycleScope.launch {
|
||||||
// create and set the adapter, needs context
|
// create and set the adapter, needs context
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
context?.let {
|
context?.let {
|
||||||
adapter = MediaItemAdapter(AoDParser.itemMediaList)
|
adapter = MediaItemAdapter(AoDParser.itemMediaList)
|
||||||
adapter!!.onItemClick = { mediaId, _ ->
|
adapter!!.onItemClick = { mediaId, _ ->
|
||||||
@ -39,7 +39,6 @@ class SearchFragment : Fragment() {
|
|||||||
binding.recyclerMediaSearch.adapter = adapter
|
binding.recyclerMediaSearch.adapter = adapter
|
||||||
binding.recyclerMediaSearch.addItemDecoration(MediaItemDecoration(9))
|
binding.recyclerMediaSearch.addItemDecoration(MediaItemDecoration(9))
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
initActions()
|
initActions()
|
||||||
|
@ -5,6 +5,7 @@ import android.view.LayoutInflater
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import org.mosad.teapod.R
|
import org.mosad.teapod.R
|
||||||
import org.mosad.teapod.databinding.FragmentOnLoginBinding
|
import org.mosad.teapod.databinding.FragmentOnLoginBinding
|
||||||
@ -35,7 +36,7 @@ class OnLoginFragment: Fragment() {
|
|||||||
EncryptedPreferences.saveCredentials(email, password, requireContext()) // save the credentials
|
EncryptedPreferences.saveCredentials(email, password, requireContext()) // save the credentials
|
||||||
|
|
||||||
binding.buttonLogin.isClickable = false
|
binding.buttonLogin.isClickable = false
|
||||||
loginJob = GlobalScope.launch {
|
loginJob = lifecycleScope.launch {
|
||||||
if (AoDParser.login()) {
|
if (AoDParser.login()) {
|
||||||
// if login was successful, switch to main
|
// if login was successful, switch to main
|
||||||
if (activity is OnboardingActivity) {
|
if (activity is OnboardingActivity) {
|
||||||
|
@ -7,6 +7,7 @@ import android.app.PictureInPictureParams
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
|
import android.graphics.Rect
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@ -19,16 +20,14 @@ import androidx.annotation.RequiresApi
|
|||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.view.GestureDetectorCompat
|
import androidx.core.view.GestureDetectorCompat
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.google.android.exoplayer2.ExoPlayer
|
import com.google.android.exoplayer2.ExoPlayer
|
||||||
import com.google.android.exoplayer2.Player
|
import com.google.android.exoplayer2.Player
|
||||||
import com.google.android.exoplayer2.ui.StyledPlayerControlView
|
import com.google.android.exoplayer2.ui.StyledPlayerControlView
|
||||||
import com.google.android.exoplayer2.util.Util
|
import com.google.android.exoplayer2.util.Util
|
||||||
import kotlinx.android.synthetic.main.activity_player.*
|
import kotlinx.android.synthetic.main.activity_player.*
|
||||||
import kotlinx.android.synthetic.main.player_controls.*
|
import kotlinx.android.synthetic.main.player_controls.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.mosad.teapod.R
|
import org.mosad.teapod.R
|
||||||
import org.mosad.teapod.preferences.Preferences
|
import org.mosad.teapod.preferences.Preferences
|
||||||
import org.mosad.teapod.ui.components.EpisodesListPlayer
|
import org.mosad.teapod.ui.components.EpisodesListPlayer
|
||||||
@ -147,8 +146,15 @@ class PlayerActivity : AppCompatActivity() {
|
|||||||
} else {
|
} else {
|
||||||
val width = model.player.videoFormat?.width ?: 0
|
val width = model.player.videoFormat?.width ?: 0
|
||||||
val height = model.player.videoFormat?.height ?: 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()
|
val params = PictureInPictureParams.Builder()
|
||||||
.setAspectRatio(Rational(width, height))
|
.setAspectRatio(Rational(width, height))
|
||||||
|
.setSourceRectHint(contentRect)
|
||||||
.build()
|
.build()
|
||||||
enterPictureInPictureMode(params)
|
enterPictureInPictureMode(params)
|
||||||
}
|
}
|
||||||
@ -187,7 +193,7 @@ class PlayerActivity : AppCompatActivity() {
|
|||||||
* set play when ready and listeners
|
* set play when ready and listeners
|
||||||
*/
|
*/
|
||||||
private fun initExoPlayer() {
|
private fun initExoPlayer() {
|
||||||
model.player.addListener(object : Player.EventListener {
|
model.player.addListener(object : Player.Listener {
|
||||||
override fun onPlaybackStateChanged(state: Int) {
|
override fun onPlaybackStateChanged(state: Int) {
|
||||||
super.onPlaybackStateChanged(state)
|
super.onPlaybackStateChanged(state)
|
||||||
|
|
||||||
@ -208,7 +214,7 @@ class PlayerActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// start playing the current episode, after all needed player components have been initialized
|
// start playing the current episode, after all needed player components have been initialized
|
||||||
model.playEpisode(model.currentEpisode, true)
|
model.playEpisode(model.currentEpisode, true)
|
||||||
}
|
}
|
||||||
@ -255,31 +261,27 @@ class PlayerActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
timerUpdates = Timer().scheduleAtFixedRate(0, 500) {
|
timerUpdates = Timer().scheduleAtFixedRate(0, 500) {
|
||||||
GlobalScope.launch {
|
lifecycleScope.launch {
|
||||||
var btnNextEpIsVisible: Boolean
|
val btnNextEpIsVisible = button_next_ep.isVisible
|
||||||
var controlsVisible: Boolean
|
val controlsVisible = controller.isVisible
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
if (model.player.duration > 0) {
|
||||||
if (model.player.duration > 0) {
|
remainingTime = model.player.duration - model.player.currentPosition
|
||||||
remainingTime = model.player.duration - model.player.currentPosition
|
remainingTime = if (remainingTime < 0) 0 else remainingTime
|
||||||
remainingTime = if (remainingTime < 0) 0 else remainingTime
|
|
||||||
}
|
|
||||||
btnNextEpIsVisible = button_next_ep.isVisible
|
|
||||||
controlsVisible = controller.isVisible
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (remainingTime in 1..20000) {
|
if (remainingTime in 1..20000) {
|
||||||
// if the next ep button is not visible, make it visible. Don't show in pip mode
|
// if the next ep button is not visible, make it visible. Don't show in pip mode
|
||||||
if (!btnNextEpIsVisible && model.nextEpisode != null && Preferences.autoplay && !isInPiPMode()) {
|
if (!btnNextEpIsVisible && model.nextEpisode != null && Preferences.autoplay && !isInPiPMode()) {
|
||||||
withContext(Dispatchers.Main) { showButtonNextEp() }
|
showButtonNextEp()
|
||||||
}
|
}
|
||||||
} else if (btnNextEpIsVisible) {
|
} else if (btnNextEpIsVisible) {
|
||||||
withContext(Dispatchers.Main) { hideButtonNextEp() }
|
hideButtonNextEp()
|
||||||
}
|
}
|
||||||
|
|
||||||
// if controls are visible, update them
|
// if controls are visible, update them
|
||||||
if (controlsVisible) {
|
if (controlsVisible) {
|
||||||
withContext(Dispatchers.Main) { updateControls() }
|
updateControls()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,15 +2,19 @@ package org.mosad.teapod.ui.activity.player
|
|||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.support.v4.media.session.MediaSessionCompat
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.lifecycle.AndroidViewModel
|
import androidx.lifecycle.AndroidViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.google.android.exoplayer2.C
|
import com.google.android.exoplayer2.C
|
||||||
import com.google.android.exoplayer2.MediaItem
|
import com.google.android.exoplayer2.MediaItem
|
||||||
import com.google.android.exoplayer2.SimpleExoPlayer
|
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.MediaSource
|
||||||
import com.google.android.exoplayer2.source.hls.HlsMediaSource
|
import com.google.android.exoplayer2.source.hls.HlsMediaSource
|
||||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory
|
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory
|
||||||
import com.google.android.exoplayer2.util.Util
|
import com.google.android.exoplayer2.util.Util
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.mosad.teapod.R
|
import org.mosad.teapod.R
|
||||||
import org.mosad.teapod.parser.AoDParser
|
import org.mosad.teapod.parser.AoDParser
|
||||||
@ -29,10 +33,11 @@ import kotlin.collections.ArrayList
|
|||||||
class PlayerViewModel(application: Application) : AndroidViewModel(application) {
|
class PlayerViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
|
|
||||||
val player = SimpleExoPlayer.Builder(application).build()
|
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 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)
|
var media: Media = Media(-1, "", DataTypes.MediaType.OTHER)
|
||||||
internal set
|
internal set
|
||||||
@ -43,13 +48,30 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
|
|||||||
var currentLanguage: Locale = Locale.ROOT
|
var currentLanguage: Locale = Locale.ROOT
|
||||||
internal set
|
internal set
|
||||||
|
|
||||||
|
init {
|
||||||
|
initMediaSession()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
|
|
||||||
|
mediaSession.release()
|
||||||
player.release()
|
player.release()
|
||||||
|
|
||||||
Log.d(javaClass.name, "Released player")
|
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) {
|
fun loadMedia(mediaId: Int, episodeId: Int) {
|
||||||
runBlocking {
|
runBlocking {
|
||||||
media = AoDParser.getMediaById(mediaId)
|
media = AoDParser.getMediaById(mediaId)
|
||||||
@ -107,10 +129,15 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
|
|||||||
|
|
||||||
// if episodes has not been watched, mark as watched
|
// if episodes has not been watched, mark as watched
|
||||||
if (!episode.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) {
|
fun playMedia(source: MediaSource, replace: Boolean = false, seekPosition: Long = 0) {
|
||||||
if (replace || player.contentDuration == C.TIME_UNSET) {
|
if (replace || player.contentDuration == C.TIME_UNSET) {
|
||||||
player.setMediaSource(source)
|
player.setMediaSource(source)
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
package org.mosad.teapod.util
|
package org.mosad.teapod.util
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import com.google.gson.JsonParser
|
import com.google.gson.JsonParser
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import java.io.File
|
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.
|
* This controller contains the logic for permanently saved data.
|
||||||
@ -19,6 +21,10 @@ object StorageController {
|
|||||||
val myList = ArrayList<Int>() // a list of saved mediaIds
|
val myList = ArrayList<Int>() // a list of saved mediaIds
|
||||||
|
|
||||||
fun load(context: Context) {
|
fun load(context: Context) {
|
||||||
|
loadMyList(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadMyList(context: Context) {
|
||||||
val file = File(context.filesDir, fileNameMyList)
|
val file = File(context.filesDir, fileNameMyList)
|
||||||
|
|
||||||
if (!file.exists()) runBlocking { saveMyList(context).join() }
|
if (!file.exists()) runBlocking { saveMyList(context).join() }
|
||||||
@ -30,15 +36,54 @@ object StorageController {
|
|||||||
myList.clear()
|
myList.clear()
|
||||||
Log.e(javaClass.name, "Parsing of My-List failed.")
|
Log.e(javaClass.name, "Parsing of My-List failed.")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveMyList(context: Context): Job {
|
fun saveMyList(context: Context): Job {
|
||||||
val file = File(context.filesDir, fileNameMyList)
|
val file = File(context.filesDir, fileNameMyList)
|
||||||
|
|
||||||
return GlobalScope.launch(Dispatchers.IO) {
|
return CoroutineScope(Dispatchers.IO).launch {
|
||||||
file.writeText(Gson().toJson(myList.distinct()))
|
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
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -4,9 +4,9 @@ import android.util.Log
|
|||||||
import com.google.gson.JsonObject
|
import com.google.gson.JsonObject
|
||||||
import com.google.gson.JsonParser
|
import com.google.gson.JsonParser
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
|
import org.mosad.teapod.util.DataTypes.MediaType
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.net.URLEncoder
|
import java.net.URLEncoder
|
||||||
import org.mosad.teapod.util.DataTypes.MediaType
|
|
||||||
|
|
||||||
class TMDBApiController {
|
class TMDBApiController {
|
||||||
|
|
||||||
@ -21,11 +21,15 @@ class TMDBApiController {
|
|||||||
private val imageUrl = "https://image.tmdb.org/t/p/w500"
|
private val imageUrl = "https://image.tmdb.org/t/p/w500"
|
||||||
|
|
||||||
suspend fun search(title: String, type: MediaType): TMDBResponse {
|
suspend fun search(title: String, type: MediaType): TMDBResponse {
|
||||||
val searchTerm = title.replace("(Sub)", "").trim()
|
// remove unneeded text from the media title before searching
|
||||||
|
val searchTerm = title.replace("(Sub)", "")
|
||||||
|
.replace(Regex("-?\\s?[0-9]+.\\s?(Staffel|Season)"), "")
|
||||||
|
.replace(Regex("(Staffel|Season)\\s?[0-9]+"), "")
|
||||||
|
.trim()
|
||||||
|
|
||||||
return when (type) {
|
return when (type) {
|
||||||
MediaType.MOVIE -> searchMovie(searchTerm).await()
|
MediaType.MOVIE -> searchMovie(searchTerm)
|
||||||
MediaType.TVSHOW -> searchTVShow(searchTerm).await()
|
MediaType.TVSHOW -> searchTVShow(searchTerm)
|
||||||
else -> {
|
else -> {
|
||||||
Log.e(javaClass.name, "Wrong Type: $type")
|
Log.e(javaClass.name, "Wrong Type: $type")
|
||||||
TMDBResponse()
|
TMDBResponse()
|
||||||
@ -34,62 +38,64 @@ class TMDBApiController {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun searchTVShow(title: String): Deferred<TMDBResponse> {
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
|
private suspend fun searchTVShow(title: String): TMDBResponse = withContext(Dispatchers.IO) {
|
||||||
val url = URL("$searchTVUrl$preparedParameters&query=${URLEncoder.encode(title, "UTF-8")}")
|
val url = URL("$searchTVUrl$preparedParameters&query=${URLEncoder.encode(title, "UTF-8")}")
|
||||||
|
val response = JsonParser.parseString(url.readText()).asJsonObject
|
||||||
|
// println(response)
|
||||||
|
|
||||||
return GlobalScope.async {
|
val sortedResults = response.get("results").asJsonArray.toList().sortedBy {
|
||||||
val response = JsonParser.parseString(url.readText()).asJsonObject
|
getStringNotNull(it.asJsonObject, "name")
|
||||||
//println(response)
|
}
|
||||||
|
|
||||||
if (response.get("total_results").asInt > 0) {
|
return@withContext if (sortedResults.isNotEmpty()) {
|
||||||
response.get("results").asJsonArray.first().asJsonObject.let {
|
sortedResults.first().asJsonObject.let {
|
||||||
val id = getStringNotNull(it, "id").toInt()
|
val id = getStringNotNull(it, "id").toInt()
|
||||||
val overview = getStringNotNull(it, "overview")
|
val overview = getStringNotNull(it, "overview")
|
||||||
val posterPath = getStringNotNullPrefix(it, "poster_path", imageUrl)
|
val posterPath = getStringNotNullPrefix(it, "poster_path", imageUrl)
|
||||||
val backdropPath = getStringNotNullPrefix(it, "backdrop_path", imageUrl)
|
val backdropPath = getStringNotNullPrefix(it, "backdrop_path", imageUrl)
|
||||||
|
|
||||||
TMDBResponse(id, "", overview, posterPath, backdropPath)
|
TMDBResponse(id, "", overview, posterPath, backdropPath)
|
||||||
}
|
|
||||||
} else {
|
|
||||||
TMDBResponse()
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
TMDBResponse()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun searchMovie(title: String): Deferred<TMDBResponse> {
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
|
private suspend fun searchMovie(title: String): TMDBResponse = withContext(Dispatchers.IO) {
|
||||||
val url = URL("$searchMovieUrl$preparedParameters&query=${URLEncoder.encode(title, "UTF-8")}")
|
val url = URL("$searchMovieUrl$preparedParameters&query=${URLEncoder.encode(title, "UTF-8")}")
|
||||||
|
val response = JsonParser.parseString(url.readText()).asJsonObject
|
||||||
|
// println(response)
|
||||||
|
|
||||||
return GlobalScope.async {
|
val sortedResults = response.get("results").asJsonArray.toList().sortedBy {
|
||||||
val response = JsonParser.parseString(url.readText()).asJsonObject
|
getStringNotNull(it.asJsonObject, "title")
|
||||||
//println(response)
|
}
|
||||||
|
|
||||||
if (response.get("total_results").asInt > 0) {
|
return@withContext if (sortedResults.isNotEmpty()) {
|
||||||
response.get("results").asJsonArray.first().asJsonObject.let {
|
sortedResults.first().asJsonObject.let {
|
||||||
val id = getStringNotNull(it,"id").toInt()
|
val id = getStringNotNull(it,"id").toInt()
|
||||||
val overview = getStringNotNull(it,"overview")
|
val overview = getStringNotNull(it,"overview")
|
||||||
val posterPath = getStringNotNullPrefix(it, "poster_path", imageUrl)
|
val posterPath = getStringNotNullPrefix(it, "poster_path", imageUrl)
|
||||||
val backdropPath = getStringNotNullPrefix(it, "backdrop_path", imageUrl)
|
val backdropPath = getStringNotNullPrefix(it, "backdrop_path", imageUrl)
|
||||||
val runtime = getMovieRuntime(id)
|
val runtime = getMovieRuntime(id)
|
||||||
|
|
||||||
TMDBResponse(id, "", overview, posterPath, backdropPath, runtime)
|
TMDBResponse(id, "", overview, posterPath, backdropPath, runtime)
|
||||||
}
|
|
||||||
} else {
|
|
||||||
TMDBResponse()
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
TMDBResponse()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* currently only used for runtime, need a rework
|
* currently only used for runtime, need a rework
|
||||||
*/
|
*/
|
||||||
fun getMovieRuntime(id: Int): Int = runBlocking {
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
|
suspend fun getMovieRuntime(id: Int): Int = withContext(Dispatchers.IO) {
|
||||||
val url = URL("$getMovieUrl/$id?api_key=$apiKey&language=$language")
|
val url = URL("$getMovieUrl/$id?api_key=$apiKey&language=$language")
|
||||||
|
|
||||||
GlobalScope.async {
|
val response = JsonParser.parseString(url.readText()).asJsonObject
|
||||||
val response = JsonParser.parseString(url.readText()).asJsonObject
|
return@withContext getStringNotNull(response,"runtime").toInt()
|
||||||
|
|
||||||
return@async getStringNotNull(response,"runtime").toInt()
|
|
||||||
}.await()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -49,14 +49,14 @@ class MediaItemAdapter(private val initMedia: List<ItemMedia>) : RecyclerView.Ad
|
|||||||
|
|
||||||
inner class MediaFilter : Filter() {
|
inner class MediaFilter : Filter() {
|
||||||
override fun performFiltering(constraint: CharSequence?): FilterResults {
|
override fun performFiltering(constraint: CharSequence?): FilterResults {
|
||||||
val filterTerm = constraint.toString().toLowerCase(Locale.ROOT)
|
val filterTerm = constraint.toString().lowercase(Locale.ROOT)
|
||||||
val results = FilterResults()
|
val results = FilterResults()
|
||||||
|
|
||||||
val filteredList = if (filterTerm.isEmpty()) {
|
val filteredList = if (filterTerm.isEmpty()) {
|
||||||
initMedia
|
initMedia
|
||||||
} else {
|
} else {
|
||||||
initMedia.filter {
|
initMedia.filter {
|
||||||
it.title.toLowerCase(Locale.ROOT).contains(filterTerm)
|
it.title.lowercase(Locale.ROOT).contains(filterTerm)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
6
app/src/main/res/drawable/ic_baseline_access_time_24.xml
Normal file
6
app/src/main/res/drawable/ic_baseline_access_time_24.xml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||||
|
android:viewportHeight="24" android:viewportWidth="24"
|
||||||
|
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z"/>
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M12.5,7H11v6l5.25,3.15 0.75,-1.23 -4.5,-2.67z"/>
|
||||||
|
</vector>
|
@ -3,17 +3,18 @@
|
|||||||
android:height="108dp"
|
android:height="108dp"
|
||||||
android:viewportWidth="108"
|
android:viewportWidth="108"
|
||||||
android:viewportHeight="108">
|
android:viewportHeight="108">
|
||||||
<group android:scaleX="0.051679686"
|
<group
|
||||||
android:scaleY="0.051679686"
|
android:scaleX="0.051679686"
|
||||||
android:translateX="27.54"
|
android:scaleY="0.051679686"
|
||||||
android:translateY="38.90954">
|
android:translateX="27.54"
|
||||||
<path
|
android:translateY="38.90954">
|
||||||
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"
|
<path
|
||||||
android:strokeLineJoin="miter"
|
android:fillColor="#000000"
|
||||||
android:strokeWidth="0.41878"
|
android:fillType="evenOdd"
|
||||||
android:fillColor="#000000"
|
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:strokeColor="#000000"
|
android:strokeWidth="0.41878"
|
||||||
android:fillType="evenOdd"
|
android:strokeColor="#000000"
|
||||||
android:strokeLineCap="butt"/>
|
android:strokeLineCap="butt"
|
||||||
</group>
|
android:strokeLineJoin="miter" />
|
||||||
|
</group>
|
||||||
</vector>
|
</vector>
|
||||||
|
10
app/src/main/res/drawable/ic_outline_download_24.xml
Normal file
10
app/src/main/res/drawable/ic_outline_download_24.xml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:tint="#FFFFFF"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M18,15v3H6v-3H4v3c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2v-3H18zM17,11l-1.41,-1.41L13,12.17V4h-2v8.17L8.41,9.59L7,11l5,5L17,11z" />
|
||||||
|
</vector>
|
@ -1,5 +1,10 @@
|
|||||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:viewportHeight="24" android:viewportWidth="24"
|
android:width="24dp"
|
||||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
android:height="24dp"
|
||||||
<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"/>
|
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>
|
</vector>
|
||||||
|
10
app/src/main/res/drawable/ic_outline_upload_24.xml
Normal file
10
app/src/main/res/drawable/ic_outline_upload_24.xml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:tint="#FFFFFF"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M18,15v3H6v-3H4v3c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2v-3H18zM7,9l1.41,1.41L11,7.83V16h2V7.83l2.59,2.58L17,9l-5,-5L7,9z" />
|
||||||
|
</vector>
|
@ -79,8 +79,52 @@
|
|||||||
android:text="@string/account_login_desc"
|
android:text="@string/account_login_desc"
|
||||||
android:textColor="?textSecondary" />
|
android:textColor="?textSecondary" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</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>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
@ -176,7 +220,7 @@
|
|||||||
android:padding="7dp">
|
android:padding="7dp">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/imageView4"
|
android:id="@+id/image_autoplay"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:contentDescription="@string/settings_autoplay"
|
android:contentDescription="@string/settings_autoplay"
|
||||||
@ -237,7 +281,7 @@
|
|||||||
android:padding="7dp">
|
android:padding="7dp">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/imageViewTheme"
|
android:id="@+id/image_theme"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:contentDescription="@string/account"
|
android:contentDescription="@string/account"
|
||||||
@ -274,6 +318,118 @@
|
|||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/linear_dev_settings"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
android:background="?themeSecondary"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:elevation="5dp"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_dev_settings"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingStart="7dp"
|
||||||
|
android:paddingEnd="7dp"
|
||||||
|
android:text="@string/dev_settings"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/linear_export_data"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:foreground="?android:selectableItemBackground"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:padding="7dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/image_export_data"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:contentDescription="@string/info"
|
||||||
|
android:minWidth="48dp"
|
||||||
|
android:minHeight="48dp"
|
||||||
|
android:padding="9dp"
|
||||||
|
android:scaleType="fitXY"
|
||||||
|
app:srcCompat="@drawable/ic_outline_upload_24"
|
||||||
|
app:tint="?iconColor" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_export_data"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/export_data"
|
||||||
|
android:textSize="16sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_export_data_desc"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/export_data_desc"
|
||||||
|
android:textColor="?textSecondary" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/linear_import_data"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:foreground="?android:selectableItemBackground"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:padding="7dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/image_import_data"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:contentDescription="@string/info"
|
||||||
|
android:minWidth="48dp"
|
||||||
|
android:minHeight="48dp"
|
||||||
|
android:padding="9dp"
|
||||||
|
android:scaleType="fitXY"
|
||||||
|
app:srcCompat="@drawable/ic_outline_download_24"
|
||||||
|
app:tint="?iconColor" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_import_data"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/import_data"
|
||||||
|
android:textSize="16sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_import_data_desc"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/import_data_desc"
|
||||||
|
android:textColor="?textSecondary" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/linear_info"
|
android:id="@+id/linear_info"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
@ -86,7 +86,7 @@
|
|||||||
android:id="@+id/button_select"
|
android:id="@+id/button_select"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/save"
|
android:text="@string/apply"
|
||||||
android:textAllCaps="false"
|
android:textAllCaps="false"
|
||||||
android:textColor="@color/themePrimaryDark"
|
android:textColor="@color/themePrimaryDark"
|
||||||
android:textSize="16sp"
|
android:textSize="16sp"
|
||||||
|
@ -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>
|
|
@ -22,7 +22,6 @@
|
|||||||
<item quantity="one">%d Episode</item>
|
<item quantity="one">%d Episode</item>
|
||||||
<item quantity="other">%d Episoden</item>
|
<item quantity="other">%d Episoden</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="text_runtime">%1$d Minuten</string>
|
|
||||||
<plurals name="text_runtime">
|
<plurals name="text_runtime">
|
||||||
<item quantity="one">%d Minute</item>
|
<item quantity="one">%d Minute</item>
|
||||||
<item quantity="other">%d Minuten</item>
|
<item quantity="other">%d Minuten</item>
|
||||||
@ -34,6 +33,8 @@
|
|||||||
<!-- settings fragment -->
|
<!-- settings fragment -->
|
||||||
<string name="account">Account</string>
|
<string name="account">Account</string>
|
||||||
<string name="account_login_desc">Zum bearbeiten tippen</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">Info</string>
|
||||||
<string name="info_about_desc">Version %1$s (%2$s)</string>
|
<string name="info_about_desc">Version %1$s (%2$s)</string>
|
||||||
<string name="settings">Einstellungen</string>
|
<string name="settings">Einstellungen</string>
|
||||||
@ -44,6 +45,12 @@
|
|||||||
<string name="theme">Design</string>
|
<string name="theme">Design</string>
|
||||||
<string name="theme_light">Hell</string>
|
<string name="theme_light">Hell</string>
|
||||||
<string name="theme_dark">Dunkel</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 -->
|
<!-- about fragment -->
|
||||||
<string name="version">Version</string>
|
<string name="version">Version</string>
|
||||||
@ -53,6 +60,8 @@
|
|||||||
<string name="about_info">Eine inoffizielle App für Anime on Demand.</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_heading">Lizenzen von Drittanbietern</string>
|
||||||
<string name="third_party_component_desc">© %1$s %2$s unter %3$s</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 -->
|
<!-- player -->
|
||||||
<string name="close_player">Player schließen</string>
|
<string name="close_player">Player schließen</string>
|
||||||
@ -75,8 +84,10 @@
|
|||||||
<string name="on_login_failed">Login nicht erfolgreich! Stelle sicher das deine Login-Daten korrekt sind und versuche es erneut.</string>
|
<string name="on_login_failed">Login nicht erfolgreich! Stelle sicher das deine Login-Daten korrekt sind und versuche es erneut.</string>
|
||||||
|
|
||||||
<!-- dialogs -->
|
<!-- 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="cancel">@android:string/cancel</string>
|
||||||
|
<string name="loading">Lädt…</string>
|
||||||
<string name="dialog_timeout_head">Anmelden fehlgeschlagen</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>
|
<string name="dialog_timeout_desc">Der Server scheint langsam zu antworten. Bitte versuche es später noch einmal.</string>
|
||||||
|
|
||||||
|
@ -29,7 +29,6 @@
|
|||||||
<item quantity="one">%d episode</item>
|
<item quantity="one">%d episode</item>
|
||||||
<item quantity="other">%d episodes</item>
|
<item quantity="other">%d episodes</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="text_runtime">%1$d Minutes</string>
|
|
||||||
<plurals name="text_runtime">
|
<plurals name="text_runtime">
|
||||||
<item quantity="one">%d Minute</item>
|
<item quantity="one">%d Minute</item>
|
||||||
<item quantity="other">%d Minutes</item>
|
<item quantity="other">%d Minutes</item>
|
||||||
@ -44,6 +43,8 @@
|
|||||||
<string name="account">Account</string>
|
<string name="account">Account</string>
|
||||||
<string name="account_login_ex" translatable="false">user@example.com</string>
|
<string name="account_login_ex" translatable="false">user@example.com</string>
|
||||||
<string name="account_login_desc">Tap to edit</string>
|
<string name="account_login_desc">Tap to edit</string>
|
||||||
|
<string name="account_subscription">Subscription %1$s</string>
|
||||||
|
<string name="account_subscription_desc">Tap to extend</string>
|
||||||
<string name="info">Info</string>
|
<string name="info">Info</string>
|
||||||
<string name="info_about" translatable="false">Teapod by @Seil0</string>
|
<string name="info_about" translatable="false">Teapod by @Seil0</string>
|
||||||
<string name="info_about_desc">Version %1$s (%2$s)</string>
|
<string name="info_about_desc">Version %1$s (%2$s)</string>
|
||||||
@ -55,6 +56,13 @@
|
|||||||
<string name="theme">Theme</string>
|
<string name="theme">Theme</string>
|
||||||
<string name="theme_light">Light</string>
|
<string name="theme_light">Light</string>
|
||||||
<string name="theme_dark">Dark</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 -->
|
<!-- about fragment -->
|
||||||
<string name="version">Version</string>
|
<string name="version">Version</string>
|
||||||
@ -69,6 +77,8 @@
|
|||||||
<string name="tmdb_notice" translatable="false">This product uses the TMDb API but is not endorsed or certified by TMDb.</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_heading">Third Party Licenses</string>
|
||||||
<string name="third_party_component_desc">© %1$s %2$s under %3$s</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 -->
|
<!-- player -->
|
||||||
<string name="close_player">close player</string>
|
<string name="close_player">close player</string>
|
||||||
@ -95,8 +105,10 @@
|
|||||||
<string name="on_login_failed">Could not login! Make sure Username and Password are correct and try again.</string>
|
<string name="on_login_failed">Could not login! Make sure Username and Password are correct and try again.</string>
|
||||||
|
|
||||||
<!-- dialogs -->
|
<!-- dialogs -->
|
||||||
<string name="save">save</string>
|
<string name="save">Save</string>
|
||||||
<string name="cancel">@android:string/cancel</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_head">Login failed</string>
|
||||||
<string name="dialog_timeout_desc">Looks like the server is taking to long to respond. Please try again later.</string>
|
<string name="dialog_timeout_desc">Looks like the server is taking to long to respond. Please try again later.</string>
|
||||||
|
|
||||||
@ -113,12 +125,10 @@
|
|||||||
<string name="save_key_user_password" translatable="false">org.mosad.teapod.user_password</string>
|
<string name="save_key_user_password" translatable="false">org.mosad.teapod.user_password</string>
|
||||||
<string name="save_key_prefer_secondary" translatable="false">org.mosad.teapod.prefer_secondary</string>
|
<string name="save_key_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_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>
|
<string name="save_key_theme" translatable="false">org.mosad.teapod.theme</string>
|
||||||
|
|
||||||
<!-- intents & states -->
|
<!-- intents & states -->
|
||||||
<string name="intent_media_id" translatable="false">intent_media_id</string>
|
<string name="intent_media_id" translatable="false">intent_media_id</string>
|
||||||
<string name="intent_episode_id" translatable="false">intent_episode_id</string>
|
<string name="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>
|
</resources>
|
@ -1,12 +1,12 @@
|
|||||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
buildscript {
|
buildscript {
|
||||||
ext.kotlin_version = "1.4.31"
|
ext.kotlin_version = "1.5.20"
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
jcenter()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:4.1.2'
|
classpath 'com.android.tools.build:gradle:4.2.2'
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
|
|
||||||
// NOTE: Do not place your application dependencies here; they belong
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
@ -17,7 +17,7 @@ buildscript {
|
|||||||
allprojects {
|
allprojects {
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
jcenter()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
5
fastlane/metadata/android/de/changelogs/4200.txt
Normal file
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)
|
5
fastlane/metadata/android/en-US/changelogs/4200.txt
Normal file
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)
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,5 +1,5 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
2
gradlew
vendored
2
gradlew
vendored
@ -130,7 +130,7 @@ fi
|
|||||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||||
|
|
||||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||||
|
|
||||||
# We build the pattern for arguments to be converted via cygpath
|
# We build the pattern for arguments to be converted via cygpath
|
||||||
|
21
gradlew.bat
vendored
21
gradlew.bat
vendored
@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
|
|||||||
|
|
||||||
set JAVA_EXE=java.exe
|
set JAVA_EXE=java.exe
|
||||||
%JAVA_EXE% -version >NUL 2>&1
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
if "%ERRORLEVEL%" == "0" goto init
|
if "%ERRORLEVEL%" == "0" goto execute
|
||||||
|
|
||||||
echo.
|
echo.
|
||||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
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_HOME=%JAVA_HOME:"=%
|
||||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
if exist "%JAVA_EXE%" goto init
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
echo.
|
echo.
|
||||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||||
@ -64,21 +64,6 @@ echo location of your Java installation.
|
|||||||
|
|
||||||
goto fail
|
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
|
:execute
|
||||||
@rem Setup the command line
|
@rem Setup the command line
|
||||||
|
|
||||||
@ -86,7 +71,7 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
|||||||
|
|
||||||
|
|
||||||
@rem Execute Gradle
|
@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
|
:end
|
||||||
@rem End local scope for the variables with windows NT shell
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
Loading…
Reference in New Issue
Block a user